class Cell: def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False): self.x = x self.y = y self.is_wall = is_wall self.is_start = is_start self.is_exit = is_exit def __lt__(self, other): return (self.x, self.y) < (other.x, other.y) 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 y in range(height)] for x in range(width)] 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[x][y] return None def get_neighbors(self, 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 def __repr__(self): rows = [] for y in range(self.height): row = [] for x in range(self.width): c = self.get_cell(x, y) if c.is_wall: row.append('#') elif c.is_start: row.append('S') elif c.is_exit: row.append('E') else: row.append(' ') rows.append(''.join(row)) return '\n'.join(rows) def set_start(self, x, y): cell = self.get_cell(x, y) if cell and cell.is_passable(): cell.is_start = True self.start = cell def set_exit(self, x, y): cell = self.get_cell(x, y) if cell and cell.is_passable(): cell.is_exit = True self.exit = cell from abc import ABC, abstractmethod 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] h = len(lines) w = len(lines[0]) if h > 0 else 0 maze = Maze(w, h) 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 if not maze.start: raise ValueError("Нет старта (S)") if not maze.exit: raise ValueError("Нет выхода (E)") return maze from collections import deque import heapq import time # ========== Strategy ========== class PathFindingStrategy(ABC): @abstractmethod def find_path(self, maze): """Возвращает список клеток от старта до выхода (включительно) или []""" pass class BFSStrategy(PathFindingStrategy): def find_path(self, maze): start = maze.start exit_cell = maze.exit if not start or not exit_cell: return [] queue = deque([start]) visited = {start} parent = {start: None} while queue: current = queue.popleft() if current == exit_cell: break for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parent[neighbor] = current queue.append(neighbor) if exit_cell not in parent: return [] # Восстановление пути path = [] step = exit_cell while step: path.append(step) step = parent[step] path.reverse() return path class DFSStrategy(PathFindingStrategy): def find_path(self, maze): start = maze.start exit_cell = maze.exit if not start or not exit_cell: return [] stack = [(start, [start])] visited = {start} while stack: current, path = stack.pop() if current == exit_cell: return path for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) stack.append((neighbor, path + [neighbor])) return [] class AStarStrategy(PathFindingStrategy): def heuristic(self, a, b): # Манхэттенское расстояние return abs(a.x - b.x) + abs(a.y - b.y) def find_path(self, maze): start = maze.start exit_cell = maze.exit if not start or not exit_cell: return [] open_set = [(self.heuristic(start, exit_cell), 0, start)] g_score = {start: 0} parent = {start: None} visited = {start} while open_set: _, cost, current = heapq.heappop(open_set) if current == exit_cell: break for neighbor in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if neighbor not in g_score or tentative_g < g_score[neighbor]: parent[neighbor] = current g_score[neighbor] = tentative_g f = tentative_g + self.heuristic(neighbor, exit_cell) heapq.heappush(open_set, (f, tentative_g, neighbor)) visited.add(neighbor) if exit_cell not in parent: return [] path = [] step = exit_cell while step: path.append(step) step = parent[step] path.reverse() return path # ========== SearchStats ========== class SearchStats: def __init__(self, time_ms=0.0, visited_cells=0, path_length=0): self.time_ms = time_ms self.visited_cells = visited_cells self.path_length = path_length def __repr__(self): return f"time={self.time_ms:.3f} ms, visited={self.visited_cells}, path_len={self.path_length}" # ========== MazeSolver ========== class MazeSolver: def __init__(self, maze, strategy=None): self.maze = maze self.strategy = strategy self.observers = [] def attach(self, observer): self.observers.append(observer) def notify(self, event_type, data=None): for obs in self.observers: obs.update(event_type, data) def set_strategy(self, strategy): self.strategy = strategy def solve(self): if not self.strategy: raise ValueError("Стратегия не установлена") start_time = time.perf_counter() path = self.strategy.find_path(self.maze) end_time = time.perf_counter() stats = SearchStats() stats.time_ms = (end_time - start_time) * 1000 stats.path_length = len(path) if path else 0 if path: self.notify("path_found", path) return path, stats # ========== Observer ========== class Observer(ABC): @abstractmethod def update(self, event_type, data): pass class ConsoleView(Observer): def __init__(self, maze): self.maze = maze def update(self, event_type, data): if event_type == "path_found": path = data self.render(path) elif event_type == "move": player_pos = data self.render(player_pos=player_pos) else: self.render() def render(self, path=None, player_pos=None): """Отрисовка лабиринта с путём и/или позицией игрока""" # Копия лабиринта для отображения display = [] for y in range(self.maze.height): row = [] for x in range(self.maze.width): cell = self.maze.get_cell(x, y) if cell.is_wall: row.append('█') elif cell.is_start: row.append('S') elif cell.is_exit: row.append('E') else: row.append(' ') display.append(row) # Отметить путь (кроме старта и выхода) if path: for cell in path: if cell != self.maze.start and cell != self.maze.exit: display[cell.y][cell.x] = '•' # Отметить игрока (если есть) if player_pos: x, y = player_pos.x, player_pos.y if display[y][x] not in ('S', 'E'): display[y][x] = 'P' # Очистка консоли (для красоты, можно закомментировать) import os os.system('cls' if os.name == 'nt' else 'clear') for row in display: print(''.join(row)) print() # ========== Command ========== 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 # (dx, dy) self.maze = maze self.previous_cell = player.current_cell def execute(self): dx, dy = self.direction 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.player.move_to(new_cell) return True return False def undo(self): self.player.move_to(self.previous_cell) class Player: def __init__(self, start_cell): self.current_cell = start_cell def move_to(self, cell): self.current_cell = cell # ========== Observer ========== class Observer(ABC): @abstractmethod def update(self, event_type, data): pass class ConsoleView(Observer): def __init__(self, maze): self.maze = maze def update(self, event_type, data): if event_type == "path_found": path = data self.render(path=path) elif event_type == "move": player_pos = data self.render(player_pos=player_pos) else: self.render() def render(self, path=None, player_pos=None): """Отрисовка лабиринта с путём и/или позицией игрока""" display = [] for y in range(self.maze.height): row = [] for x in range(self.maze.width): cell = self.maze.get_cell(x, y) if cell.is_wall: row.append('#') elif cell.is_start: row.append('S') elif cell.is_exit: row.append('E') else: row.append(' ') display.append(row) if path: for cell in path: if cell != self.maze.start and cell != self.maze.exit: display[cell.y][cell.x] = '•' if player_pos: x, y = player_pos.x, player_pos.y if display[y][x] not in ('S', 'E'): display[y][x] = 'P' # Очистка консоли для красоты (можно закомментировать) import os os.system('cls' if os.name == 'nt' else 'clear') for row in display: print(''.join(row)) print() # ========== Command ========== 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.previous_cell = player.current_cell def execute(self): dx, dy = self.direction 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.player.move_to(new_cell) return True return False def undo(self): self.player.move_to(self.previous_cell) class Player: def __init__(self, start_cell): self.current_cell = start_cell def move_to(self, cell): self.current_cell = cell # ========== ЭКСПЕРИМЕНТЫ ========== import csv import random def generate_test_mazes(): """Создаёт несколько лабиринтов для тестирования""" mazes = {} # 1. Маленький лабиринт 5x5 small = Maze(5, 5) for x in range(5): small.get_cell(x, 0).is_wall = True small.get_cell(x, 4).is_wall = True for y in range(5): small.get_cell(0, y).is_wall = True small.get_cell(4, y).is_wall = True small.get_cell(1, 1).is_wall = False small.get_cell(2, 1).is_wall = False small.get_cell(3, 1).is_wall = False small.get_cell(3, 2).is_wall = False small.get_cell(3, 3).is_wall = False small.set_start(1, 1) small.set_exit(3, 3) mazes["small"] = small # 2. Средний лабиринт 15x15 (стены по краям и простой коридор) medium = Maze(15, 15) for x in range(15): medium.get_cell(x, 0).is_wall = True medium.get_cell(x, 14).is_wall = True for y in range(15): medium.get_cell(0, y).is_wall = True medium.get_cell(14, y).is_wall = True # Простой зигзаг for i in range(1, 14): medium.get_cell(i, i).is_wall = False medium.set_start(1, 1) medium.set_exit(13, 13) mazes["medium"] = medium # 3. Пустой лабиринт (нет стен) empty = Maze(20, 20) for x in range(20): for y in range(20): empty.get_cell(x, y).is_wall = False empty.set_start(0, 0) empty.set_exit(19, 19) mazes["empty"] = empty # 4. Лабиринт без выхода (путь заблокирован) no_exit = Maze(10, 10) for x in range(10): for y in range(10): no_exit.get_cell(x, y).is_wall = False for x in range(5, 10): no_exit.get_cell(x, 5).is_wall = True # стена блокирует no_exit.set_start(0, 0) no_exit.set_exit(9, 9) mazes["no_exit"] = no_exit return mazes def run_experiments(mazes, strategies, repeats=5): """Прогоняет все стратегии на всех лабиринтах repeats раз, возвращает список результатов""" results = [] for maze_name, maze in mazes.items(): for strategy_name, strategy in strategies.items(): solver = MazeSolver(maze) solver.set_strategy(strategy) for _ in range(repeats): path, stats = solver.solve() results.append({ "maze": maze_name, "strategy": strategy_name, "time_ms": stats.time_ms, "path_length": stats.path_length }) return results def save_results_to_csv(results, filename="maze_results.csv"): with open(filename, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "time_ms", "path_length"]) writer.writeheader() writer.writerows(results) print(f"Результаты сохранены в {filename}") def plot_maze_results(csv_file="maze_results.csv", output_png="maze_graphs.png"): try: import matplotlib.pyplot as plt import pandas as pd except ImportError: print("matplotlib или pandas не установлены. Установи: pip install matplotlib pandas") return df = pd.read_csv(csv_file) fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # График времени for strategy in df["strategy"].unique(): subset = df[df["strategy"] == strategy] axes[0].plot(subset["maze"], subset["time_ms"], marker='o', label=strategy) axes[0].set_title("Время поиска пути") axes[0].set_ylabel("Время (мс)") axes[0].legend() # График длины пути for strategy in df["strategy"].unique(): subset = df[df["strategy"] == strategy] axes[1].plot(subset["maze"], subset["path_length"], marker='s', label=strategy) axes[1].set_title("Длина найденного пути") axes[1].set_ylabel("Клеток") axes[1].legend() plt.tight_layout() plt.savefig(output_png) print(f"График сохранён как {output_png}") # plt.show() # раскомментируй, если хочешь увидеть окно с графиком if __name__ == "__main__": # Генерируем тестовые лабиринты mazes = generate_test_mazes() strategies = { "BFS": BFSStrategy(), "DFS": DFSStrategy(), "A*": AStarStrategy(), } print("Запуск экспериментов (может занять 10–20 секунд)...") results = run_experiments(mazes, strategies, repeats=5) save_results_to_csv(results) plot_maze_results() print("Готово! Файлы maze_results.csv и maze_graphs.png созданы.")