280 lines
15 KiB
Markdown
280 lines
15 KiB
Markdown
# Задание 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)
|
||
|
||
```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** |
|