[2] to merge

This commit is contained in:
SerKin0 2026-05-25 10:23:00 +03:00
parent 27c2f99467
commit 1d4bd0bf73
43 changed files with 4390 additions and 274 deletions

View File

@ -14,7 +14,7 @@ __pycache__/
# Сборка документации Sphinx - ЭТО ВАЖНО!
docs/build/
docs/source/_build/
docs/_build/
# Системные файлы
.DS_Store
@ -32,5 +32,4 @@ Thumbs.db
.ruff_cache/
/.idea
pupu.py

1185
skorohodovsa/task_2/docs.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,470 @@
# 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 включительно).
Пустой список, если путь не найден.
<a id="module-source.strategy.bfs"></a>
### *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 включительно).
Пустой список, если путь не найден.
<a id="module-source.strategy.dfs"></a>
### *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 включительно).
Пустой список, если путь не найден.
<a id="module-source.strategy.astar"></a>
### *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** Начальная клетка игрока.

View File

@ -0,0 +1,91 @@
# Этап 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).

View File

@ -0,0 +1,60 @@
# Этап 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) это не проявляется, но на прямоугольных даст некорректный результат.

View File

@ -0,0 +1,90 @@
# Этап 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) в худшем случае.

View File

@ -0,0 +1,44 @@
# Этап 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`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки.

View File

@ -0,0 +1,84 @@
# Этап 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() # отмена последнего хода
```

View File

@ -0,0 +1,61 @@
# Этап 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.030.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]]

View File

@ -0,0 +1,33 @@
# Этап 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** — надёжный выбор, всегда кратчайший путь.<br />
\\\\
**DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток.<br />
\\\\
**A**\* — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`.
## Выводы
Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода.

View File

@ -0,0 +1,103 @@
# Задание
## Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 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()` 510 раз, усреднить время, количество посещённых клеток, длину пути.
- Записать результаты в 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')` для перерисовки.

View File

@ -2,16 +2,25 @@
## Базовые модели
````{eval-rst}
```{eval-rst}
.. automodule:: source.models.base
:members:
:undoc-members:
:show-inheritance:
````
```
## Загрузка лабиринта
```{eval-rst}
.. automodule:: source.build.builder
:members:
:undoc-members:
:show-inheritance:
```
## Стратегии поиска пути
````{eval-rst}
```{eval-rst}
.. automodule:: source.strategy.algorithms
:members:
:undoc-members:
@ -31,4 +40,31 @@
:members:
:undoc-members:
:show-inheritance:
````
```
## Оркестратор
```{eval-rst}
.. automodule:: source.strategy.solver
:members:
:undoc-members:
:show-inheritance:
```
## Визуализация
```{eval-rst}
.. automodule:: source.view.observer
:members:
:undoc-members:
:show-inheritance:
```
## Управление игроком
```{eval-rst}
.. automodule:: source.view.command
:members:
:undoc-members:
:show-inheritance:
```

View File

@ -35,6 +35,7 @@ extensions = [
"sphinx.ext.mathjax",
"sphinx_new_tab_link",
"sphinx.ext.autosummary",
"sphinxcontrib.mermaid",
]
autosummary_generate = True

View File

@ -1,6 +1,15 @@
# Лабораторная работа "Поиск выхода из лабиринта"
:::{toctree}
:maxdepth: 2
naming_maze
task
stage1
stage2
stage3
stage4
stage5
stage6
stage7
api

View File

@ -1,95 +0,0 @@
# Этап 2. Загрузка лабиринта из файла
Во втором этапе разработки необходимо реализовать загрузку лабиринта из текстового файла, где: `#` стена, ` ` проход, `S` старт, `E` выход.
## Систематизация файлов
Для удобного хранения лабиринтов было решено сделать систему наименования текстовых файлов в папке `source/templates`.
Общая структура:
```
{размер}_{свойство 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, простой путь)
```
10x10_path_v1.txt
10x10_path_v2.txt
...
10x10_path_v10.txt
```
#### Средние (50×50, тупики)
```
50x50_deadends_v1.txt
50x50_deadends_v2.txt
...
50x50_deadends_v10.txt
```
#### Большие (100×100, запутанные)
```
100x100_spaghetti_v1.txt
100x100_spaghetti_v2.txt
...
100x100_spaghetti_v10.txt
```
#### Пустые (30×30)
```
30x30_empty_v1.txt
30x30_empty_v2.txt
...
30x30_empty_v10.txt
```
#### Без выхода (20×20)
```
20x20_noexit_v1.txt
20x20_noexit_v2.txt
...
20x20_noexit_v10.txt
```
#### Комбинированные свойства
```
50x50_deadends-noexit_v1.txt
100x100_spaghetti-noexit_v1.txt
10x10_path-empty_v1.txt (избыточно, но допустимо)
```
### Примечание
- Регистр имён файлов: **нижний регистр**
- Разделители: только `_` и `-`
- Расширение: `.txt`
- Кодировка: UTF-8

View File

@ -0,0 +1,91 @@
# Этап 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).

View File

@ -0,0 +1,60 @@
# Этап 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) это не проявляется, но на прямоугольных даст некорректный результат.

View File

@ -0,0 +1,90 @@
# Этап 3. Стратегии поиска пути
В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**.
## Паттерн Strategy
Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии:
```python
solver = MazeSolver(maze, BFSStrategy())
solver.set_strategy(AStarStrategy())
```
Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно.
## Структура пакета
Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт:
```
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`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса.
Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список:
```
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) в худшем случае.

View File

@ -0,0 +1,44 @@
# Этап 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`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки.

View File

