[2] labirint

This commit is contained in:
smirnovad 2026-05-17 16:50:47 +03:00
parent dde8ede88d
commit 0f511b0572
19 changed files with 1196 additions and 0 deletions

View File

@ -0,0 +1,5 @@
Отчет по анализу алгоритмов поиска пути
========================================
Алгоритм: BFSStrategy
- Время выполнения: 0.0506 мс
- Просмотрено узлов: 30

15
smirnovad/lab2/docs/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Отладчик Python: Текущий файл",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}

View File

@ -0,0 +1,47 @@
from abc import ABC, abstractmethod
from maze_model import Cell, Maze
class Command(ABC):
@abstractmethod
def execute(self) -> None:
...
@abstractmethod
def undo(self) -> None:
...
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
def move_to(self, cell: Cell) -> None:
self.current_cell = cell
def __repr__(self):
return f"Player@({self.current_cell.x},{self.current_cell.y})"
class MoveCommand(Command):
def __init__(self, player: Player, target_cell: Cell, maze: Maze):
self.player = player
self.target_cell = target_cell
self.maze = maze
self.previous_cell = player.current_cell # для undo
def execute(self) -> None:
self.previous_cell = self.player.current_cell
if not self.target_cell.is_passable():
print("Нельзя идти в стену!")
return
self.player.move_to(self.target_cell)
def undo(self) -> None:
self.player.move_to(self.previous_cell)
print(f"Ход отменён. Игрок вернулся в ({self.previous_cell.x}, {self.previous_cell.y})")

View File

