import heapq import time from abc import ABC, abstractmethod from collections import deque from dataclasses import dataclass, field from typing import List, Optional class Cell: def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False): self.x = x self.y = y self.is_wall = is_wall self.is_start = is_start self.is_exit = is_exit def is_passable(self): return not self.is_wall def __eq__(self, other): return isinstance(other, Cell) and self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y)) def __repr__(self): return f"Cell({self.x},{self.y})" class Maze: def __init__(self, cells, width, height, start, exit_cell): self.cells = cells self.width = width self.height = height self.start = start self.exit = exit_cell def get_cell(self, x, y): if 0 <= x < self.width and 0 <= y < self.height: return self.cells[y][x] return None def get_neighbors(self, cell): result = [] for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: n = self.get_cell(cell.x + dx, cell.y + dy) if n and n.is_passable(): result.append(n) return result def render(self, path=None): path_set = set(path) if path else set() lines = [] for row in self.cells: line = "" for cell in row: if cell.is_start: line += " S" elif cell.is_exit: line += " E" elif cell.is_wall: line += "##" elif cell in path_set: line += " ." else: line += " " lines.append(line) return "\n".join(lines) class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename) -> Maze: pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename) -> Maze: with open(filename, encoding="utf-8") as f: lines = [l.rstrip("\n") for l in f] height = len(lines) width = max(len(l) for l in lines) cells = [] start = exit_cell = None for y, line in enumerate(lines): row = [] for x in range(width): ch = line[x] if x < len(line) else " " is_wall = ch == "#" is_start = ch == "S" is_exit = ch == "E" c = Cell(x, y, is_wall, is_start, is_exit) if is_start: start = c if is_exit: exit_cell = c row.append(c) cells.append(row) if not start or not exit_cell: raise ValueError("Maze must have S and E") return Maze(cells, width, height, start, exit_cell) @dataclass class SearchStats: strategy: str time_ms: float visited: int path_length: int path: List[Cell] = field(default_factory=list) class PathFindingStrategy(ABC): _visited = 0 @property def name(self): return self.__class__.__name__ @abstractmethod def find_path(self, maze: Maze, start: Cell, end: Cell) -> List[Cell]: pass @staticmethod def _build_path(parent, start, end): path, cur = [], end while cur: path.append(cur) cur = parent.get(cur) path.reverse() return path if path and path[0] == start else [] class BFSStrategy(PathFindingStrategy): @property def name(self): return "BFS" def find_path(self, maze, start, end): queue = deque([start]) parent = {start: None} visited = 0 while queue: cur = queue.popleft() visited += 1 if cur == end: self._visited = visited return self._build_path(parent, start, end) for nb in maze.get_neighbors(cur): if nb not in parent: parent[nb] = cur queue.append(nb) self._visited = visited return [] class DFSStrategy(PathFindingStrategy): @property def name(self): return "DFS" def find_path(self, maze, start, end): stack = [start] parent = {start: None} visited = 0 while stack: cur = stack.pop() visited += 1 if cur == end: self._visited = visited return self._build_path(parent, start, end) for nb in maze.get_neighbors(cur): if nb not in parent: parent[nb] = cur stack.append(nb) self._visited = visited return [] class AStarStrategy(PathFindingStrategy): @property def name(self): return "A*" @staticmethod def _h(a, b): return abs(a.x - b.x) + abs(a.y - b.y) def find_path(self, maze, start, end): counter = 0 heap = [(0, counter, start)] parent = {start: None} g = {start: 0} closed = set() visited = 0 while heap: _, _, cur = heapq.heappop(heap) if cur in closed: continue closed.add(cur) visited += 1 if cur == end: self._visited = visited return self._build_path(parent, start, end) for nb in maze.get_neighbors(cur): if nb in closed: continue ng = g[cur] + 1 if ng < g.get(nb, float("inf")): g[nb] = ng counter += 1 heapq.heappush(heap, (ng + self._h(nb, end), counter, nb)) parent[nb] = cur self._visited = visited return [] class MazeSolver: def __init__(self, maze: Maze, strategy: PathFindingStrategy): self.maze = maze self.strategy = strategy def set_strategy(self, strategy: PathFindingStrategy): self.strategy = strategy def solve(self) -> SearchStats: t0 = time.perf_counter() path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) t1 = time.perf_counter() return SearchStats( strategy=self.strategy.name, time_ms=(t1 - t0) * 1000, visited=self.strategy._visited, path_length=len(path), path=path )