import sys 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 не установлен. Графики не построены.")