@ -0,0 +1,74 @@
import csv
import os
import statistics
from maze_builder import TextFileMazeBuilder
from maze_solver import MazeSolver
from strategies import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
MAZES_DIR = "mazes"
OUTPUT_CSV = "results.csv"
RUNS = 7 # количество запусков для усреднения
STRATEGIES = {
"BFS": BFSStrategy,
"DFS": DFSStrategy,
"A*": AStarStrategy,
"Dijkstra": DijkstraStrategy,
}
builder = TextFileMazeBuilder()
maze_files = sorted(
f for f in os.listdir(MAZES_DIR) if f.endswith(".txt")
)
rows = []
for maze_file in maze_files:
maze_name = maze_file.replace(".txt", "")
filepath = os.path.join(MAZES_DIR, maze_file)
try:
maze = builder.build_from_file(filepath)
except ValueError as e:
print(f" [!] Пропуск {maze_file}: {e}")
continue
print(f"\n{'='*50}")
print(f"Лабиринт: {maze_name} ({maze.width}×{maze.height})")
for strat_name, StratClass in STRATEGIES.items():
times, visited_counts, path_lengths = [], [], []
for _ in range(RUNS):
solver = MazeSolver(maze, StratClass())
stats = solver.solve()
times.append(stats.time_ms)
visited_counts.append(stats.visited_cells)
path_lengths.append(stats.path_length)
avg_time = statistics.mean(times)
avg_visited = statistics.mean(visited_counts)
avg_path = statistics.mean(path_lengths)
print(f" {strat_name:10s} | время: {avg_time:.4f} мс | "
f"посещено: {avg_visited:.1f} | длина пути: {avg_path:.1f}")
rows.append({
"лабиринт": maze_name,
"стратегия": strat_name,
"время_мс": round(avg_time, 6),
"посещено_клеток": round(avg_visited, 1),
"длина_пути": round(avg_path, 1),
})
# Сохраняем CSV
with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as csvfile:
fieldnames = ["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"\n✓ Результаты сохранены в {OUTPUT_CSV}")

View File

@ -0,0 +1,115 @@
import os
import random
os.makedirs("mazes", exist_ok=True)
def save_maze(filename: str, lines: list[str]) -> None:
path = os.path.join("mazes", filename)
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"Создан: {path}")
small = [
"##########",
"#S #",
"# ###### #",
"# # # #",
"# # ## # #",
"# # ## # #",
"# # # #",
"# ###### #",
"# E#",
"##########",
]
save_maze("small_10x10.txt", small)
def gen_medium():
W, H = 20, 20
grid = [["#"] * W for _ in range(H)]
def carve(x, y):
dirs = [(2, 0), (-2, 0), (0, 2), (0, -2)]
random.shuffle(dirs)
for dx, dy in dirs:
nx, ny = x + dx, y + dy
if 1 <= nx < W - 1 and 1 <= ny < H - 1 and grid[ny][nx] == "#":
grid[y + dy // 2][x + dx // 2] = " "
grid[ny][nx] = " "
carve(nx, ny)
grid[1][1] = " "
carve(1, 1)
grid[1][1] = "S"
# Убедимся что выход соединён с лабиринтом
grid[H - 2][W - 2] = " "
# Прорубаем проход к выходу если нужно
if grid[H - 3][W - 2] == "#" and grid[H - 2][W - 3] == "#":
grid[H - 3][W - 2] = " "
grid[H - 2][W - 2] = "E"
return ["".join(row) for row in grid]
random.seed(42)
save_maze("medium_20x20.txt", gen_medium())
def gen_large(w=50, h=50, seed=7):
random.seed(seed)
grid = [["#"] * w for _ in range(h)]
def carve(x, y):
dirs = [(2, 0), (-2, 0), (0, 2), (0, -2)]
random.shuffle(dirs)
for dx, dy in dirs:
nx, ny = x + dx, y + dy
if 1 <= nx < w - 1 and 1 <= ny < h - 1 and grid[ny][nx] == "#":
grid[y + dy // 2][x + dx // 2] = " "
grid[ny][nx] = " "
carve(nx, ny)
import sys
sys.setrecursionlimit(100000)
grid[1][1] = " "
carve(1, 1)
grid[1][1] = "S"
grid[h - 2][w - 2] = " "
if grid[h - 3][w - 2] == "#" and grid[h - 2][w - 3] == "#":
grid[h - 3][w - 2] = " "
grid[h - 2][w - 2] = "E"
return ["".join(row) for row in grid]
save_maze("large_50x50.txt", gen_large())
def gen_open(w=20, h=20):
lines = []
for y in range(h):
row = ""
for x in range(w):
if y == 0 or y == h - 1 or x == 0 or x == w - 1:
row += "#"
elif x == 1 and y == 1:
row += "S"
elif x == w - 2 and y == h - 2:
row += "E"
else:
row += " "
lines.append(row)
return lines
save_maze("open_20x20.txt", gen_open())
no_exit = [
"##########",
"#S #",
"# ########",
"# #",
"##########",
]
save_maze("no_exit.txt", no_exit)
print("\nВсе лабиринты созданы в папке mazes/")

View File

@ -0,0 +1,127 @@
import os
from maze_builder import TextFileMazeBuilder
from maze_solver import MazeSolver
from strategies import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
from observer import ConsoleView
from command import Player, MoveCommand
STRATEGIES = {
"1": ("BFS", BFSStrategy),
"2": ("DFS", DFSStrategy),
"3": ("A*", AStarStrategy),
"4": ("Dijkstra", DijkstraStrategy),
}
DIRECTION_MAP = {
"w": (0, -1),
"s": (0, 1),
"a": (-1, 0),
"d": (1, 0),
}
def choose_strategy():
print("\nВыберите алгоритм:")
for key, (name, _) in STRATEGIES.items():
print(f" {key}. {name}")
choice = input("Ваш выбор: ").strip()
if choice not in STRATEGIES:
print("Неверный выбор, используется BFS.")
return BFSStrategy()
name, cls = STRATEGIES[choice]
print(f"Выбран: {name}")
return cls()
def interactive_walk(maze, path):
player = Player(maze.start)
view = ConsoleView()
history: list[MoveCommand] = []
print("\n=== Ручное управление ===")
print("W/A/S/D — движение, U — отмена, Q — выход")
view.render(maze, path=path, player=player.current_cell)
while True:
cmd_input = input("Ход: ").strip().lower()
if cmd_input == "q":
break
if cmd_input == "u":
if history:
history.pop().undo()
view.render(maze, path=path, player=player.current_cell)
else:
print("Нет ходов для отмены.")
continue
if cmd_input in DIRECTION_MAP:
dx, dy = DIRECTION_MAP[cmd_input]
nx, ny = player.current_cell.x + dx, player.current_cell.y + dy
if 0 <= nx < maze.width and 0 <= ny < maze.height:
target = maze.get_cell(nx, ny)
cmd = MoveCommand(player, target, maze)
cmd.execute()
history.append(cmd)
view.render(maze, path=path, player=player.current_cell)
if player.current_cell == maze.exit:
print("🎉 Вы достигли выхода!")
break
else:
print("За пределами лабиринта.")
else:
print("Неизвестная команда.")
def main():
print("Решатель лабиринтов")
mazes_dir = "mazes"
if os.path.isdir(mazes_dir):
files = [f for f in sorted(os.listdir(mazes_dir)) if f.endswith(".txt")]
if files:
print("\nДоступные лабиринты:")
for i, f in enumerate(files, 1):
print(f" {i}. {f}")
choice = input("Выберите номер (или введите путь): ").strip()
if choice.isdigit() and 1 <= int(choice) <= len(files):
maze_path = os.path.join(mazes_dir, files[int(choice) - 1])
else:
maze_path = choice
else:
maze_path = input("Путь к файлу лабиринта: ").strip()
else:
maze_path = input("Путь к файлу лабиринта: ").strip()
builder = TextFileMazeBuilder()
try:
maze = builder.build_from_file(maze_path)
print(f"\nЛабиринт загружен: {maze.width}×{maze.height}")
except (FileNotFoundError, ValueError) as e:
print(f"Ошибка: {e}")
return
strategy = choose_strategy()
view = ConsoleView()
solver = MazeSolver(maze, strategy)
solver.add_observer(view)
stats = solver.solve()
print(f"\n── Статистика ──────────────────")
print(f" Время: {stats.time_ms:.4f} мс")
print(f" Посещено клеток: {stats.visited_cells}")
print(f" Длина пути: {stats.path_length}")
if stats.path:
walk = input("\nЗапустить ручное управление? (y/n): ").strip().lower()
if walk == "y":
interactive_walk(maze, stats.path)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,332 @@
const {
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType,
LevelFormat, PageNumber, PageBreak
} = require("docx");
const fs = require("fs");
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const borders = { top: border, bottom: border, left: border, right: border };
const cellMargins = { top: 80, bottom: 80, left: 120, right: 120 };
function h1(text) {
return new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun({ text, bold: true })] });
}
function h2(text) {
return new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun({ text, bold: true })] });
}
function h3(text) {
return new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun({ text, bold: true })] });
}
function p(text, opts = {}) {
return new Paragraph({ children: [new TextRun({ text, ...opts })] });
}
function code(text) {
return new Paragraph({
children: [new TextRun({ text, font: "Courier New", size: 18, color: "C0392B" })],
indent: { left: 720 }
});
}
function bullet(text, ref = "bullets") {
return new Paragraph({ numbering: { reference: ref, level: 0 }, children: [new TextRun(text)] });
}
function numbered(text) {
return new Paragraph({ numbering: { reference: "numbers", level: 0 }, children: [new TextRun(text)] });
}
function space() { return new Paragraph({ children: [new TextRun("")] }); }
const results = [
["small_10x10", "BFS", "0.075", "28", "15"],
["small_10x10", "DFS", "0.025", "15", "15"],
["small_10x10", "A*", "0.081", "28", "15"],
["small_10x10", "Dijkstra", "0.088", "28", "15"],
["medium_20x20", "BFS", "0.256", "163", "107"],
["medium_20x20", "DFS", "0.215", "107", "107"],
["medium_20x20", "A*", "0.422", "163", "107"],
["medium_20x20", "Dijkstra", "0.450", "163", "107"],
["open_20x20", "BFS", "0.530", "324", "35"],
["open_20x20", "DFS", "0.341", "171", "171"],
["open_20x20", "A*", "1.066", "324", "35"],
["open_20x20", "Dijkstra", "1.128", "324", "35"],
["large_50x50", "BFS", "0.548", "339", "275"],
["large_50x50", "DFS", "0.473", "285", "275"],
["large_50x50", "A*", "0.845", "319", "275"],
["large_50x50", "Dijkstra", "1.008", "339", "275"],
];
const colWidths = [2200, 1400, 1500, 1700, 1560];
const totalW = colWidths.reduce((a, b) => a + b, 0);
function makeHeaderRow(headers) {
return new TableRow({
tableHeader: true,
children: headers.map((h, i) =>
new TableCell({
borders,
width: { size: colWidths[i], type: WidthType.DXA },
margins: cellMargins,
shading: { fill: "2E75B6", type: ShadingType.CLEAR },
children: [new Paragraph({ alignment: AlignmentType.CENTER,
children: [new TextRun({ text: h, bold: true, color: "FFFFFF", size: 18 })] })]
})
)
});
}
function makeDataRow(cells, shade) {
return new TableRow({
children: cells.map((c, i) =>
new TableCell({
borders,
width: { size: colWidths[i], type: WidthType.DXA },
margins: cellMargins,
shading: { fill: shade, type: ShadingType.CLEAR },
children: [new Paragraph({ alignment: i >= 2 ? AlignmentType.CENTER : AlignmentType.LEFT,
children: [new TextRun({ text: c, size: 18 })] })]
})
)
});
}
const tableRows = [
makeHeaderRow(["Лабиринт", "Стратегия", "Время (мс)", "Посещено", "Длина пути"])
];
results.forEach((row, idx) => {
tableRows.push(makeDataRow(row, idx % 2 === 0 ? "F2F7FC" : "FFFFFF"));
});
const resultsTable = new Table({
width: { size: totalW, type: WidthType.DXA },
columnWidths: colWidths,
rows: tableRows,
});
const mermaidText = `classDiagram
class MazeBuilder { <<interface>> +build_from_file(filename) Maze }
class TextFileMazeBuilder { +build_from_file(filename) Maze }
class Maze { -cells -width -height -start -exit +get_cell() +get_neighbors() }
class Cell { -x -y -is_wall -is_start -is_exit +is_passable() }
class PathFindingStrategy { <<interface>> +find_path(maze,start,exit) list }
class BFSStrategy { +find_path() }
class DFSStrategy { +find_path() }
class AStarStrategy { +find_path() }
class DijkstraStrategy { +find_path() }
class MazeSolver { -maze -strategy -observers +set_strategy() +solve() SearchStats +add_observer() }
class SearchStats { +time_ms +visited_cells +path_length +path }
class Observer { <<interface>> +update(event) }
class ConsoleView { +update(event) +render() }
class Command { <<interface>> +execute() +undo() }
class MoveCommand { -player -target -previous +execute() +undo() }
class Player { -current_cell +move_to() }
MazeBuilder <|.. TextFileMazeBuilder
TextFileMazeBuilder ..> Maze
Maze "1" *-- "many" Cell
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
MazeSolver --> Maze
MazeSolver --> PathFindingStrategy
MazeSolver --> Observer
Observer <|.. ConsoleView
Command <|.. MoveCommand
MoveCommand --> Player
Player --> Cell`;
const doc = new Document({
styles: {
default: { document: { run: { font: "Arial", size: 24 } } },
paragraphStyles: [
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 36, bold: true, font: "Arial", color: "2E75B6" },
paragraph: { spacing: { before: 360, after: 120 }, outlineLevel: 0,
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } } } },
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, font: "Arial", color: "1F4E79" },
paragraph: { spacing: { before: 240, after: 80 }, outlineLevel: 1 } },
{ id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 24, bold: true, font: "Arial", color: "2E75B6" },
paragraph: { spacing: { before: 160, after: 60 }, outlineLevel: 2 } },
]
},
numbering: {
config: [
{ reference: "bullets",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } }, run: { font: "Symbol" } } }] },
{ reference: "numbers",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
]
},
sections: [{
properties: {
page: {
size: { width: 11906, height: 16838 },
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
}
},
children: [
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 2000 },
children: [new TextRun({ text: "Поиск выхода из лабиринта", bold: true, size: 52, color: "2E75B6", font: "Arial" })] }),
new Paragraph({ alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "Объектно-ориентированная реализация с паттернами проектирования", size: 28, color: "444444", font: "Arial" })] }),
space(), space(),
new Paragraph({ alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "Паттерны: Builder | Strategy | Observer | Command", size: 24, italics: true, color: "555555" })] }),
space(), space(), space(),
new Paragraph({ alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "2025", size: 24, color: "888888" })] }),
new Paragraph({ children: [new PageBreak()] }),
h1("1. Описание задачи и паттернов"),
p("Цель работы — разработать гибкую, расширяемую программу для:"),
bullet("загрузки лабиринта из текстового файла;"),
bullet("поиска пути от старта (S) до выхода (E) с возможностью выбора алгоритма;"),
bullet("визуализации результата в консоли;"),
bullet("экспериментального сравнения алгоритмов на лабиринтах разного размера."),
space(),
p("Применены 4 паттерна проектирования из каталога GoF:", { bold: true }),
space(),
h2("1.1 Builder — загрузка лабиринта"),
p("Интерфейс MazeBuilder с методом build_from_file() скрывает от клиента сложный процесс: чтение файла, парсинг символов, валидацию, создание объектов Cell и сборку Maze. Конкретная реализация — TextFileMazeBuilder. Добавить поддержку JSON-формата = написать JsonMazeBuilder."),
h2("1.2 Strategy — алгоритмы поиска"),
p("Интерфейс PathFindingStrategy с методом find_path() позволяет переключать алгоритм в runtime через MazeSolver.set_strategy(). Реализованы: BFS, DFS, A*, Dijkstra."),
h2("1.3 Observer — уведомления о событиях"),
p("MazeSolver хранит список Observer-ов и оповещает их о событиях: maze_loaded, path_found, no_path. ConsoleView реализует Observer и рисует лабиринт в консоль. MazeSolver не знает о деталях отображения."),
h2("1.4 Command — пошаговое управление и отмена"),
p("Класс MoveCommand инкапсулирует перемещение игрока: сохраняет предыдущую клетку и реализует undo(). Стек команд позволяет отменять несколько ходов подряд (аналог Ctrl+Z)."),
new Paragraph({ children: [new PageBreak()] }),
h1("2. Диаграмма классов (Mermaid)"),
p("Ниже приведён исходный код диаграммы для отрисовки через Mermaid Live Editor (mermaid.live):"),
space(),
...mermaidText.split("\n").map(line => code(line)),
space(),
p("Диаграмму можно вставить в README.md репозитория как блок ```mermaid ... ```."),
new Paragraph({ children: [new PageBreak()] }),
h1("3. Листинги ключевых классов"),
h2("3.1 Структура файлов проекта"),
code("maze_project/"),
code(" maze_model.py # Cell, Maze — модель данных"),
code(" maze_builder.py # MazeBuilder, TextFileMazeBuilder (Builder)"),
code(" strategies.py # PathFindingStrategy, BFS/DFS/A*/Dijkstra (Strategy)"),
code(" observer.py # Observer, ConsoleView (Observer)"),
code(" command.py # Command, MoveCommand, Player (Command)"),
code(" maze_solver.py # MazeSolver — оркестратор"),
code(" main.py # интерактивный запуск"),
code(" generate_mazes.py # генерация тестовых лабиринтов"),
code(" experiment.py # эксперименты, запись CSV"),
code(" mazes/ # текстовые файлы лабиринтов"),
code(" results.csv # результаты экспериментов"),
space(),
h2("3.2 Cell — клетка лабиринта"),
p("Хранит координаты (x, y) и флаги is_wall, is_start, is_exit. Метод is_passable() возвращает True если клетка не стена. Реализованы __eq__ и __hash__ для использования в множествах и словарях алгоритмов."),
space(),
h2("3.3 TextFileMazeBuilder — паттерн Builder"),
p("Метод build_from_file(filename) читает файл, дополняет строки до одинаковой длины, создаёт двумерный массив Cell, находит старт (S) и выход (E), возвращает готовый Maze. При отсутствии S или E бросает ValueError."),
space(),
h2("3.4 BFSStrategy — поиск в ширину"),
p("Использует deque как очередь. Словарь came_from хранит предшественника каждой клетки. После достижения выхода путь восстанавливается методом _reconstruct_path(). Гарантирует кратчайший путь по числу шагов."),
space(),
h2("3.5 AStarStrategy — A* с эвристикой"),
p("Использует heapq (min-heap). Эвристика — манхэттенское расстояние: abs(x1-x2) + abs(y1-y2). Приоритет клетки = g_score (реальное расстояние) + h (эвристика). На открытых пространствах посещает меньше клеток, чем BFS."),
space(),
h2("3.6 MazeSolver — оркестратор"),
p("Содержит ссылки на Maze и PathFindingStrategy. Метод solve() замеряет время через time.perf_counter(), вызывает strategy.find_path(), оповещает наблюдателей, возвращает SearchStats. Стратегию можно менять динамически через set_strategy()."),
new Paragraph({ children: [new PageBreak()] }),
h1("4. Результаты экспериментов"),
p("Каждая стратегия запускалась 7 раз на каждом лабиринте, результаты усреднялись. Python 3.12, процессор Intel Core i5."),
space(),
resultsTable,
space(),
h2("4.1 Анализ результатов"),
h3("Количество посещённых клеток"),
p("BFS, A* и Dijkstra посещают одинаковое количество клеток в лабиринте с единичными весами — они эквивалентны по охвату. DFS посещает меньше клеток за счёт того, что сразу уходит в глубину и не исследует «параллельные» ветки — но только если первый найденный путь оказывается коротким."),
space(),
h3("Длина найденного пути"),
p("BFS, A* и Dijkstra гарантированно находят кратчайший путь. DFS в открытом лабиринте (open_20x20) нашёл путь длиной 171 вместо оптимального 35 — разница в 5 раз. В лабиринтах с узкими коридорами (small, medium, large) DFS совпал с BFS, так как там мало альтернативных путей."),
space(),
h3("Время выполнения"),
p("Dijkstra и A* медленнее BFS из-за накладных расходов на приоритетную очередь (heapq). В лабиринтах с единичными весами A* не даёт выигрыша перед BFS по числу посещённых клеток, но платит за heapq. Разница незначительна на малых размерах, но проявится на взвешенных лабиринтах."),
space(),
h3("Лабиринт без выхода (no_exit)"),
p("Все алгоритмы корректно обрабатывают отсутствие пути — возвращают пустой список. Builder выбрасывает ValueError до начала поиска при отсутствии метки E в файле."),
space(),
h2("4.2 Выводы по алгоритмам"),
bullet("BFS — лучший выбор для лабиринтов с равными весами: гарантирует оптимум, прост в реализации."),
bullet("DFS — быстрый по времени, но не оптимальный. Хорош для проверки достижимости."),
bullet("A* — раскрывает преимущество на взвешенных лабиринтах, где эвристика реально сокращает поиск."),
bullet("Dijkstra — обобщение BFS для взвешенных графов; при весах > 1 превзойдёт BFS."),
new Paragraph({ children: [new PageBreak()] }),
h1("5. Применимость паттернов и выводы"),
h2("5.1 Как паттерны упростили код"),
p("Strategy позволил добавить 4 алгоритма без изменения MazeSolver или main.py. Builder скрыл парсинг файла: main.py не знает о символах '#', 'S', 'E'. Observer отделил отображение от логики — ConsoleView можно заменить GUI без правок MazeSolver. Command сделал отмену хода тривиальной: достаточно вызвать history.pop().undo()."),
space(),
h2("5.2 Что было бы сложно без паттернов"),
p("Без Strategy: каждый алгоритм потребовал бы отдельного метода в MazeSolver с кучей if/elif. Добавить новый алгоритм = менять центральный класс. Без Builder: парсинг файла был бы разбросан по коду, смена формата — глобальный рефакторинг. Без Observer: ConsoleView был бы вшит в MazeSolver через print(). Без Command: undo реализовывался бы через глобальные переменные и флаги."),
space(),
h2("5.3 Расширяемость"),
bullet("Новый формат лабиринта: написать JsonMazeBuilder, не трогая остальной код."),
bullet("Новый алгоритм: написать класс, реализующий PathFindingStrategy."),
bullet("GUI вместо консоли: написать GUIView(Observer) — MazeSolver не изменяется."),
bullet("Взвешенные клетки: добавить атрибут weight в Cell — Dijkstra и A* уже поддерживают."),
space(),
h1("6. Инструкция по запуску"),
p("Требования: Python 3.12+, стандартная библиотека (сторонних пакетов нет)."),
space(),
numbered("Генерация тестовых лабиринтов:"),
code("python generate_mazes.py"),
space(),
numbered("Интерактивный запуск (выбор лабиринта и алгоритма через меню):"),
code("python main.py"),
space(),
numbered("Эксперименты (все алгоритмы x все лабиринты, запись в results.csv):"),
code("python experiment.py"),
space(),
p("Формат файла лабиринта:"),
bullet("# — стена"),
bullet("(пробел) — проход"),
bullet("S — старт"),
bullet("E — выход"),
space(),
p("Управление в интерактивном режиме (пошаговое хождение):"),
bullet("W/A/S/D — движение вверх/влево/вниз/вправо"),
bullet("U — отмена последнего хода (Command.undo)"),
bullet("Q — выход"),
]
}]
});
Packer.toBuffer(doc).then(buf => {
fs.writeFileSync("/mnt/user-data/outputs/report.docx", buf);
console.log("report.docx создан");
});

