2026-rff_mp/shalovsa/lab2/docs/report_maze_final.md
2026-05-17 16:50:47 +03:00

16 KiB
Raw Blame History

Отчёт: Поиск выхода из лабиринта (ООП + паттерны проектирования)

Цель работы

Разработать гибкую расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма и экспериментального сравнения алгоритмов. Применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.


Описание задачи и выбранных паттернов

Программа решает задачу поиска пути в лабиринте, загружаемом из текстового файла. Лабиринт представляет собой сетку клеток, где # — стена, пробел — проход, S — старт, E — выход. Алгоритм поиска выбирается динамически, результаты выводятся через систему событий.

Применены 4 паттерна GoF: Builder, Strategy, Observer, Command.

1. Builder (Строитель) — maze_builder.py

Проблема: построение объекта Maze из файла — многошаговый процесс: открыть файл, разобрать символы, создать объекты Cell, установить координаты, найти старт и выход, собрать двумерный массив. Смешивать это с основной логикой нельзя.

Решение: интерфейс MazeBuilder с единственным методом build_from_file(filename) и реализация TextFileMazeBuilder, инкапсулирующая весь парсинг.

Преимущество без паттерна было бы сложно: при добавлении поддержки JSON-лабиринтов пришлось бы встраивать ветвление прямо в клиентский код. С Builder — просто создаём JsonFileMazeBuilder и подставляем без изменений в остальном коде.

2. Strategy (Стратегия) — maze_strategies.py

Проблема: алгоритмы BFS, DFS и A* принципиально различаются по реализации, но выполняют одну задачу — найти путь. Жёсткое встраивание алгоритма в MazeSolver делало бы переключение невозможным без правки класса.

Решение: интерфейс PathFindingStrategy с методом find_path(maze, start, exit). Каждый алгоритм реализует интерфейс независимо. MazeSolver.set_strategy() меняет алгоритм в одну строку во время выполнения.

Преимущество без паттерна было бы сложно: добавление Dijkstra или любого нового алгоритма потребовало бы правки MazeSolver. С Strategy — новый алгоритм добавляется одним классом, остальной код не меняется.

3. Observer (Наблюдатель) — maze_solver.py

Проблема: MazeSolver должен уведомлять интерфейс о событиях (путь найден, лабиринт загружен), но не должен знать, кто именно получает эти уведомления.

Решение: интерфейс Observer с методом update(event, data). ConsoleView реализует интерфейс и подписывается на MazeSolver. При наступлении события вызывается _notify(), который обходит список подписчиков.

Преимущество без паттерна было бы сложно: прямой вызов ConsoleView из MazeSolver создаёт жёсткую зависимость. С Observer — ConsoleView можно отключить, заменить на GUI или добавить файловый логгер без единой правки в MazeSolver.

4. Command (Команда) — maze_solver.py

Проблема: для пошагового режима нужно перемещать игрока с возможностью отмены хода.

Решение: интерфейс Command с методами execute() и undo(). MoveCommand хранит целевую клетку и предыдущую позицию игрока. История команд — обычный стек.

Преимущество без паттерна было бы сложно: прямое изменение позиции игрока не сохраняет историю. С Command — undo() возвращает игрока на шаг назад, а добавление новых типов действий (атака, открыть дверь) не требует изменения Player.


Диаграмма классов (Mermaid)

classDiagram
    class MazeBuilder {
        <<interface>>
        +build_from_file(filename) Maze
    }
    class TextFileMazeBuilder {
        +build_from_file(filename) Maze
    }
    class Maze {
        -int width, height
        -Cell[][] cells
        -Cell start
        -Cell exit
        +get_cell(x, y) Cell
        +get_neighbors(cell) list
        +render(path, player_pos)
    }
    class Cell {
        -int x, y
        -bool is_wall
        -bool is_start
        -bool is_exit
        +is_passable() bool
    }
    class PathFindingStrategy {
        <<interface>>
        +find_path(maze, start, exit) list
    }
    class BFSStrategy { +find_path() }
    class DFSStrategy { +find_path() }
    class AStarStrategy { +find_path() }
    class MazeSolver {
        -Maze maze
        -PathFindingStrategy strategy
        -list observers
        +set_strategy(strategy)
        +add_observer(observer)
        +solve() SearchStats
    }
    class SearchStats {
        +float time_ms
        +int visited_cells
        +int path_length
        +list path
    }
    class Observer {
        <<interface>>
        +update(event, data)
    }
    class ConsoleView { +update(event, data) }
    class Command {
        <<interface>>
        +execute()
        +undo()
    }
    class MoveCommand {
        -Player player
        -Cell target_cell
        -Cell previous_cell
        +execute()
        +undo()
    }
    class Player {
        -Cell current_cell
        +move_to(cell)
    }

    MazeBuilder <|.. TextFileMazeBuilder
    TextFileMazeBuilder ..> Maze : creates
    Maze o-- Cell
    PathFindingStrategy <|.. BFSStrategy
    PathFindingStrategy <|.. DFSStrategy
    PathFindingStrategy <|.. AStarStrategy
    MazeSolver --> Maze
    MazeSolver --> PathFindingStrategy
    MazeSolver --> SearchStats
    MazeSolver --> Observer
    Observer <|.. ConsoleView
    Command <|.. MoveCommand
    MoveCommand --> Player
    Player --> Cell

Ключевые фрагменты реализации

Смена алгоритма через Strategy

