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 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): class ConsoleView(Observer): class MoveCommand(ABC): class Player: class MoveCommandImpl(MoveCommand):