import time import csv from collections import deque from heapq import heappush, heappop from abc import ABC, abstractmethod from typing import List, Optional, Tuple, Dict, Any import os RESULTS_DIR = "lab2_results" class Cell: def __init__(self, x: int, y: int, is_wall: bool = False): self.x = x self.y = y self.is_wall = is_wall self.is_start = False self.is_exit = False def is_passable(self) -> bool: return not self.is_wall def __repr__(self): return f"Cell({self.x},{self.y})" class Maze: def __init__(self, width: int, height: int): self.width = width self.height = height self.cells: List[List[Cell]] = [] self.start: Optional[Cell] = None self.exit: Optional[Cell] = None def set_cell(self, x: int, y: int, cell: Cell): if not self.cells: self.cells = [[None] * self.width for _ in range(self.height)] self.cells[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.cells[y][x] return None def get_neighbors(self, cell: Cell) -> List[Cell]: neighbors = [] for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]: 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 f: lines = [line.rstrip('\n') for line in f.readlines()] if not lines: raise ValueError("Файл пуст") height = len(lines) width = max(len(line) for line in lines) maze = Maze(width, height) start_cell = None exit_cell = None for y, line in enumerate(lines): for x, ch in enumerate(line): is_wall = (ch == '#') cell = Cell(x, y, is_wall) if ch == 'S': cell.is_start = True start_cell = cell elif ch == 'E': cell.is_exit = True exit_cell = cell maze.set_cell(x, y, cell) if start_cell is None or exit_cell is None: for y in range(height): for x in range(width): cell = maze.get_cell(x, y) if cell and cell.is_start: start_cell = cell if cell and cell.is_exit: exit_cell = cell if start_cell is None: raise ValueError("Нет стартовой клетки (S)") if exit_cell is None: raise ValueError("Нет выходной клетки (E)") maze.start = start_cell maze.exit = exit_cell return maze class PathFindingStrategy(ABC): @abstractmethod def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: pass class BFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: if start == exit: self.last_visited = 1 return [start] queue = deque() queue.append(start) parent = {start: None} visited = {start} visited_count = 1 while queue: current = queue.popleft() if current == exit: break for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) visited_count += 1 parent[neighbor] = current queue.append(neighbor) self.last_visited = visited_count if exit not in parent: return [] path = [] cur = exit while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path class DFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: stack = [(start, [start])] visited = {start} visited_count = 1 while stack: current, path = stack.pop() if current == exit: self.last_visited = visited_count return path for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) visited_count += 1 stack.append((neighbor, path + [neighbor])) self.last_visited = visited_count return [] 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 = [] counter = 0 heappush(open_set, (0, counter, start)) came_from = {} g_score = {start: 0} f_score = {start: self.heuristic(start, exit)} visited_count = 0 while open_set: _, _, current = heappop(open_set) visited_count += 1 if current == exit: path = [] while current in came_from: path.append(current) current = came_from[current] path.append(start) path.reverse() self.last_visited = visited_count return path 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]: came_from[neighbor] = current g_score[neighbor] = tentative_g f = tentative_g + self.heuristic(neighbor, exit) f_score[neighbor] = f counter += 1 heappush(open_set, (f, counter, neighbor)) self.last_visited = visited_count return [] 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 __repr__(self): return f"Stats(time={self.time_ms:.2f}ms, visited={self.visited_cells}, path_len={self.path_length})" class MazeSolver: def __init__(self, maze: Maze, strategy: PathFindingStrategy): self.maze = maze self.strategy = strategy self.observers = [] def set_strategy(self, strategy: PathFindingStrategy): self.strategy = strategy def attach(self, observer): self.observers.append(observer) def detach(self, observer): self.observers.remove(observer) def notify(self, event: str, data: Any = None): for obs in self.observers: obs.update(event, data) def solve(self) -> Tuple[List[Cell], SearchStats]: start_time = time.perf_counter() path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() elapsed_ms = (end_time - start_time) * 1000.0 visited_cells = getattr(self.strategy, 'last_visited', len(path) if path else 0) stats = SearchStats(elapsed_ms, visited_cells, len(path)) self.notify("solved", {"path": path, "stats": stats}) return path, stats class Observer(ABC): @abstractmethod def update(self, event: str, data: Any): pass class ConsoleView(Observer): def __init__(self): self.player_pos = None self.path = [] def update(self, event: str, data: Any): if event == "maze_loaded": self.maze = data["maze"] self.render() elif event == "player_moved": self.player_pos = data["player_cell"] self.render() elif event == "path_found": self.path = data["path"] self.render() elif event == "solved": self.path = data["path"] self.render() def render(self, maze: Maze = None, player_cell: Cell = None, path: List[Cell] = None): if maze: self.maze = maze if player_cell: self.player_pos = player_cell if path is not None: self.path = path if not hasattr(self, 'maze'): print("Нет лабиринта для отображения") return 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 None: row += " " continue if self.player_pos and cell == self.player_pos: row += "P" elif cell == self.maze.start: row += "S" elif cell == self.maze.exit: row += "E" elif self.path and cell in self.path: row += "." elif cell.is_wall: row += "#" else: row += " " print(row) print() class MoveCommand(ABC): @abstractmethod def execute(self): pass @abstractmethod def undo(self): pass class Player: def __init__(self, start_cell: Cell): self.current_cell = start_cell def move_to(self, cell: Cell): self.current_cell = cell class MoveCommandImpl(MoveCommand): def __init__(self, player: Player, direction: str, maze: Maze): self.player = player self.direction = direction self.maze = maze self.previous_cell = player.current_cell def execute(self): dx, dy = 0, 0 if self.direction == 'w': dy = -1 elif self.direction == 's': dy = 1 elif self.direction == 'a': dx = -1 elif self.direction == 'd': dx = 1 else: return False 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) def ensure_results_dir(): if not os.path.exists(RESULTS_DIR): os.makedirs(RESULTS_DIR) print(f"Создана папка: {RESULTS_DIR}") def generate_test_maze_file(filename: str, maze_type: str): full_path = os.path.join(RESULTS_DIR, filename) if maze_type == "small": lines = [ "##########", "#S #", "# #", "# #", "# #", "# #", "# #", "# #", "# E#", "##########" ] elif maze_type == "medium": height, width = 50, 50 lines = [] for y in range(height): row = [] for x in range(width): if y == 0 or y == height-1 or x == 0 or x == width-1: row.append('#') elif (y % 5 == 0 and x % 7 == 0) or (y % 8 == 0 and x % 3 == 0): row.append('#') else: row.append(' ') row_str = ''.join(row) lines.append(row_str) lines[1] = 'S' + lines[1][1:] lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:] elif maze_type == "large": import random height, width = 100, 100 random.seed(42) lines = [] for y in range(height): row = [] for x in range(width): if y == 0 or y == height-1 or x == 0 or x == width-1: row.append('#') else: if random.random() < 0.2: row.append('#') else: row.append(' ') lines.append(''.join(row)) lines[1] = 'S' + lines[1][1:] lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:] elif maze_type == "empty": height, width = 50, 50 lines = [] for y in range(height): if y == 0 or y == height-1: lines.append('#' * width) else: lines.append('#' + ' ' * (width-2) + '#') lines[1] = 'S' + lines[1][1:] lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:] elif maze_type == "no_exit": lines = [ "##########", "#S #", "# #", "# #", "# #", "# #", "# #", "# #", "# #", "##########" ] else: raise ValueError("Unknown maze type") with open(full_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) def run_experiment(): ensure_results_dir() maze_types = ["small", "medium", "large", "empty", "no_exit"] strategies = { "BFS": BFSStrategy(), "DFS": DFSStrategy(), "AStar": AStarStrategy() } results = [] for maze_type in maze_types: filename = f"maze_{maze_type}.txt" generate_test_maze_file(filename, maze_type) full_path = os.path.join(RESULTS_DIR, filename) builder = TextFileMazeBuilder() try: maze = builder.build_from_file(full_path) except ValueError as e: print(f"Лабиринт {maze_type} пропущен: {e}") continue for strat_name, strat_obj in strategies.items(): times = [] path_lengths = [] visited_counts = [] for run in range(5): solver = MazeSolver(maze, strat_obj) path, stats = solver.solve() times.append(stats.time_ms) path_lengths.append(stats.path_length) visited_counts.append(stats.visited_cells) avg_time = sum(times) / len(times) avg_path_len = sum(path_lengths) / len(path_lengths) avg_visited = sum(visited_counts) / len(visited_counts) results.append({ "maze": maze_type, "strategy": strat_name, "avg_time_ms": avg_time, "avg_visited": avg_visited, "avg_path_length": avg_path_len }) print(f"{maze_type} / {strat_name}: время={avg_time:.2f}ms, посещено={avg_visited:.1f}, путь={avg_path_len:.1f}") csv_path = os.path.join(RESULTS_DIR, "experiment_results.csv") with open(csv_path, "w", newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "avg_time_ms", "avg_visited", "avg_path_length"]) writer.writeheader() writer.writerows(results) try: import matplotlib.pyplot as plt for maze_type in ["small", "medium", "large", "empty"]: data = [r for r in results if r["maze"] == maze_type] if not data: continue names = [d["strategy"] for d in data] times = [d["avg_time_ms"] for d in data] visited = [d["avg_visited"] for d in data] fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) ax1.bar(names, times) ax1.set_title(f"Время (мс) - {maze_type}") ax2.bar(names, visited) ax2.set_title(f"Посещено клеток - {maze_type}") plt.tight_layout() plot_path = os.path.join(RESULTS_DIR, f"plot_{maze_type}.png") plt.savefig(plot_path) plt.close() print(f"Графики сохранены в папку {RESULTS_DIR}") except ImportError: print("matplotlib не установлен. Графики не построены.") def demo_interactive(): ensure_results_dir() builder = TextFileMazeBuilder() filename = input("Введите имя файла с лабиринтом (например, maze_small.txt): ").strip() if not os.path.exists(filename) and not os.path.exists(os.path.join(RESULTS_DIR, filename)): print(f"Файл {filename} не найден. Создаю тестовый лабиринт small в папке {RESULTS_DIR}") generate_test_maze_file("demo_maze.txt", "small") filename = os.path.join(RESULTS_DIR, "demo_maze.txt") elif os.path.exists(os.path.join(RESULTS_DIR, filename)): filename = os.path.join(RESULTS_DIR, filename) maze = builder.build_from_file(filename) view = ConsoleView() view.update("maze_loaded", {"maze": maze}) print("Выберите алгоритм поиска:") print("1. BFS") print("2. DFS") print("3. A*") choice = input("Ваш выбор: ") if choice == "1": strategy = BFSStrategy() elif choice == "2": strategy = DFSStrategy() else: strategy = AStarStrategy() solver = MazeSolver(maze, strategy) solver.attach(view) path, stats = solver.solve() print(f"Поиск завершён. Статистика: {stats}") if path: print("Найден путь. Хотите пройти по нему пошагово? (y/n): ", end="") ans = input().lower() if ans == 'y': player = Player(maze.start) cmd_history = [] for step_cell in path[1:]: dx = step_cell.x - player.current_cell.x dy = step_cell.y - player.current_cell.y if dx == 1: dir_char = 'd' elif dx == -1: dir_char = 'a' elif dy == 1: dir_char = 's' else: dir_char = 'w' cmd = MoveCommandImpl(player, dir_char, maze) if cmd.execute(): cmd_history.append(cmd) view.update("player_moved", {"player_cell": player.current_cell}) input("Нажмите Enter для следующего шага...") print("Вы достигли выхода!") print("Отменить последний шаг? (y/n): ", end="") if input().lower() == 'y' and cmd_history: cmd_history[-1].undo() view.update("player_moved", {"player_cell": player.current_cell}) print("Последний шаг отменён.") else: print("Путь не найден.") if __name__ == "__main__": print("Лабораторная работа: Поиск выхода из лабиринта") print("1. Запустить эксперименты (сравнение алгоритмов)") print("2. Интерактивный режим (загрузка своего лабиринта)") mode = input("Выберите режим (1 или 2): ") if mode == "1": run_experiment() elif mode == "2": demo_interactive() else: print("Неверный выбор.")