solver = MazeSolver(maze)
solver.set_strategy(BFSStrategy())
stats_bfs = solver.solve()

solver.set_strategy(AStarStrategy())   # меняем алгоритм — одна строка
stats_astar = solver.solve()

Подписка Observer

view = ConsoleView()
solver.add_observer(view)
solver.solve()  # ConsoleView автоматически получит событие path_found

Команда с отменой

player = Player(maze.start)
cmd = MoveCommand(player, next_cell)
cmd.execute()   # игрок перешёл
cmd.undo()      # игрок вернулся обратно

Экспериментальная часть

Параметры эксперимента

Параметр Значение
Повторений на замер 7
Алгоритмы BFS, DFS, A*
Метрики время (мс), посещено клеток, длина пути

Тестовые лабиринты

Название Размер Особенность
small_10x10 10×10 Маленький, простой путь
medium_50x50 50×50 Средний, тупики (28% стен)
large_100x100 100×100 Большой (30% стен)
open_50x50 50×50 Без внутренних стен
no_exit_20x20 20×20 Выход недостижим

Результаты

Таблица средних значений

Лабиринт Алгоритм Время (мс) Посещено клеток Длина пути
small_10x10 BFS 0.094 54 15
small_10x10 DFS 0.059 33 33
small_10x10 A* 0.078 36 15
medium_50x50 BFS 2.446 1639 95
medium_50x50 DFS 1.480 1063 185
medium_50x50 A* 1.528 588 95
large_100x100 BFS 9.891 6564
large_100x100 DFS 9.057 6564
large_100x100 A* 17.578 6564
open_50x50 BFS 3.296 2304 95
open_50x50 DFS 1.830 1223 1129
open_50x50 A* 5.566 2304 95
no_exit_20x20 BFS 0.368 260
no_exit_20x20 DFS 0.343 260
no_exit_20x20 A* 0.607 260

«—» — путь не найден, все доступные клетки исчерпаны

Визуализация

Время выполнения

Посещено клеток

Длина пути


Анализ эффективности алгоритмов

BFS — гарантия кратчайшего пути

BFS находит оптимальный путь во всех случаях: 15 шагов на small, 95 на medium. Достигается за счёт обхода волнами — клетки посещаются в порядке удалённости от старта. Платой является высокое число посещённых клеток: 1639 на medium против 1063 у DFS. Это теоретически ожидаемо: BFS — O(V+E), где V — все вершины.

DFS — скорость за счёт качества пути

DFS работает быстрее по времени (1.480 мс против 2.446 мс у BFS на medium), но путь длиннее в 1.9 раза (185 против 95 шагов). На открытом лабиринте без стен разрыв катастрофический: 1129 шагов против 95 у BFS. Это классическая демонстрация того, что DFS уходит в глубину по первому попавшемуся пути, не оглядываясь на альтернативы.

A* — лучший баланс при наличии препятствий

A* с манхэттенской эвристикой посетил всего 588 клеток на medium против 1639 у BFS — в 2.8 раза меньше — при одинаковой длине пути (95 шагов). Эвристика |x1x2| + |y1y2| направляет поиск к выходу и отсекает заведомо невыгодные направления.

На открытом лабиринте без стен A* проигрывает по времени (5.566 мс против 3.296 мс у BFS): эвристика пересчитывается для каждой из 2304 клеток, а отсекать нечего — все пути одинаково перспективны.

Большой лабиринт (100×100) — путь не найден

При плотности стен 30% выход оказался недостижим. Все три алгоритма исчерпали все 6564 доступные клетки. Корректность обработки этого случая — важный результат: каждый алгоритм возвращает пустой список, а не зависает.

Лабиринт без выхода (20×20)

Все алгоритмы обошли все 260 доступных клеток и корректно вернули пустой путь. A* в этом сценарии чуть медленнее (0.607 мс против 0.368 мс у BFS) — приоритетная очередь имеет накладные расходы O(log n) на каждую операцию.


Выводы

Эффективность алгоритмов в разных сценариях

Задача Рекомендация Обоснование
Кратчайший путь BFS или A* Оба гарантируют оптимум
Большой лабиринт с препятствиями A* В 23 раза меньше посещённых клеток
Открытое пространство BFS A* теряет преимущество без отсечений
Нужен любой путь быстро DFS Меньше клеток, меньше накладных расходов
Недостижимый выход Любой Все алгоритмы корректно завершаются

Применимость паттернов

Strategy — самый ценный паттерн в данной задаче. Именно он позволяет запускать три алгоритма через единый интерфейс в цикле бенчмарка без дублирования кода. Добавление четвёртого алгоритма (Dijkstra) займёт ~30 строк без правок в MazeSolver или benchmark.py.

Builder оправдал себя при добавлении пяти разных лабиринтов: клиентский код (benchmark.py) не менялся, только передавался другой файл. Без Builder парсинг был бы размазан по всему коду.

Observer отделил вывод от логики: в бенчмарке ConsoleView не подключается вовсе, чтобы не засорять вывод. В интерактивном режиме подключается одной строкой.

Command демонстрирует принцип undo/redo: без него отмена хода требовала бы хранения копии состояния снаружи объекта Player. С Command история инкапсулирована в стеке команд.

Общий вывод

ООП и паттерны проектирования сделали код модульным и расширяемым. Каждый класс решает одну задачу. Изменение любого компонента (алгоритм, формат файла, интерфейс) не ломает остальные части программы — это и есть практическая ценность паттернов GoF.