276 lines
16 KiB
Markdown
276 lines
16 KiB
Markdown
|
|
# Отчёт: Поиск выхода из лабиринта (ООП + паттерны проектирования)
|
|||
|
|
|
|||
|
|
## Цель работы
|
|||
|
|
|
|||
|
|
Разработать гибкую расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма и экспериментального сравнения алгоритмов. Применить минимум 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)
|
|||
|
|
|
|||
|
|
```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
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
solver = MazeSolver(maze)
|
|||
|
|
solver.set_strategy(BFSStrategy())
|
|||
|
|
stats_bfs = solver.solve()
|
|||
|
|
|
|||
|
|
solver.set_strategy(AStarStrategy()) # меняем алгоритм — одна строка
|
|||
|
|
stats_astar = solver.solve()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Подписка Observer
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
view = ConsoleView()
|
|||
|
|
solver.add_observer(view)
|
|||
|
|
solver.solve() # ConsoleView автоматически получит событие path_found
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Команда с отменой
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
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 шагов). Эвристика `|x1−x2| + |y1−y2|` направляет поиск к выходу и отсекает заведомо невыгодные направления.
|
|||
|
|
|
|||
|
|
На открытом лабиринте без стен 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* | В 2–3 раза меньше посещённых клеток |
|
|||
|
|
| Открытое пространство | BFS | A* теряет преимущество без отсечений |
|
|||
|
|
| Нужен любой путь быстро | DFS | Меньше клеток, меньше накладных расходов |
|
|||
|
|
| Недостижимый выход | Любой | Все алгоритмы корректно завершаются |
|
|||
|
|
|
|||
|
|
### Применимость паттернов
|
|||
|
|
|
|||
|
|
**Strategy** — самый ценный паттерн в данной задаче. Именно он позволяет запускать три алгоритма через единый интерфейс в цикле бенчмарка без дублирования кода. Добавление четвёртого алгоритма (Dijkstra) займёт ~30 строк без правок в `MazeSolver` или `benchmark.py`.
|
|||
|
|
|
|||
|
|
**Builder** оправдал себя при добавлении пяти разных лабиринтов: клиентский код (`benchmark.py`) не менялся, только передавался другой файл. Без Builder парсинг был бы размазан по всему коду.
|
|||
|
|
|
|||
|
|
**Observer** отделил вывод от логики: в бенчмарке `ConsoleView` не подключается вовсе, чтобы не засорять вывод. В интерактивном режиме подключается одной строкой.
|
|||
|
|
|
|||
|
|
**Command** демонстрирует принцип undo/redo: без него отмена хода требовала бы хранения копии состояния снаружи объекта `Player`. С Command история инкапсулирована в стеке команд.
|
|||
|
|
|
|||
|
|
### Общий вывод
|
|||
|
|
|
|||
|
|
ООП и паттерны проектирования сделали код модульным и расширяемым. Каждый класс решает одну задачу. Изменение любого компонента (алгоритм, формат файла, интерфейс) не ломает остальные части программы — это и есть практическая ценность паттернов GoF.
|