import time import heapq from collections import deque from typing import List, Optional, Dict, Tuple from abc import ABC, abstractmethod import csv import random class Cell: def __init__(self, x: int, y: int): self.x = x self.y = y self.is_wall = False self.is_start = False self.is_exit = False def is_passable(self) -> bool: return not self.is_wall class Maze: def __init__(self, width: int, height: int): self.width = width self.height = height self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)] self.start: Optional[Cell] = None self.exit: Optional[Cell] = None def get_cell(self, x: int, y: int) -> Optional[Cell]: if 0 <= x < self.width and 0 <= y < self.height: return self.cells[x][y] return None def get_neighbors(self, cell: Cell) -> List[Cell]: neighbors = [] for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: nx, ny = cell.x + dx, cell.y + dy nb = self.get_cell(nx, ny) if nb and nb.is_passable(): neighbors.append(nb) return neighbors class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename: str) -> Maze: pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename: str) -> Maze: with open(filename, 'r', encoding='utf-8') as f: lines = [line.rstrip('\n') for line in f.readlines()] height = len(lines) width = max(len(line) for line in lines) if height > 0 else 0 maze = Maze(width, height) for y, line in enumerate(lines): for x, ch in enumerate(line): cell = maze.get_cell(x, y) if cell is None: continue if ch == '#': cell.is_wall = True elif ch == 'S': cell.is_start = True maze.start = cell elif ch == 'E': cell.is_exit = True maze.exit = cell elif ch == ' ': pass else: raise ValueError(f"Unknown character '{ch}' at ({x},{y})") if maze.start is None or maze.exit is None: raise ValueError("Maze must have start (S) and exit (E)") return maze class PathFindingStrategy(ABC): @abstractmethod def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: pass @abstractmethod def get_name(self) -> str: pass class BFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: queue = deque([start]) came_from = {start: None} while queue: current = queue.popleft() if current == exit: break for nb in maze.get_neighbors(current): if nb not in came_from: came_from[nb] = current queue.append(nb) if exit not in came_from: return [] path = [] cur = exit while cur: path.append(cur) cur = came_from[cur] path.reverse() return path def get_name(self) -> str: return "BFS" class DFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: stack = [start] came_from = {start: None} while stack: current = stack.pop() if current == exit: break for nb in maze.get_neighbors(current): if nb not in came_from: came_from[nb] = current stack.append(nb) if exit not in came_from: return [] path = [] cur = exit while cur: path.append(cur) cur = came_from[cur] path.reverse() return path def get_name(self) -> str: return "DFS" class AStarStrategy(PathFindingStrategy): def _heuristic(self, 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) -> List[Cell]: open_set = [] heapq.heappush(open_set, (0, id(start), start)) came_from = {} g_score = {start: 0} f_score = {start: self._heuristic(start, exit)} while open_set: _, _, current = heapq.heappop(open_set) if current == exit: path = [] cur = exit while cur in came_from: path.append(cur) cur = came_from[cur] path.append(start) path.reverse() return path for neighbor in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if tentative_g < g_score.get(neighbor, float('inf')): came_from[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit) heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor)) return [] def get_name(self) -> str: return "A*" class DijkstraStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: pq = [(0, id(start), start)] distances = {start: 0} came_from = {start: None} while pq: dist, _, current = heapq.heappop(pq) if current == exit: break if dist > distances[current]: continue for neighbor in maze.get_neighbors(current): new_dist = dist + 1 if new_dist < distances.get(neighbor, float('inf')): distances[neighbor] = new_dist came_from[neighbor] = current heapq.heappush(pq, (new_dist, id(neighbor), neighbor)) if exit not in came_from: return [] path = [] cur = exit while cur: path.append(cur) cur = came_from[cur] path.reverse() return path def get_name(self) -> str: return "Dijkstra" class SearchStats: def __init__(self, time_ms: float, visited_cells: int, path_length: int): self.time_ms = time_ms self.visited_cells = visited_cells self.path_length = path_length def __str__(self): return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}" 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) -> Tuple[List[Cell], SearchStats]: visited_before = set() for x in range(self.maze.width): for y in range(self.maze.height): cell = self.maze.get_cell(x, y) if cell and cell.is_passable(): visited_before.add(cell) start_time = time.perf_counter() path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() visited_after = set() for x in range(self.maze.width): for y in range(self.maze.height): cell = self.maze.get_cell(x, y) if cell and cell.is_passable(): visited_after.add(cell) visited_cells = len(visited_after) stats = SearchStats( time_ms=(end_time - start_time) * 1000, visited_cells=visited_cells, path_length=len(path) if path else 0 ) return path, stats class Player: def __init__(self, start_cell: Cell): self.current_cell = start_cell self.previous_cell = None def move_to(self, cell: Cell) -> bool: if cell.is_passable(): self.previous_cell = self.current_cell self.current_cell = cell return True return False def undo(self): if self.previous_cell: self.current_cell, self.previous_cell = self.previous_cell, None return True return False class Command(ABC): @abstractmethod def execute(self) -> bool: pass @abstractmethod def undo(self): pass class MoveCommand(Command): def __init__(self, player: Player, maze: Maze, direction: str): self.player = player self.maze = maze self.direction = direction self.executed = False def execute(self) -> bool: dx, dy = 0, 0 if self.direction == 'W' or self.direction == 'w': dy = -1 elif self.direction == 'S' or self.direction == 's': dy = 1 elif self.direction == 'A' or self.direction == 'a': dx = -1 elif self.direction == 'D' or self.direction == 'd': dx = 1 new_x = self.player.current_cell.x + dx new_y = self.player.current_cell.y + dy new_cell = self.maze.get_cell(new_x, new_y) if new_cell and new_cell.is_passable(): self.executed = self.player.move_to(new_cell) return self.executed return False def undo(self): if self.executed: self.player.undo() self.executed = False class ConsoleView: @staticmethod def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None): path_set = set() if path: path_set = set(path) for y in range(maze.height): line = "" for x in range(maze.width): cell = maze.get_cell(x, y) if not cell: line += " " elif player and player.current_cell == cell: line += "P" elif cell.is_start: line += "S" elif cell.is_exit: line += "E" elif cell.is_wall: line += "#" elif path and cell in path_set: line += "." else: line += " " print(line) print() @staticmethod def show_stats(stats: SearchStats, algo_name: str): print(f"=== {algo_name} Results ===") print(stats) print() def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze: maze = Maze(width, height) for x in range(width): for y in range(height): if random.random() < complexity: maze.cells[x][y].is_wall = True maze.start = maze.get_cell(0, 0) if maze.start: maze.start.is_start = True maze.start.is_wall = False maze.exit = maze.get_cell(width - 1, height - 1) if maze.exit: maze.exit.is_exit = True maze.exit.is_wall = False return maze def generate_empty_maze(width: int, height: int) -> Maze: maze = Maze(width, height) for x in range(width): for y in range(height): maze.cells[x][y].is_wall = False maze.start = maze.get_cell(0, 0) if maze.start: maze.start.is_start = True maze.exit = maze.get_cell(width - 1, height - 1) if maze.exit: maze.exit.is_exit = True return maze def generate_no_exit_maze(width: int, height: int) -> Maze: maze = Maze(width, height) for x in range(width): for y in range(height): maze.cells[x][y].is_wall = False for x in range(width): maze.cells[x][height // 2].is_wall = True maze.start = maze.get_cell(0, 0) if maze.start: maze.start.is_start = True maze.exit = maze.get_cell(width - 1, height - 1) if maze.exit: maze.exit.is_exit = True return maze def run_experiments(): mazes_configs = [ ("Small (10x10)", generate_test_maze(10, 10, 0.2)), ("Medium (50x50)", generate_test_maze(50, 50, 0.25)), ("Large (100x100)", generate_test_maze(100, 100, 0.3)), ("Empty (30x30)", generate_empty_maze(30, 30)), ("No Exit (20x20)", generate_no_exit_maze(20, 20)) ] strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()] results = [] for maze_name, maze in mazes_configs: print(f"\n=== Testing: {maze_name} ===") for strategy in strategies: times = [] visited = [] path_lengths = [] solver = MazeSolver(maze, strategy) for run in range(5): maze_copy = Maze(maze.width, maze.height) for x in range(maze.width): for y in range(maze.height): orig = maze.get_cell(x, y) copy = maze_copy.get_cell(x, y) if orig: copy.is_wall = orig.is_wall copy.is_start = orig.is_start copy.is_exit = orig.is_exit maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None solver.maze = maze_copy solver.set_strategy(strategy) path, stats = solver.solve() times.append(stats.time_ms) visited.append(stats.visited_cells) path_lengths.append(stats.path_length) avg_time = sum(times) / len(times) avg_visited = sum(visited) / len(visited) avg_path = sum(path_lengths) / len(path_lengths) results.append({ 'maze': maze_name, 'algorithm': strategy.get_name(), 'avg_time_ms': avg_time, 'avg_visited_cells': avg_visited, 'avg_path_length': avg_path }) print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}") with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length']) writer.writeheader() writer.writerows(results) print("\nResults saved to experiment_results.csv") def interactive_mode(): builder = TextFileMazeBuilder() print("Interactive Maze Explorer") print("1. Load maze from file") print("2. Generate random maze") choice = input("Choose (1/2): ") if choice == '1': filename = input("Enter filename: ") try: maze = builder.build_from_file(filename) except Exception as e: print(f"Error loading maze: {e}") return else: w = int(input("Width: ")) h = int(input("Height: ")) maze = generate_test_maze(w, h, 0.3) player = Player(maze.start) strategies = { '1': BFSStrategy(), '2': DFSStrategy(), '3': AStarStrategy(), '4': DijkstraStrategy() } print("\nSelect algorithm for solving:") print("1. BFS (shortest path)") print("2. DFS (fast, not optimal)") print("3. A* (heuristic)") print("4. Dijkstra") algo_choice = input("Choose: ") solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy())) path, stats = solver.solve() view = ConsoleView() if path: print(f"\nPath found! Length: {len(path)}") view.show_stats(stats, solver.strategy.get_name()) else: print("\nNo path found!") while True: view.render(maze, player, path if path else None) if player.current_cell == maze.exit: print("Congratulations! You reached the exit!") break cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper() if cmd == 'Q': break elif cmd == 'U': player.undo() print("Undo last move") elif cmd == 'S' and path: for cell in path: if cell == player.current_cell: continue player.move_to(cell) view.render(maze, player, path) input("Press Enter to continue...") if player.current_cell == maze.exit: print("You reached the exit!") break elif cmd in ['W', 'A', 'S', 'D']: move_cmd = MoveCommand(player, maze, cmd) if move_cmd.execute(): print("Moved") else: print("Can't move there!") def main(): print("Maze Solver with Design Patterns") print("1. Run experiments") print("2. Interactive mode") choice = input("Choose (1/2): ") if choice == '1': run_experiments() else: interactive_mode() if __name__ == "__main__": main()