""" Этап 3: Паттерн Strategy — алгоритмы поиска пути. Зачем Strategy? Позволяет менять алгоритм поиска во время выполнения без изменения остального кода. Добавить новый алгоритм = написать новый класс. """ from abc import ABC, abstractmethod from collections import deque import heapq from maze_model import Cell, Maze class PathFindingStrategy(ABC): """Интерфейс стратегии поиска пути.""" @abstractmethod def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]: """ Возвращает список клеток от старта до выхода (включительно). Пустой список — если путь не найден. Счётчик посещённых клеток сохраняется в self.visited_count. """ ... # Вспомогательный метод восстановления пути по словарю предшественников @staticmethod def _reconstruct_path(came_from: dict, start: Cell, goal: Cell) -> list[Cell]: path = [] current = goal while current != start: path.append(current) current = came_from[current] path.append(start) path.reverse() return path # ── BFS ────────────────────────────────────────────────────────────────────── class BFSStrategy(PathFindingStrategy): """ Поиск в ширину (BFS). Гарантирует кратчайший путь по числу шагов. Использует очередь (deque). """ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]: queue = deque([start]) came_from: dict[Cell, Cell | None] = {start: None} self.visited_count = 0 while queue: current = queue.popleft() self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(came_from, start, exit_cell) for neighbor in maze.get_neighbors(current): if neighbor not in came_from: came_from[neighbor] = current queue.append(neighbor) return [] # путь не найден # ── DFS ────────────────────────────────────────────────────────────────────── class DFSStrategy(PathFindingStrategy): """ Поиск в глубину (DFS). Быстр, но не гарантирует кратчайший путь. Использует стек (list). """ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]: stack = [start] came_from: dict[Cell, Cell | None] = {start: None} self.visited_count = 0 while stack: current = stack.pop() self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(came_from, start, exit_cell) for neighbor in maze.get_neighbors(current): if neighbor not in came_from: came_from[neighbor] = current stack.append(neighbor) return [] # ── A* ─────────────────────────────────────────────────────────────────────── class AStarStrategy(PathFindingStrategy): """ Алгоритм A* с манхэттенской эвристикой. Компромисс между BFS (оптимальность) и скоростью. Использует приоритетную очередь (heapq). """ @staticmethod def _heuristic(a: Cell, b: Cell) -> int: return abs(a.x - b.x) + abs(a.y - b.y) def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]: # (f_score, уникальный_счётчик, клетка) — счётчик нужен для стабильного сравнения counter = 0 open_heap = [(0, counter, start)] came_from: dict[Cell, Cell | None] = {start: None} g_score: dict[Cell, int] = {start: 0} self.visited_count = 0 while open_heap: _, _, current = heapq.heappop(open_heap) self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(came_from, start, exit_cell) for neighbor in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if tentative_g < g_score.get(neighbor, float("inf")): g_score[neighbor] = tentative_g came_from[neighbor] = current f = tentative_g + self._heuristic(neighbor, exit_cell) counter += 1 heapq.heappush(open_heap, (f, counter, neighbor)) return [] # ── Dijkstra ───────────────────────────────────────────────────────────────── class DijkstraStrategy(PathFindingStrategy): """ Алгоритм Дейкстры. В базовом лабиринте (все веса = 1) совпадает с BFS, но полезен при взвешенных клетках (болото, песок и т.д.). """ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]: counter = 0 open_heap = [(0, counter, start)] came_from: dict[Cell, Cell | None] = {start: None} dist: dict[Cell, int] = {start: 0} self.visited_count = 0 while open_heap: cost, _, current = heapq.heappop(open_heap) self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(came_from, start, exit_cell) if cost > dist.get(current, float("inf")): continue # устаревшая запись for neighbor in maze.get_neighbors(current): # Вес клетки: можно расширить через cell.weight weight = getattr(neighbor, "weight", 1) new_cost = dist[current] + weight if new_cost < dist.get(neighbor, float("inf")): dist[neighbor] = new_cost came_from[neighbor] = current counter += 1 heapq.heappush(open_heap, (new_cost, counter, neighbor)) return []