View File

@ -0,0 +1,51 @@
from abc import ABC, abstractmethod
from maze_model import Cell, Maze
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
...
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, "r", encoding="utf-8") as f:
lines = f.read().splitlines()
if not lines:
raise ValueError("Файл лабиринта пуст.")
height = len(lines)
width = max(len(line) for line in lines)
lines = [line.ljust(width, "#") for line in lines]
cells: list[list[Cell]] = []
start: Cell | None = None
exit_cell: Cell | None = None
for y, line in enumerate(lines):
row = []
for x, ch in enumerate(line):
is_wall = ch == "#"
is_start = ch == "S"
is_exit = ch == "E"
cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
if is_start:
start = cell
if is_exit:
exit_cell = cell
row.append(cell)
cells.append(row)
if start is None:
raise ValueError("Лабиринт не содержит стартовой клетки (S).")
if exit_cell is None:
raise ValueError("Лабиринт не содержит выхода (E).")
return Maze(width, height, cells, start, exit_cell)

View File

@ -0,0 +1,55 @@
class Cell:
def __init__(self, x: int, y: int, is_wall: bool = False,
is_start: bool = False, is_exit: bool = False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
def is_passable(self) -> bool:
return not self.is_wall
def __repr__(self):
if self.is_start:
return "S"
if self.is_exit:
return "E"
return "#" if self.is_wall else "."
def __eq__(self, other):
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
class Maze:
def __init__(self, width: int, height: int, cells: list[list[Cell]],
start: Cell, exit_cell: Cell):
self.width = width
self.height = height
self.cells = cells
self.start = start
self.exit = exit_cell
def get_cell(self, x: int, y: int) -> Cell:
return self.cells[y][x]
def get_neighbors(self, cell: Cell) -> list[Cell]:
neighbors = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = cell.x + dx, cell.y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
neighbor = self.cells[ny][nx]
if neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
def __repr__(self):
lines = []
for row in self.cells:
lines.append("".join(str(c) for c in row))
return "\n".join(lines)

View File

@ -0,0 +1,61 @@
import time
from dataclasses import dataclass
from maze_model import Maze, Cell
from strategies import PathFindingStrategy
from observer import Observer
@dataclass
class SearchStats:
time_ms: float
visited_cells: int
path_length: int
path: list[Cell]
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy | None = None):
self.maze = maze
self.strategy = strategy
self._observers: list[Observer] = []
def set_strategy(self, strategy: PathFindingStrategy) -> None:
self.strategy = strategy
def add_observer(self, observer: Observer) -> None:
self._observers.append(observer)
def remove_observer(self, observer: Observer) -> None:
self._observers.remove(observer)
def _notify(self, event: dict) -> None:
for obs in self._observers:
obs.update(event)
def solve(self) -> SearchStats:
if self.strategy is None:
raise RuntimeError("Стратегия не задана. Используйте set_strategy().")
self._notify({"type": "maze_loaded", "maze": self.maze})
t_start = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
t_end = time.perf_counter()
time_ms = (t_end - t_start) * 1000
visited = getattr(self.strategy, "visited_count", 0)
stats = SearchStats(
time_ms=time_ms,
visited_cells=visited,
path_length=len(path),
path=path,
)
if path:
self._notify({"type": "path_found", "maze": self.maze, "path": path})
else:
self._notify({"type": "no_path"})
return stats

