62 KiB
файл является компиляцией документации sphinx в папке docs
Лабораторная работа «Поиск выхода из лабиринта»
- Задание
- Этап 1. Модель лабиринта
- Этап 2. Загрузка лабиринта из файла
- Этап 3. Стратегии поиска пути
- Этап 4. Класс-оркестратор MazeSolver
- Этап 5. Визуализация и пошаговое управление
- Этап 6. Экспериментальная часть
- Этап 7. Отчёт
- API Reference
Этап 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
Этап 1. Модель лабиринта
В первом этапе разработки необходимо создать базовые классы Cell и Maze, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы.
Класс Cell
Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая.
По условию задания клетка должна иметь флаги isWall, isStart, isExit и метод isPassable(). В реализации флаги оформлены как свойства (@property) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа.
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__, что позволяет обращаться к клеткам естественным образом:
maze[0, 0] = cell_mapping['wall'] # установить стену
cell = maze[2, 3] # получить клетку
Обратите внимание: индексация идёт в формате [row, col], то есть сначала строка (Y), потом столбец (X) — аналогично numpy.
Свойства start и exit
Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода:
maze.start # Cell или None
maze.exit # Cell или None
Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают start и exit автоматически из лабиринта.
Свойство shape
По аналогии с numpy добавлено свойство shape, возвращающее (height, width):
rows, cols = maze.shape
Используется в стратегиях поиска и тестах для итерации по лабиринту.
get_neighbors
Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает None.
neighbors = maze.get_neighbors(2, 2) # список Cell
Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS).
Этап 2. Загрузка лабиринта из файла
Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна Builder.
Паттерн Builder
Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод build_from_file(), внутри которого сосредоточена вся логика построения.
Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию MazeBuilder без изменения остального кода.
Класс MazeBuilder
Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод build_from_file(), который обязан реализовать каждый конкретный строитель.
По условию задания интерфейс назывался MazeBuilder с методом buildFromFile. В реализации название метода приведено к PEP 8 — build_from_file. Сам класс оформлен через ABC — попытка создать объект MazeBuilder() напрямую вызовет TypeError.
Класс TextFileBuilder
Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из .txt файла где # — стена, — проход, S — старт, E — выход.
Процесс построения разбит на три приватных шага:
_read_file
Читает файл построчно и обрезает символы переноса строки \n и \r. Возвращает список строк — каждая строка соответствует одной строке лабиринта.
_test_text_maze
Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и _create_maze выбросит ValueError.
Реализован как @staticmethod — не использует состояние объекта, только входные данные.
_create_maze
Создаёт объект Maze нужного размера и заполняет его клетки символами из файла через maze[y, x] = symbol. Тип каждой клетки определяется автоматически через cell_mapping в __setitem__ лабиринта.
Использование
from source.build.builder import TextFileBuilder
maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt')
print(maze)
Известная ошибка
В текущей реализации _create_maze есть опечатка при вычислении width:
height, width = len(text_maze), len(text_maze) # width всегда равен height
Правильная версия:
height, width = len(text_maze), len(text_maze[0])
На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат.
Этап 3. Стратегии поиска пути
В третьем этапе реализованы алгоритмы поиска пути с применением паттерна Strategy.
Паттерн Strategy
Все три алгоритма реализуют один интерфейс PathFindingStrategy. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии:
solver = MazeSolver(maze, BFSStrategy())
solver.set_strategy(AStarStrategy())
Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно.
Структура пакета
Стратегии разбиты по отдельным файлам, а __init__.py собирает всё в один импорт:
source/strategy/
├── __init__.py ← единственный импорт для пользователя
├── algorithms.py ← базовый класс PathFindingStrategy
├── bfs.py
├── dfs.py
└── astar.py
from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy
Класс PathFindingStrategy
Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод find_path() и содержит два вспомогательных метода, общих для всех стратегий.
По условию задания метод назывался findPath — приведён к PEP 8 как find_path.
_validate
Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа noexit: алгоритм падал с AttributeError внутри, вместо понятного сообщения.
_validate подставляет start и exit из лабиринта если они не переданы явно, и выбрасывает ValueError с понятным сообщением если клетки не найдены:
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:
heapq.heappush(open_heap, (f, counter, neighbor))
Сложность: O(E · log V) в худшем случае.
Этап 4. Класс-оркестратор MazeSolver
В четвёртом этапе реализован класс MazeSolver, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику.
Роль в архитектуре
MazeSolver — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время:
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 в действии — один объект, разные алгоритмы:
solver = MazeSolver(maze, BFSStrategy())
stats_bfs = solver.solve()
solver.set_strategy(AStarStrategy())
stats_astar = solver.solve()
solve
Замеряет время через time.perf_counter() — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000.
start и exit можно не передавать — стратегия найдёт их сама через _validate. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки.
Этап 5. Визуализация и пошаговое управление
В пятом этапе реализованы два паттерна: Observer для отображения событий и Command для пошагового управления игроком.
5.1. Паттерн Observer
Идея
MazeSolver и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, FileLogger или графический интерфейс без изменения основного кода.
Класс Event
Оформлен через @dataclass. Хранит тип события строкой и словарь payload с дополнительными данными. Поддерживаются четыре типа событий:
| Тип | Когда генерируется |
|---|---|
maze_loaded |
Лабиринт загружен из файла |
path_found |
Алгоритм нашёл путь |
no_path |
Путь не найден |
move |
Игрок сделал ход |
Класс Observer
Абстрактный базовый класс с единственным методом update(event). Любой наблюдатель обязан его реализовать.
Класс ConsoleView
Конкретная реализация наблюдателя. Обрабатывает события через match/case и вызывает render() для перерисовки лабиринта.
Метод render() принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в set для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе:
path_set = set(path) if path else set()
Лабиринт обрамляется рамкой из + и ─ для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики:
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 на уровне модуля:
DIRECTIONS = {
"w": (0, -1), # вверх
"s": (0, 1), # вниз
"a": (-1, 0), # влево
"d": (1, 0), # вправо
}
Класс CommandHistory
Стек выполненных команд. Хранит только успешные ходы — неуспешные (execute() вернул False) в историю не добавляются. undo() снимает последнюю команду со стека и вызывает её undo().
Пример игрового цикла:
cmd = MoveCommand(player, 'd', maze)
if cmd.execute():
history.push(cmd) # добавляем только успешный ход
history.undo() # отмена последнего хода
Этап 6. Экспериментальная часть
В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (practice/main.ipynb).
Подготовка
Лабиринты загружаются из папки source/templates автоматически — все файлы считываются через os.listdir и передаются в TextFileBuilder. Стратегии собраны в словарь для удобной итерации:
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 за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь.
Визуализация
Этап 7. Отчёт
Описание задачи
Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF:
| Паттерн | Где применён |
|---|---|
| Builder | TextFileBuilder |
| Strategy | BFSStrategy, DFSStrategy, AStarStrategy |
| Observer | ConsoleView |
| Command | MoveCommand, CommandHistory |
Диаграмма классов
Результаты экспериментов
| Лабиринт | Быстрее всех | Кратчайший путь |
|---|---|---|
| 10×10 path | все одинаково | все одинаково |
| 50×50 deadends | BFS | BFS = A* |
| 100×100 spaghetti | DFS | BFS = A* |
| 30×30 empty | DFS | BFS = A* |
- BFS — надёжный выбор, всегда кратчайший путь.
- DFS — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток.
- A* — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на
heapq.
Выводы
Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода.
Задание
Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
Общая схема приложения (пример)
Выполнение
Этап 1. Модель лабиринта (без паттернов, просто классы)
Задача: Создать классы Cell и Maze, которые представляют карту лабиринта.
Cellхранит координаты (x, y), флагиisWall,isStart,isExit, методisPassable()(возвращаетTrueдля прохода, если не стена).Mazeхранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы:getCell(x, y),getNeighbors(cell)– возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена).
Результат: Лабиринт можно создать вручную в коде, но загрузку пока не делаем.
Этап 2. Загрузка лабиринта из файла – применение паттерна Builder
Задача: Реализовать загрузку лабиринта из текстового файла, где # – стена, (пробел) – проход, S – старт, E – выход.
- Создать интерфейс
MazeBuilderс методомbuildFromFile(filename). - Реализовать класс
TextFileMazeBuilder, который читает файл, парсит символы, создаёт объектыCell, задаёт координаты и флаги, после чего возвращает готовыйMaze.
Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию MazeBuilder.
Этап 3. Стратегии поиска пути – паттерн Strategy
Задача: Реализовать семейство алгоритмов поиска пути от старта до выхода.
- Создать интерфейс
PathFindingStrategyс методомfindPath(maze, start, exit), возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет. - Реализовать минимум 3 стратегии:
- BFS (поиск в ширину) – гарантирует кратчайший путь по количеству шагов.
- DFS (поиск в глубину) – быстрый, но не обязательно кратчайший.
- A* (с эвристикой, например, манхэттенское расстояние) – компромисс между скоростью и оптимальностью.
- (Опционально) Дейкстра – полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS.
Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A* – приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток.
Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс.
Этап 4. Класс-оркестратор – MazeSolver (использует Strategy)
Задача: Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику.
MazeSolverсодержит поляmazeиstrategy.- Метод
setStrategy(strategy)для динамической смены алгоритма. - Метод
solve()вызываетstrategy.findPath(...)и возвращает объектSearchStats(время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути). - Для замера времени используйте
time.perf_counter()до и после вызова стратегии.
Этап 5. Визуализация и пошаговое управление – паттерны Observer и Command (по желанию)
5.1. Наблюдатель (Observer) – обновление консольного интерфейса.
- Создать интерфейс
Observerс методомupdate(event), гдеeventможет быть строкой или объектом с типом события ("path_found","move","maze_loaded"). - Реализовать класс
ConsoleView, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Методrender(maze, player_position, path)рисует карту в консоли. MazeSolver(или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния.
5.2. Команда (Command) – для пошагового перемещения игрока по найденному пути (или ручного управления).
- Создать интерфейс
Commandс методамиexecute()иundo(). - Реализовать
MoveCommand, который принимает игрока (Player), направление и изменяет его позицию, сохраняя предыдущую для отмены. - Создать класс
Player, хранящий текущую клетку. - Консольное меню позволяет вводить команды (W/A/S/D), выполнять
MoveCommand, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн.
Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command – для демонстрации undo при ручном исследовании лабиринта.
Этап 6. Экспериментальная часть (аналогично заданию со структурами данных)
Задача: Сравнить эффективность реализованных стратегий на лабиринтах разной сложности.
- Подготовка тестовых лабиринтов:
- Маленький (10×10) с простым путём.
- Средний (50×50) с тупиками.
- Большой (100×100) с запутанной структурой.
- «Пустой» лабиринт (без стен) – для демонстрации максимальной производительности.
- «Без выхода» – чтобы проверить обработку отсутствия пути.
- Замеры:
- Для каждого лабиринта и каждой стратегии запустить
solve()5–10 раз, усреднить время, количество посещённых клеток, длину пути. - Записать результаты в CSV:
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути.
- Для каждого лабиринта и каждой стратегии запустить
- Анализ:
- Построить графики для каждого лабиринта.
- Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
- Дополнительное задание: Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A* на взвешенном графе.
Этап 7. Отчёт
Структура отчёта:
- Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
- Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
- Результаты экспериментов (таблицы, графики).
- Анализ эффективности алгоритмов и применимости паттернов.
- Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.
Советы
- Для A* самая простая эвристика:
abs(x1 - x2) + abs(y1 - y2). - При поиске пути надо хранить предшественников (
parentдля каждой посещённой клетки), чтобы восстановить путь. - Для BFS/DFS используй
deque(очередь) иlist(стек). - Визуализацию в консоли можно сделать с помощью
os.system('cls' if os.name == 'nt' else 'clear')для перерисовки.
API Reference
Базовые модели
class source.models.base.Cell(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False)
Базовые классы: object
Представляет одну клетку поля лабиринта.
Каждая клетка хранит свои координаты и один из четырёх возможных типов: стена, старт, выход или пустая клетка. Типы взаимоисключают друг друга: установка одного автоматически сбрасывает остальные.
__init__(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False)
Инициализирует клетку с заданными координатами и типом.
- Параметры:
- x – Координата клетки по оси X.
- y – Координата клетки по оси Y.
- is_wall – Если True — клетка является стеной.
- is_start – Если True — клетка является стартом.
- is_exit – Если True — клетка является выходом.
property is_exit : bool
True, если клетка является выходом из лабиринта.
is_possible() → bool
Проверяет, можно ли переместиться в эту клетку.
- Результат: True, если клетка не является стеной, иначе False.
property is_start : bool
True, если клетка является стартовой позицией.
property is_wall : bool
True, если клетка является стеной.
class source.models.base.Maze(size: tuple[int, int] = (10, 10))
Базовые классы: object
Представляет двумерный лабиринт из клеток Cell.
Лабиринт хранится как список списков клеток. Доступ к отдельным клеткам и их изменение возможны через индексацию вида maze[row, col].
__init__(size: tuple[int, int] = (10, 10))
Создаёт пустой лабиринт заданного размера.
- Параметры: size – Кортеж (width, height) — ширина и высота лабиринта в клетках.
property exit : Cell | None
get_cell(x: int, y: int) → Cell | None
Возвращает клетку по координатам или None, если координаты вне границ.
- Параметры:
- x – Координата по оси X.
- y – Координата по оси Y.
- Результат: Объект Cell, если координаты корректны, иначе None.
get_neighbors(x: int, y: int) → list[Cell] | None
Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево).
- Параметры:
- x – Координата клетки по оси X.
- y – Координата клетки по оси Y.
- Результат: Список проходимых соседних клеток, или None если (x, y) вне границ.
property shape : tuple[int, int]
property start : Cell | None
Загрузка лабиринта
class source.build.builder.MazeBuilder
Базовые классы: ABC
abstractmethod build_from_file(filename: str) → Maze
Возвращает объект лабиринта по указанному пути файлу.
- Параметры: filename (str) – Путь к файлу
- Исключение: TypeError – Если введен путь файла с нерассмотренным расширением
- Результат: Объект лабиринта
- Тип результата: Maze
class source.build.builder.TextFileBuilder
Базовые классы: MazeBuilder
build_from_file(filename: str) → Maze
Возвращает объект лабиринта по указанному пути файлу.
- Параметры: filename (str) – Путь к файлу
- Исключение: TypeError – Если введен путь файла с нерассмотренным расширением
- Результат: Объект лабиринта
- Тип результата: Maze
Стратегии поиска пути
class source.strategy.algorithms.PathFindingStrategy
Базовые классы: ABC
Интерфейс стратегии поиска пути в лабиринте.
abstractmethod find_path(maze: Maze, start: Cell = None, exit: Cell = None) → list[Cell]
Найти путь от start до exit.
- Параметры:
- maze – Объект лабиринта.
- start – Стартовая клетка.
- exit – Целевая клетка.
- Результат: Список клеток пути (от start до exit включительно). Пустой список, если путь не найден.
class source.strategy.bfs.BFSStrategy
Базовые классы: PathFindingStrategy
Поиск в ширину (Breadth-First Search).
Гарантирует кратчайший путь по количеству шагов. Сложность: O(V + E) по времени и памяти.
find_path(maze: Maze, start: Cell | None = None, exit: Cell | None = None) → list[Cell]
Найти путь от start до exit.
- Параметры:
- maze – Объект лабиринта.
- start – Стартовая клетка.
- exit – Целевая клетка.
- Результат: Список клеток пути (от start до exit включительно). Пустой список, если путь не найден.
class source.strategy.dfs.DFSStrategy
Базовые классы: PathFindingStrategy
Поиск в глубину (Depth-First Search).
Находит путь, но не гарантирует кратчайший.
find_path(maze: Maze, start: Cell | None = None, exit: Cell | None = None) → list[Cell]
Найти путь от start до exit.
- Параметры:
- maze – Объект лабиринта.
- start – Стартовая клетка.
- exit – Целевая клетка.
- Результат: Список клеток пути (от start до exit включительно). Пустой список, если путь не найден.
class source.strategy.astar.AStarStrategy
Базовые классы: PathFindingStrategy
Алгоритм A* с манхэттенской эвристикой.
find_path(maze: Maze, start: Cell | None = None, exit: Cell | None = None) → list[Cell]
Найти путь от start до exit.
- Параметры:
- maze – Объект лабиринта.
- start – Стартовая клетка.
- exit – Целевая клетка.
- Результат: Список клеток пути (от start до exit включительно). Пустой список, если путь не найден.
Оркестратор
class source.strategy.solver.MazeSolver(maze: Maze, strategy: PathFindingStrategy)
Базовые классы: object
Оркестратор поиска пути в лабиринте.
Принимает лабиринт и стратегию поиска, выполняет поиск и возвращает результат вместе со статистикой выполнения.
Пример
solver = MazeSolver(maze, BFSStrategy()) stats = solver.solve() print(stats)
solver.set_strategy(AStarStrategy()) stats = solver.solve()
__init__(maze: Maze, strategy: PathFindingStrategy) → None
Инициализирует солвер с лабиринтом и стратегией поиска.
- Параметры:
- maze – Объект лабиринта.
- strategy – Стратегия поиска пути.
set_strategy(strategy: PathFindingStrategy) → None
Заменяет текущую стратегию поиска.
- Параметры: strategy – Новая стратегия поиска пути.
solve(start: Cell = None, exit: Cell = None) → 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])
Базовые классы: object
Статистика выполнения поиска пути.
elapsed_ms
Время выполнения в миллисекундах.
- Type: float
visited_count
Количество посещённых клеток.
- Type: int
path_length
Длина найденного пути (0 если путь не найден).
- Type: int
path
Найденный путь — список клеток от старта до выхода.
- Type: list[source.models.base.Cell]
__init__(elapsed_ms: float, visited_count: int, path_length: int, path: list[Cell]) → None
elapsed_ms : float
path : list[Cell]
path_length : int
visited_count : int
Визуализация
class source.view.observer.ConsoleView
Базовые классы: Observer
Отображает состояние лабиринта и события в консоли.
PATH_SYMBOL = '·'
PLAYER_SYMBOL = 'P'
render(maze: Maze, player: Cell | None = None, path: list[Cell] | None = None) → None
Рисует лабиринт в консоли.
Путь отмечается символом „·“, позиция игрока — „P“.
- Параметры:
- maze – Объект лабиринта.
- player – Текущая клетка игрока (опционально).
- path – Список клеток найденного пути (опционально).
update(event: 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) → 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) → None
Добавляет выполненную команду в историю.
- Параметры: command – Успешно выполненная команда.
undo() → bool
Отменяет последнюю команду из истории.
- Результат: True если отмена выполнена, False если история пуста.
class source.view.command.MoveCommand(player: Player, direction: str, maze: Maze)
Базовые классы: Command
Перемещает игрока в заданном направлении.
Сохраняет предыдущую клетку для возможности отмены хода.
__init__(player: Player, direction: str, maze: Maze) → None
Инициализирует команду перемещения.
- Параметры:
- player – Объект игрока.
- direction – Направление („w“, „a“, „s“, „d“).
- maze – Объект лабиринта для проверки проходимости.
- Исключение: ValueError – Если направление не распознано.
execute() → bool
Перемещает игрока если целевая клетка проходима.
- Результат: True если перемещение выполнено, False если клетка непроходима.
undo() → None
Возвращает игрока на предыдущую клетку.
class source.view.command.Player(cell: Cell)
Базовые классы: object
Хранит текущее положение игрока в лабиринте.
cell
Текущая клетка игрока.
__init__(cell: Cell) → None
Инициализирует игрока на заданной клетке.
- Параметры: cell – Начальная клетка игрока.
