2026-rff_mp/skorohodovsa/task_2/docs.md
2026-05-25 10:23:00 +03:00

62 KiB
Raw Blame History

файл является компиляцией документации sphinx в папке docs

Лабораторная работа «Поиск выхода из лабиринта»

Этап 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 8build_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.030.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A* давала преимущество.

50×50 (тупики)

BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A* показывает время между BFS и DFS.

100×100 (запутанный, spaghetti)

Наиболее показательные результаты:

Стратегия Время (мс) Длина пути
BFS ~9 ~210
DFS ~7 ~2200
A* ~8 ~210

DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A* находят кратчайший путь, A* при этом чуть быстрее за счёт эвристики.

30×30 (пустой)

Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных.

A* на пустом лабиринте медленнее всех — накладные расходы на heapq и вычисление эвристики не окупаются когда препятствий нет.

Выводы

  • BFS — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо.
  • DFS — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути.
  • A* — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь.

Визуализация

!results.png

Этап 7. Отчёт

Описание задачи

Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF:

Паттерн Где применён
Builder TextFileBuilder
Strategy BFSStrategy, DFSStrategy, AStarStrategy
Observer ConsoleView
Command MoveCommand, CommandHistory

Диаграмма классов

Результаты экспериментов

Лабиринт Быстрее всех Кратчайший путь
10×10 path все одинаково все одинаково
50×50 deadends BFS BFS = A*
100×100 spaghetti DFS BFS = A*
30×30 empty DFS BFS = A*
  • BFS — надёжный выбор, всегда кратчайший путь.
  • DFS — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток.
  • A* — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на heapq.

Выводы

Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода.

Задание

Цель работы

Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.

Общая схема приложения (пример)

Выполнение

Этап 1. Модель лабиринта (без паттернов, просто классы)

Задача: Создать классы Cell и Maze, которые представляют карту лабиринта.

  • Cell хранит координаты (x, y), флаги isWall, isStart, isExit, метод isPassable() (возвращает True для прохода, если не стена).
  • Maze хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: getCell(x, y), getNeighbors(cell) возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена).

Результат: Лабиринт можно создать вручную в коде, но загрузку пока не делаем.

Этап 2. Загрузка лабиринта из файла применение паттерна Builder

Задача: Реализовать загрузку лабиринта из текстового файла, где # стена, (пробел) проход, S старт, E выход.

  • Создать интерфейс MazeBuilder с методом buildFromFile(filename).
  • Реализовать класс TextFileMazeBuilder, который читает файл, парсит символы, создаёт объекты Cell, задаёт координаты и флаги, после чего возвращает готовый Maze.

Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию MazeBuilder.

Этап 3. Стратегии поиска пути паттерн Strategy

Задача: Реализовать семейство алгоритмов поиска пути от старта до выхода.

  • Создать интерфейс PathFindingStrategy с методом findPath(maze, start, exit), возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет.
  • Реализовать минимум 3 стратегии:
    • BFS (поиск в ширину) гарантирует кратчайший путь по количеству шагов.
    • DFS (поиск в глубину) быстрый, но не обязательно кратчайший.
    • A* (с эвристикой, например, манхэттенское расстояние) компромисс между скоростью и оптимальностью.
    • (Опционально) Дейкстра полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS.

Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A* приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток.

Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс.

Этап 4. Класс-оркестратор MazeSolver (использует Strategy)

Задача: Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику.

  • MazeSolver содержит поля maze и strategy.
  • Метод setStrategy(strategy) для динамической смены алгоритма.
  • Метод solve() вызывает strategy.findPath(...) и возвращает объект SearchStats (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути).
  • Для замера времени используйте time.perf_counter() до и после вызова стратегии.

Этап 5. Визуализация и пошаговое управление паттерны Observer и Command (по желанию)

5.1. Наблюдатель (Observer) обновление консольного интерфейса.

  • Создать интерфейс Observer с методом update(event), где event может быть строкой или объектом с типом события ("path_found", "move", "maze_loaded").
  • Реализовать класс ConsoleView, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод render(maze, player_position, path) рисует карту в консоли.
  • MazeSolver (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния.

5.2. Команда (Command) для пошагового перемещения игрока по найденному пути (или ручного управления).

  • Создать интерфейс Command с методами execute() и undo().
  • Реализовать MoveCommand, который принимает игрока (Player), направление и изменяет его позицию, сохраняя предыдущую для отмены.
  • Создать класс Player, хранящий текущую клетку.
  • Консольное меню позволяет вводить команды (W/A/S/D), выполнять MoveCommand, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн.

Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command для демонстрации undo при ручном исследовании лабиринта.

Этап 6. Экспериментальная часть (аналогично заданию со структурами данных)

Задача: Сравнить эффективность реализованных стратегий на лабиринтах разной сложности.

  1. Подготовка тестовых лабиринтов:
    • Маленький (10×10) с простым путём.
    • Средний (50×50) с тупиками.
    • Большой (100×100) с запутанной структурой.
    • «Пустой» лабиринт (без стен) для демонстрации максимальной производительности.
    • «Без выхода» чтобы проверить обработку отсутствия пути.
  2. Замеры:
    • Для каждого лабиринта и каждой стратегии запустить solve() 510 раз, усреднить время, количество посещённых клеток, длину пути.
    • Записать результаты в CSV: лабиринт,стратегия,время_мс,посещено_клеток,длина_пути.
  3. Анализ:
    • Построить графики для каждого лабиринта.
    • Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
  4. Дополнительное задание: Реализовать взвешенные клетки (например, болото вес 3, песок вес 2, асфальт вес 1) и сравнить Дейкстру с A* на взвешенном графе.

Этап 7. Отчёт

Структура отчёта:

  1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
  2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
  3. Результаты экспериментов (таблицы, графики).
  4. Анализ эффективности алгоритмов и применимости паттернов.
  5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.

Советы

  • Для A* самая простая эвристика: abs(x1 - x2) + abs(y1 - y2).
  • При поиске пути надо хранить предшественников (parent для каждой посещённой клетки), чтобы восстановить путь.
  • Для BFS/DFS используй deque (очередь) и list (стек).
  • Визуализацию в консоли можно сделать с помощью os.system('cls' if os.name == 'nt' else 'clear') для перерисовки.

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

Найденный путь — список клеток от старта до выхода.

__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 Начальная клетка игрока.