import sys from collections import deque import heapq import time import os import csv import matplotlib.pyplot as plt import numpy as np # ----------------------------- Модель клетки ----------------------------- class GridCell: def __init__(self, x, y): self._x = x self._y = y self._blocked = False self._entry = False self._exit_flag = False @property def x(self): return self._x @property def y(self): return self._y @property def is_wall(self): return self._blocked @is_wall.setter def is_wall(self, value): self._blocked = value @property def is_start(self): return self._entry @is_start.setter def is_start(self, value): self._entry = value @property def is_exit(self): return self._exit_flag @is_exit.setter def is_exit(self, value): self._exit_flag = value def passable(self): return not self._blocked # ----------------------------- Модель лабиринта ----------------------------- class Labyrinth: def __init__(self, width, height): self._width = width self._height = height self._cells = [[GridCell(x, y) for x in range(width)] for y in range(height)] self._start_cell = None self._exit_cell = None @property def width(self): return self._width @property def height(self): return self._height @property def start(self): return self._start_cell @property def exit(self): return self._exit_cell def cell_at(self, x, y): if 0 <= x < self._width and 0 <= y < self._height: return self._cells[y][x] return None def configure_cell(self, x, y, cell_type): cell = self.cell_at(x, y) if cell is None: return if cell_type == 'wall': cell.is_wall = True elif cell_type == 'start': if self._start_cell: self._start_cell.is_start = False cell.is_start = True cell.is_wall = False self._start_cell = cell elif cell_type == 'exit': if self._exit_cell: self._exit_cell.is_exit = False cell.is_exit = True cell.is_wall = False self._exit_cell = cell elif cell_type == 'path': cell.is_wall = False def adjacent_cells(self, cell): neighbours = [] directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] for dx, dy in directions: nx, ny = cell.x + dx, cell.y + dy neighbour = self.cell_at(nx, ny) if neighbour and neighbour.passable(): neighbours.append(neighbour) return neighbours # ----------------------------- Загрузка лабиринта ----------------------------- class LabyrinthBuilder: def build_from_file(self, filename): raise NotImplementedError class TxtLabyrinthBuilder(LabyrinthBuilder): def build_from_file(self, filename): with open(filename, 'r') 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 start_cnt = 0 exit_cnt = 0 lab = Labyrinth(width, height) for y, line in enumerate(lines): for x, ch in enumerate(line): if ch == "#": lab.configure_cell(x, y, "wall") elif ch == "S": lab.configure_cell(x, y, "start") start_cnt += 1 elif ch == "E": lab.configure_cell(x, y, "exit") exit_cnt += 1 else: lab.configure_cell(x, y, 'path') if start_cnt != 1 or exit_cnt != 1: raise ValueError(f"Maze must have exactly one S and one E. Found S={start_cnt}, E={exit_cnt}") return lab # ----------------------------- Алгоритмы поиска ----------------------------- class SearchAlgorithm: def compute_path(self, maze, start, goal): raise NotImplementedError def _build_path(self, came_from, start, goal): path = [] cur = goal while cur is not None: path.append(cur) cur = came_from.get(cur) path.reverse() return path def visited_nodes(self): return getattr(self, '_visited', 0) class BFS(SearchAlgorithm): def compute_path(self, maze, start, goal): q = deque() q.append(start) came_from = {start: None} visited = {start} while q: cur = q.popleft() if cur == goal: self._visited = len(visited) return self._build_path(came_from, start, goal) for nb in maze.adjacent_cells(cur): if nb not in visited: visited.add(nb) came_from[nb] = cur q.append(nb) self._visited = len(visited) return [] class DFS(SearchAlgorithm): def compute_path(self, maze, start, goal): stack = [start] came_from = {start: None} visited = {start} while stack: cur = stack.pop() if cur == goal: self._visited = len(visited) return self._build_path(came_from, start, goal) for nb in maze.adjacent_cells(cur): if nb not in visited: visited.add(nb) came_from[nb] = cur stack.append(nb) self._visited = len(visited) return [] class AStar(SearchAlgorithm): def _heuristic(self, cell, goal): return abs(cell.x - goal.x) + abs(cell.y - goal.y) def compute_path(self, maze, start, goal): heap = [] counter = 0 start_f = self._heuristic(start, goal) heapq.heappush(heap, (start_f, counter, start)) counter += 1 came_from = {} g_score = {start: 0} f_score = {start: start_f} visited = set() while heap: cur_f, _, cur = heapq.heappop(heap) visited.add(cur) if cur == goal: self._visited = len(visited) return self._build_path(came_from, start, goal) if cur_f > f_score.get(cur, float('inf')): continue for nb in maze.adjacent_cells(cur): tentative_g = g_score[cur] + 1 if tentative_g < g_score.get(nb, float('inf')): came_from[nb] = cur g_score[nb] = tentative_g new_f = tentative_g + self._heuristic(nb, goal) f_score[nb] = new_f heapq.heappush(heap, (new_f, counter, nb)) counter += 1 self._visited = len(visited) return [] # ----------------------------- Оркестратор ----------------------------- class Pathfinder: def __init__(self, maze): self._maze = maze self._algorithm = None self._listeners = [] def attach(self, listener): self._listeners.append(listener) def notify(self, event, data): for lst in self._listeners: lst.update(event, data) def set_algorithm(self, algorithm): self._algorithm = algorithm def solve(self): if self._algorithm is None: return None t0 = time.perf_counter() path = self._algorithm.compute_path(self._maze, self._maze.start, self._maze.exit) t1 = time.perf_counter() elapsed_ms = (t1 - t0) * 1000 self.notify("path_found", path) return PerformanceData(elapsed_ms, self._algorithm.visited_nodes(), len(path)) class PerformanceData: def __init__(self, time_ms, visited, length): self.time_ms = time_ms self.visited_cells = visited self.path_length = length # ----------------------------- Наблюдатель и отображение ----------------------------- class EventListener: def update(self, event_type, data): raise NotImplementedError class ConsoleDisplay(EventListener): def __init__(self, walker=None): self._last_path = None self._walker = walker def update(self, event_type, data): if event_type == "maze_loaded": self._render_maze(data) elif event_type == "path_found": self._last_path = data self._render_path(data) elif event_type == "player_moved": self._render_maze_with_player(data) def _render_maze(self, maze): os.system('cls' if os.name == 'nt' else 'clear') print("=" * (maze.width * 2 + 4)) print(" LABYRINTH") print("=" * (maze.width * 2 + 4)) for y in range(maze.height): print(" ", end='') for x in range(maze.width): cell = maze.cell_at(x, y) if cell == maze.start: print('S', end=' ') elif cell == maze.exit: print('E', end=' ') elif cell.is_wall: print('#', end=' ') else: print('.', end=' ') print() print("=" * (maze.width * 2 + 4)) print(" S - start E - exit # - wall . - path") def _render_maze_with_player(self, maze): os.system('cls' if os.name == 'nt' else 'clear') print("=" * (maze.width * 2 + 4)) print(" LABYRINTH (P - player)") print("=" * (maze.width * 2 + 4)) for y in range(maze.height): print(" ", end='') for x in range(maze.width): cell = maze.cell_at(x, y) if self._walker and cell == self._walker.current: print('P', end=' ') elif cell == maze.start: print('S', end=' ') elif cell == maze.exit: print('E', end=' ') elif cell.is_wall: print('#', end=' ') else: print('.', end=' ') print() print("=" * (maze.width * 2 + 4)) print(f" Player position: ({self._walker.current.x}, {self._walker.current.y})") print(" S - start E - exit # - wall . - path P - player") def _render_path(self, path): if not path: print("\n Path not found!") return print(f"\n Path found! Length: {len(path)}") # ----------------------------- Игрок и команды ----------------------------- class Walker: def __init__(self, start_cell, lab): self._current = start_cell self._previous = None self._labyrinth = lab @property def current(self): return self._current def move_to(self, cell): if cell and cell.passable(): self._previous = self._current self._current = cell return True return False def undo_move(self): if self._previous: self._current, self._previous = self._previous, None return True return False class Action: def execute(self): raise NotImplementedError def undo(self): raise NotImplementedError class MoveAction(Action): def __init__(self, walker, direction, lab): self._walker = walker self._dx, self._dy = direction self._lab = lab self._executed = False def execute(self): new_x = self._walker.current.x + self._dx new_y = self._walker.current.y + self._dy target = self._lab.cell_at(new_x, new_y) if target and target.passable(): self._walker.move_to(target) self._executed = True return True return False def undo(self): if self._executed: self._walker.undo_move() self._executed = False return True return False # ----------------------------- Эксперименты и статистика ----------------------------- def run_benchmark(maze_file, algorithm, runs=5): builder = TxtLabyrinthBuilder() maze = builder.build_from_file(maze_file) total_time = 0.0 total_visited = 0 total_length = 0 for _ in range(runs): solver = Pathfinder(maze) solver.set_algorithm(algorithm) stats = solver.solve() if stats: total_time += stats.time_ms total_visited += stats.visited_cells total_length += stats.path_length return { 'time_ms': total_time / runs, 'visited_cells': total_visited / runs, 'path_length': total_length / runs } def generate_charts(results): mazes = list(set(r['maze'] for r in results)) alg_names = ['BFS', 'DFS', 'AStar'] fig, axes = plt.subplots(1, 3, figsize=(15, 5)) x = np.arange(len(mazes)) width = 0.25 for i, alg in enumerate(alg_names): times = [] for m in mazes: val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == alg), 0) times.append(val) axes[0].bar(x + i * width, times, width, label=alg) axes[0].set_xlabel('Maze') axes[0].set_ylabel('Time (ms)') axes[0].set_title('Execution Time') axes[0].set_xticks(x + width) axes[0].set_xticklabels(mazes, rotation=45, ha='right') axes[0].legend() axes[0].grid(True, alpha=0.3) for i, alg in enumerate(alg_names): visited = [] for m in mazes: val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == alg), 0) visited.append(val) axes[1].bar(x + i * width, visited, width, label=alg) axes[1].set_xlabel('Maze') axes[1].set_ylabel('Visited Cells') axes[1].set_title('Visited Nodes') axes[1].set_xticks(x + width) axes[1].set_xticklabels(mazes, rotation=45, ha='right') axes[1].legend() axes[1].grid(True, alpha=0.3) for i, alg in enumerate(alg_names): lengths = [] for m in mazes: val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == alg), 0) lengths.append(val) axes[2].bar(x + i * width, lengths, width, label=alg) axes[2].set_xlabel('Maze') axes[2].set_ylabel('Path Length') axes[2].set_title('Optimality') axes[2].set_xticks(x + width) axes[2].set_xticklabels(mazes, rotation=45, ha='right') axes[2].legend() axes[2].grid(True, alpha=0.3) plt.tight_layout() plt.savefig('maze_benchmark.png', dpi=150, bbox_inches='tight') plt.show() def run_experiments(): test_mazes = [ ("maze/level1.txt", "Small 10x6"), ("maze/medium10x10.txt", "Medium 10x10"), ("maze/large20x20.txt", "Large 20x20"), ("maze/empty15x15.txt", "Empty 15x15"), ("maze/no_exit10x10.txt", "No exit 10x10") ] algorithms = [ ("BFS", BFS()), ("DFS", DFS()), ("AStar", AStar()) ] results = [] for filepath, display_name in test_mazes: print(f"Testing {display_name}...") for alg_name, alg_obj in algorithms: try: stats = run_benchmark(filepath, alg_obj, runs=3) results.append({ 'maze': display_name, 'strategy': alg_name, 'time_ms': stats['time_ms'], 'visited_cells': stats['visited_cells'], 'path_length': stats['path_length'] }) print(f" {alg_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}") except Exception as e: print(f" {alg_name}: ERROR - {e}") results.append({ 'maze': display_name, 'strategy': alg_name, 'time_ms': -1, 'visited_cells': -1, 'path_length': -1 }) valid = [r for r in results if r['time_ms'] >= 0] if not valid: print("No valid results to save.") return with open('maze_experiment.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length']) writer.writeheader() writer.writerows(valid) generate_charts(valid) print("\nResults saved to maze_experiment.csv") print("Plot saved to maze_benchmark.png") def play_game(): builder = TxtLabyrinthBuilder() maze = builder.build_from_file("maze/level1.txt") walker = Walker(maze.start, maze) view = ConsoleDisplay(walker) view._render_maze(maze) solver = Pathfinder(maze) solver.attach(view) print("\n CONTROLS:") print(" H (left) J (down) K (up) L (right)") print(" U - undo Q - quit") print("\n AUTO SEARCH:") print(" B - BFS D - DFS A - A*") print("\n" + "=" * 50) action_stack = [] while True: cmd = input("\n Command > ").lower() if cmd == 'q': print("\n Goodbye!") break elif cmd == 'b': solver.set_algorithm(BFS()) stats = solver.solve() if stats: print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") elif cmd == 'd': solver.set_algorithm(DFS()) stats = solver.solve() if stats: print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") elif cmd == 'a': solver.set_algorithm(AStar()) stats = solver.solve() if stats: print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") elif cmd in ['h', 'j', 'k', 'l']: dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)} action = MoveAction(walker, dir_map[cmd], maze) if action.execute(): action_stack.append(action) view._render_maze_with_player(maze) if walker.current == maze.exit: print("\n CONGRATULATIONS! YOU FOUND THE EXIT!") print(f" Total moves: {len(action_stack)}") break else: print("\n Cannot go there! It's a wall.") elif cmd == 'u': if action_stack: last = action_stack.pop() last.undo() view._render_maze_with_player(maze) print("\n Undo last move") else: print("\n Nothing to undo") else: print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit") print("\n Game over. Thanks for playing!") if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] in ('experiment', 'benchmark'): run_experiments() else: play_game()