10 KiB
Отчёт: Задание 2 — Поиск выхода из лабиринта
Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов
Выбранные паттерны и их обоснование
Builder
Для загрузки лабиринта из файла был использован паттерн Builder. Создан интерфейс: class MazeBuilder(): и его реализация: class TextFileMazeBuilder(MazeBuilder): Преимущества использования Builder: пользоватеь не знает деталей создания лабиринта; можно добавить новые форматы (JSON, XML, бинарный); код загрузки изолирован от остальной программы.
Strategy
Для алгоритмов поиска пути использован паттерн Strategy Создан общий интерфейс: class PathFindingStrategy(): Реализованы стратегии: BFSStrategy; DFSStrategy; AStrategy; Каждая стратегия реализует собственный алгоритм поиска пути по правилам. Преимущества паттерна: алгоритмы можно менять во время выполнения; код MazeSolver не зависит от конкретного алгоритма; новые алгоритмы можно добавлять без изменения существующего кода.
Observer
Для уведомления интерфейса о событиях использован паттерн Observer Создан интерфейс: class Observer(): и реализация: class ConsoleView(Observer): MazeSolver хранит список наблюдателей и уведомляет их о событиях: начало поиска; окончание поиска; перемещение игрока. Преимущества: логика интерфейса отделена от логики поиска; можно легко добавить графический интерфейс;
Command
Для пошагового перемещения игрока использован паттерн Command. Создан интерфейс: class Command(): и реализация: class MoveCommand(Command): Каждая команда умеет: execute() — выполнить действие; undo() — отменить действие Преимущества: поддержка undo; возможность расширения системы команд
Листинги ключевых классов
Паттерн Strategy
class PathFindingStrategy: def findPath(self, maze, start, exit): pass
class BFSStrategy(PathFindingStrategy): def findPath(self, maze, start, exit): if exit is None: return [] queue = deque([start]) visited = {start} parent = {start: None}
while queue:
current = queue.popleft()
if current == exit:
return self._reconstruct_path(parent, start, exit)
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return []
class AStrategy(PathFindingStrategy): def _heuristic(self, cell, exit): if exit is None: return 0 return abs(cell.x - exit.x) + abs(cell.y - exit.y)
def findPath(self, maze, start, exit):
if exit is None:
return []
open_set = []
heapq.heappush(open_set, (0, start))
came_from = {start: None}
g_score = {start: 0}
while open_set:
current = heapq.heappop(open_set)[1]
if current == exit:
return self._reconstruct_path(came_from, start, exit)
for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + self._heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score, neighbor))
return []
Паттерн Command
class Command: def execute(self): pass def undo(self): pass
class MoveCommand(Command): def init(self, player, direction, maze): self.player = player self.dx, self.dy = direction self.maze = maze self.executed = False
def execute(self):
new_x = self.player.currentCell.x + self.dx
new_y = self.player.currentCell.y + self.dy
new_cell = self.maze.getCell(new_x, new_y)
if new_cell and new_cell.isPassable():
self.player.moveTo(new_cell)
self.executed = True
return True
return False
def undo(self):
if self.executed:
self.player.undoMove()
self.executed = False
return True
return False
Результаты
| Лабиринт | Стратегия | Время (с) | Посещено | Длина пути | Путь найден |
|---|---|---|---|---|---|
| маленький (10x10) | BFS | 0.9148200158961117 | 19.0 | 19.0 | True |
| маленький (10x10) | DFS | 0.717819994315505 | 39.0 | 39.0 | True |
| маленький (10x10) | A | 1.577159995213151 | 19.0 | 19.0 | True |
| средний (50x50) | BFS | 14.496059995144606 | 99.0 | 99.0 | True |
| средний (50x50) | DFS | 8.470179990399629 | 393.0 | 393.0 | True |
| средний (50x50) | A | 9.11291999509558 | 99.0 | 99.0 | True |
| большой (100x100) | BFS | 0.013179995585232973 | 0.0 | 0.0 | False |
| большой (100x100) | A | 0.013079994823783636 | 0.0 | 0.0 | False |
| пустой (50x50) | BFS | 29.2012800113298 | 99.0 | 99.0 | True |
| пустой (50x50) | DFS | 13.176999986171722 | 1275.0 | 1275.0 | True |
| пустой (50x50) | A | 50.366899999789894 | 99.0 | 99.0 | True |
| без выхода (20x20) | BFS | 0.004239997360855341 | 0.0 | 0.0 | False |
| без выхода (20x20) | DFS | 0.006399990525096655 | 0.0 | 0.0 | False |
| без выхода (20x20) | A | 0.008680007886141539 | 0.0 | 0.0 | False |
Графики
Анализ эффективности алгоритмов
В ходе экспериментов были получены следующие результаты.
BFS
Преимущества: всегда находит кратчайший путь; простая реализация. Недостатки: посещает большое количество клеток; требует много памяти. Выходит, что наиболее эффективен в небольших невзвешенных лабиринтах.
DFS
Преимущества: простая реализация; самым быстрым находит произвольный путь. Недостатки: не гарантирует кратчайший путь; может уходить в тупики. Подходит для быстрого поиска любого решения.
A
Преимущества: высокая скорость; посещает меньше клеток; Недостатки: требует выбора хорошей эвристики. Показал хорошие результаты на больших лабиринтах.
Анализ применимости паттернов
Builder
Без Builder код загрузки лабиринта был бы жёстко связан с классом Maze, а добавление нового формата потребовало бы изменения существующего кода. Strategy Без Strategy пришлось бы: хранить все алгоритмы внутри одного класса; использовать большое количество условных операторов; изменять код MazeSolver при добавлении новых алгоритмов Strategy помог полностью отделить алгоритмы друг от друга.
Observer
Без Observer логика интерфейса смешивалась бы с логикой поиска. Это усложнило бы: добавление GUI; логирование; визуализацию.
Command
Без Command было бы сложно реализовать: undo; историю действий; расширяемую систему управления.
Выводы
В проекте были успешно реализованы:
загрузка лабиринта из файла; несколько алгоритмов поиска пути; визуализация; система наблюдателей; система команд; экспериментальное сравнение алгоритмов.
Использование паттернов GoF позволило:
сделать архитектуру гибкой; уменьшить связанность компонентов; упростить расширение программы; облегчить сопровождение кода.

