15 KiB
Задание 2 — Поиск выхода из лабиринта (ООП + паттерны GoF)
1. Описание задачи
Разработать расширяемую программу поиска пути в лабиринте с возможностью смены алгоритма, визуализации и пошагового управления. Применить минимум 3 паттерна GoF.
2. Применённые паттерны GoF
2.1 Builder (Строитель) — builder.py
Проблема: Создание Maze — многошаговый процесс: чтение файла → парсинг символов → валидация (есть ли S и E) → расстановка координат. Если делать всё в конструкторе Maze — нарушение Single Responsibility.
Решение: Интерфейс MazeBuilder с методом build_from_file(). Реализации:
TextFileMazeBuilder— текстовый формат (#, пробел,S,E,W,N)JsonMazeBuilder— JSON-формат (для демонстрации расширяемости)
Преимущество: Добавление нового формата (бинарный, XML) не требует изменения Maze, MazeSolver или стратегий.
2.2 Strategy (Стратегия) — strategy.py
Проблема: Алгоритмы поиска (BFS, DFS, A*, Dijkstra) взаимозаменяемы по интерфейсу, но различаются реализацией. Жёсткая связь с конкретным алгоритмом нарушает Open/Closed Principle.
Решение: Интерфейс PathFindingStrategy с методом find_path(maze, start, exit_). MazeSolver хранит ссылку на стратегию и вызывает strategy.find_path() — не зная, какой именно алгоритм работает. Метод set_strategy() меняет алгоритм в рантайме.
Преимущество: Новый алгоритм (IDA*, Bidirectional BFS) — это один новый класс без изменения остального кода.
2.3 Observer (Наблюдатель) — solver.py
Проблема: MazeSolver не должен знать о способе отображения. Консоль, GUI, файловый лог — разные задачи.
Решение: MazeSolver наследует Subject (хранит список _observers, метод notify()). При событиях (maze_loaded, path_found, no_path, player_moved) уведомляет всех наблюдателей. Реализации: ConsoleView, FileLogObserver.
Преимущество: GUI-интерфейс добавляется как новый Observer без изменения MazeSolver.
2.4 Command (Команда) — solver.py
Проблема: Пошаговое перемещение игрока должно поддерживать отмену (undo).
Решение: Интерфейс Command с методами execute() / undo(). MoveCommand хранит предыдущую позицию. CommandHistory — стек команд. Undo — pop() + cmd.undo().
Преимущество: Любое новое действие (телепортация, открытие двери) реализуется как отдельная команда, не меняя Player или MazeSolver.
3. Диаграмма классов (Mermaid)
classDiagram
%% ── Model ──────────────────────────────────
class Cell {
+int x, y
+str symbol
+bool is_wall
+bool is_start
+bool is_exit
+int weight
+is_passable() bool
}
class Maze {
+int width, height
+Cell start, exit
+get_cell(x,y) Cell
+get_neighbors(cell) list
+render(path, visited, player) str
}
Maze "1" *-- "many" Cell
%% ── Builder ─────────────────────────────────
class MazeBuilder {
<<interface>>
+build_from_file(filename) Maze
+build_empty(w, h) Maze
}
class TextFileMazeBuilder {
+build_from_file(filename) Maze
+build_from_string(text) Maze
+build_empty(w, h) Maze
}
class JsonMazeBuilder {
+build_from_file(filename) Maze
+build_empty(w, h) Maze
}
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder <|.. JsonMazeBuilder
TextFileMazeBuilder ..> Maze : creates
JsonMazeBuilder ..> Maze : creates
%% ── Strategy ────────────────────────────────
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit) tuple
+name() str
}
class BFSStrategy { +find_path() tuple }
class DFSStrategy { +find_path() tuple }
class AStarStrategy { +find_path() tuple }
class DijkstraStrategy { +find_path() tuple }
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
%% ── Observer ────────────────────────────────
class Observer {
<<interface>>
+update(event, payload)
}
class Subject {
-observers list
+add_observer(obs)
+notify(event, payload)
}
class ConsoleView { +update(event, payload) }
class FileLogObserver { +update(event, payload) }
Observer <|.. ConsoleView
Observer <|.. FileLogObserver
Subject "1" o-- "many" Observer
%% ── MazeSolver ──────────────────────────────
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+set_strategy(strategy)
+load_maze(maze)
+solve() tuple
}
MazeSolver --|> Subject
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
%% ── Command ─────────────────────────────────
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Cell target, previous
+execute()
+undo()
}
class CommandHistory {
-stack list
+execute(cmd)
+undo()
}
class Player { +Cell position }
Command <|.. MoveCommand
CommandHistory o-- Command
MoveCommand --> Player
4. Структура файлов
maze/
├── model.py — Cell, Maze
├── builder.py — MazeBuilder, TextFileMazeBuilder, JsonMazeBuilder
├── strategy.py — PathFindingStrategy, BFS, DFS, A*, Dijkstra
├── solver.py — Observer, Subject, ConsoleView, MazeSolver, Command, Player
├── generator.py — Генератор лабиринтов (DFS-backtracker)
├── experiment.py — Экспериментальная часть, запись CSV
├── main.py — Интерактивная демонстрация
└── docs/
└── data/
├── maze_results.csv
├── maze_benchmark.png
├── path_lengths.png
└── mazes/ — текстовые файлы лабиринтов
5. Результаты экспериментов
5.1 Таблица результатов (N = 7 повторов, среднее)
| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути |
|---|---|---|---|---|
| Маленький 11×11 | BFS | 0.077 | 37 | 29 |
| Маленький 11×11 | DFS | 0.059 | 29 | 29 |
| Маленький 11×11 | A* | 0.111 | 29 | 29 |
| Маленький 11×11 | Dijkstra | 0.115 | 37 | 29 |
| Средний 51×51 | BFS | 2.228 | 755 | 405 |
| Средний 51×51 | DFS | 0.935 | 423 | 405 |
| Средний 51×51 | A* | 2.343 | 663 | 405 |
| Средний 51×51 | Dijkstra | 2.528 | 755 | 405 |
| Большой 101×101 | BFS | 3.299 | 1533 | 993 |
| Большой 101×101 | DFS | 2.289 | 1061 | 993 |
| Большой 101×101 | A* | 5.948 | 1515 | 993 |
| Большой 101×101 | Dijkstra | 5.137 | 1533 | 993 |
| Пустой 52×52 | BFS | 5.859 | 2500 | 99 |
| Пустой 52×52 | DFS | 3.518 | 1275 | 1275 |
| Пустой 52×52 | A* | 10.803 | 2500 | 99 |
| Пустой 52×52 | Dijkstra | 10.227 | 2500 | 99 |
| Без выхода 21×21 | BFS | 0.213 | 105 | 0 |
| Без выхода 21×21 | DFS | 0.220 | 105 | 0 |
| Без выхода 21×21 | A* | 0.361 | 105 | 0 |
| Без выхода 21×21 | Dijkstra | 0.336 | 105 | 0 |
| Взвешенный 51×51 | BFS | 0.722 | 335 | 221 |
| Взвешенный 51×51 | DFS | 0.518 | 221 | 221 |
| Взвешенный 51×51 | A* | 0.970 | 278 | 221 |
| Взвешенный 51×51 | Dijkstra | 1.091 | 337 | 221 |
5.2 Графики
6. Анализ алгоритмов
BFS (поиск в ширину)
- Гарантирует кратчайший путь по количеству шагов.
- Посещает все клетки на расстоянии k до нахождения выхода на расстоянии k+1.
- На пустом лабиринте (максимум свободного пространства) посещает все 2500 клеток — это худший сценарий по памяти (хранит всю «волну»).
- Практически эквивалентен Dijkstra для равновесных лабиринтов.
DFS (поиск в глубину)
- Самый быстрый по времени на лабиринтах с длинными коридорами.
- На правильно построенном лабиринте (DFS-backtracker) коридоры длинные → DFS «угадывает» направление и находит путь, посетив меньше клеток.
- На пустом поле даёт огромный зигзагообразный путь (1275 клеток вместо оптимального 99) — классическая проблема DFS.
- Не гарантирует оптимальность, зато экономит память (O(глубина) вместо O(ширина)).
A* (с манхэттенской эвристикой)
- На лабиринтах с коридорами посещает меньше клеток, чем BFS, но при этом тоже гарантирует оптимальный путь.
- На пустом поле ведёт себя хуже BFS по времени из-за накладных расходов на приоритетную очередь (heapq).
- Манхэттенская эвристика работает хуже на лабиринтах с длинными обходами препятствий: оценка расстояния «по прямой» слишком оптимистична.
- Выигрывает когда карта большая и путь очевиден по направлению (прямые коридоры к выходу).
Dijkstra
- На равновесных лабиринтах идентичен BFS, но медленнее из-за приоритетной очереди.
- Незаменим на взвешенных картах: учитывает клетки с весом 2 (песок) и 3 (болото) и находит минимальный суммарный вес пути, а не минимальное число шагов.
- На взвешенном лабиринте Dijkstra и A* могут выдать более короткий (по весу) путь, чем BFS, при одинаковой длине в клетках.
Без выхода
- Все алгоритмы обошли все 105 достижимых клеток и вернули пустой путь — корректная обработка.
- Время идентично для всех алгоритмов (доминирует полный обход, а не структура поиска).
7. Выводы: ООП и паттерны
Что дали паттерны
Builder отделил детали парсинга от модели. Без него клиент сам читал бы файл и вручную собирал Maze — хрупкий, нерасширяемый код. С Builder: добавление JSON-формата заняло 10 строк.
Strategy позволил сравнить 4 алгоритма, не меняя MazeSolver. Переключение алгоритма — одна строка solver.set_strategy(new_algo). Без паттерна потребовался бы if/elif на 4 ветки в каждом методе.
Observer полностью развязал логику поиска и отображение. MazeSolver не знает, куда идут уведомления. Добавление FileLogObserver — новый класс из 5 строк, нулевые изменения в MazeSolver.
Command дал возможность реализовать undo за счёт хранения предыдущего состояния в объекте команды. Без паттерна — ручное сохранение/восстановление переменных.
Что было бы сложно без ООП и паттернов
| Задача | Без паттернов | С паттернами |
|---|---|---|
| Добавить новый алгоритм | Правка MazeSolver + if/elif | Новый класс, наследующий интерфейс |
| Добавить формат файла | Правка загрузчика | Новый Builder |
| Добавить GUI | Вставка GUI-кода в логику поиска | Новый Observer |
| Отмена шага игрока | Ручное сохранение переменной | Command.undo() |
Рекомендации по выбору алгоритма
| Задача | Рекомендация |
|---|---|
| Гарантированно кратчайший путь, равные веса | BFS |
| Быстро найти какой-нибудь путь (поиск с препятствиями) | DFS |
| Большая карта, путь по направлению к цели | A* |
| Взвешенная карта (болото, песок, дорога) | Dijkstra или A* с весовой эвристикой |
| Нужна гарантия оптимальности на взвешенном графе | Dijkstra |

