[2] to merge
This commit is contained in:
parent
27c2f99467
commit
1d4bd0bf73
3
skorohodovsa/task_2/.gitignore
vendored
3
skorohodovsa/task_2/.gitignore
vendored
|
|
@ -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
1185
skorohodovsa/task_2/docs.md
Normal file
File diff suppressed because it is too large
Load Diff
470
skorohodovsa/task_2/docs/_build/markdown/api.md
vendored
Normal file
470
skorohodovsa/task_2/docs/_build/markdown/api.md
vendored
Normal 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** – Начальная клетка игрока.
|
||||
91
skorohodovsa/task_2/docs/_build/markdown/stage1.md
vendored
Normal file
91
skorohodovsa/task_2/docs/_build/markdown/stage1.md
vendored
Normal 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).
|
||||
60
skorohodovsa/task_2/docs/_build/markdown/stage2.md
vendored
Normal file
60
skorohodovsa/task_2/docs/_build/markdown/stage2.md
vendored
Normal 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) это не проявляется, но на прямоугольных даст некорректный результат.
|
||||
90
skorohodovsa/task_2/docs/_build/markdown/stage3.md
vendored
Normal file
90
skorohodovsa/task_2/docs/_build/markdown/stage3.md
vendored
Normal 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) в худшем случае.
|
||||
44
skorohodovsa/task_2/docs/_build/markdown/stage4.md
vendored
Normal file
44
skorohodovsa/task_2/docs/_build/markdown/stage4.md
vendored
Normal 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`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки.
|
||||
84
skorohodovsa/task_2/docs/_build/markdown/stage5.md
vendored
Normal file
84
skorohodovsa/task_2/docs/_build/markdown/stage5.md
vendored
Normal 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() # отмена последнего хода
|
||||
```
|
||||
61
skorohodovsa/task_2/docs/_build/markdown/stage6.md
vendored
Normal file
61
skorohodovsa/task_2/docs/_build/markdown/stage6.md
vendored
Normal 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.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A\* давала преимущество.
|
||||
|
||||
### 50×50 (тупики)
|
||||
|
||||
BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A\* показывает время между BFS и DFS.
|
||||
|
||||
### 100×100 (запутанный, spaghetti)
|
||||
|
||||
Наиболее показательные результаты:
|
||||
|
||||
| Стратегия | Время (мс) | Длина пути |
|
||||
|-------------|--------------|--------------|
|
||||
| BFS | ~9 | ~210 |
|
||||
| DFS | ~7 | ~2200 |
|
||||
| A\* | ~8 | ~210 |
|
||||
|
||||
DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A\* находят кратчайший путь, A\* при этом чуть быстрее за счёт эвристики.
|
||||
|
||||
### 30×30 (пустой)
|
||||
|
||||
Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A\*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных.
|
||||
|
||||
A\* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет.
|
||||
|
||||
## Выводы
|
||||
|
||||
- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо.
|
||||
- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути.
|
||||
- **A**\* — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь.
|
||||
|
||||
## Визуализация
|
||||
|
||||
![[results.png]]
|
||||
33
skorohodovsa/task_2/docs/_build/markdown/stage7.md
vendored
Normal file
33
skorohodovsa/task_2/docs/_build/markdown/stage7.md
vendored
Normal 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`.
|
||||
|
||||
## Выводы
|
||||
|
||||
Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода.
|
||||
103
skorohodovsa/task_2/docs/_build/markdown/task.md
vendored
Normal file
103
skorohodovsa/task_2/docs/_build/markdown/task.md
vendored
Normal 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()` 5–10 раз, усреднить время, количество посещённых клеток, длину пути.
|
||||
- Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`.
|
||||
3. **Анализ:**
|
||||
- Построить графики для каждого лабиринта.
|
||||
- Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
|
||||
4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A\* на взвешенном графе.
|
||||
|
||||
### Этап 7. Отчёт
|
||||
|
||||
**Структура отчёта:**
|
||||
|
||||
1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
|
||||
2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
|
||||
3. Результаты экспериментов (таблицы, графики).
|
||||
4. Анализ эффективности алгоритмов и применимости паттернов.
|
||||
5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.
|
||||
|
||||
## Советы
|
||||
|
||||
- Для A\* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`.
|
||||
- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь.
|
||||
- Для BFS/DFS используй `deque` (очередь) и `list` (стек).
|
||||
- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки.
|
||||
|
|
@ -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:
|
||||
```
|
||||
|
|
@ -35,6 +35,7 @@ extensions = [
|
|||
"sphinx.ext.mathjax",
|
||||
"sphinx_new_tab_link",
|
||||
"sphinx.ext.autosummary",
|
||||
"sphinxcontrib.mermaid",
|
||||
]
|
||||
|
||||
autosummary_generate = True
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
# Лабораторная работа "Поиск выхода из лабиринта"
|
||||
|
||||
|
||||
:::{toctree}
|
||||
:maxdepth: 2
|
||||
naming_maze
|
||||
task
|
||||
stage1
|
||||
stage2
|
||||
stage3
|
||||
stage4
|
||||
stage5
|
||||
stage6
|
||||
stage7
|
||||
api
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
91
skorohodovsa/task_2/docs/source/stage1.md
Normal file
91
skorohodovsa/task_2/docs/source/stage1.md
Normal 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).
|
||||
60
skorohodovsa/task_2/docs/source/stage2.md
Normal file
60
skorohodovsa/task_2/docs/source/stage2.md
Normal 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) это не проявляется, но на прямоугольных даст некорректный результат.
|
||||
90
skorohodovsa/task_2/docs/source/stage3.md
Normal file
90
skorohodovsa/task_2/docs/source/stage3.md
Normal 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) в худшем случае.
|
||||
44
skorohodovsa/task_2/docs/source/stage4.md
Normal file
44
skorohodovsa/task_2/docs/source/stage4.md
Normal 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`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки.
|
||||
84
skorohodovsa/task_2/docs/source/stage5.md
Normal file
84
skorohodovsa/task_2/docs/source/stage5.md
Normal 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() # отмена последнего хода
|
||||
```
|
||||
63
skorohodovsa/task_2/docs/source/stage6.md
Normal file
63
skorohodovsa/task_2/docs/source/stage6.md
Normal 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.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A* давала преимущество.
|
||||
|
||||
### 50×50 (тупики)
|
||||
|
||||
BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A* показывает время между BFS и DFS.
|
||||
|
||||
### 100×100 (запутанный, spaghetti)
|
||||
|
||||
Наиболее показательные результаты:
|
||||
|
||||
|Стратегия|Время (мс)|Длина пути|
|
||||
|---|---|---|
|
||||
|BFS|~9|~210|
|
||||
|DFS|~7|~2200|
|
||||
|A*|~8|~210|
|
||||
|
||||
DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A* находят кратчайший путь, A* при этом чуть быстрее за счёт эвристики.
|
||||
|
||||
### 30×30 (пустой)
|
||||
|
||||
Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных.
|
||||
|
||||
A* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет.
|
||||
|
||||
## Выводы
|
||||
|
||||
- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо.
|
||||
|
||||
- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути.
|
||||
|
||||
- **A*** — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь.
|
||||
|
||||
## Визуализация
|
||||
|
||||
![[results.png]]
|
||||
81
skorohodovsa/task_2/docs/source/stage7.md
Normal file
81
skorohodovsa/task_2/docs/source/stage7.md
Normal 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`.
|
||||
|
||||
## Выводы
|
||||
|
||||
Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода.
|
||||
183
skorohodovsa/task_2/docs/source/task.md
Normal file
183
skorohodovsa/task_2/docs/source/task.md
Normal 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()` 5–10 раз, усреднить время, количество посещённых клеток, длину пути.
|
||||
- Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`.
|
||||
3. **Анализ:**
|
||||
- Построить графики для каждого лабиринта.
|
||||
- Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
|
||||
|
||||
4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A* на взвешенном графе.
|
||||
|
||||
### Этап 7. Отчёт
|
||||
**Структура отчёта:**
|
||||
1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
|
||||
2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
|
||||
3. Результаты экспериментов (таблицы, графики).
|
||||
4. Анализ эффективности алгоритмов и применимости паттернов.
|
||||
5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.
|
||||
|
||||
## Советы
|
||||
- Для A* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`.
|
||||
- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь.
|
||||
- Для BFS/DFS используй `deque` (очередь) и `list` (стек).
|
||||
- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки.
|
||||
937
skorohodovsa/task_2/practice/main.ipynb
Normal file
937
skorohodovsa/task_2/practice/main.ipynb
Normal file
File diff suppressed because one or more lines are too long
121
skorohodovsa/task_2/practice/results.csv
Normal file
121
skorohodovsa/task_2/practice/results.csv
Normal 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
|
||||
|
BIN
skorohodovsa/task_2/practice/results.png
Normal file
BIN
skorohodovsa/task_2/practice/results.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
|
|
@ -6,3 +6,6 @@ myst-nb
|
|||
tabulate
|
||||
bibtexparser
|
||||
pytest
|
||||
sphinxcontrib-mermaid
|
||||
matplotlib
|
||||
pandas
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class SearchStats:
|
|||
path_length: Длина найденного пути (0 если путь не найден).
|
||||
path: Найденный путь — список клеток от старта до выхода.
|
||||
"""
|
||||
|
||||
elapsed_ms: float
|
||||
visited_count: int
|
||||
path_length: int
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""Хранит историю выполненных команд и позволяет отменять их.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""Реагирует на события и выводит информацию в консоль.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
162
skorohodovsa/task_2/test/test_strategies.py
Normal file
162
skorohodovsa/task_2/test/test_strategies.py
Normal 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)
|
||||
Loading…
Reference in New Issue
Block a user