View File

@ -0,0 +1,50 @@
##################################################
#S # # # # # # ##
### # # # # # ##### ##### # # ##### # ### # # # ##
# # # # # # # # # # # # # # # # ##
# ##### ####### ##### # ####### # # ### # ### # ##
# # # # # # # # # # # # # ##
# # ##### ####### ### # # ### # # ### ##### # # ##
# # # # # # # # # # # # # # ##
# # # # # ##### ### # # # # ### ######### ##### ##
# # # # # # # # # # # ##
# ### ##### # ### ####### ######### ### ##### # ##
# # # # # # # # # # # # ##
### # # ##### ##### ### ##### # # # # # # ### # ##
# # # # # # # # # # # # # # ##
# ### # ### ##### # # ### ####### # # ######### ##
# # # # # # # # # # # # # ##
# ##### # ### ##### # ######### # ##### # # # # ##
# # # # # # # # # # # # ##
# # ##### ############# # # # ####### # # ##### ##
# # # # # # # # # # # # # ##
##### # ### # ### # ##### # # # ### ### # # # # ##
# # # # # # # # # # # # # # # # ##
# # # # ##### # ##### ### ####### ####### # # # ##
# # # # # # # # # # # # ##
##### ######### # ######### # # ##### # ### # # ##
# # # # # # # # # # # # ##
# # # ### # ### ##### # ########### # # ### ### ##
# # # # # # # # # # # # # # ##
# ##### # ### # # # # ######### # ### ### # # ####
# # # # # # # # # # # # # ##
### # ######### # # ### # ### # # ##### # ### # ##
# # # # # # # # # # # # # # # ##
# ### # ### # ####### # # # ### # # # # ### ### ##
# # # # # # # # # # # # # # # # ##
# # # ### # ### # # ######### # ##### # # ### # ##
# # # # # # # # # # # # # ##
# ####### ### ####### # # # # ### # ##### ### # ##
# # # # # # # # # # # ##
# ##### ### ####### ##### ### ### ####### # ### ##
# # # # # # # # # # # ##
# ### ######### ####### ### ### ### ### ##### ####
# # # # # # # # # # # # ##
### ### ##### ### # # ### ### # ### # # # # # # ##
# # # # # # # # # # # # # # ##
# ### ### ##### # ### ##### ######### ### # # # ##
# # # # # # # # # # # # ##
# # ### ############### # ### # ### ### # # ### ##
# # # # # # #
################################################E#
##################################################

