import os import time import heapq from collections import deque from typing import List, Dict, Optional, Tuple from abc import ABC, abstractmethod from dataclasses import dataclass class Cell: def __init__(self, x: int, y: int, is_wall: bool = True, is_start: bool = False, is_exit: bool = 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) -> bool: return not self.is_wall def __eq__(self, other): if not isinstance(other, Cell): return False return 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, width: int = 0, height: int = 0): self.width = width self.height = height self.grid: List[List[Cell]] = [] self.start: Optional[Cell] = None self.exit: Optional[Cell] = None def set_cell(self, x: int, y: int, cell: Cell) -> None: if 0 <= x < self.width and 0 <= y < self.height: self.grid[y][x] = cell def get_cell(self, x: int, y: int) -> Optional[Cell]: if 0 <= x < self.width and 0 <= y < self.height: return self.grid[y][x] return None def get_neighbors(self, cell: Cell) -> List[Cell]: neighbors = [] directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] for dx, dy in directions: nx, ny = cell.x + dx, cell.y + dy neighbor = self.get_cell(nx, ny) if neighbor and neighbor.is_passable(): neighbors.append(neighbor) 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 file: lines = [line.rstrip('\n') for line in file.readlines()] while lines and not lines[0].strip(): lines.pop(0) while lines and not lines[-1].strip(): lines.pop() height = len(lines) width = max(len(line) for line in lines) if height > 0 else 0 maze = Maze(width, height) maze.grid = [[None for _ in range(width)] for _ in range(height)] start_count = 0 exit_count = 0 for y, line in enumerate(lines): for x in range(width): char = line[x] if x < len(line) else '#' if char == '#': cell = Cell(x, y, is_wall=True) elif char == ' ': cell = Cell(x, y, is_wall=False) elif char == 'S': cell = Cell(x, y, is_wall=False, is_start=True) maze.start = cell start_count += 1 elif char == 'E': cell = Cell(x, y, is_wall=False, is_exit=True) maze.exit = cell exit_count += 1 else: cell = Cell(x, y, is_wall=True) maze.set_cell(x, y, cell) if start_count == 0: raise ValueError("Лабиринт должен содержать старт (S)") if start_count > 1: raise ValueError("Лабиринт может содержать только один старт (S)") if exit_count == 0: raise ValueError("Лабиринт должен содержать выход (E)") if exit_count > 1: raise ValueError("Лабиринт может содержать только один выход (E)") return maze class PathFindingStrategy(ABC): @abstractmethod def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]: pass def _reconstruct_path(self, parents: Dict[Cell, Cell], start: Cell, exit_cell: Cell) -> List[Cell]: path = [] current = exit_cell while current != start: path.append(current) if current not in parents: return [] current = parents[current] path.append(start) path.reverse() return path @property def name(self) -> str: return self.__class__.__name__.replace('Strategy', '') class BFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]: queue = deque([start]) visited = {start} parents: Dict[Cell, Cell] = {} visited_count = 1 while queue: current = queue.popleft() if current == exit_cell: return self._reconstruct_path(parents, start, exit_cell), visited_count for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) visited_count += 1 parents[neighbor] = current queue.append(neighbor) return [], visited_count class DFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]: stack = [(start, [start])] visited = {start} visited_count = 1 while stack: current, path = stack.pop() if current == exit_cell: return path, visited_count for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) visited_count += 1 stack.append((neighbor, path + [neighbor])) return [], visited_count class AStarStrategy(PathFindingStrategy): def _heuristic(self, cell: Cell, target: Cell) -> int: return abs(cell.x - target.x) + abs(cell.y - target.y) def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]: counter = 0 open_set = [(0, counter, start)] came_from: Dict[Cell, Cell] = {} g_score: Dict[Cell, float] = {start: 0} f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)} open_set_hash = {start} visited_count = 1 while open_set: current = heapq.heappop(open_set)[2] open_set_hash.remove(current) if current == exit_cell: path = self._reconstruct_path(came_from, start, exit_cell) return path, visited_count for neighbor in maze.get_neighbors(current): tentative_g_score = g_score[current] + 1 if tentative_g_score < g_score.get(neighbor, float('inf')): came_from[neighbor] = current g_score[neighbor] = tentative_g_score f_score[neighbor] = tentative_g_score + self._heuristic(neighbor, exit_cell) if neighbor not in open_set_hash: visited_count += 1 counter += 1 heapq.heappush(open_set, (f_score[neighbor], counter, neighbor)) open_set_hash.add(neighbor) return [], visited_count @dataclass class SearchStats: execution_time_ms: float path_length: int visited_cells: int success: bool class MazeSolver: def __init__(self, maze: Maze, strategy: PathFindingStrategy): self.maze = maze self.strategy = strategy def set_strategy(self, strategy: PathFindingStrategy) -> None: self.strategy = strategy def solve(self) -> Tuple[List[Cell], SearchStats]: if not self.maze.start or not self.maze.exit: raise ValueError("Лабиринт должен содержать старт и выход") start_time = time.perf_counter() path, visited_cells = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() execution_time = (end_time - start_time) * 1000 stats = SearchStats( execution_time_ms=execution_time, path_length=len(path), visited_cells=visited_cells, success=len(path) > 0 ) return path, stats class MazeVisualizer: @staticmethod def render(maze: Maze, path: List[Cell] = None, player_pos: Cell = None): print("\n" + "=" * (maze.width + 2)) for y in range(maze.height): row = "|" for x in range(maze.width): cell = maze.get_cell(x, y) if cell: if player_pos and cell == player_pos: row += "P" elif path and cell in path and not cell.is_start and not cell.is_exit: row += "." elif cell.is_start: row += "S" elif cell.is_exit: row += "E" elif cell.is_wall: row += "#" else: row += " " else: row += " " row += "|" print(row) print("=" * (maze.width + 2)) def create_test_mazes(): current_dir = os.path.dirname(os.path.abspath(__file__)) small_maze = """########## #S # # ### ## # # # # ### # #### # # # # ### # # # # E# ##########""" medium_maze = """#################### #S # # ### ########### # # # # # # # # # # # ####### # # # # # # # # # # ######### # # # # # # # # # # ########### # # # # # # # ############### # # E# ####################""" large_maze = """################################################## #S # # ############################################# # # # # # # # ######################################### # # # # # # # # # # # ##################################### # # # # # # # # # # # # # # # ################################# # # # # # # # # # # # # # # # # # # # ############################# # # # # # # # # # # # # # # # # # # # # # # # ######################### # # # # # # # # # # # # # # # # # # # # # # # # # # # ##################### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ############# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ######### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##### # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E# ##################################################""" empty_maze_lines = ["#" + "#" * 38 + "#"] empty_maze_lines.append("#S" + " " * 37 + "#") for i in range(35): empty_maze_lines.append("#" + " " * 38 + "#") empty_maze_lines.append("#" + " " * 37 + "E#") empty_maze_lines.append("#" + "#" * 38 + "#") empty_maze = "\n".join(empty_maze_lines) no_exit_maze = """########## #S # # ### ## # # # # ### # #### # # # # ### # # # # ##########""" mazes = [ ("01_small_maze.txt", small_maze), ("02_medium_maze.txt", medium_maze), ("03_large_maze.txt", large_maze), ("04_empty_maze.txt", empty_maze), ("05_no_exit_maze.txt", no_exit_maze) ] print("\n" + "="*60) print("CREATING TEST MAZES") print("="*60) for filename, content in mazes: filepath = os.path.join(current_dir, filename) with open(filepath, 'w', encoding='utf-8') as f: f.write(content) print(f"Created: {filename}") print("="*60) def list_available_mazes(): current_dir = os.path.dirname(os.path.abspath(__file__)) maze_files = [f for f in os.listdir(current_dir) if f.endswith('.txt') and ('maze' in f.lower() or f.startswith('0'))] if not maze_files: return [] print("\nAVAILABLE MAZES:") print("-" * 50) for i, file in enumerate(maze_files, 1): print(f" {i}. {file}") return maze_files class Benchmark: def __init__(self): self.strategies = [ BFSStrategy(), DFSStrategy(), AStarStrategy() ] def run_experiment(self, maze: Maze, runs: int = 5) -> Dict: results = {} for strategy in self.strategies: times = [] path_lengths = [] visited_counts = [] for _ in range(runs): solver = MazeSolver(maze, strategy) path, stats = solver.solve() if stats.success: times.append(stats.execution_time_ms) path_lengths.append(stats.path_length) visited_counts.append(stats.visited_cells) if times: results[strategy.name] = { 'avg_time_ms': sum(times) / len(times), 'avg_path_length': sum(path_lengths) / len(path_lengths), 'avg_visited_cells': sum(visited_counts) / len(visited_counts), 'success_rate': len(times) / runs * 100 } else: results[strategy.name] = { 'avg_time_ms': float('inf'), 'avg_path_length': 0, 'avg_visited_cells': 0, 'success_rate': 0 } return results @staticmethod def print_results(results: Dict, maze_name: str): print(f"\n{'='*60}") print(f"RESULTS FOR MAZE: {maze_name}") print(f"{'='*60}") print(f"{'Algorithm':<12} {'Time(ms)':<12} {'PathLen':<12} {'Visited':<12} {'Success':<8}") print(f"{'-'*60}") for strategy_name, stats in results.items(): print(f"{strategy_name:<12} {stats['avg_time_ms']:>8.3f} " f"{stats['avg_path_length']:>8.1f} " f"{stats['avg_visited_cells']:>8.1f} " f"{stats['success_rate']:>6.1f}%") def interactive_mode(): builder = TextFileMazeBuilder() visualizer = MazeVisualizer() current_dir = os.path.dirname(os.path.abspath(__file__)) while True: print("\n" + "="*60) print("MAZE PATHFINDING PROGRAM") print("="*60) print("1. Load maze and find path") print("2. Compare all algorithms on maze") print("3. Create test mazes (5 pieces)") print("4. Run benchmark on all mazes") print("5. Show available mazes") print("6. Exit") print("="*60) choice = input("Choose action (1-6): ").strip() if choice == '6': print("\nGoodbye!") break elif choice == '3': create_test_mazes() input("\nPress Enter to continue...") elif choice == '5': maze_files = list_available_mazes() if not maze_files: print("\nNo available mazes. Create them first (action 3)") input("\nPress Enter to continue...") elif choice == '1': print("\nAvailable mazes:") maze_files = list_available_mazes() if not maze_files: print("\nNo available mazes. Create them first (action 3)") continue filename = input("\nEnter path to maze file: ").strip() if not os.path.exists(filename): test_path = os.path.join(current_dir, filename) if os.path.exists(test_path): filename = test_path try: maze = builder.build_from_file(filename) print("\nLOADED MAZE:") visualizer.render(maze) print("\nChoose algorithm:") print(" 1. BFS - finds SHORTEST path") print(" 2. DFS - FAST but path may be longer") print(" 3. A* - OPTIMAL balance") algo_choice = input("\nYour choice (1-3): ").strip() strategies = { '1': BFSStrategy(), '2': DFSStrategy(), '3': AStarStrategy() } if algo_choice in strategies: print("\nSearching for path...") solver = MazeSolver(maze, strategies[algo_choice]) path, stats = solver.solve() if stats.success: print(f"\nPATH FOUND!") print(f"\nSTATISTICS:") print(f" Time: {stats.execution_time_ms:.3f} ms") print(f" Path length: {stats.path_length} steps") print(f" Visited cells: {stats.visited_cells}") print(f" Efficiency: {stats.visited_cells/stats.path_length:.1f} cells per step") print("\nPATH ON MAP (. = path):") visualizer.render(maze, path) else: print("\nPATH NOT FOUND! Exit unreachable from start.") else: print("Invalid choice!") except FileNotFoundError: print(f"\nFile '{filename}' not found!") except Exception as e: print(f"\nError: {e}") input("\nPress Enter to continue...") elif choice == '2': print("\nAvailable mazes:") maze_files = list_available_mazes() if not maze_files: print("\nNo available mazes. Create them first (action 3)") continue filename = input("\nEnter path to maze file: ").strip() if not os.path.exists(filename): test_path = os.path.join(current_dir, filename) if os.path.exists(test_path): filename = test_path try: maze = builder.build_from_file(filename) print("\nLOADED MAZE:") visualizer.render(maze) print("\nRunning algorithm comparison (3 runs each)...") benchmark = Benchmark() results = benchmark.run_experiment(maze, runs=3) maze_name = os.path.basename(filename) benchmark.print_results(results, maze_name) print("\nANALYSIS:") print("-" * 40) fastest = min(results.items(), key=lambda x: x[1]['avg_time_ms']) print(f"Fastest: {fastest[0]} ({fastest[1]['avg_time_ms']:.3f} ms)") shortest = min(results.items(), key=lambda x: x[1]['avg_path_length']) print(f"Shortest path: {shortest[0]} ({shortest[1]['avg_path_length']:.0f} steps)") efficient = min(results.items(), key=lambda x: x[1]['avg_visited_cells']) print(f"Most efficient: {efficient[0]} (checked {efficient[1]['avg_visited_cells']:.0f} cells)") except FileNotFoundError: print(f"\nFile '{filename}' not found!") except Exception as e: print(f"\nError: {e}") input("\nPress Enter to continue...") elif choice == '4': print("\nRUNNING FULL BENCHMARK") print("="*60) test_files = [ "01_small_maze.txt", "02_medium_maze.txt", "03_large_maze.txt", "04_empty_maze.txt", "05_no_exit_maze.txt" ] benchmark = Benchmark() all_results = {} for test_file in test_files: filepath = os.path.join(current_dir, test_file) if not os.path.exists(filepath): print(f"\nFile {test_file} not found. Creating test mazes...") create_test_mazes() break try: print(f"\nTesting: {test_file}") maze = builder.build_from_file(filepath) results = benchmark.run_experiment(maze, runs=5) benchmark.print_results(results, test_file) all_results[test_file] = results except Exception as e: print(f"Error testing {test_file}: {e}") if all_results: csv_filename = os.path.join(current_dir, "benchmark_results.csv") with open(csv_filename, "w", encoding='utf-8') as f: f.write("Maze,Algorithm,AvgTimeMs,AvgPathLength,AvgVisitedCells,SuccessPercent\n") for maze_name, results in all_results.items(): for strategy_name, stats in results.items(): f.write(f"{maze_name},{strategy_name}," f"{stats['avg_time_ms']:.3f},{stats['avg_path_length']:.1f}," f"{stats['avg_visited_cells']:.1f},{stats['success_rate']:.1f}\n") print(f"\nResults saved to: {csv_filename}") input("\nPress Enter to continue...") def main(): print("="*60) print("MAZE PATHFINDING PROGRAM") print("Patterns: Builder, Strategy") print("Algorithms: BFS, DFS, A*") print("="*60) current_dir = os.path.dirname(os.path.abspath(__file__)) existing_mazes = [f for f in os.listdir(current_dir) if f.endswith('.txt') and ('maze' in f.lower() or f.startswith('0'))] if not existing_mazes: print("First run: create test mazes (action 3)\n") interactive_mode() if __name__ == "__main__": main()