`файл является компиляцией документации sphinx в папке docs`
# Лабораторная работа «Поиск выхода из лабиринта»
* [Задание](task.md)
* [Цель работы](task.md#id2)
* [Общая схема приложения (пример)](task.md#id3)
* [Выполнение](task.md#id4)
* [Советы](task.md#id8)
* [Этап 1. Модель лабиринта](stage1.md)
* [Класс `Cell`](stage1.md#cell)
* [Класс `Maze`](stage1.md#maze)
* [Этап 2. Загрузка лабиринта из файла](stage2.md)
* [Паттерн Builder](stage2.md#builder)
* [Класс `MazeBuilder`](stage2.md#mazebuilder)
* [Класс `TextFileBuilder`](stage2.md#textfilebuilder)
* [Использование](stage2.md#id2)
* [Известная ошибка](stage2.md#id3)
* [Этап 3. Стратегии поиска пути](stage3.md)
* [Паттерн Strategy](stage3.md#strategy)
* [Структура пакета](stage3.md#id2)
* [Класс `PathFindingStrategy`](stage3.md#pathfindingstrategy)
* [Алгоритмы](stage3.md#id3)
* [Этап 4. Класс-оркестратор MazeSolver](stage4.md)
* [Роль в архитектуре](stage4.md#id1)
* [Класс `SearchStats`](stage4.md#searchstats)
* [Класс `MazeSolver`](stage4.md#id3)
* [Этап 5. Визуализация и пошаговое управление](stage5.md)
* [5.1. Паттерн Observer](stage5.md#observer)
* [5.2. Паттерн Command](stage5.md#command)
* [Этап 6. Экспериментальная часть](stage6.md)
* [Подготовка](stage6.md#id2)
* [Замеры](stage6.md#id3)
* [Результаты](stage6.md#id4)
* [Выводы](stage6.md#id8)
* [Визуализация](stage6.md#id9)
* [Этап 7. Отчёт](stage7.md)
* [Описание задачи](stage7.md#id2)
* [Диаграмма классов](stage7.md#id3)
* [Результаты экспериментов](stage7.md#id4)
* [Выводы](stage7.md#id5)
* [API Reference](api.md)
* [Базовые модели](api.md#module-source.models.base)
* [Загрузка лабиринта](api.md#module-source.build.builder)
* [Стратегии поиска пути](api.md#module-source.strategy.algorithms)
* [Оркестратор](api.md#module-source.strategy.solver)
* [Визуализация](api.md#module-source.view.observer)
* [Управление игроком](api.md#module-source.view.command)
# Этап 2. Загрузка лабиринта из файла
Во втором этапе разработки необходимо реализовать загрузку лабиринта из текстового файла, где: `#` – стена, ` ` – проход, `S` – старт, `E` – выход.
## Систематизация файлов
Для удобного хранения лабиринтов было решено сделать систему наименования текстовых файлов в папке `source/templates`.
Общая структура:
```default
{размер}_{свойство 1}-{свойство 2}-{свойство n}_{версия}.txt
```
### Размер
Формат: `{ширина}x{высота}`
| Пример | Значение |
|-----------|----------------|
| `10x10` | 10×10 клеток |
| `50x50` | 50×50 клеток |
| `100x100` | 100×100 клеток |
| `30x30` | 30×30 клеток |
| `20x20` | 20×20 клеток |
### Свойства
| Свойство | Код | Описание |
|--------------|-------------|----------------------------------------------------------------------------------------------|
| Простой путь | `path` | Существует маршрут от S до E |
| Тупики | `deadends` | Лабиринт специально содержит тупики (могут быть и в других типах, но здесь — гарантированно) |
| Запутанный | `spaghetti` | Сложная структура с циклами и ложными ходами |
| Пустой | `empty` | Нет стен (`#`), только пробелы, S и E |
| Без выхода | `noexit` | В лабиринте отсутствует символ `E` |
### Версия
Формат: `v{номер}`
- `v1`, `v2`, `v10`
### Примеры
#### Маленькие (10×10, простой путь)
```default
10x10_path_v1.txt
10x10_path_v2.txt
...
10x10_path_v10.txt
```
#### Средние (50×50, тупики)
```default
50x50_deadends_v1.txt
50x50_deadends_v2.txt
...
50x50_deadends_v10.txt
```
#### Большие (100×100, запутанные)
```default
100x100_spaghetti_v1.txt
100x100_spaghetti_v2.txt
...
100x100_spaghetti_v10.txt
```
#### Пустые (30×30)
```default
30x30_empty_v1.txt
30x30_empty_v2.txt
...
30x30_empty_v10.txt
```
#### Без выхода (20×20)
```default
20x20_noexit_v1.txt
20x20_noexit_v2.txt
...
20x20_noexit_v10.txt
```
#### Комбинированные свойства
```default
50x50_deadends-noexit_v1.txt
100x100_spaghetti-noexit_v1.txt
10x10_path-empty_v1.txt (избыточно, но допустимо)
```
### Примечание
- Регистр имён файлов: **нижний регистр**
- Разделители: только `_` и `-`
- Расширение: `.txt`
- Кодировка: UTF-8
# Этап 1. Модель лабиринта
В первом этапе разработки необходимо создать базовые классы `Cell` и `Maze`, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы.
## Класс `Cell`
Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая.
По условию задания клетка должна иметь флаги `isWall`, `isStart`, `isExit` и метод `isPassable()`. В реализации флаги оформлены как **свойства** (`@property`) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа.
```python
cell = Cell(1, 1)
cell.is_wall = True
```
Типы клетки взаимоисключают друг друга — клетка не может быть одновременно стеной и стартом. Логика сброса вынесена в приватный метод `_clear_flags()`.
### Символьное представление
Для вывода лабиринта в консоль каждая клетка возвращает символ через `__str__`. Символы берутся из `cell_mapping` в `source/settings.py`, что позволяет менять отображение без правки классов:
| Тип | Символ по умолчанию |
|--------|-----------------------|
| Стена | `#` |
| Старт | `S` |
| Выход | `E` |
| Пустая | |
## Класс `Maze`
Лабиринт хранит двумерный список клеток и предоставляет методы для работы с ними.
По условию задания требовались методы `getCell(x, y)` и `getNeighbors(cell)`. В реализации добавлено несколько вещей сверх задания:
### Именование методов
Задание написано в стиле Java/pseudocode — названия методов и полей используют `camelCase` (`isWall`, `getCell`, `isPassable`). В Python принят другой стандарт именования — **PEP 8**, который предписывает `snake_case` для методов и атрибутов. Поэтому все названия были приведены к Python стилю:
| Задание | Реализация |
|---------------------------|-----------------------------|
| `isWall` | `is_wall` |
| `isStart` | `is_start` |
| `isExit` | `is_exit` |
| `isPassable()` | `is_possible()` |
| `getCell(x, y)` | `get_cell(x, y)` |
| `getNeighbors(cell)` | `get_neighbors(x, y)` |
| `buildFromFile(filename)` | `build_from_file(filename)` |
Это соответствует стандарту оформления кода на Python и делает API классов идиоматичным для языка.
### Индексация `maze[row, col]`
Вместо явного вызова `get_cell()` реализованы `__getitem__` и `__setitem__`, что позволяет обращаться к клеткам естественным образом:
```python
maze[0, 0] = cell_mapping['wall'] # установить стену
cell = maze[2, 3] # получить клетку
```
Обратите внимание: индексация идёт в формате `[row, col]`, то есть сначала строка (Y), потом столбец (X) — аналогично numpy.
### Свойства `start` и `exit`
Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода:
```python
maze.start # Cell или None
maze.exit # Cell или None
```
Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают `start` и `exit` автоматически из лабиринта.
### Свойство `shape`
По аналогии с numpy добавлено свойство `shape`, возвращающее `(height, width)`:
```python
rows, cols = maze.shape
```
Используется в стратегиях поиска и тестах для итерации по лабиринту.
### `get_neighbors`
Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает `None`.
```python
neighbors = maze.get_neighbors(2, 2) # список Cell
```
Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS).
# Этап 2. Загрузка лабиринта из файла
Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна **Builder**.
## Паттерн Builder
Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод `build_from_file()`, внутри которого сосредоточена вся логика построения.
Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию `MazeBuilder` без изменения остального кода.
## Класс `MazeBuilder`
Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод `build_from_file()`, который обязан реализовать каждый конкретный строитель.
По условию задания интерфейс назывался `MazeBuilder` с методом `buildFromFile`. В реализации название метода приведено к **PEP 8** — `build_from_file`. Сам класс оформлен через `ABC` — попытка создать объект `MazeBuilder()` напрямую вызовет `TypeError`.
## Класс `TextFileBuilder`
Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из `.txt` файла где `#` — стена, — проход, `S` — старт, `E` — выход.
Процесс построения разбит на три приватных шага:
### `_read_file`
Читает файл построчно и обрезает символы переноса строки `\n` и `\r`. Возвращает список строк — каждая строка соответствует одной строке лабиринта.
### `_test_text_maze`
Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и `_create_maze` выбросит `ValueError`.
Реализован как `@staticmethod` — не использует состояние объекта, только входные данные.
### `_create_maze`
Создаёт объект `Maze` нужного размера и заполняет его клетки символами из файла через `maze[y, x] = symbol`. Тип каждой клетки определяется автоматически через `cell_mapping` в `__setitem__` лабиринта.
## Использование
```python
from source.build.builder import TextFileBuilder
maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt')
print(maze)
```
## Известная ошибка
В текущей реализации `_create_maze` есть опечатка при вычислении `width`:
```python
height, width = len(text_maze), len(text_maze) # width всегда равен height
```
Правильная версия:
```python
height, width = len(text_maze), len(text_maze[0])
```
На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат.
# Этап 3. Стратегии поиска пути
В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**.
## Паттерн Strategy
Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии:
```python
solver = MazeSolver(maze, BFSStrategy())
solver.set_strategy(AStarStrategy())
```
Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно.
## Структура пакета
Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт:
```default
source/strategy/
├── __init__.py ← единственный импорт для пользователя
├── algorithms.py ← базовый класс PathFindingStrategy
├── bfs.py
├── dfs.py
└── astar.py
```
```python
from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy
```
## Класс `PathFindingStrategy`
Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод `find_path()` и содержит два вспомогательных метода, общих для всех стратегий.
По условию задания метод назывался `findPath` — приведён к **PEP 8** как `find_path`.
### `_validate`
Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа `noexit`: алгоритм падал с `AttributeError` внутри, вместо понятного сообщения.
`_validate` подставляет `start` и `exit` из лабиринта если они не переданы явно, и выбрасывает `ValueError` с понятным сообщением если клетки не найдены:
```python
start, exit = self._validate(maze, start, exit)
```
Вынесен в базовый класс чтобы не дублировать в каждом алгоритме.
### `_reconstruct_path`
Восстанавливает путь по словарю предков `came_from`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса.
Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список:
```default
exit → D → C → B → start (идём по came_from)
start → B → C → D → exit (после reverse)
```
## Алгоритмы
### BFS — `BFSStrategy`
Поиск в ширину. Использует `deque` как очередь (FIFO) — каждый раз берём самую старую клетку из начала. Это гарантирует послойный обход и кратчайший путь по количеству шагов.
Сложность: O(V + E) по времени и памяти.
### DFS — `DFSStrategy`
Поиск в глубину. Использует `list` как стек (LIFO) — каждый раз берём самую свежую клетку с конца. Алгоритм ныряет вглубь по одному направлению до тупика, затем возвращается.
Не гарантирует кратчайший путь. На запутанных лабиринтах может обойти почти все клетки прежде чем найти выход, хотя по времени часто быстрее BFS из-за меньших накладных расходов на структуру данных.
Сложность: O(V + E) по времени и памяти.
### A\* — `AStarStrategy`
Использует `heapq` как приоритетную очередь. На каждом шаге выбирает клетку с минимальным значением `f = g + h`, где `g` — стоимость пути от старта, `h` — манхэттенская эвристика до выхода.
Эвристика направляет поиск в сторону выхода, поэтому A\* обходит меньше клеток чем BFS при том же гарантированно кратчайшем пути.
В кортеж приоритетной очереди добавлен счётчик `counter` как tie-breaker — без него `heapq` попытался бы сравнивать объекты `Cell` при одинаковом `f`, что вызвало бы `TypeError`:
```python
heapq.heappush(open_heap, (f, counter, neighbor))
```
Сложность: O(E · log V) в худшем случае.
# Этап 4. Класс-оркестратор MazeSolver
В четвёртом этапе реализован класс `MazeSolver`, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику.
## Роль в архитектуре
`MazeSolver` — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время:
```python
solver = MazeSolver(maze, BFSStrategy())
stats = solver.solve()
print(stats)
# Время: 0.041 мс | Посещено клеток: 13 | Длина пути: 13
```
## Класс `SearchStats`
Оформлен через `@dataclass` — это избавляет от ручного `__init__` и автоматически даёт `__repr__`. Хранит четыре поля: время выполнения, количество посещённых клеток, длину пути и сам путь как список клеток.
`__str__` переопределён для удобного вывода в консоль и отчётах.
### Ограничение
В текущей реализации `visited_count` и `path_length` всегда равны друг другу — оба вычисляются как `len(path)`. Это потому что стратегии возвращают только финальный путь, а не все посещённые клетки. Чтобы получить точное количество посещений, потребовалось бы дорабатывать каждую стратегию — добавлять счётчик внутри `find_path`. На данном этапе это сознательное упрощение.
## Класс `MazeSolver`
### `set_strategy`
Позволяет менять алгоритм без пересоздания солвера. Это и есть основная демонстрация паттерна Strategy в действии — один объект, разные алгоритмы:
```python
solver = MazeSolver(maze, BFSStrategy())
stats_bfs = solver.solve()
solver.set_strategy(AStarStrategy())
stats_astar = solver.solve()
```
### `solve`
Замеряет время через `time.perf_counter()` — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000.
`start` и `exit` можно не передавать — стратегия найдёт их сама через `_validate`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки.
# Этап 5. Визуализация и пошаговое управление
В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком.
## 5.1. Паттерн Observer
### Идея
`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода.
### Класс `Event`
Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий:
| Тип | Когда генерируется |
|---------------|----------------------------|
| `maze_loaded` | Лабиринт загружен из файла |
| `path_found` | Алгоритм нашёл путь |
| `no_path` | Путь не найден |
| `move` | Игрок сделал ход |
### Класс `Observer`
Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать.
### Класс `ConsoleView`
Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта.
Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе:
```python
path_set = set(path) if path else set()
```
Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики:
```python
PLAYER_SYMBOL = "P"
PATH_SYMBOL = "·"
```
## 5.2. Паттерн Command
### Идея
Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной.
### Класс `Player`
Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке.
### Класс `Command`
Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю.
### Класс `MoveCommand`
Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`.
Направления вынесены в словарь `DIRECTIONS` на уровне модуля:
```python
DIRECTIONS = {
"w": (0, -1), # вверх
"s": (0, 1), # вниз
"a": (-1, 0), # влево
"d": (1, 0), # вправо
}
```
### Класс `CommandHistory`
Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`.
Пример игрового цикла:
```python
cmd = MoveCommand(player, 'd', maze)
if cmd.execute():
history.push(cmd) # добавляем только успешный ход
history.undo() # отмена последнего хода
```
# Этап 6. Экспериментальная часть
В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (`practice/main.ipynb`).
## Подготовка
Лабиринты загружаются из папки `source/templates` автоматически — все файлы считываются через `os.listdir` и передаются в `TextFileBuilder`. Стратегии собраны в словарь для удобной итерации:
```python
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy(),
}
```
## Замеры
Каждая пара лабиринт + стратегия запускается **10 раз**, результаты усредняются. Это сглаживает разброс из-за кэширования и фоновой активности системы.
Лабиринты типа `noexit` пропускаются автоматически — стратегия выбрасывает `ValueError`, который перехватывается через `try/except`, и выполнение продолжается.
Результаты собираются в список словарей и затем преобразуются в `DataFrame` через pandas.
## Результаты
### 10×10 (простой путь)
На маленьких лабиринтах все три алгоритма показывают практически одинаковое время (~0.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A\* давала преимущество.
### 50×50 (тупики)
BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A\* показывает время между BFS и DFS.
### 100×100 (запутанный, spaghetti)
Наиболее показательные результаты:
| Стратегия | Время (мс) | Длина пути |
|-------------|--------------|--------------|
| BFS | ~9 | ~210 |
| DFS | ~7 | ~2200 |
| A\* | ~8 | ~210 |
DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A\* находят кратчайший путь, A\* при этом чуть быстрее за счёт эвристики.
### 30×30 (пустой)
Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A\*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных.
A\* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет.
## Выводы
- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо.
- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути.
- **A**\* — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь.
## Визуализация
![[results.png]]
# Этап 7. Отчёт
## Описание задачи
Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF:
| Паттерн | Где применён |
|--------------|-----------------------------------------------|
| **Builder** | `TextFileBuilder` |
| **Strategy** | `BFSStrategy`, `DFSStrategy`, `AStarStrategy` |
| **Observer** | `ConsoleView` |
| **Command** | `MoveCommand`, `CommandHistory` |
## Диаграмма классов
## Результаты экспериментов
| Лабиринт | Быстрее всех | Кратчайший путь |
|-------------------|----------------|-------------------|
| 10×10 path | все одинаково | все одинаково |
| 50×50 deadends | BFS | BFS = A\* |
| 100×100 spaghetti | DFS | BFS = A\* |
| 30×30 empty | DFS | BFS = A\* |
- **BFS** — надёжный выбор, всегда кратчайший путь.
- **DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток.
- **A\*** — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`.
## Выводы
Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода.
# Задание
## Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
## Общая схема приложения (пример)
## Выполнение
### Этап 1. Модель лабиринта (без паттернов, просто классы)
**Задача:** Создать классы `Cell` и `Maze`, которые представляют карту лабиринта.
- `Cell` хранит координаты (x, y), флаги `isWall`, `isStart`, `isExit`, метод `isPassable()` (возвращает `True` для прохода, если не стена).
- `Maze` хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: `getCell(x, y)`, `getNeighbors(cell)` – возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена).
**Результат:** Лабиринт можно создать вручную в коде, но загрузку пока не делаем.
### Этап 2. Загрузка лабиринта из файла – применение паттерна **Builder**
**Задача:** Реализовать загрузку лабиринта из текстового файла, где `#` – стена, ` ` (пробел) – проход, `S` – старт, `E` – выход.
- Создать интерфейс `MazeBuilder` с методом `buildFromFile(filename)`.
- Реализовать класс `TextFileMazeBuilder`, который читает файл, парсит символы, создаёт объекты `Cell`, задаёт координаты и флаги, после чего возвращает готовый `Maze`.
Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию `MazeBuilder`.
### Этап 3. Стратегии поиска пути – паттерн **Strategy**
**Задача:** Реализовать семейство алгоритмов поиска пути от старта до выхода.
- Создать интерфейс `PathFindingStrategy` с методом `findPath(maze, start, exit)`, возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет.
- Реализовать минимум 3 стратегии:
- **BFS** (поиск в ширину) – гарантирует кратчайший путь по количеству шагов.
- **DFS** (поиск в глубину) – быстрый, но не обязательно кратчайший.
- **A**\* (с эвристикой, например, манхэттенское расстояние) – компромисс между скоростью и оптимальностью.
- (Опционально) **Дейкстра** – полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS.
Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A\* – приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток.
Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс.
### Этап 4. Класс-оркестратор – **MazeSolver** (использует Strategy)
**Задача:** Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику.
- `MazeSolver` содержит поля `maze` и `strategy`.
- Метод `setStrategy(strategy)` для динамической смены алгоритма.
- Метод `solve()` вызывает `strategy.findPath(...)` и возвращает объект `SearchStats` (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути).
- Для замера времени используйте `time.perf_counter()` до и после вызова стратегии.
### Этап 5. Визуализация и пошаговое управление – паттерны **Observer** и **Command** (по желанию)
**5.1. Наблюдатель (Observer)** – обновление консольного интерфейса.
- Создать интерфейс `Observer` с методом `update(event)`, где `event` может быть строкой или объектом с типом события (`"path_found"`, `"move"`, `"maze_loaded"`).
- Реализовать класс `ConsoleView`, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод `render(maze, player_position, path)` рисует карту в консоли.
- `MazeSolver` (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния.
**5.2. Команда (Command)** – для пошагового перемещения игрока по найденному пути (или ручного управления).
- Создать интерфейс `Command` с методами `execute()` и `undo()`.
- Реализовать `MoveCommand`, который принимает игрока (`Player`), направление и изменяет его позицию, сохраняя предыдущую для отмены.
- Создать класс `Player`, хранящий текущую клетку.
- Консольное меню позволяет вводить команды (W/A/S/D), выполнять `MoveCommand`, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн.
*Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command – для демонстрации undo при ручном исследовании лабиринта.*
### Этап 6. Экспериментальная часть (аналогично заданию со структурами данных)
**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности.
1. **Подготовка тестовых лабиринтов:**
- Маленький (10×10) с простым путём.
- Средний (50×50) с тупиками.
- Большой (100×100) с запутанной структурой.
- «Пустой» лабиринт (без стен) – для демонстрации максимальной производительности.
- «Без выхода» – чтобы проверить обработку отсутствия пути.
2. **Замеры:**
- Для каждого лабиринта и каждой стратегии запустить `solve()` 5–10 раз, усреднить время, количество посещённых клеток, длину пути.
- Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`.
3. **Анализ:**
- Построить графики для каждого лабиринта.
- Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A\* на взвешенном графе.
### Этап 7. Отчёт
**Структура отчёта:**
1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
3. Результаты экспериментов (таблицы, графики).
4. Анализ эффективности алгоритмов и применимости паттернов.
5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.
## Советы
- Для A\* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`.
- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь.
- Для BFS/DFS используй `deque` (очередь) и `list` (стек).
- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки.
# API Reference
## Базовые модели
### *class* source.models.base.Cell(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False)
Базовые классы: `object`
Представляет одну клетку поля лабиринта.
Каждая клетка хранит свои координаты и один из четырёх возможных
типов: стена, старт, выход или пустая клетка. Типы взаимоисключают
друг друга: установка одного автоматически сбрасывает остальные.
#### \_\_init_\_(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False)
Инициализирует клетку с заданными координатами и типом.
* **Параметры:**
* **x** – Координата клетки по оси X.
* **y** – Координата клетки по оси Y.
* **is_wall** – Если True — клетка является стеной.
* **is_start** – Если True — клетка является стартом.
* **is_exit** – Если True — клетка является выходом.
#### *property* is_exit *: bool*
True, если клетка является выходом из лабиринта.
#### is_possible() → bool
Проверяет, можно ли переместиться в эту клетку.
* **Результат:**
True, если клетка не является стеной, иначе False.
#### *property* is_start *: bool*
True, если клетка является стартовой позицией.
#### *property* is_wall *: bool*
True, если клетка является стеной.
### *class* source.models.base.Maze(size: tuple[int, int] = (10, 10))
Базовые классы: `object`
Представляет двумерный лабиринт из клеток Cell.
Лабиринт хранится как список списков клеток. Доступ к отдельным
клеткам и их изменение возможны через индексацию вида maze[row, col].
#### \_\_init_\_(size: tuple[int, int] = (10, 10))
Создаёт пустой лабиринт заданного размера.
* **Параметры:**
**size** – Кортеж (width, height) — ширина и высота лабиринта в клетках.
#### *property* exit *: [Cell](#source.models.base.Cell) | None*
#### get_cell(x: int, y: int) → [Cell](#source.models.base.Cell) | None
Возвращает клетку по координатам или None, если координаты вне границ.
* **Параметры:**
* **x** – Координата по оси X.
* **y** – Координата по оси Y.
* **Результат:**
Объект Cell, если координаты корректны, иначе None.
#### get_neighbors(x: int, y: int) → list[[Cell](#source.models.base.Cell)] | None
Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево).
* **Параметры:**
* **x** – Координата клетки по оси X.
* **y** – Координата клетки по оси Y.
* **Результат:**
Список проходимых соседних клеток, или None если (x, y) вне границ.
#### *property* shape *: tuple[int, int]*
#### *property* start *: [Cell](#source.models.base.Cell) | None*
## Загрузка лабиринта
### *class* source.build.builder.MazeBuilder
Базовые классы: `ABC`
#### *abstractmethod* build_from_file(filename: str) → [Maze](#source.models.base.Maze)
Возвращает объект лабиринта по указанному пути файлу.
* **Параметры:**
**filename** (*str*) – Путь к файлу
* **Исключение:**
**TypeError** – Если введен путь файла с нерассмотренным расширением
* **Результат:**
Объект лабиринта
* **Тип результата:**
[Maze](#source.models.base.Maze)
### *class* source.build.builder.TextFileBuilder
Базовые классы: [`MazeBuilder`](#source.build.builder.MazeBuilder)
#### build_from_file(filename: str) → [Maze](#source.models.base.Maze)
Возвращает объект лабиринта по указанному пути файлу.
* **Параметры:**
**filename** (*str*) – Путь к файлу
* **Исключение:**
**TypeError** – Если введен путь файла с нерассмотренным расширением
* **Результат:**
Объект лабиринта
* **Тип результата:**
[Maze](#source.models.base.Maze)
## Стратегии поиска пути
### *class* source.strategy.algorithms.PathFindingStrategy
Базовые классы: `ABC`
Интерфейс стратегии поиска пути в лабиринте.
#### *abstractmethod* find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → list[[Cell](#source.models.base.Cell)]
Найти путь от start до exit.
* **Параметры:**
* **maze** – Объект лабиринта.
* **start** – Стартовая клетка.
* **exit** – Целевая клетка.
* **Результат:**
Список клеток пути (от start до exit включительно).
Пустой список, если путь не найден.
### *class* source.strategy.bfs.BFSStrategy
Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy)
Поиск в ширину (Breadth-First Search).
Гарантирует кратчайший путь по количеству шагов.
Сложность: O(V + E) по времени и памяти.
#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)]
Найти путь от start до exit.
* **Параметры:**
* **maze** – Объект лабиринта.
* **start** – Стартовая клетка.
* **exit** – Целевая клетка.
* **Результат:**
Список клеток пути (от start до exit включительно).
Пустой список, если путь не найден.
### *class* source.strategy.dfs.DFSStrategy
Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy)
Поиск в глубину (Depth-First Search).
Находит путь, но не гарантирует кратчайший.
#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)]
Найти путь от start до exit.
* **Параметры:**
* **maze** – Объект лабиринта.
* **start** – Стартовая клетка.
* **exit** – Целевая клетка.
* **Результат:**
Список клеток пути (от start до exit включительно).
Пустой список, если путь не найден.
### *class* source.strategy.astar.AStarStrategy
Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy)
Алгоритм A\* с манхэттенской эвристикой.
#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)]
Найти путь от start до exit.
* **Параметры:**
* **maze** – Объект лабиринта.
* **start** – Стартовая клетка.
* **exit** – Целевая клетка.
* **Результат:**
Список клеток пути (от start до exit включительно).
Пустой список, если путь не найден.
## Оркестратор
### *class* source.strategy.solver.MazeSolver(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy))
Базовые классы: `object`
Оркестратор поиска пути в лабиринте.
Принимает лабиринт и стратегию поиска, выполняет поиск
и возвращает результат вместе со статистикой выполнения.
### Пример
solver = MazeSolver(maze, BFSStrategy())
stats = solver.solve()
print(stats)
solver.set_strategy(AStarStrategy())
stats = solver.solve()
#### \_\_init_\_(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None
Инициализирует солвер с лабиринтом и стратегией поиска.
* **Параметры:**
* **maze** – Объект лабиринта.
* **strategy** – Стратегия поиска пути.
#### set_strategy(strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None
Заменяет текущую стратегию поиска.
* **Параметры:**
**strategy** – Новая стратегия поиска пути.
#### solve(start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → [SearchStats](#source.strategy.solver.SearchStats)
Выполняет поиск пути и собирает статистику.
Если start или exit не переданы явно, стратегия найдёт
их самостоятельно по флагам is_start / is_exit в лабиринте.
* **Параметры:**
* **start** – Стартовая клетка (опционально).
* **exit** – Конечная клетка (опционально).
* **Результат:**
Объект SearchStats с временем выполнения, количеством
посещённых клеток и длиной найденного пути.
### *class* source.strategy.solver.SearchStats(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)])
Базовые классы: `object`
Статистика выполнения поиска пути.
#### elapsed_ms
Время выполнения в миллисекундах.
* **Type:**
float
#### visited_count
Количество посещённых клеток.
* **Type:**
int
#### path_length
Длина найденного пути (0 если путь не найден).
* **Type:**
int
#### path
Найденный путь — список клеток от старта до выхода.
* **Type:**
list[[source.models.base.Cell](#source.models.base.Cell)]
#### \_\_init_\_(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) → None
#### elapsed_ms *: float*
#### path *: list[[Cell](#source.models.base.Cell)]*
#### path_length *: int*
#### visited_count *: int*
## Визуализация
### *class* source.view.observer.ConsoleView
Базовые классы: [`Observer`](#source.view.observer.Observer)
Отображает состояние лабиринта и события в консоли.
#### PATH_SYMBOL *= '·'*
#### PLAYER_SYMBOL *= 'P'*
#### render(maze: [Maze](#source.models.base.Maze), player: [Cell](#source.models.base.Cell) | None = None, path: list[[Cell](#source.models.base.Cell)] | None = None) → None
Рисует лабиринт в консоли.
Путь отмечается символом „·“, позиция игрока — „P“.
* **Параметры:**
* **maze** – Объект лабиринта.
* **player** – Текущая клетка игрока (опционально).
* **path** – Список клеток найденного пути (опционально).
#### update(event: [Event](#source.view.observer.Event)) → None
Реагирует на события и выводит информацию в консоль.
* **Параметры:**
**event** – Объект события.
### *class* source.view.observer.Event(type: str, payload: dict = None)
Базовые классы: `object`
Событие, передаваемое наблюдателям.
#### type
Тип события („maze_loaded“, „path_found“, „move“, „no_path“).
* **Type:**
str
#### payload
Дополнительные данные события.
* **Type:**
dict
#### \_\_init_\_(type: str, payload: dict = None) → None
#### payload *: dict* *= None*
#### type *: str*
### *class* source.view.observer.Observer
Базовые классы: `ABC`
Интерфейс наблюдателя за событиями лабиринта.
#### *abstractmethod* update(event: [Event](#source.view.observer.Event)) → None
Обрабатывает входящее событие.
* **Параметры:**
**event** – Объект события с типом и данными.
## Управление игроком
### *class* source.view.command.Command
Базовые классы: `ABC`
Интерфейс команды с поддержкой отмены.
#### *abstractmethod* execute() → bool
Выполняет команду.
* **Результат:**
True если команда выполнена успешно, False иначе.
#### *abstractmethod* undo() → None
Отменяет команду, восстанавливая предыдущее состояние.
### *class* source.view.command.CommandHistory
Базовые классы: `object`
Хранит историю выполненных команд и позволяет отменять их.
### Пример
history = CommandHistory()
cmd = MoveCommand(player, „w“, maze)
if cmd.execute():
> history.push(cmd)
history.undo() # отменяет последний успешный ход
#### \_\_init_\_() → None
#### clear() → None
Очищает историю команд.
#### push(command: [Command](#source.view.command.Command)) → None
Добавляет выполненную команду в историю.
* **Параметры:**
**command** – Успешно выполненная команда.
#### undo() → bool
Отменяет последнюю команду из истории.
* **Результат:**
True если отмена выполнена, False если история пуста.
### *class* source.view.command.MoveCommand(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze))
Базовые классы: [`Command`](#source.view.command.Command)
Перемещает игрока в заданном направлении.
Сохраняет предыдущую клетку для возможности отмены хода.
#### \_\_init_\_(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) → None
Инициализирует команду перемещения.
* **Параметры:**
* **player** – Объект игрока.
* **direction** – Направление („w“, „a“, „s“, „d“).
* **maze** – Объект лабиринта для проверки проходимости.
* **Исключение:**
**ValueError** – Если направление не распознано.
#### execute() → bool
Перемещает игрока если целевая клетка проходима.
* **Результат:**
True если перемещение выполнено, False если клетка непроходима.
#### undo() → None
Возвращает игрока на предыдущую клетку.
### *class* source.view.command.Player(cell: [Cell](#source.models.base.Cell))
Базовые классы: `object`
Хранит текущее положение игрока в лабиринте.
#### cell
Текущая клетка игрока.
#### \_\_init_\_(cell: [Cell](#source.models.base.Cell)) → None
Инициализирует игрока на заданной клетке.
* **Параметры:**
**cell** – Начальная клетка игрока.