import abc import heapq import time from collections import deque from dataclasses import dataclass from typing import List, Optional, Dict, Set, Tuple, Any class Cell: #тут что такое клетка def __init__(self, x: int, y: int, is_wall: bool = False, is_exit: bool = False, is_start: bool = False): self.x = x self.y = y self.is_wall = is_wall self.is_exit = is_exit self.is_start = is_start def is_passable(self) -> bool: #можно ли пройти через клетку return not self.is_wall def __repr__(self) -> str: return f"Cell({self.x},{self.y})" class Maze: def __init__(self, width: int, height: int): #что содержит лабиринт, начало конец и тд self.width = width self.height = height self.grid: List[List[Cell]] = [] self.start_cell: Optional[Cell] = None self.exit_cell: Optional[Cell] = None def set_cell(self, x: int, y: int, cell: Cell) -> None: #ставим клетку куда надо или не ставим если в границы не попала if not (0 <= x < self.width and 0 <= y < self.height): raise IndexError("координаты вне границ лабиринта") self.grid[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.grid[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.ABC): """Абстрактный строитель лабиринта.""" @abc.abstractmethod def build_from_file(self, filename: str) -> Maze: """Построить лабиринт из файла.""" pass class TextFileMazeBuilder(MazeBuilder): """Строитель лабиринта из текстового файла. # - стена, пробел - проход, S - старт, E - выход.""" def build_from_file(self, filename: str) -> Maze: lines = [] with open(filename, 'r', encoding='utf-8') as f: for line in f: line = line.rstrip('\n') if line: #игнорируем пустые строки lines.append(line) if not lines: raise ValueError("Файл пуст") height = len(lines) width = max(len(line) for line in lines) maze = Maze(width, height) # Инициализируем сетку пустыми клетками (по умолчанию стенами) maze.grid = [[Cell(x, y, is_wall=True) for x in range(width)] for y in range(height)] start_cell = None exit_cell = None for y, line in enumerate(lines): for x, ch in enumerate(line): if x >= width: continue if ch == '#': # стена (уже создана по умолчанию) continue elif ch == ' ': # проход cell = Cell(x, y, is_wall=False) elif ch == 'S': cell = Cell(x, y, is_wall=False, is_start=True) start_cell = cell elif ch == 'E': cell = Cell(x, y, is_wall=False, is_exit=True) exit_cell = cell else: # любой другой символ считаем проходом cell = Cell(x, y, is_wall=False) maze.set_cell(x, y, cell) # Проверяем наличие старта и выхода if start_cell is None: raise ValueError("В лабиринте отсутствует стартовая клетка (S)") if exit_cell is None: raise ValueError("В лабиринте отсутствует выход (E)") maze.start_cell = start_cell maze.exit_cell = exit_cell return maze # ============================== Паттерн Strategy ============================== class PathFindingStrategy(abc.ABC): """Интерфейс стратегии поиска пути.""" @abc.abstractmethod def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: """ Возвращает (путь в виде списка клеток от start до exit_, количество посещённых клеток). Если пути нет, возвращает ([], visited_count). """ pass class BFSStrategy(PathFindingStrategy): """Поиск в ширину — гарантирует кратчайший путь.""" def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: if start is exit_: return [start], 1 queue = deque([start]) visited: Set[Cell] = {start} parent: Dict[Cell, Optional[Cell]] = {start: None} while queue: current = queue.popleft() if current is exit_: # восстановление пути path = [] cur = current while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path, len(visited) for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parent[neighbor] = current queue.append(neighbor) return [], len(visited) class DFSStrategy(PathFindingStrategy): """Поиск в глубину — не гарантирует кратчайший путь, но быстр.""" def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: if start is exit_: return [start], 1 stack = [start] visited: Set[Cell] = {start} parent: Dict[Cell, Optional[Cell]] = {start: None} while stack: current = stack.pop() if current is exit_: path = [] cur = current while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path, len(visited) for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parent[neighbor] = current stack.append(neighbor) return [], len(visited) class AStarStrategy(PathFindingStrategy): """Алгоритм A* с манхэттенской эвристикой.""" @staticmethod def _heuristic(cell: Cell, target: Cell) -> int: """Манхэттенское расстояние.""" return abs(cell.x - target.x) + abs(cell.y - target.y) def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]: if start is exit_: return [start], 1 # Приоритетная очередь: (f, counter, cell) counter = 0 open_set = [(0, counter, start)] g_score: Dict[Cell, int] = {start: 0} f_score: Dict[Cell, int] = {start: self._heuristic(start, exit_)} parent: Dict[Cell, Optional[Cell]] = {start: None} closed_set: Set[Cell] = set() visited_count = 0 while open_set: _, _, current = heapq.heappop(open_set) if current in closed_set: continue closed_set.add(current) visited_count = len(closed_set) # количество обработанных клеток if current is exit_: path = [] cur = current while cur is not None: path.append(cur) cur = parent[cur] path.reverse() return path, visited_count for neighbor in maze.get_neighbors(current): if neighbor in closed_set: continue 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_) f_score[neighbor] = f counter += 1 heapq.heappush(open_set, (f, counter, neighbor)) return [], visited_count # ============================== Статистика ============================== @dataclass class SearchStats: """Статистика поиска.""" time_ms: float # время выполнения в миллисекундах visited_cells: int # количество посещённых клеток path_length: int # длина найденного пути (0 если пути нет) path_found: bool # найден ли путь # ============================== Паттерн Observer ============================== class Observer(abc.ABC): """Интерфейс наблюдателя.""" @abc.abstractmethod def update(self, event_type: str, data: Any = None) -> None: """Получить уведомление от субъекта.""" pass class Subject: """Субъект, за которым наблюдают.""" def __init__(self): self._observers: List[Observer] = [] def attach(self, observer: Observer) -> None: if observer not in self._observers: self._observers.append(observer) def detach(self, observer: Observer) -> None: if observer in self._observers: self._observers.remove(observer) def notify(self, event_type: str, data: Any = None) -> None: for observer in self._observers: observer.update(event_type, data) # ============================== MazeSolver (оркестратор) ============================== class MazeSolver(Subject): """Решатель лабиринта, использующий стратегию поиска.""" def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): super().__init__() self.maze = maze self._strategy = strategy def set_strategy(self, strategy: PathFindingStrategy) -> None: """Сменить алгоритм поиска.""" self._strategy = strategy def solve(self) -> Optional[SearchStats]: """Выполнить поиск пути с текущей стратегией. Возвращает статистику или None, если стратегия не установлена.""" if self._strategy is None: print("Стратегия не установлена.") return None self.notify("solving_start", {"strategy": self._strategy.__class__.__name__}) start_time = time.perf_counter() path, visited = self._strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell) end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000.0 path_found = len(path) > 0 stats = SearchStats( time_ms=time_ms, visited_cells=visited, path_length=len(path) if path_found else 0, path_found=path_found ) if path_found: self.notify("path_found", {"path": path, "stats": stats}) else: self.notify("no_path", {"stats": stats}) self.notify("solving_end", {"stats": stats}) return stats class ConsoleView(Observer): """Отображает лабиринт и найденный путь в консоли.""" def __init__(self, maze: Maze): self.maze = maze self.last_path: List[Cell] = [] def update(self, event_type: str, data: Any = None) -> None: if event_type == "path_found": self.last_path = data["path"] self.render() elif event_type == "no_path": self.last_path = [] self.render(no_path=True) elif event_type == "solving_start": print(f"\n== Поиск пути (алгоритм: {data['strategy']}) ==\n") def render(self, no_path: bool = False) -> None: """Отрисовать лабиринт с текущим найденным путём.""" # Создаём множество клеток пути для быстрой проверки path_set = set(self.last_path) if self.last_path else set() 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.append('?') continue if cell is self.maze.start_cell: row.append('S') elif cell is self.maze.exit_cell: row.append('E') elif cell in path_set: row.append('*') elif cell.is_wall: row.append('#') else: row.append(' ') print(''.join(row)) if no_path: print("\nПуть не найден!") elif self.last_path: print(f"\nНайден путь длиной {len(self.last_path)} клеток.") else: print("\nОжидание решения...") def main(): import sys if len(sys.argv) < 2: print("для запуска: python main.py <имя_лабиринта>.txt") return filename = sys.argv[1] # Строим лабиринт из файла (Builder) builder = TextFileMazeBuilder() try: maze = builder.build_from_file(filename) except Exception as e: print(f"Ошибка загрузки лабиринта: {e}") return # Создаём решатель и прикрепляем наблюдателя solver = MazeSolver(maze) view = ConsoleView(maze) solver.attach(view) # Меню выбора стратегии strategies = { "1": BFSStrategy(), "2": DFSStrategy(), "3": AStarStrategy() } print("\nвыберите алгоритм поиска:") print("1. BFS") print("2. DFS") print("3. A*") choice = input("введите (1/2/3): ").strip() strategy = strategies.get(choice) if not strategy: print("неверный выбор, по умолчанию используется BFS.") strategy = BFSStrategy() solver.set_strategy(strategy) stats = solver.solve() if stats: print("\nстатистика по поиску пути в данном лабиринте") print(f"выбранный алгоритм: {strategy.__class__.__name__}") print(f"время выполнения: {stats.time_ms:.3f} мс") print(f"посещено клеток: {stats.visited_cells}") print(f"путь найден?: {'да' if stats.path_found else 'нет'}") if stats.path_found: print(f"длина пути: {stats.path_length}") if __name__ == "__main__": main()