From bef23a7200611846ef2614d1faa7ed1f48f84bb8 Mon Sep 17 00:00:00 2001 From: krasnovia Date: Wed, 20 May 2026 22:29:49 +0300 Subject: [PATCH] [2] lab2 --- krasnovia/lab2/docs/data/command.py | 61 ++++ krasnovia/lab2/docs/data/experiment.py | 82 +++++ krasnovia/lab2/docs/data/generate_mazes.py | 125 +++++++ krasnovia/lab2/docs/data/main.py | 138 +++++++ krasnovia/lab2/docs/data/make_report.js | 340 ++++++++++++++++++ krasnovia/lab2/docs/data/maze_builder.py | 71 ++++ krasnovia/lab2/docs/data/maze_model.py | 63 ++++ krasnovia/lab2/docs/data/maze_solver.py | 85 +++++ .../lab2/docs/data/mazes/large_50x50.txt | 50 +++ .../lab2/docs/data/mazes/medium_20x20.txt | 20 ++ krasnovia/lab2/docs/data/mazes/no_exit.txt | 5 + krasnovia/lab2/docs/data/mazes/open_20x20.txt | 20 ++ .../lab2/docs/data/mazes/small_10x10.txt | 10 + krasnovia/lab2/docs/data/observer.py | 79 ++++ krasnovia/lab2/docs/data/results.csv | 17 + krasnovia/lab2/docs/data/strategies.py | 175 +++++++++ krasnovia/lab2/docs/report.docx | Bin 0 -> 20215 bytes 17 files changed, 1341 insertions(+) create mode 100644 krasnovia/lab2/docs/data/command.py create mode 100644 krasnovia/lab2/docs/data/experiment.py create mode 100644 krasnovia/lab2/docs/data/generate_mazes.py create mode 100644 krasnovia/lab2/docs/data/main.py create mode 100644 krasnovia/lab2/docs/data/make_report.js create mode 100644 krasnovia/lab2/docs/data/maze_builder.py create mode 100644 krasnovia/lab2/docs/data/maze_model.py create mode 100644 krasnovia/lab2/docs/data/maze_solver.py create mode 100644 krasnovia/lab2/docs/data/mazes/large_50x50.txt create mode 100644 krasnovia/lab2/docs/data/mazes/medium_20x20.txt create mode 100644 krasnovia/lab2/docs/data/mazes/no_exit.txt create mode 100644 krasnovia/lab2/docs/data/mazes/open_20x20.txt create mode 100644 krasnovia/lab2/docs/data/mazes/small_10x10.txt create mode 100644 krasnovia/lab2/docs/data/observer.py create mode 100644 krasnovia/lab2/docs/data/results.csv create mode 100644 krasnovia/lab2/docs/data/strategies.py create mode 100644 krasnovia/lab2/docs/report.docx diff --git a/krasnovia/lab2/docs/data/command.py b/krasnovia/lab2/docs/data/command.py new file mode 100644 index 0000000..ef2bc76 --- /dev/null +++ b/krasnovia/lab2/docs/data/command.py @@ -0,0 +1,61 @@ +""" +Этап 5.2: Паттерн Command — пошаговое перемещение игрока с отменой хода. + +Зачем Command? +Каждое перемещение инкапсулировано в объекте. Это позволяет: +- хранить историю ходов +- отменять последний ход (undo) через Ctrl+Z +- повторять ходы +""" + +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})") diff --git a/krasnovia/lab2/docs/data/experiment.py b/krasnovia/lab2/docs/data/experiment.py new file mode 100644 index 0000000..6d60e01 --- /dev/null +++ b/krasnovia/lab2/docs/data/experiment.py @@ -0,0 +1,82 @@ +""" +Этап 6: Экспериментальная часть. + +Запускает все стратегии на всех лабиринтах 7 раз, +усредняет результаты и сохраняет в results.csv. + +Запуск: python experiment.py +""" + +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}") diff --git a/krasnovia/lab2/docs/data/generate_mazes.py b/krasnovia/lab2/docs/data/generate_mazes.py new file mode 100644 index 0000000..368a065 --- /dev/null +++ b/krasnovia/lab2/docs/data/generate_mazes.py @@ -0,0 +1,125 @@ +""" +Генерирует тестовые лабиринты в папку mazes/. + +Запуск: python generate_mazes.py +""" + +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}") + + +# ── 1. Маленький 10×10 с простым путём ─────────────────────────────────────── +small = [ + "##########", + "#S #", + "# ###### #", + "# # # #", + "# # ## # #", + "# # ## # #", + "# # # #", + "# ###### #", + "# E#", + "##########", +] +save_maze("small_10x10.txt", small) + + +# ── 2. Средний 20×20 с тупиками ────────────────────────────────────────────── +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()) + + +# ── 3. Большой 50×50 с запутанной структурой ───────────────────────────────── +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()) + + +# ── 4. «Пустой» лабиринт (без стен внутри) ─────────────────────────────────── +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()) + + +# ── 5. Лабиринт без выхода ─────────────────────────────────────────────────── +no_exit = [ + "##########", + "#S #", + "# ########", + "# #", + "##########", +] +save_maze("no_exit.txt", no_exit) + +print("\nВсе лабиринты созданы в папке mazes/") diff --git a/krasnovia/lab2/docs/data/main.py b/krasnovia/lab2/docs/data/main.py new file mode 100644 index 0000000..4274b6e --- /dev/null +++ b/krasnovia/lab2/docs/data/main.py @@ -0,0 +1,138 @@ +""" +Главный файл запуска — интерактивное меню. + +Запуск: python main.py +""" + +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): + """Пошаговое ручное перемещение игрока по лабиринту (паттерн Command).""" + 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("╔══════════════════════════════╗") + print("║ Решатель лабиринтов ║") + 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() diff --git a/krasnovia/lab2/docs/data/make_report.js b/krasnovia/lab2/docs/data/make_report.js new file mode 100644 index 0000000..6a8f0f6 --- /dev/null +++ b/krasnovia/lab2/docs/data/make_report.js @@ -0,0 +1,340 @@ +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, +}); + +// ── Диаграмма классов (mermaid текст) ──────────────────────────────────────── +const mermaidText = `classDiagram + class MazeBuilder { <> +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 { <> +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 { <> +update(event) } + class ConsoleView { +update(event) +render() } + class Command { <> +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()] }), + + // ── 1. ОПИСАНИЕ ЗАДАЧИ ──────────────────────────────────────────────── + 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()] }), + + // ── 2. ДИАГРАММА КЛАССОВ ────────────────────────────────────────────── + 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()] }), + + // ── 3. СТРУКТУРА ПРОЕКТА ───────────────────────────────────────────── + 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()] }), + + // ── 4. ЭКСПЕРИМЕНТЫ ─────────────────────────────────────────────────── + 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()] }), + + // ── 5. ПРИМЕНИМОСТЬ ПАТТЕРНОВ ───────────────────────────────────────── + 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 создан"); +}); diff --git a/krasnovia/lab2/docs/data/maze_builder.py b/krasnovia/lab2/docs/data/maze_builder.py new file mode 100644 index 0000000..910169d --- /dev/null +++ b/krasnovia/lab2/docs/data/maze_builder.py @@ -0,0 +1,71 @@ +""" +Этап 2: Паттерн Builder — загрузка лабиринта из файла. + +Формат файла: + # — стена + ' ' (пробел) — проход + S — старт + E — выход +""" + +from abc import ABC, abstractmethod +from maze_model import Cell, Maze + + +class MazeBuilder(ABC): + """Интерфейс строителя лабиринта (паттерн Builder).""" + + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + """Читает файл и возвращает готовый объект Maze.""" + ... + + +class TextFileMazeBuilder(MazeBuilder): + """ + Конкретный строитель: читает текстовый файл и строит Maze. + + Зачем Builder? + Процесс построения многошаговый: чтение, парсинг, валидация, + поиск старта/выхода, создание объектов Cell. Builder скрывает + эту сложность от клиента. Чтобы добавить JSON-формат достаточно + написать JsonMazeBuilder, не трогая остальной код. + """ + + 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) diff --git a/krasnovia/lab2/docs/data/maze_model.py b/krasnovia/lab2/docs/data/maze_model.py new file mode 100644 index 0000000..7a32090 --- /dev/null +++ b/krasnovia/lab2/docs/data/maze_model.py @@ -0,0 +1,63 @@ +""" +Этап 1: Модель лабиринта — классы Cell и Maze +""" + +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: + """True, если клетка проходима (не стена).""" + 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 # cells[y][x] + 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) diff --git a/krasnovia/lab2/docs/data/maze_solver.py b/krasnovia/lab2/docs/data/maze_solver.py new file mode 100644 index 0000000..cd927c8 --- /dev/null +++ b/krasnovia/lab2/docs/data/maze_solver.py @@ -0,0 +1,85 @@ +""" +Этап 4: Класс-оркестратор MazeSolver. + +Принимает лабиринт и стратегию, запускает поиск, +собирает статистику и уведомляет наблюдателей. +""" + +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 # длина найденного пути (0 если не найден) + path: list[Cell] # сам путь + + +class MazeSolver: + """ + Оркестратор: связывает лабиринт, стратегию и наблюдателей. + + Паттерны внутри: + - Strategy: алгоритм задаётся снаружи через set_strategy() + - Observer: подписчики получают события о ходе поиска + """ + + def __init__(self, maze: Maze, strategy: PathFindingStrategy | None = None): + self.maze = maze + self.strategy = strategy + self._observers: list[Observer] = [] + + # ── Strategy ────────────────────────────────────────────────────────────── + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + """Динамически меняет алгоритм поиска.""" + self.strategy = strategy + + # ── Observer ────────────────────────────────────────────────────────────── + + 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) + + # ── Solve ───────────────────────────────────────────────────────────────── + + 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 diff --git a/krasnovia/lab2/docs/data/mazes/large_50x50.txt b/krasnovia/lab2/docs/data/mazes/large_50x50.txt new file mode 100644 index 0000000..60a2c26 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/large_50x50.txt @@ -0,0 +1,50 @@ +################################################## +#S # # # # # # ## +### # # # # # ##### ##### # # ##### # ### # # # ## +# # # # # # # # # # # # # # # # ## +# ##### ####### ##### # ####### # # ### # ### # ## +# # # # # # # # # # # # # ## +# # ##### ####### ### # # ### # # ### ##### # # ## +# # # # # # # # # # # # # # ## +# # # # # ##### ### # # # # ### ######### ##### ## +# # # # # # # # # # # ## +# ### ##### # ### ####### ######### ### ##### # ## +# # # # # # # # # # # # ## +### # # ##### ##### ### ##### # # # # # # ### # ## +# # # # # # # # # # # # # # ## +# ### # ### ##### # # ### ####### # # ######### ## +# # # # # # # # # # # # # ## +# ##### # ### ##### # ######### # ##### # # # # ## +# # # # # # # # # # # # ## +# # ##### ############# # # # ####### # # ##### ## +# # # # # # # # # # # # # ## +##### # ### # ### # ##### # # # ### ### # # # # ## +# # # # # # # # # # # # # # # # ## +# # # # ##### # ##### ### ####### ####### # # # ## +# # # # # # # # # # # # ## +##### ######### # ######### # # ##### # ### # # ## +# # # # # # # # # # # # ## +# # # ### # ### ##### # ########### # # ### ### ## +# # # # # # # # # # # # # # ## +# ##### # ### # # # # ######### # ### ### # # #### +# # # # # # # # # # # # # ## +### # ######### # # ### # ### # # ##### # ### # ## +# # # # # # # # # # # # # # # ## +# ### # ### # ####### # # # ### # # # # ### ### ## +# # # # # # # # # # # # # # # # ## +# # # ### # ### # # ######### # ##### # # ### # ## +# # # # # # # # # # # # # ## +# ####### ### ####### # # # # ### # ##### ### # ## +# # # # # # # # # # # ## +# ##### ### ####### ##### ### ### ####### # ### ## +# # # # # # # # # # # ## +# ### ######### ####### ### ### ### ### ##### #### +# # # # # # # # # # # # ## +### ### ##### ### # # ### ### # ### # # # # # # ## +# # # # # # # # # # # # # # ## +# ### ### ##### # ### ##### ######### ### # # # ## +# # # # # # # # # # # # ## +# # ### ############### # ### # ### ### # # ### ## +# # # # # # # +################################################E# +################################################## \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/medium_20x20.txt b/krasnovia/lab2/docs/data/mazes/medium_20x20.txt new file mode 100644 index 0000000..d834468 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/medium_20x20.txt @@ -0,0 +1,20 @@ +#################### +#S# # ## +# # ##### # ##### ## +# # # # # ## +# # ### # ### # #### +# # # # # # ## +# ### ####### # # ## +# # # # # # ## +# # # # # # ##### ## +# # # # # ## +# # ####### # ###### +# # # # # ## +# # # ####### # # ## +# # # # # # ## +# ####### # ### # ## +# # # # # ## +##### # ##### # # ## +# # # # +##################E# +#################### \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/no_exit.txt b/krasnovia/lab2/docs/data/mazes/no_exit.txt new file mode 100644 index 0000000..f7d20d8 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/no_exit.txt @@ -0,0 +1,5 @@ +########## +#S # +# ######## +# # +########## \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/open_20x20.txt b/krasnovia/lab2/docs/data/mazes/open_20x20.txt new file mode 100644 index 0000000..10bbaf0 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/open_20x20.txt @@ -0,0 +1,20 @@ +#################### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +#################### \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/small_10x10.txt b/krasnovia/lab2/docs/data/mazes/small_10x10.txt new file mode 100644 index 0000000..5595bf5 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/small_10x10.txt @@ -0,0 +1,10 @@ +########## +#S # +# ###### # +# # # # +# # ## # # +# # ## # # +# # # # +# ###### # +# E# +########## \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/observer.py b/krasnovia/lab2/docs/data/observer.py new file mode 100644 index 0000000..8975cc8 --- /dev/null +++ b/krasnovia/lab2/docs/data/observer.py @@ -0,0 +1,79 @@ +""" +Этап 5.1: Паттерн Observer — уведомления об изменении состояния. + +Зачем Observer? +MazeSolver не знает, кто хочет получать уведомления о событиях. +Он просто оповещает всех подписчиков. ConsoleView можно заменить +на GUI-вью или логгер без изменения MazeSolver. +""" + +from abc import ABC, abstractmethod +from maze_model import Maze, Cell + + +class Observer(ABC): + """Интерфейс наблюдателя.""" + + @abstractmethod + def update(self, event: dict) -> None: + """ + event — словарь с ключом "type": + "maze_loaded" — лабиринт загружен + "path_found" — путь найден + "no_path" — путь не найден + """ + ... + + +class ConsoleView(Observer): + """ + Наблюдатель: выводит лабиринт и путь в консоль. + + Символы: + # — стена + . — проход + S — старт + E — выход + * — найденный путь + @ — текущее положение игрока + """ + + 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) diff --git a/krasnovia/lab2/docs/data/results.csv b/krasnovia/lab2/docs/data/results.csv new file mode 100644 index 0000000..2dabd6f --- /dev/null +++ b/krasnovia/lab2/docs/data/results.csv @@ -0,0 +1,17 @@ +лабиринт,стратегия,время_мс,посещено_клеток,длина_пути +large_50x50,BFS,0.547751,339,275 +large_50x50,DFS,0.47275,285,275 +large_50x50,A*,0.844742,319,275 +large_50x50,Dijkstra,1.007925,339,275 +medium_20x20,BFS,0.255465,163,107 +medium_20x20,DFS,0.214549,107,107 +medium_20x20,A*,0.422447,163,107 +medium_20x20,Dijkstra,0.449911,163,107 +open_20x20,BFS,0.53038,324,35 +open_20x20,DFS,0.341214,171,171 +open_20x20,A*,1.065749,324,35 +open_20x20,Dijkstra,1.128275,324,35 +small_10x10,BFS,0.075246,28,15 +small_10x10,DFS,0.024954,15,15 +small_10x10,A*,0.080671,28,15 +small_10x10,Dijkstra,0.088109,28,15 diff --git a/krasnovia/lab2/docs/data/strategies.py b/krasnovia/lab2/docs/data/strategies.py new file mode 100644 index 0000000..e4fe1ad --- /dev/null +++ b/krasnovia/lab2/docs/data/strategies.py @@ -0,0 +1,175 @@ +""" +Этап 3: Паттерн Strategy — алгоритмы поиска пути. + +Зачем Strategy? +Позволяет менять алгоритм поиска во время выполнения без изменения +остального кода. Добавить новый алгоритм = написать новый класс. +""" + +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]: + """ + Возвращает список клеток от старта до выхода (включительно). + Пустой список — если путь не найден. + Счётчик посещённых клеток сохраняется в self.visited_count. + """ + ... + + # Вспомогательный метод восстановления пути по словарю предшественников + @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 + + +# ── BFS ────────────────────────────────────────────────────────────────────── + +class BFSStrategy(PathFindingStrategy): + """ + Поиск в ширину (BFS). + Гарантирует кратчайший путь по числу шагов. + Использует очередь (deque). + """ + + 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 [] # путь не найден + + +# ── DFS ────────────────────────────────────────────────────────────────────── + +class DFSStrategy(PathFindingStrategy): + """ + Поиск в глубину (DFS). + Быстр, но не гарантирует кратчайший путь. + Использует стек (list). + """ + + 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): + """ + Алгоритм A* с манхэттенской эвристикой. + Компромисс между BFS (оптимальность) и скоростью. + Использует приоритетную очередь (heapq). + """ + + @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]: + # (f_score, уникальный_счётчик, клетка) — счётчик нужен для стабильного сравнения + 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 [] + + +# ── Dijkstra ───────────────────────────────────────────────────────────────── + +class DijkstraStrategy(PathFindingStrategy): + """ + Алгоритм Дейкстры. + В базовом лабиринте (все веса = 1) совпадает с BFS, + но полезен при взвешенных клетках (болото, песок и т.д.). + """ + + 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): + # Вес клетки: можно расширить через cell.weight + 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 [] diff --git a/krasnovia/lab2/docs/report.docx b/krasnovia/lab2/docs/report.docx new file mode 100644 index 0000000000000000000000000000000000000000..066f13cb74f1a8bf6585d3595331e66b315a4a2e GIT binary patch literal 20215 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 literal 0 HcmV?d00001