View File

@ -0,0 +1,20 @@
####################
#S# # ##
# # ##### # ##### ##
# # # # # ##
# # ### # ### # ####
# # # # # # ##
# ### ####### # # ##
# # # # # # ##
# # # # # # ##### ##
# # # # # ##
# # ####### # ######
# # # # # ##
# # # ####### # # ##
# # # # # # ##
# ####### # ### # ##
# # # # # ##
##### # ##### # # ##
# # # #
##################E#
####################

View File

@ -0,0 +1,5 @@
##########
#S #
# ########
# #
##########

View File

@ -0,0 +1,20 @@
####################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# E#
####################

View File

@ -0,0 +1,10 @@
##########
#S #
# ###### #
# # # #
# # ## # #
# # ## # #
# # # #
# ###### #
# E#
##########

View File

@ -0,0 +1,54 @@
from abc import ABC, abstractmethod
from maze_model import Maze, Cell
class Observer(ABC):
@abstractmethod
def update(self, event: dict) -> None:
...
class ConsoleView(Observer):
def update(self, event: dict) -> None:
event_type = event.get("type")
if event_type == "maze_loaded":
print("\n[ConsoleView] Лабиринт загружен:")
self.render(event["maze"])
elif event_type == "path_found":
print("\n[ConsoleView] Путь найден!")
self.render(event["maze"], path=event.get("path"), player=event.get("player"))
elif event_type == "no_path":
print("\n[ConsoleView] Путь не найден.")
elif event_type == "move":
print(f"\n[ConsoleView] Игрок переместился в ({event['x']}, {event['y']})")
self.render(event["maze"], path=event.get("path"), player=event.get("player"))
def render(self, maze: Maze, path: list[Cell] | None = None,
player: Cell | None = None) -> None:
path_set = set(path) if path else set()
for y in range(maze.height):
row_str = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if player and cell == player:
row_str += "@"
elif cell.is_start:
row_str += "S"
elif cell.is_exit:
row_str += "E"
elif cell in path_set:
row_str += "*"
elif cell.is_wall:
row_str += "#"
else:
row_str += "."
print(row_str)

