import time import csv import heapq from collections import deque from abc import ABC, abstractmethod import matplotlib.pyplot as plt import pandas as pd from dataclasses import dataclass import os class Cell: """Клетка лабиринта""" def __init__(self, x, y, is_wall=False): self.x = x self.y = y self.is_wall = is_wall self.is_start = False self.is_exit = False def is_passable(self): return not self.is_wall class Maze: """Лабиринт""" def __init__(self, width, height): self.width = width self.height = height self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)] self.start = None self.exit = None 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): neighbors = [] for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]: 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 def __str__(self): result = "" for y in range(self.height): for x in range(self.width): cell = self.get_cell(x, y) if cell is None: result += "?" elif cell.is_wall: result += "#" elif cell.is_start: result += "S" elif cell.is_exit: result += "E" else: result += " " result += "\n" return result class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename): pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename): 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) maze = Maze(width, height) for y, line in enumerate(lines): for x, ch in enumerate(line): cell = maze.get_cell(x, y) 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 else: cell.is_wall = False return maze class PathFindingStrategy(ABC): @abstractmethod def find_path(self, maze, start, exit): pass class BFSStrategy(PathFindingStrategy): """Поиск в ширину""" def find_path(self, maze, start, exit): visited = set() if start == exit: return [start], 1 queue = deque([start]) visited.add(start) parent = {start: None} while queue: current = queue.popleft() for nb in maze.get_neighbors(current): if nb not in visited: visited.add(nb) parent[nb] = current if nb == exit: path = [] node = nb while node is not None: path.append(node) node = parent[node] path.reverse() return path, len(visited) queue.append(nb) return [], len(visited) class DFSStrategy(PathFindingStrategy): """Поиск в глубину""" def find_path(self, maze, start, exit): visited = set() stack = [(start, [start])] while stack: current, path = stack.pop() if current == exit: return path, len(visited) visited.add(current) for nb in maze.get_neighbors(current): if nb not in visited: stack.append((nb, path + [nb])) return [], len(visited) class AStarStrategy(PathFindingStrategy): """Алгоритм A""" def heuristic(self, cell, exit): return abs(cell.x - exit.x) + abs(cell.y - exit.y) def find_path(self, maze, start, exit): open_set = [] counter = 0 heapq.heappush(open_set, (0, counter, start)) counter += 1 came_from = {} g_score = {start: 0} f_score = {start: self.heuristic(start, exit)} visited = set() while open_set: _, _, current = heapq.heappop(open_set) visited.add(current) if current == exit: path = [] node = current while node in came_from: path.append(node) node = came_from[node] path.append(start) path.reverse() return path, len(visited) for nb in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if tentative_g < g_score.get(nb, float('inf')): came_from[nb] = current g_score[nb] = tentative_g f = tentative_g + self.heuristic(nb, exit) heapq.heappush(open_set, (f, counter, nb)) counter += 1 return [], len(visited) @dataclass class SearchStats: time_ms: float visited_cells: int path_length: int algorithm: str class MazeSolver: def __init__(self, maze, strategy): self.maze = maze self.strategy = strategy def set_strategy(self, strategy): self.strategy = strategy def solve(self): if self.maze.start is None or self.maze.exit is None: raise ValueError("Лабиринт не имеет старта или выхода") start_time = time.perf_counter() path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() stats = SearchStats( time_ms=(end_time - start_time) * 1000, visited_cells=visited, path_length=len(path), algorithm=self.strategy.__class__.__name__ ) return path, stats class Observer(ABC): @abstractmethod def update(self, event_type, data=None): pass class ConsoleLogger(Observer): def update(self, event_type, data=None): if event_type == "search_start": print(f"[LOG] Поиск пути начат") elif event_type == "path_found": print(f"[LOG] Путь найден! Длина: {data}") elif event_type == "no_path": print("[LOG] Путь не найден") elif event_type == "step": print(f"[LOG] Шаг: {data}") class MazeSolverWithObserver(MazeSolver): def __init__(self, maze, strategy, observers=None): super().__init__(maze, strategy) self.observers = observers if observers else [] def attach(self, observer): self.observers.append(observer) def detach(self, observer): self.observers.remove(observer) def notify(self, event_type, data=None): for obs in self.observers: obs.update(event_type, data) def solve(self): if self.maze.start is None or self.maze.exit is None: raise ValueError("Лабиринт не имеет старта или выхода") self.notify("search_start") start_time = time.perf_counter() path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() if path: self.notify("path_found", len(path)) else: self.notify("no_path") stats = SearchStats( time_ms=(end_time - start_time) * 1000, visited_cells=visited, path_length=len(path), algorithm=self.strategy.__class__.__name__ ) return path, stats class Command(ABC): @abstractmethod def execute(self): pass @abstractmethod def undo(self): pass class MoveCommand(Command): def __init__(self, player, direction, maze): self.player = player self.direction = direction self.maze = maze self.prev_pos = None def execute(self): self.prev_pos = self.player.current_cell dx, dy = self.direction nx, ny = self.player.current_cell.x + dx, self.player.current_cell.y + dy new_cell = self.maze.get_cell(nx, ny) if new_cell and new_cell.is_passable(): self.player.current_cell = new_cell return True return False def undo(self): if self.prev_pos: self.player.current_cell = self.prev_pos return True return False class Player: def __init__(self, start_cell): self.current_cell = start_cell def interactive_move_demo(maze, path): """Демонстрация движения с отменой последнего шага""" if not path: print("Путь не найден, демонстрация движения невозможна.") return player = Player(maze.start) command_history = [] print("\n Интерактивное движение по найденному пути") print("Текущая позиция: старт") for step, cell in enumerate(path): if cell == maze.start: continue prev = path[step-1] dx = cell.x - prev.x dy = cell.y - prev.y cmd = MoveCommand(player, (dx, dy), maze) cmd.execute() command_history.append(cmd) print(f"Шаг {step}: перемещение на ({dx},{dy}), позиция ({player.current_cell.x},{player.current_cell.y})") if cell == maze.exit: print("Достигнут выход!") break if command_history: print("\nДемонстрация отмены последнего шага") cmd = command_history[-1] cmd.undo() print(f"Отменён последний шаг, позиция: ({player.current_cell.x},{player.current_cell.y})") def test_single_maze(filename, strategies, repeats=5): """Тестирование одного лабиринта с разными стратегиями""" builder = TextFileMazeBuilder() maze = builder.build_from_file(filename) results = [] for strategy in strategies: solver = MazeSolver(maze, strategy) times = [] visits = [] lengths = [] for _ in range(repeats): _, stats = solver.solve() times.append(stats.time_ms) visits.append(stats.visited_cells) lengths.append(stats.path_length) results.append({ 'algorithm': strategy.__class__.__name__, 'avg_time_ms': sum(times) / repeats, 'avg_visited': sum(visits) / repeats, 'avg_path_len': sum(lengths) / repeats }) return results def save_maze_to_file(maze, filename): """Сохранение лабиринта в файл""" os.makedirs(os.path.dirname(filename), exist_ok=True) with open(filename, 'w', encoding='utf-8') as f: for y in range(maze.height): line = "" for x in range(maze.width): cell = maze.get_cell(x, y) if cell.is_wall: line += "#" elif cell.is_start: line += "S" elif cell.is_exit: line += "E" else: line += " " f.write(line + "\n") def create_test_mazes(): """Создание тестовых лабиринтов""" os.makedirs("mazes", exist_ok=True) # 1. Простой лабиринт 10x10 (tiny.txt) maze1 = Maze(10, 10) for y in range(10): for x in range(10): is_start = (x == 0 and y == 0) is_exit = (x == 9 and y == 0) is_wall = False if y == 1 and x not in [0, 1, 9]: is_wall = True if y == 2 and x not in [9]: is_wall = True if y == 3 and x not in [0, 9]: is_wall = True if y == 4 and x not in [0, 1, 9]: is_wall = True if y == 5 and x not in [9]: is_wall = True if y == 6 and x not in [0, 9]: is_wall = True if y == 7 and x not in [9]: is_wall = True if y == 8 and x not in [0, 9]: is_wall = True cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze1.cells[y][x] = cell if is_start: maze1.start = cell if is_exit: maze1.exit = cell save_maze_to_file(maze1, "mazes/tiny.txt") # 2. Средний лабиринт 15x15 (medium.txt) maze2 = Maze(15, 15) for y in range(15): for x in range(15): is_start = (x == 0 and y == 0) is_exit = (x == 14 and y == 14) is_wall = (x % 3 == 1 and y % 2 == 0) and not is_start and not is_exit cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze2.cells[y][x] = cell if is_start: maze2.start = cell if is_exit: maze2.exit = cell save_maze_to_file(maze2, "mazes/medium.txt") # 3. Большой лабиринт 30x30 (large.txt) maze3 = Maze(30, 30) for y in range(30): for x in range(30): is_start = (x == 0 and y == 0) is_exit = (x == 29 and y == 29) is_wall = (x % 2 == 0 and y % 3 == 0) and not is_start and not is_exit cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze3.cells[y][x] = cell if is_start: maze3.start = cell if is_exit: maze3.exit = cell save_maze_to_file(maze3, "mazes/large.txt") # 4. Пустой лабиринт 15x15 (empty.txt) maze4 = Maze(15, 15) for y in range(15): for x in range(15): is_start = (x == 0 and y == 0) is_exit = (x == 14 and y == 14) cell = Cell(x, y, is_wall=False) cell.is_start = is_start cell.is_exit = is_exit maze4.cells[y][x] = cell if is_start: maze4.start = cell if is_exit: maze4.exit = cell save_maze_to_file(maze4, "mazes/empty.txt") # 5. Лабиринт без выхода 10x10 (no_exit.txt) maze5 = Maze(10, 10) for y in range(10): for x in range(10): is_start = (x == 0 and y == 0) is_exit = (x == 9 and y == 9) is_wall = (x > 0 and y > 0) and not is_start cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze5.cells[y][x] = cell if is_start: maze5.start = cell if is_exit: maze5.exit = cell save_maze_to_file(maze5, "mazes/no_exit.txt")