@ -0,0 +1,84 @@
# Этап 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() # отмена последнего хода
```

View File

@ -0,0 +1,63 @@
# Этап 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.030.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]]

View File

@ -0,0 +1,81 @@
# Этап 7. Отчёт
## Описание задачи
Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF:
|Паттерн|Где применён|
|---|---|
|**Builder**|`TextFileBuilder`|
|**Strategy**|`BFSStrategy`, `DFSStrategy`, `AStarStrategy`|
|**Observer**|`ConsoleView`|
|**Command**|`MoveCommand`, `CommandHistory`|
## Диаграмма классов
```{mermaid}
classDiagram
class Cell {
+int x, y
+bool is_wall, is_start, is_exit
+is_possible() bool
}
class Maze {
+get_cell(x, y) Cell
+get_neighbors(x, y) list
+start, exit, shape
}
class MazeBuilder { <<abstract>> }
class TextFileBuilder
class PathFindingStrategy {
<<abstract>>
+find_path(maze, start, exit) list
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class MazeSolver {
+set_strategy(strategy)
+solve() SearchStats
}
class ConsoleView {
+update(event)
+render(maze, player, path)
}
class MoveCommand {
+execute() bool
+undo()
}
class CommandHistory {
+push(command)
+undo() bool
}
Maze *-- Cell
MazeBuilder <|-- TextFileBuilder
TextFileBuilder ..> Maze : creates
PathFindingStrategy <|-- BFSStrategy
PathFindingStrategy <|-- DFSStrategy
PathFindingStrategy <|-- AStarStrategy
MazeSolver --> PathFindingStrategy
MazeSolver --> Maze
MoveCommand --> Maze
CommandHistory --> MoveCommand
```
## Результаты экспериментов
| Лабиринт | Быстрее всех | Кратчайший путь |
| ----------------- | ------------- | --------------- |
| 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`.
## Выводы
Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода.

View File

@ -0,0 +1,183 @@
# Задание
## Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
## Общая схема приложения (пример)
```{mermaid}
classDiagram
class Maze {
-Cell[] cells
-int width, height
-Cell start
-Cell exit
+getCell(x,y): Cell
+getNeighbors(cell): List~Cell~
}
class Cell {
-int x, y
-bool isWall
-bool isStart
-bool isExit
+isPassable(): bool
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename): Maze
}
class TextFileMazeBuilder {
+buildFromFile(filename): Maze
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit): List~Cell~
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class DijkstraStrategy
class SearchStats {
+timeMs: float
+visitedCells: int
+pathLength: int
}
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+setStrategy(strategy)
+solve(): SearchStats
}
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Direction dir
-Cell previousCell
+execute()
+undo()
}
class Player {
-Cell currentCell
+moveTo(cell)
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player, path)
}
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder --> Maze : creates
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
Command <|.. MoveCommand
MoveCommand --> Player
Player --> Cell
Observer <|.. ConsoleView
MazeSolver --> Observer : notifies
```
## Выполнение
### Этап 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()` 510 раз, усреднить время, количество посещённых клеток, длину пути.
- Записать результаты в 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')` для перерисовки.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,121 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
100x100_spaghetti_v1.txt,BFS,9.2513,205.0,205.0
100x100_spaghetti_v1.txt,DFS,8.2451,2129.0,2129.0
100x100_spaghetti_v1.txt,A*,7.1113,205.0,205.0
100x100_spaghetti_v10.txt,BFS,9.2555,207.0,207.0
100x100_spaghetti_v10.txt,DFS,8.1821,2489.0,2489.0
100x100_spaghetti_v10.txt,A*,8.4803,207.0,207.0
100x100_spaghetti_v2.txt,BFS,9.3921,217.0,217.0
100x100_spaghetti_v2.txt,DFS,6.7196,2063.0,2063.0
100x100_spaghetti_v2.txt,A*,10.5764,217.0,217.0
100x100_spaghetti_v3.txt,BFS,8.4084,217.0,217.0
100x100_spaghetti_v3.txt,DFS,5.7855,2107.0,2107.0
100x100_spaghetti_v3.txt,A*,6.3385,217.0,217.0
100x100_spaghetti_v4.txt,BFS,8.8661,205.0,205.0
100x100_spaghetti_v4.txt,DFS,6.8166,2409.0,2409.0
100x100_spaghetti_v4.txt,A*,6.1874,205.0,205.0
100x100_spaghetti_v5.txt,BFS,8.3117,217.0,217.0
100x100_spaghetti_v5.txt,DFS,6.3364,2071.0,2071.0
100x100_spaghetti_v5.txt,A*,8.495,217.0,217.0
100x100_spaghetti_v6.txt,BFS,8.212,243.0,243.0
100x100_spaghetti_v6.txt,DFS,7.0348,1869.0,1869.0
100x100_spaghetti_v6.txt,A*,12.8413,243.0,243.0
100x100_spaghetti_v7.txt,BFS,8.3471,211.0,211.0
100x100_spaghetti_v7.txt,DFS,6.1699,2283.0,2283.0
100x100_spaghetti_v7.txt,A*,6.9637,211.0,211.0
100x100_spaghetti_v8.txt,BFS,8.3499,221.0,221.0
100x100_spaghetti_v8.txt,DFS,7.1166,2473.0,2473.0
100x100_spaghetti_v8.txt,A*,9.5093,221.0,221.0
100x100_spaghetti_v9.txt,BFS,8.5536,209.0,209.0
100x100_spaghetti_v9.txt,DFS,5.4126,1939.0,1939.0
100x100_spaghetti_v9.txt,A*,6.7365,209.0,209.0
10x10_path_v1.txt,BFS,0.032,13.0,13.0
10x10_path_v1.txt,DFS,0.0341,13.0,13.0
10x10_path_v1.txt,A*,0.0399,13.0,13.0
10x10_path_v10.txt,BFS,0.0323,13.0,13.0
10x10_path_v10.txt,DFS,0.036,13.0,13.0
10x10_path_v10.txt,A*,0.037,13.0,13.0
10x10_path_v2.txt,BFS,0.0354,17.0,17.0
10x10_path_v2.txt,DFS,0.0433,17.0,17.0
10x10_path_v2.txt,A*,0.044,17.0,17.0
10x10_path_v3.txt,BFS,0.0348,17.0,17.0
10x10_path_v3.txt,DFS,0.0492,17.0,17.0
10x10_path_v3.txt,A*,0.0439,17.0,17.0
10x10_path_v4.txt,BFS,0.0476,29.0,29.0
10x10_path_v4.txt,DFS,0.0475,29.0,29.0
10x10_path_v4.txt,A*,0.0652,29.0,29.0
10x10_path_v5.txt,BFS,0.0302,13.0,13.0
10x10_path_v5.txt,DFS,0.0334,13.0,13.0
10x10_path_v5.txt,A*,0.0371,13.0,13.0
10x10_path_v6.txt,BFS,0.0307,13.0,13.0
10x10_path_v6.txt,DFS,0.0339,13.0,13.0
10x10_path_v6.txt,A*,0.0375,13.0,13.0
10x10_path_v7.txt,BFS,0.0401,17.0,17.0
10x10_path_v7.txt,DFS,0.0499,17.0,17.0
10x10_path_v7.txt,A*,0.0489,17.0,17.0
10x10_path_v8.txt,BFS,0.0615,29.0,29.0
10x10_path_v8.txt,DFS,0.0536,29.0,29.0
10x10_path_v8.txt,A*,0.0801,29.0,29.0
10x10_path_v9.txt,BFS,0.0579,17.0,17.0
10x10_path_v9.txt,DFS,0.046,17.0,17.0
10x10_path_v9.txt,A*,0.0468,17.0,17.0
30x30_empty_v1.txt,BFS,1.1046,55.0,55.0
30x30_empty_v1.txt,DFS,0.7781,379.0,379.0
30x30_empty_v1.txt,A*,1.9965,55.0,55.0
30x30_empty_v10.txt,BFS,1.1246,55.0,55.0
30x30_empty_v10.txt,DFS,0.7002,379.0,379.0
30x30_empty_v10.txt,A*,2.0086,55.0,55.0
30x30_empty_v2.txt,BFS,1.1401,55.0,55.0
30x30_empty_v2.txt,DFS,0.7263,379.0,379.0
30x30_empty_v2.txt,A*,2.0245,55.0,55.0
30x30_empty_v3.txt,BFS,1.1038,55.0,55.0
30x30_empty_v3.txt,DFS,0.7249,379.0,379.0
30x30_empty_v3.txt,A*,2.007,55.0,55.0
30x30_empty_v4.txt,BFS,1.1224,55.0,55.0
30x30_empty_v4.txt,DFS,0.7053,379.0,379.0
30x30_empty_v4.txt,A*,1.989,55.0,55.0
30x30_empty_v5.txt,BFS,1.1294,55.0,55.0
30x30_empty_v5.txt,DFS,0.7202,379.0,379.0
30x30_empty_v5.txt,A*,2.1138,55.0,55.0
30x30_empty_v6.txt,BFS,1.0843,55.0,55.0
30x30_empty_v6.txt,DFS,0.7746,379.0,379.0
30x30_empty_v6.txt,A*,2.009,55.0,55.0
30x30_empty_v7.txt,BFS,1.1449,55.0,55.0
30x30_empty_v7.txt,DFS,0.7076,379.0,379.0
30x30_empty_v7.txt,A*,2.033,55.0,55.0
30x30_empty_v8.txt,BFS,1.3196,55.0,55.0
30x30_empty_v8.txt,DFS,0.7794,379.0,379.0
30x30_empty_v8.txt,A*,1.9972,55.0,55.0
30x30_empty_v9.txt,BFS,1.1088,55.0,55.0
30x30_empty_v9.txt,DFS,0.7131,379.0,379.0
30x30_empty_v9.txt,A*,2.0128,55.0,55.0
50x50_deadends_v1.txt,BFS,1.7809,729.0,729.0
50x50_deadends_v1.txt,DFS,1.7167,729.0,729.0
50x50_deadends_v1.txt,A*,2.5217,729.0,729.0
50x50_deadends_v10.txt,BFS,0.7362,261.0,261.0
50x50_deadends_v10.txt,DFS,1.7627,261.0,261.0
50x50_deadends_v10.txt,A*,0.9753,261.0,261.0
50x50_deadends_v2.txt,BFS,0.9246,249.0,249.0
50x50_deadends_v2.txt,DFS,1.7347,249.0,249.0
50x50_deadends_v2.txt,A*,1.0804,249.0,249.0
50x50_deadends_v3.txt,BFS,0.945,297.0,297.0
50x50_deadends_v3.txt,DFS,1.7483,297.0,297.0
50x50_deadends_v3.txt,A*,1.0832,297.0,297.0
50x50_deadends_v4.txt,BFS,1.5487,413.0,413.0
50x50_deadends_v4.txt,DFS,1.6526,413.0,413.0
50x50_deadends_v4.txt,A*,1.9521,413.0,413.0
50x50_deadends_v5.txt,BFS,0.9255,309.0,309.0
50x50_deadends_v5.txt,DFS,1.7299,309.0,309.0
50x50_deadends_v5.txt,A*,1.1469,309.0,309.0
50x50_deadends_v6.txt,BFS,1.0637,337.0,337.0
50x50_deadends_v6.txt,DFS,1.7728,337.0,337.0
50x50_deadends_v6.txt,A*,1.3449,337.0,337.0
50x50_deadends_v7.txt,BFS,0.7827,261.0,261.0
50x50_deadends_v7.txt,DFS,1.6948,261.0,261.0
50x50_deadends_v7.txt,A*,0.9527,261.0,261.0
50x50_deadends_v8.txt,BFS,1.5551,565.0,565.0
50x50_deadends_v8.txt,DFS,1.7707,565.0,565.0
50x50_deadends_v8.txt,A*,2.3158,565.0,565.0
50x50_deadends_v9.txt,BFS,0.6693,209.0,209.0
50x50_deadends_v9.txt,DFS,1.052,209.0,209.0
50x50_deadends_v9.txt,A*,0.7957,209.0,209.0
1 лабиринт стратегия время_мс посещено_клеток длина_пути
2 100x100_spaghetti_v1.txt BFS 9.2513 205.0 205.0
3 100x100_spaghetti_v1.txt DFS 8.2451 2129.0 2129.0
4 100x100_spaghetti_v1.txt A* 7.1113 205.0 205.0
5 100x100_spaghetti_v10.txt BFS 9.2555 207.0 207.0
6 100x100_spaghetti_v10.txt DFS 8.1821 2489.0 2489.0
7 100x100_spaghetti_v10.txt A* 8.4803 207.0 207.0
8 100x100_spaghetti_v2.txt BFS 9.3921 217.0 217.0
9 100x100_spaghetti_v2.txt DFS 6.7196 2063.0 2063.0
10 100x100_spaghetti_v2.txt A* 10.5764 217.0 217.0
11 100x100_spaghetti_v3.txt BFS 8.4084 217.0 217.0
12 100x100_spaghetti_v3.txt DFS 5.7855 2107.0 2107.0
13 100x100_spaghetti_v3.txt A* 6.3385 217.0 217.0
14 100x100_spaghetti_v4.txt BFS 8.8661 205.0 205.0
15 100x100_spaghetti_v4.txt DFS 6.8166 2409.0 2409.0
16 100x100_spaghetti_v4.txt A* 6.1874 205.0 205.0
17 100x100_spaghetti_v5.txt BFS 8.3117 217.0 217.0
18 100x100_spaghetti_v5.txt DFS 6.3364 2071.0 2071.0
19 100x100_spaghetti_v5.txt A* 8.495 217.0 217.0
20 100x100_spaghetti_v6.txt BFS 8.212 243.0 243.0
21 100x100_spaghetti_v6.txt DFS 7.0348 1869.0 1869.0
22 100x100_spaghetti_v6.txt A* 12.8413 243.0 243.0
23 100x100_spaghetti_v7.txt BFS 8.3471 211.0 211.0
24 100x100_spaghetti_v7.txt DFS 6.1699 2283.0 2283.0
25 100x100_spaghetti_v7.txt A* 6.9637 211.0 211.0
26 100x100_spaghetti_v8.txt BFS 8.3499 221.0 221.0
27 100x100_spaghetti_v8.txt DFS 7.1166 2473.0 2473.0
28 100x100_spaghetti_v8.txt A* 9.5093 221.0 221.0
29 100x100_spaghetti_v9.txt BFS 8.5536 209.0 209.0
30 100x100_spaghetti_v9.txt DFS 5.4126 1939.0 1939.0
31 100x100_spaghetti_v9.txt A* 6.7365 209.0 209.0
32 10x10_path_v1.txt BFS 0.032 13.0 13.0
33 10x10_path_v1.txt DFS 0.0341 13.0 13.0
34 10x10_path_v1.txt A* 0.0399 13.0 13.0
35 10x10_path_v10.txt BFS 0.0323 13.0 13.0
36 10x10_path_v10.txt DFS 0.036 13.0 13.0
37 10x10_path_v10.txt A* 0.037 13.0 13.0
38 10x10_path_v2.txt BFS 0.0354 17.0 17.0
39 10x10_path_v2.txt DFS 0.0433 17.0 17.0
40 10x10_path_v2.txt A* 0.044 17.0 17.0
41 10x10_path_v3.txt BFS 0.0348 17.0 17.0
42 10x10_path_v3.txt DFS 0.0492 17.0 17.0
43 10x10_path_v3.txt A* 0.0439 17.0 17.0
44 10x10_path_v4.txt BFS 0.0476 29.0 29.0
45 10x10_path_v4.txt DFS 0.0475 29.0 29.0
46 10x10_path_v4.txt A* 0.0652 29.0 29.0
47 10x10_path_v5.txt BFS 0.0302 13.0 13.0
48 10x10_path_v5.txt DFS 0.0334 13.0 13.0
49 10x10_path_v5.txt A* 0.0371 13.0 13.0
50 10x10_path_v6.txt BFS 0.0307 13.0 13.0
51 10x10_path_v6.txt DFS 0.0339 13.0 13.0
52 10x10_path_v6.txt A* 0.0375 13.0 13.0
53 10x10_path_v7.txt BFS 0.0401 17.0 17.0
54 10x10_path_v7.txt DFS 0.0499 17.0 17.0
55 10x10_path_v7.txt A* 0.0489 17.0 17.0
56 10x10_path_v8.txt BFS 0.0615 29.0 29.0
57 10x10_path_v8.txt DFS 0.0536 29.0 29.0
58 10x10_path_v8.txt A* 0.0801 29.0 29.0
59 10x10_path_v9.txt BFS 0.0579 17.0 17.0
60 10x10_path_v9.txt DFS 0.046 17.0 17.0
61 10x10_path_v9.txt A* 0.0468 17.0 17.0
62 30x30_empty_v1.txt BFS 1.1046 55.0 55.0
63 30x30_empty_v1.txt DFS 0.7781 379.0 379.0
64 30x30_empty_v1.txt A* 1.9965 55.0 55.0
65 30x30_empty_v10.txt BFS 1.1246 55.0 55.0
66 30x30_empty_v10.txt DFS 0.7002 379.0 379.0
67 30x30_empty_v10.txt A* 2.0086 55.0 55.0
68 30x30_empty_v2.txt BFS 1.1401 55.0 55.0
69 30x30_empty_v2.txt DFS 0.7263 379.0 379.0
70 30x30_empty_v2.txt A* 2.0245 55.0 55.0
71 30x30_empty_v3.txt BFS 1.1038 55.0 55.0
72 30x30_empty_v3.txt DFS 0.7249 379.0 379.0
73 30x30_empty_v3.txt A* 2.007 55.0 55.0
74 30x30_empty_v4.txt BFS 1.1224 55.0 55.0
75 30x30_empty_v4.txt DFS 0.7053 379.0 379.0
76 30x30_empty_v4.txt A* 1.989 55.0 55.0
77 30x30_empty_v5.txt BFS 1.1294 55.0 55.0
78 30x30_empty_v5.txt DFS 0.7202 379.0 379.0
79 30x30_empty_v5.txt A* 2.1138 55.0 55.0
80 30x30_empty_v6.txt BFS 1.0843 55.0 55.0
81 30x30_empty_v6.txt DFS 0.7746 379.0 379.0
82 30x30_empty_v6.txt A* 2.009 55.0 55.0
83 30x30_empty_v7.txt BFS 1.1449 55.0 55.0
84 30x30_empty_v7.txt DFS 0.7076 379.0 379.0
85 30x30_empty_v7.txt A* 2.033 55.0 55.0
86 30x30_empty_v8.txt BFS 1.3196 55.0 55.0
87 30x30_empty_v8.txt DFS 0.7794 379.0 379.0
88 30x30_empty_v8.txt A* 1.9972 55.0 55.0
89 30x30_empty_v9.txt BFS 1.1088 55.0 55.0
90 30x30_empty_v9.txt DFS 0.7131 379.0 379.0
91 30x30_empty_v9.txt A* 2.0128 55.0 55.0
92 50x50_deadends_v1.txt BFS 1.7809 729.0 729.0
93 50x50_deadends_v1.txt DFS 1.7167 729.0 729.0
94 50x50_deadends_v1.txt A* 2.5217 729.0 729.0
95 50x50_deadends_v10.txt BFS 0.7362 261.0 261.0
96 50x50_deadends_v10.txt DFS 1.7627 261.0 261.0
97 50x50_deadends_v10.txt A* 0.9753 261.0 261.0
98 50x50_deadends_v2.txt BFS 0.9246 249.0 249.0
99 50x50_deadends_v2.txt DFS 1.7347 249.0 249.0
100 50x50_deadends_v2.txt A* 1.0804 249.0 249.0
101 50x50_deadends_v3.txt BFS 0.945 297.0 297.0
102 50x50_deadends_v3.txt DFS 1.7483 297.0 297.0
103 50x50_deadends_v3.txt A* 1.0832 297.0 297.0
104 50x50_deadends_v4.txt BFS 1.5487 413.0 413.0
105 50x50_deadends_v4.txt DFS 1.6526 413.0 413.0
106 50x50_deadends_v4.txt A* 1.9521 413.0 413.0
107 50x50_deadends_v5.txt BFS 0.9255 309.0 309.0
108 50x50_deadends_v5.txt DFS 1.7299 309.0 309.0
109 50x50_deadends_v5.txt A* 1.1469 309.0 309.0
110 50x50_deadends_v6.txt BFS 1.0637 337.0 337.0
111 50x50_deadends_v6.txt DFS 1.7728 337.0 337.0
112 50x50_deadends_v6.txt A* 1.3449 337.0 337.0
113 50x50_deadends_v7.txt BFS 0.7827 261.0 261.0
114 50x50_deadends_v7.txt DFS 1.6948 261.0 261.0
115 50x50_deadends_v7.txt A* 0.9527 261.0 261.0
116 50x50_deadends_v8.txt BFS 1.5551 565.0 565.0
117 50x50_deadends_v8.txt DFS 1.7707 565.0 565.0
118 50x50_deadends_v8.txt A* 2.3158 565.0 565.0
119 50x50_deadends_v9.txt BFS 0.6693 209.0 209.0
120 50x50_deadends_v9.txt DFS 1.052 209.0 209.0
121 50x50_deadends_v9.txt A* 0.7957 209.0 209.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

View File

@ -6,3 +6,6 @@ myst-nb
tabulate
bibtexparser
pytest
sphinxcontrib-mermaid
matplotlib
pandas

View File

@ -23,6 +23,26 @@ class PathFindingStrategy(ABC):
Пустой список, если путь не найден.
"""
def _validate(
self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]
) -> tuple[Optional[Cell], Optional[Cell]]:
"""Подставляет start/exit из лабиринта если не переданы явно.
Raises:
ValueError: Если старт или выход не найдены.
"""
if start is None:
start = maze.start
if exit is None:
exit = maze.exit
if start is None:
raise ValueError("Стартовая клетка не найдена в лабиринте")
if exit is None:
raise ValueError("Выходная клетка не найдена в лабиринте")
return start, exit
def _reconstruct_path(
self, came_from: dict[Cell, Optional[Cell]], end: Cell
) -> list[Cell]:
@ -46,11 +66,3 @@ class PathFindingStrategy(ABC):
current = came_from[current]
path.reverse()
return path

View File

@ -4,6 +4,11 @@ from typing import Optional
from source.models.base import Cell, Maze
from source.strategy.algorithms import PathFindingStrategy
# ---------------------------------------------------------------------------- #
# Моя азиатская жена называет меня расистом. Но как я могу #
# быть расистом, если я женился на женщине низшей расы?! #
# ---------------------------------------------------------------------------- #
def _manhattan(a: Cell, b: Cell) -> int:
"""Манхэттенское расстояние между двумя клетками."""
@ -13,11 +18,10 @@ def _manhattan(a: Cell, b: Cell) -> int:
class AStarStrategy(PathFindingStrategy):
"""Алгоритм A* с манхэттенской эвристикой."""
def find_path(self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None) -> list[Cell]:
if start is None:
start = maze.start
if exit is None:
exit = self.exit
def find_path(
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
) -> list[Cell]:
start, exit = self._validate(maze, start, exit)
g_score: dict[Cell, int] = {start: 0}
came_from: dict[Cell, Optional[Cell]] = {start: None}

View File

@ -15,10 +15,7 @@ class BFSStrategy(PathFindingStrategy):
def find_path(
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
) -> list[Cell]:
if start is None:
start = maze.start
if exit is None:
exit = maze.exit
start, exit = self._validate(maze, start, exit)
came_from: dict[Cell, Optional[Cell]] = {start: None}
queue: deque[Cell] = deque([start])

View File

@ -3,6 +3,12 @@ from typing import Optional
from source.models.base import Maze, Cell
from source.strategy.algorithms import PathFindingStrategy
# ---------------------------------------------------------------------------- #
# Как называется пресмыкающийся, который в прошлом был программистом? #
# ---------------------------------------------------------------------------- #
# крокодил #
# ---------------------------------------------------------------------------- #
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину (Depth-First Search).
@ -13,10 +19,7 @@ class DFSStrategy(PathFindingStrategy):
def find_path(
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
) -> list[Cell]:
if start is None:
start = maze.start
if exit is None:
exit = maze.exit
start, exit = self._validate(maze, start, exit)
came_from: dict[Cell, Optional[Cell]] = {start: None}
stack: list[Cell] = [start]

View File

@ -15,6 +15,7 @@ class SearchStats:
path_length: Длина найденного пути (0 если путь не найден).
path: Найденный путь список клеток от старта до выхода.
"""
elapsed_ms: float
visited_count: int
path_length: int

View File

@ -8,6 +8,7 @@ from source.models.base import Maze, Cell
# Игрок #
# ---------------------------------------------------------------------------- #
class Player:
"""Хранит текущее положение игрока в лабиринте.
@ -31,6 +32,7 @@ class Player:
# Интерфейс команды #
# ---------------------------------------------------------------------------- #
class Command(ABC):
"""Интерфейс команды с поддержкой отмены."""
@ -53,9 +55,9 @@ class Command(ABC):
DIRECTIONS = {
"w": (0, -1),
"s": (0, 1),
"s": (0, 1),
"a": (-1, 0),
"d": (1, 0),
"d": (1, 0),
}
@ -77,11 +79,13 @@ class MoveCommand(Command):
ValueError: Если направление не распознано.
"""
if direction not in DIRECTIONS:
raise ValueError(f"Неизвестное направление '{direction}'. Используй: w/a/s/d")
raise ValueError(
f"Неизвестное направление '{direction}'. Используй: w/a/s/d"
)
self._player = player
self._player = player
self._direction = direction
self._maze = maze
self._maze = maze
self._prev_cell: Optional[Cell] = None
def execute(self) -> bool:
@ -99,7 +103,7 @@ class MoveCommand(Command):
if target is None or not target.is_possible():
return False
self._prev_cell = self._player.cell
self._prev_cell = self._player.cell
self._player.cell = target
return True
@ -113,6 +117,7 @@ class MoveCommand(Command):
# История команд #
# ---------------------------------------------------------------------------- #
class CommandHistory:
"""Хранит историю выполненных команд и позволяет отменять их.

View File

@ -10,6 +10,7 @@ from source.settings import cell_mapping
# События #
# ---------------------------------------------------------------------------- #
@dataclass
class Event:
"""Событие, передаваемое наблюдателям.
@ -18,6 +19,7 @@ class Event:
type: Тип события ('maze_loaded', 'path_found', 'move', 'no_path').
payload: Дополнительные данные события.
"""
type: str
payload: dict = None
@ -26,6 +28,7 @@ class Event:
# Интерфейс наблюдателя #
# ---------------------------------------------------------------------------- #
class Observer(ABC):
"""Интерфейс наблюдателя за событиями лабиринта."""
@ -42,12 +45,13 @@ class Observer(ABC):
# Консольный наблюдатель
# ---------------------------------------------------------------------------
class ConsoleView(Observer):
"""Отображает состояние лабиринта и события в консоли."""
# Символ игрока на карте
PLAYER_SYMBOL = "P"
PATH_SYMBOL = "·"
PATH_SYMBOL = "·"
def update(self, event: Event) -> None:
"""Реагирует на события и выводит информацию в консоль.

View File

@ -1,111 +0,0 @@
import pytest
import sys
import os
from source.models.base import Cell
class TestCellCreation:
"""Тесты создания клетки и начальных значений."""
def test_coordinates_are_set(self):
cell = Cell(3, 7)
assert cell.x == 3
assert cell.y == 7
def test_default_flags_are_false(self):
cell = Cell(0, 0)
assert cell.is_wall is False
assert cell.is_start is False
assert cell.is_exit is False
def test_create_wall(self):
cell = Cell(0, 0, is_wall=True)
assert cell.is_wall is True
def test_create_start(self):
cell = Cell(0, 0, is_start=True)
assert cell.is_start is True
def test_create_exit(self):
cell = Cell(0, 0, is_exit=True)
assert cell.is_exit is True
class TestCellIsPassable:
"""Тесты метода is_possible."""
def test_empty_cell_is_passable(self):
cell = Cell(0, 0)
assert cell.is_possible() is True
def test_wall_is_not_passable(self):
cell = Cell(0, 0, is_wall=True)
assert cell.is_possible() is False
def test_start_cell_is_passable(self):
cell = Cell(0, 0, is_start=True)
assert cell.is_possible() is True
def test_exit_cell_is_passable(self):
cell = Cell(0, 0, is_exit=True)
assert cell.is_possible() is True
class TestCellFlagsAreMutuallyExclusive:
"""Тесты взаимного исключения флагов."""
def test_set_wall_clears_start(self):
cell = Cell(0, 0, is_start=True)
cell.is_wall = True
assert cell.is_start is False
assert cell.is_wall is True
def test_set_wall_clears_exit(self):
cell = Cell(0, 0, is_exit=True)
cell.is_wall = True
assert cell.is_exit is False
assert cell.is_wall is True
def test_set_start_clears_wall(self):
cell = Cell(0, 0, is_wall=True)
cell.is_start = True
assert cell.is_wall is False
assert cell.is_start is True
def test_set_start_clears_exit(self):
cell = Cell(0, 0, is_exit=True)
cell.is_start = True
assert cell.is_exit is False
assert cell.is_start is True
def test_set_exit_clears_wall(self):
cell = Cell(0, 0, is_wall=True)
cell.is_exit = True
assert cell.is_wall is False
assert cell.is_exit is True
def test_set_exit_clears_start(self):
cell = Cell(0, 0, is_start=True)
cell.is_exit = True
assert cell.is_start is False
assert cell.is_exit is True
def test_unset_wall_does_not_clear_others(self):
# снятие флага (False) не должно трогать остальные
cell = Cell(0, 0, is_wall=True)
cell.is_wall = False
assert cell.is_start is False
assert cell.is_exit is False
class TestCellStr:
"""Тесты строкового представления клетки."""
def test_str_returns_string(self):
cell = Cell(0, 0)
assert isinstance(str(cell), str)
def test_repr_contains_coordinates(self):
cell = Cell(4, 9)
assert "4" in repr(cell)
assert "9" in repr(cell)

View File

@ -19,9 +19,9 @@ if start is None:
print("Стартовая клетка не найдена!")
exit()
player = Player(start)
player = Player(start)
history = CommandHistory()
view = ConsoleView()
view = ConsoleView()
view.update(Event("maze_loaded", {"maze": maze}))
print("Управление: w/a/s/d — движение, z — отмена, q — выход\n")
@ -29,24 +29,29 @@ print("Управление: w/a/s/d — движение, z — отмена, q
while True:
key = input(">>> ").strip().lower()
if key == 'q':
if key == "q":
print("Выход.")
break
elif key == 'z':
elif key == "z":
if history.undo():
print("Ход отменён.")
view.render(maze, player=player.cell)
elif key in ('w', 'a', 's', 'd'):
elif key in ("w", "a", "s", "d"):
cmd = MoveCommand(player, key, maze)
if cmd.execute():
history.push(cmd)
view.update(Event("move", {
"maze": maze,
"player_cell": player.cell,
"direction": key,
}))
view.update(
Event(
"move",
{
"maze": maze,
"player_cell": player.cell,
"direction": key,
},
)
)
if player.cell.is_exit:
print("Выход найден! Победа!")
break

View File

@ -7,8 +7,114 @@ from source.models.base import Cell, Maze
from source.settings import cell_mapping
class TestMaze:
class TestCellCreation:
"""Тесты создания клетки и начальных значений."""
def test_coordinates_are_set(self):
cell = Cell(3, 7)
assert cell.x == 3
assert cell.y == 7
def test_default_flags_are_false(self):
cell = Cell(0, 0)
assert cell.is_wall is False
assert cell.is_start is False
assert cell.is_exit is False
def test_create_wall(self):
cell = Cell(0, 0, is_wall=True)
assert cell.is_wall is True
def test_create_start(self):
cell = Cell(0, 0, is_start=True)
assert cell.is_start is True
def test_create_exit(self):
cell = Cell(0, 0, is_exit=True)
assert cell.is_exit is True
class TestCellIsPassable:
"""Тесты метода is_possible."""
def test_empty_cell_is_passable(self):
cell = Cell(0, 0)
assert cell.is_possible() is True
def test_wall_is_not_passable(self):
cell = Cell(0, 0, is_wall=True)
assert cell.is_possible() is False
def test_start_cell_is_passable(self):
cell = Cell(0, 0, is_start=True)
assert cell.is_possible() is True
def test_exit_cell_is_passable(self):
cell = Cell(0, 0, is_exit=True)
assert cell.is_possible() is True
class TestCellFlagsAreMutuallyExclusive:
"""Тесты взаимного исключения флагов."""
def test_set_wall_clears_start(self):
cell = Cell(0, 0, is_start=True)
cell.is_wall = True
assert cell.is_start is False
assert cell.is_wall is True
def test_set_wall_clears_exit(self):
cell = Cell(0, 0, is_exit=True)
cell.is_wall = True
assert cell.is_exit is False
assert cell.is_wall is True
def test_set_start_clears_wall(self):
cell = Cell(0, 0, is_wall=True)
cell.is_start = True
assert cell.is_wall is False
assert cell.is_start is True
def test_set_start_clears_exit(self):
cell = Cell(0, 0, is_exit=True)
cell.is_start = True
assert cell.is_exit is False
assert cell.is_start is True
def test_set_exit_clears_wall(self):
cell = Cell(0, 0, is_wall=True)
cell.is_exit = True
assert cell.is_wall is False
assert cell.is_exit is True
def test_set_exit_clears_start(self):
cell = Cell(0, 0, is_start=True)
cell.is_exit = True
assert cell.is_start is False
assert cell.is_exit is True
def test_unset_wall_does_not_clear_others(self):
# снятие флага (False) не должно трогать остальные
cell = Cell(0, 0, is_wall=True)
cell.is_wall = False
assert cell.is_start is False
assert cell.is_exit is False
class TestCellStr:
"""Тесты строкового представления клетки."""
def test_str_returns_string(self):
cell = Cell(0, 0)
assert isinstance(str(cell), str)
def test_repr_contains_coordinates(self):
cell = Cell(4, 9)
assert "4" in repr(cell)
assert "9" in repr(cell)
class TestMaze:
def test_default_size(self):
"""Проверка размеров лабиринта со значениями по умолчанию"""
maze = Maze()
@ -42,8 +148,8 @@ class TestMaze:
maze = Maze(size=(5, 5))
assert maze.get_cell(-1, 0) is None
assert maze.get_cell(0, -1) is None
assert maze.get_cell(5, 0) is None
assert maze.get_cell(0, 5) is None
assert maze.get_cell(5, 0) is None
assert maze.get_cell(0, 5) is None
def test_center_has_four_neighbors(self):
"""Проверка нахождения соседей"""
@ -58,32 +164,32 @@ class TestMaze:
def test_wall_excluded_from_neighbors(self):
"""Проверка что стена не попадает в список соседей"""
maze = Maze(size=(5, 5))
maze[1, 2] = cell_mapping['wall']
maze[1, 2] = cell_mapping["wall"]
assert all(not n.is_wall for n in maze.get_neighbors(2, 2))
def test_setitem_wall(self):
"""Проверка установки стены через оператор []"""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping['wall']
maze[0, 0] = cell_mapping["wall"]
assert maze[0, 0].is_wall is True
def test_setitem_start(self):
"""Проверка установки старта через оператор []"""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping['start']
maze[0, 0] = cell_mapping["start"]
assert maze[0, 0].is_start is True
def test_setitem_exit(self):
"""Проверка установки выхода через оператор []"""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping['exit']
maze[0, 0] = cell_mapping["exit"]
assert maze[0, 0].is_exit is True
def test_setitem_empty_clears_flags(self):
"""Проверка сброса флагов клетки при установке пустого типа"""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping['wall']
maze[0, 0] = cell_mapping['empty']
maze[0, 0] = cell_mapping["wall"]
maze[0, 0] = cell_mapping["empty"]
assert not maze[0, 0].is_wall
def test_getitem_out_of_bounds_raises(self):

View File

@ -0,0 +1,162 @@
import pytest
from source.models.base import Maze, Cell
from source.settings import cell_mapping
from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy
def make_open_maze(width: int = 5, height: int = 5) -> Maze:
"""Открытый лабиринт без внутренних стен, S в углу, E в противоположном."""
maze = Maze(size=(width, height))
maze[0, 0] = cell_mapping["start"]
maze[height - 1, width - 1] = cell_mapping["exit"]
return maze
def make_blocked_maze() -> Maze:
"""Лабиринт где S и E разделены сплошной стеной — пути нет."""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping["start"]
maze[4, 4] = cell_mapping["exit"]
for col in range(5):
maze[2, col] = cell_mapping["wall"]
return maze
def make_corridor_maze() -> Maze:
"""Узкий коридор 1×5: S → . → . → . → E."""
maze = Maze(size=(5, 1))
maze[0, 0] = cell_mapping["start"]
maze[0, 4] = cell_mapping["exit"]
return maze
STRATEGIES = [BFSStrategy, DFSStrategy, AStarStrategy]
STRATEGY_IDS = ["BFS", "DFS", "A*"]
# ---------------------------------------------------------------------------- #
# Общие тесты для всех стратегий #
# ---------------------------------------------------------------------------- #
class TestAllStrategies:
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_returns_list(self, StrategyClass):
"""find_path всегда возвращает список."""
maze = make_open_maze()
result = StrategyClass().find_path(maze)
assert isinstance(result, list)
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_starts_with_start(self, StrategyClass):
"""Первая клетка пути — старт."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
assert path[0] is maze.start
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_ends_with_exit(self, StrategyClass):
"""Последняя клетка пути — выход."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
assert path[-1] is maze.exit
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_cells_are_passable(self, StrategyClass):
"""Все клетки пути проходимы."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
assert all(cell.is_possible() for cell in path)
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_path_cells_are_neighbors(self, StrategyClass):
"""Каждая следующая клетка пути — сосед предыдущей."""
maze = make_open_maze()
path = StrategyClass().find_path(maze)
for a, b in zip(path, path[1:]):
assert abs(a.x - b.x) + abs(a.y - b.y) == 1
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_no_path_returns_empty(self, StrategyClass):
"""Если пути нет — возвращает пустой список."""
maze = make_blocked_maze()
path = StrategyClass().find_path(maze)
assert path == []
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_corridor_path_length(self, StrategyClass):
"""В коридоре 1×5 путь содержит ровно 5 клеток."""
maze = make_corridor_maze()
path = StrategyClass().find_path(maze)
assert len(path) == 5
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_maze_not_modified(self, StrategyClass):
"""Алгоритм не изменяет состояние лабиринта."""
maze = make_open_maze()
before = str(maze)
StrategyClass().find_path(maze)
assert str(maze) == before
# ---------------------------------------------------------------------------- #
# Тесты специфичные для BFS и A* (оптимальность пути) #
# ---------------------------------------------------------------------------- #
class TestOptimalStrategies:
@pytest.mark.parametrize(
"StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"]
)
def test_shortest_path_in_corridor(self, StrategyClass):
"""BFS и A* находят кратчайший путь в коридоре."""
maze = make_corridor_maze()
path = StrategyClass().find_path(maze)
assert len(path) == 5
@pytest.mark.parametrize(
"StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"]
)
def test_bfs_and_astar_same_length(self, StrategyClass):
"""BFS и A* возвращают путь одинаковой длины на открытом лабиринте."""
maze = make_open_maze(7, 7)
bfs_len = len(BFSStrategy().find_path(maze))
astar_len = len(AStarStrategy().find_path(maze))
assert bfs_len == astar_len
# ---------------------------------------------------------------------------- #
# Тесты с явной передачей start / exit #
# ---------------------------------------------------------------------------- #
class TestExplicitStartExit:
@pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS)
def test_explicit_start_and_exit(self, StrategyClass):
"""find_path работает с явно переданными start и exit."""
maze = Maze(size=(5, 5))
start = maze.get_cell(0, 0)
exit = maze.get_cell(4, 4)
maze[0, 0] = cell_mapping["start"]
maze[4, 4] = cell_mapping["exit"]
path = StrategyClass().find_path(maze, start=start, exit=exit)
assert path[0] is start
assert path[-1] is exit
class TestEdgeCases:
def test_no_start_raises(self):
"""Если нет старта — ValueError."""
maze = Maze(size=(5, 5))
maze[4, 4] = cell_mapping["exit"]
with pytest.raises(ValueError):
BFSStrategy().find_path(maze)
def test_no_exit_raises(self):
"""Если нет выхода — ValueError."""
maze = Maze(size=(5, 5))
maze[0, 0] = cell_mapping["start"]
with pytest.raises(ValueError):
BFSStrategy().find_path(maze)