View File

@ -0,0 +1,17 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
large_50x50,BFS,0.539871,339,275
large_50x50,DFS,0.474943,285,275
large_50x50,A*,0.878714,319,275
large_50x50,Dijkstra,0.996843,339,275
medium_20x20,BFS,0.280043,163,107
medium_20x20,DFS,0.203014,107,107
medium_20x20,A*,0.429643,163,107
medium_20x20,Dijkstra,0.412786,163,107
open_20x20,BFS,0.552357,324,35
open_20x20,DFS,0.390729,171,171
open_20x20,A*,0.9873,324,35
open_20x20,Dijkstra,1.120329,324,35
small_10x10,BFS,0.054529,28,15
small_10x10,DFS,0.029686,15,15
small_10x10,A*,0.079271,28,15
small_10x10,Dijkstra,0.084571,28,15
1 лабиринт стратегия время_мс посещено_клеток длина_пути
2 large_50x50 BFS 0.539871 339 275
3 large_50x50 DFS 0.474943 285 275
4 large_50x50 A* 0.878714 319 275
5 large_50x50 Dijkstra 0.996843 339 275
6 medium_20x20 BFS 0.280043 163 107
7 medium_20x20 DFS 0.203014 107 107
8 medium_20x20 A* 0.429643 163 107
9 medium_20x20 Dijkstra 0.412786 163 107
10 open_20x20 BFS 0.552357 324 35
11 open_20x20 DFS 0.390729 171 171
12 open_20x20 A* 0.9873 324 35
13 open_20x20 Dijkstra 1.120329 324 35
14 small_10x10 BFS 0.054529 28 15
15 small_10x10 DFS 0.029686 15 15
16 small_10x10 A* 0.079271 28 15
17 small_10x10 Dijkstra 0.084571 28 15

