import heapq import time import os from collections import deque from abc import ABC, abstractmethod import itertools class Cell: def __init__(self, x, y): self.x = x self.y = y self.isWall = False self.isStart = False self.isExit = False self.weight = 1 def isPassable(self): return not self.isWall def __repr__(self): return f"({self.x},{self.y})" class Maze: def __init__(self, width, height): self.width = width self.height = height self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)] self.start = None self.exit = None def getCell(self, x, y): if 0 <= x < self.width and 0 <= y < self.height: return self.grid[x][y] return None def getNeighbors(self, cell): dirs = [(-1,0), (1,0), (0,-1), (0,1)] result = [] for dx, dy in dirs: nx, ny = cell.x + dx, cell.y + dy ncell = self.getCell(nx, ny) if ncell and ncell.isPassable(): result.append(ncell) return result class MazeBuilder(ABC): @abstractmethod def buildFromFile(self, filename): pass class TextFileMazeBuilder(MazeBuilder): def buildFromFile(self, filename): with open(filename, 'r') as f: lines = [line.rstrip('\n') for line in f if line.strip() != ''] height = len(lines) width = len(lines[0]) if height > 0 else 0 maze = Maze(width, height) for y, line in enumerate(lines): for x, ch in enumerate(line): cell = maze.getCell(x, y) if ch == '#': cell.isWall = True elif ch == 'S': cell.isStart = True maze.start = cell elif ch == 'E': cell.isExit = True maze.exit = cell elif ch == ' ': pass else: if ch.isdigit(): cell.weight = int(ch) else: raise ValueError(f"err '{ch}' at ({x},{y})") if maze.start is None or maze.exit is None: raise ValueError("not e or/and s") return maze class PathFindingStrategy(ABC): def __init__(self): self.visited_count = 0 @abstractmethod def findPath(self, maze, start, exit_cell): pass class BFSPathFinding(PathFindingStrategy): def findPath(self, maze, start, exit_cell): self.visited_count = 0 queue = deque() queue.append(start) parent = {start: None} while queue: current = queue.popleft() self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(parent, exit_cell) for neighbor in maze.getNeighbors(current): if neighbor not in parent: parent[neighbor] = current queue.append(neighbor) return [] def _reconstruct_path(self, parent, end): path = [] cur = end while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path class DFSPathFinding(PathFindingStrategy): def findPath(self, maze, start, exit_cell): self.visited_count = 0 stack = [start] parent = {start: None} while stack: current = stack.pop() self.visited_count += 1 if current == exit_cell: return self._reconstruct_path(parent, exit_cell) for neighbor in maze.getNeighbors(current): if neighbor not in parent: parent[neighbor] = current stack.append(neighbor) return [] def _reconstruct_path(self, parent, end): path = [] cur = end while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path class AStarPathFinding(PathFindingStrategy): def findPath(self, maze, start, exit_cell): self.visited_count = 0 def heuristic(cell): return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) open_set = [] counter = itertools.count() # heapq.heappush(open_set, (0 + heuristic(start), 0, next(counter), start)) parent = {start: None} g_score = {start: 0} closed = set() while open_set: _, cost, _, current = heapq.heappop(open_set) self.visited_count += 1 if current in closed: continue if current == exit_cell: return self._reconstruct_path(parent, exit_cell) closed.add(current) for neighbor in maze.getNeighbors(current): tentative_g = g_score[current] + neighbor.weight if neighbor not in g_score or tentative_g < g_score[neighbor]: g_score[neighbor] = tentative_g f = tentative_g + heuristic(neighbor) heapq.heappush(open_set, (f, tentative_g, next(counter), neighbor)) parent[neighbor] = current return [] def _reconstruct_path(self, parent, end): path = [] cur = end while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path class SearchStats: def __init__(self, time_ms, visited, path_length, path): self.time_ms = time_ms self.visited = visited self.path_length = path_length self.path = path class MazeSolver: def __init__(self, maze, strategy): self.maze = maze self.strategy = strategy self.observers = [] def setStrategy(self, strategy): self.strategy = strategy def solve(self): start = self.maze.start exit_cell = self.maze.exit t0 = time.perf_counter() path = self.strategy.findPath(self.maze, start, exit_cell) t1 = time.perf_counter() ms = (t1 - t0) * 1000 visited = self.strategy.visited_count stats = SearchStats(ms, visited, len(path), path) self.notify("path_found", stats) return stats def addObserver(self, observer): self.observers.append(observer) def notify(self, event, data=None): for obs in self.observers: obs.update(event, data) class Observer(ABC): @abstractmethod def update(self, event, data): pass class ConsoleView(Observer): def __init__(self, maze): self.maze = maze def update(self, event, data): if event == "path_found": self.render(data.path, data) def render(self, path, stats=None): os.system('cls' if os.name == 'nt' else 'clear') path_set = set(path) if path else set() for y in range(self.maze.height): line = "" for x in range(self.maze.width): cell = self.maze.getCell(x, y) if cell == self.maze.start: line += "S" elif cell == self.maze.exit: line += "E" elif cell.isWall: line += "#" elif cell in path_set: line += "." else: line += " " print(line) if stats: print(f"\npath: {stats.path_length}, visit: {stats.visited}, time: {stats.time_ms:.2f} ms") class Player: def __init__(self, start_cell): self.current = start_cell self.history = [] def move(self, dx, dy, maze): nx, ny = self.current.x + dx, self.current.y + dy ncell = maze.getCell(nx, ny) if ncell and ncell.isPassable(): self.history.append(self.current) self.current = ncell return True return False def undo(self): if self.history: self.current = self.history.pop() return True return False class Command(ABC): @abstractmethod def execute(self): pass @abstractmethod def undo(self): pass class MoveCommand(Command): def __init__(self, player, maze, dx, dy): self.player = player self.maze = maze self.dx = dx self.dy = dy self.executed = False def execute(self): if not self.executed: success = self.player.move(self.dx, self.dy, self.maze) self.executed = success return success return False def undo(self): if self.executed: self.player.undo() self.executed = False return True return False