[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 - ЭТО ВАЖНО!
|
# Сборка документации Sphinx - ЭТО ВАЖНО!
|
||||||
docs/build/
|
docs/build/
|
||||||
docs/source/_build/
|
docs/_build/
|
||||||
|
|
||||||
# Системные файлы
|
# Системные файлы
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
@ -32,5 +32,4 @@ Thumbs.db
|
||||||
|
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
|
||||||
/.idea
|
|
||||||
pupu.py
|
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
|
.. automodule:: source.models.base
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
````
|
```
|
||||||
|
|
||||||
|
## Загрузка лабиринта
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. automodule:: source.build.builder
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
```
|
||||||
|
|
||||||
## Стратегии поиска пути
|
## Стратегии поиска пути
|
||||||
|
|
||||||
````{eval-rst}
|
```{eval-rst}
|
||||||
.. automodule:: source.strategy.algorithms
|
.. automodule:: source.strategy.algorithms
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
|
@ -31,4 +40,31 @@
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
: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.ext.mathjax",
|
||||||
"sphinx_new_tab_link",
|
"sphinx_new_tab_link",
|
||||||
"sphinx.ext.autosummary",
|
"sphinx.ext.autosummary",
|
||||||
|
"sphinxcontrib.mermaid",
|
||||||
]
|
]
|
||||||
|
|
||||||
autosummary_generate = True
|
autosummary_generate = True
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
# Лабораторная работа "Поиск выхода из лабиринта"
|
# Лабораторная работа "Поиск выхода из лабиринта"
|
||||||
|
|
||||||
|
|
||||||
:::{toctree}
|
:::{toctree}
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
naming_maze
|
task
|
||||||
|
stage1
|
||||||
|
stage2
|
||||||
|
stage3
|
||||||
|
stage4
|
||||||
|
stage5
|
||||||
|
stage6
|
||||||
|
stage7
|
||||||
api
|
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
|
tabulate
|
||||||
bibtexparser
|
bibtexparser
|
||||||
pytest
|
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(
|
def _reconstruct_path(
|
||||||
self, came_from: dict[Cell, Optional[Cell]], end: Cell
|
self, came_from: dict[Cell, Optional[Cell]], end: Cell
|
||||||
) -> list[Cell]:
|
) -> list[Cell]:
|
||||||
|
|
@ -46,11 +66,3 @@ class PathFindingStrategy(ABC):
|
||||||
current = came_from[current]
|
current = came_from[current]
|
||||||
path.reverse()
|
path.reverse()
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ from typing import Optional
|
||||||
from source.models.base import Cell, Maze
|
from source.models.base import Cell, Maze
|
||||||
from source.strategy.algorithms import PathFindingStrategy
|
from source.strategy.algorithms import PathFindingStrategy
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------- #
|
||||||
|
# Моя азиатская жена называет меня расистом. Но как я могу #
|
||||||
|
# быть расистом, если я женился на женщине низшей расы?! #
|
||||||
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
def _manhattan(a: Cell, b: Cell) -> int:
|
def _manhattan(a: Cell, b: Cell) -> int:
|
||||||
"""Манхэттенское расстояние между двумя клетками."""
|
"""Манхэттенское расстояние между двумя клетками."""
|
||||||
|
|
@ -13,11 +18,10 @@ def _manhattan(a: Cell, b: Cell) -> int:
|
||||||
class AStarStrategy(PathFindingStrategy):
|
class AStarStrategy(PathFindingStrategy):
|
||||||
"""Алгоритм A* с манхэттенской эвристикой."""
|
"""Алгоритм A* с манхэттенской эвристикой."""
|
||||||
|
|
||||||
def find_path(self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None) -> list[Cell]:
|
def find_path(
|
||||||
if start is None:
|
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
|
||||||
start = maze.start
|
) -> list[Cell]:
|
||||||
if exit is None:
|
start, exit = self._validate(maze, start, exit)
|
||||||
exit = self.exit
|
|
||||||
|
|
||||||
g_score: dict[Cell, int] = {start: 0}
|
g_score: dict[Cell, int] = {start: 0}
|
||||||
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@ class BFSStrategy(PathFindingStrategy):
|
||||||
def find_path(
|
def find_path(
|
||||||
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
|
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
|
||||||
) -> list[Cell]:
|
) -> list[Cell]:
|
||||||
if start is None:
|
start, exit = self._validate(maze, start, exit)
|
||||||
start = maze.start
|
|
||||||
if exit is None:
|
|
||||||
exit = maze.exit
|
|
||||||
|
|
||||||
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
||||||
queue: deque[Cell] = deque([start])
|
queue: deque[Cell] = deque([start])
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ from typing import Optional
|
||||||
from source.models.base import Maze, Cell
|
from source.models.base import Maze, Cell
|
||||||
from source.strategy.algorithms import PathFindingStrategy
|
from source.strategy.algorithms import PathFindingStrategy
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------- #
|
||||||
|
# Как называется пресмыкающийся, который в прошлом был программистом? #
|
||||||
|
# ---------------------------------------------------------------------------- #
|
||||||
|
# крокодил #
|
||||||
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
class DFSStrategy(PathFindingStrategy):
|
class DFSStrategy(PathFindingStrategy):
|
||||||
"""Поиск в глубину (Depth-First Search).
|
"""Поиск в глубину (Depth-First Search).
|
||||||
|
|
@ -13,10 +19,7 @@ class DFSStrategy(PathFindingStrategy):
|
||||||
def find_path(
|
def find_path(
|
||||||
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
|
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
|
||||||
) -> list[Cell]:
|
) -> list[Cell]:
|
||||||
if start is None:
|
start, exit = self._validate(maze, start, exit)
|
||||||
start = maze.start
|
|
||||||
if exit is None:
|
|
||||||
exit = maze.exit
|
|
||||||
|
|
||||||
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
||||||
stack: list[Cell] = [start]
|
stack: list[Cell] = [start]
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ class SearchStats:
|
||||||
path_length: Длина найденного пути (0 если путь не найден).
|
path_length: Длина найденного пути (0 если путь не найден).
|
||||||
path: Найденный путь — список клеток от старта до выхода.
|
path: Найденный путь — список клеток от старта до выхода.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
elapsed_ms: float
|
elapsed_ms: float
|
||||||
visited_count: int
|
visited_count: int
|
||||||
path_length: int
|
path_length: int
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from source.models.base import Maze, Cell
|
||||||
# Игрок #
|
# Игрок #
|
||||||
# ---------------------------------------------------------------------------- #
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Player:
|
||||||
"""Хранит текущее положение игрока в лабиринте.
|
"""Хранит текущее положение игрока в лабиринте.
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ class Player:
|
||||||
# Интерфейс команды #
|
# Интерфейс команды #
|
||||||
# ---------------------------------------------------------------------------- #
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
class Command(ABC):
|
class Command(ABC):
|
||||||
"""Интерфейс команды с поддержкой отмены."""
|
"""Интерфейс команды с поддержкой отмены."""
|
||||||
|
|
||||||
|
|
@ -53,9 +55,9 @@ class Command(ABC):
|
||||||
|
|
||||||
DIRECTIONS = {
|
DIRECTIONS = {
|
||||||
"w": (0, -1),
|
"w": (0, -1),
|
||||||
"s": (0, 1),
|
"s": (0, 1),
|
||||||
"a": (-1, 0),
|
"a": (-1, 0),
|
||||||
"d": (1, 0),
|
"d": (1, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -77,11 +79,13 @@ class MoveCommand(Command):
|
||||||
ValueError: Если направление не распознано.
|
ValueError: Если направление не распознано.
|
||||||
"""
|
"""
|
||||||
if direction not in DIRECTIONS:
|
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._direction = direction
|
||||||
self._maze = maze
|
self._maze = maze
|
||||||
self._prev_cell: Optional[Cell] = None
|
self._prev_cell: Optional[Cell] = None
|
||||||
|
|
||||||
def execute(self) -> bool:
|
def execute(self) -> bool:
|
||||||
|
|
@ -99,7 +103,7 @@ class MoveCommand(Command):
|
||||||
if target is None or not target.is_possible():
|
if target is None or not target.is_possible():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._prev_cell = self._player.cell
|
self._prev_cell = self._player.cell
|
||||||
self._player.cell = target
|
self._player.cell = target
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -113,6 +117,7 @@ class MoveCommand(Command):
|
||||||
# История команд #
|
# История команд #
|
||||||
# ---------------------------------------------------------------------------- #
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
class CommandHistory:
|
class CommandHistory:
|
||||||
"""Хранит историю выполненных команд и позволяет отменять их.
|
"""Хранит историю выполненных команд и позволяет отменять их.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from source.settings import cell_mapping
|
||||||
# События #
|
# События #
|
||||||
# ---------------------------------------------------------------------------- #
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Event:
|
class Event:
|
||||||
"""Событие, передаваемое наблюдателям.
|
"""Событие, передаваемое наблюдателям.
|
||||||
|
|
@ -18,6 +19,7 @@ class Event:
|
||||||
type: Тип события ('maze_loaded', 'path_found', 'move', 'no_path').
|
type: Тип события ('maze_loaded', 'path_found', 'move', 'no_path').
|
||||||
payload: Дополнительные данные события.
|
payload: Дополнительные данные события.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
type: str
|
type: str
|
||||||
payload: dict = None
|
payload: dict = None
|
||||||
|
|
||||||
|
|
@ -26,6 +28,7 @@ class Event:
|
||||||
# Интерфейс наблюдателя #
|
# Интерфейс наблюдателя #
|
||||||
# ---------------------------------------------------------------------------- #
|
# ---------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
class Observer(ABC):
|
class Observer(ABC):
|
||||||
"""Интерфейс наблюдателя за событиями лабиринта."""
|
"""Интерфейс наблюдателя за событиями лабиринта."""
|
||||||
|
|
||||||
|
|
@ -42,12 +45,13 @@ class Observer(ABC):
|
||||||
# Консольный наблюдатель
|
# Консольный наблюдатель
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class ConsoleView(Observer):
|
class ConsoleView(Observer):
|
||||||
"""Отображает состояние лабиринта и события в консоли."""
|
"""Отображает состояние лабиринта и события в консоли."""
|
||||||
|
|
||||||
# Символ игрока на карте
|
# Символ игрока на карте
|
||||||
PLAYER_SYMBOL = "P"
|
PLAYER_SYMBOL = "P"
|
||||||
PATH_SYMBOL = "·"
|
PATH_SYMBOL = "·"
|
||||||
|
|
||||||
def update(self, event: Event) -> None:
|
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("Стартовая клетка не найдена!")
|
print("Стартовая клетка не найдена!")
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
player = Player(start)
|
player = Player(start)
|
||||||
history = CommandHistory()
|
history = CommandHistory()
|
||||||
view = ConsoleView()
|
view = ConsoleView()
|
||||||
|
|
||||||
view.update(Event("maze_loaded", {"maze": maze}))
|
view.update(Event("maze_loaded", {"maze": maze}))
|
||||||
print("Управление: w/a/s/d — движение, z — отмена, q — выход\n")
|
print("Управление: w/a/s/d — движение, z — отмена, q — выход\n")
|
||||||
|
|
@ -29,24 +29,29 @@ print("Управление: w/a/s/d — движение, z — отмена, q
|
||||||
while True:
|
while True:
|
||||||
key = input(">>> ").strip().lower()
|
key = input(">>> ").strip().lower()
|
||||||
|
|
||||||
if key == 'q':
|
if key == "q":
|
||||||
print("Выход.")
|
print("Выход.")
|
||||||
break
|
break
|
||||||
|
|
||||||
elif key == 'z':
|
elif key == "z":
|
||||||
if history.undo():
|
if history.undo():
|
||||||
print("Ход отменён.")
|
print("Ход отменён.")
|
||||||
view.render(maze, player=player.cell)
|
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)
|
cmd = MoveCommand(player, key, maze)
|
||||||
if cmd.execute():
|
if cmd.execute():
|
||||||
history.push(cmd)
|
history.push(cmd)
|
||||||
view.update(Event("move", {
|
view.update(
|
||||||
"maze": maze,
|
Event(
|
||||||
"player_cell": player.cell,
|
"move",
|
||||||
"direction": key,
|
{
|
||||||
}))
|
"maze": maze,
|
||||||
|
"player_cell": player.cell,
|
||||||
|
"direction": key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
if player.cell.is_exit:
|
if player.cell.is_exit:
|
||||||
print("Выход найден! Победа!")
|
print("Выход найден! Победа!")
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,114 @@ from source.models.base import Cell, Maze
|
||||||
from source.settings import cell_mapping
|
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):
|
def test_default_size(self):
|
||||||
"""Проверка размеров лабиринта со значениями по умолчанию"""
|
"""Проверка размеров лабиринта со значениями по умолчанию"""
|
||||||
maze = Maze()
|
maze = Maze()
|
||||||
|
|
@ -42,8 +148,8 @@ class TestMaze:
|
||||||
maze = Maze(size=(5, 5))
|
maze = Maze(size=(5, 5))
|
||||||
assert maze.get_cell(-1, 0) is None
|
assert maze.get_cell(-1, 0) is None
|
||||||
assert maze.get_cell(0, -1) is None
|
assert maze.get_cell(0, -1) is None
|
||||||
assert maze.get_cell(5, 0) is None
|
assert maze.get_cell(5, 0) is None
|
||||||
assert maze.get_cell(0, 5) is None
|
assert maze.get_cell(0, 5) is None
|
||||||
|
|
||||||
def test_center_has_four_neighbors(self):
|
def test_center_has_four_neighbors(self):
|
||||||
"""Проверка нахождения соседей"""
|
"""Проверка нахождения соседей"""
|
||||||
|
|
@ -58,32 +164,32 @@ class TestMaze:
|
||||||
def test_wall_excluded_from_neighbors(self):
|
def test_wall_excluded_from_neighbors(self):
|
||||||
"""Проверка что стена не попадает в список соседей"""
|
"""Проверка что стена не попадает в список соседей"""
|
||||||
maze = Maze(size=(5, 5))
|
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))
|
assert all(not n.is_wall for n in maze.get_neighbors(2, 2))
|
||||||
|
|
||||||
def test_setitem_wall(self):
|
def test_setitem_wall(self):
|
||||||
"""Проверка установки стены через оператор []"""
|
"""Проверка установки стены через оператор []"""
|
||||||
maze = Maze(size=(5, 5))
|
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
|
assert maze[0, 0].is_wall is True
|
||||||
|
|
||||||
def test_setitem_start(self):
|
def test_setitem_start(self):
|
||||||
"""Проверка установки старта через оператор []"""
|
"""Проверка установки старта через оператор []"""
|
||||||
maze = Maze(size=(5, 5))
|
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
|
assert maze[0, 0].is_start is True
|
||||||
|
|
||||||
def test_setitem_exit(self):
|
def test_setitem_exit(self):
|
||||||
"""Проверка установки выхода через оператор []"""
|
"""Проверка установки выхода через оператор []"""
|
||||||
maze = Maze(size=(5, 5))
|
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
|
assert maze[0, 0].is_exit is True
|
||||||
|
|
||||||
def test_setitem_empty_clears_flags(self):
|
def test_setitem_empty_clears_flags(self):
|
||||||
"""Проверка сброса флагов клетки при установке пустого типа"""
|
"""Проверка сброса флагов клетки при установке пустого типа"""
|
||||||
maze = Maze(size=(5, 5))
|
maze = Maze(size=(5, 5))
|
||||||
maze[0, 0] = cell_mapping['wall']
|
maze[0, 0] = cell_mapping["wall"]
|
||||||
maze[0, 0] = cell_mapping['empty']
|
maze[0, 0] = cell_mapping["empty"]
|
||||||
assert not maze[0, 0].is_wall
|
assert not maze[0, 0].is_wall
|
||||||
|
|
||||||
def test_getitem_out_of_bounds_raises(self):
|
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