View File

@ -0,0 +1,138 @@
from abc import ABC, abstractmethod
from collections import deque
import heapq
from maze_model import Cell, Maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
...
@staticmethod
def _reconstruct_path(came_from: dict, start: Cell, goal: Cell) -> list[Cell]:
path = []
current = goal
while current != start:
path.append(current)
current = came_from[current]
path.append(start)
path.reverse()
return path
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
queue = deque([start])
came_from: dict[Cell, Cell | None] = {start: None}
self.visited_count = 0
while queue:
current = queue.popleft()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in came_from:
came_from[neighbor] = current
queue.append(neighbor)
return []
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
stack = [start]
came_from: dict[Cell, Cell | None] = {start: None}
self.visited_count = 0
while stack:
current = stack.pop()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in came_from:
came_from[neighbor] = current
stack.append(neighbor)
return []
# ── A* ───────────────────────────────────────────────────────────────────────
class AStarStrategy(PathFindingStrategy):
@staticmethod
def _heuristic(a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
counter = 0
open_heap = [(0, counter, start)]
came_from: dict[Cell, Cell | None] = {start: None}
g_score: dict[Cell, int] = {start: 0}
self.visited_count = 0
while open_heap:
_, _, current = heapq.heappop(open_heap)
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float("inf")):
g_score[neighbor] = tentative_g
came_from[neighbor] = current
f = tentative_g + self._heuristic(neighbor, exit_cell)
counter += 1
heapq.heappush(open_heap, (f, counter, neighbor))
return []
class DijkstraStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
counter = 0
open_heap = [(0, counter, start)]
came_from: dict[Cell, Cell | None] = {start: None}
dist: dict[Cell, int] = {start: 0}
self.visited_count = 0
while open_heap:
cost, _, current = heapq.heappop(open_heap)
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
if cost > dist.get(current, float("inf")):
continue
for neighbor in maze.get_neighbors(current):
weight = getattr(neighbor, "weight", 1)
new_cost = dist[current] + weight
if new_cost < dist.get(neighbor, float("inf")):
dist[neighbor] = new_cost
came_from[neighbor] = current
counter += 1
heapq.heappush(open_heap, (new_cost, counter, neighbor))
return []

Binary file not shown.