import time, os from collections import deque from abc import ABC, abstractmethod import heapq # <-- Добавлен импорт для A* # --- ЭТАП 1: МОДЕЛЬ --- class Cell: def __init__(self, x, y, is_wall=False): self.x = x self.y = y self.is_wall = is_wall def isPassable(self): return not self.is_wall class Maze: def __init__(self, width, height, grid): self.width = width self.height = height self.grid = grid self.start_cell = grid[0][0] self.exit_cell = grid[height-1][width-1] def getNeighbors(self, cell): neighbors = [] directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] for d in directions: nx = cell.x + d[0] ny = cell.y + d[1] if nx >= 0 and nx < self.width and ny >= 0 and ny < self.height: if not self.grid[ny][nx].is_wall: neighbors.append(self.grid[ny][nx]) return neighbors # --- ЭТАП 2: BUILDER --- class MazeBuilder: def buildFromFile(self, filename): path = filename if "docs/data/" not in path: path = os.path.join("docs", "data", filename) with open(path, 'r') as f: lines = [] for line in f: stripped = line.strip() if stripped: lines.append(stripped) h = len(lines) w = len(lines[0]) grid = [] for y in range(h): row = [] for x in range(w): is_wall = False if x < len(lines[y]): if lines[y][x] == '#': is_wall = True row.append(Cell(x, y, is_wall)) grid.append(row) maze = Maze(w, h, grid) for y in range(h): for x in range(len(lines[y])): if lines[y][x] == 'S': maze.start_cell = maze.grid[y][x] if lines[y][x] == 'E': maze.exit_cell = maze.grid[y][x] return maze # --- ЭТАП 3: STRATEGY --- class SearchStats: def __init__(self, time_ms, visited, length): self.time_ms = time_ms self.visited = visited self.length = length class PathFindingStrategy(ABC): @abstractmethod def findPath(self, maze, start, exit): pass # 1. Поиск в ширину (BFS) - оригинальный алгоритм class BFSStrategy(PathFindingStrategy): def findPath(self, maze, start, exit): queue = deque([start]) visited = {start: None} while len(queue) > 0: curr = queue.popleft() if curr == exit: break for n in maze.getNeighbors(curr): if n not in visited: visited[n] = curr queue.append(n) path = [] curr = exit while curr is not None: path.append(curr) curr = visited.get(curr) return path[::-1], len(visited) # 2. Поиск в глубину (DFS) - добавлен class DFSStrategy(PathFindingStrategy): def findPath(self, maze, start, exit): stack = [start] visited = {start: None} while len(stack) > 0: curr = stack.pop() if curr == exit: break for n in maze.getNeighbors(curr): if n not in visited: visited[n] = curr stack.append(n) path = [] curr = exit while curr is not None: path.append(curr) curr = visited.get(curr) return path[::-1], len(visited) # 3. Алгоритм A* class AStarStrategy(PathFindingStrategy): def findPath(self, maze, start, exit): counter = 0 queue = [(0, counter, start)] came_from = {start: None} g_score = {start: 0} while len(queue) > 0: _, _, curr = heapq.heappop(queue) if curr == exit: break for n in maze.getNeighbors(curr): tentative_g_score = g_score[curr] + 1 if n not in g_score or tentative_g_score < g_score[n]: came_from[n] = curr g_score[n] = tentative_g_score # Эвристика: Манхэттенское расстояние f_score = tentative_g_score + abs(n.x - exit.x) + abs(n.y - exit.y) counter += 1 heapq.heappush(queue, (f_score, counter, n)) path = [] curr = exit while curr is not None: path.append(curr) curr = came_from.get(curr) return path[::-1], len(came_from) # --- ЭТАП 4: ORCHESTRATOR --- class MazeSolver: def __init__(self, maze, player=None): self.maze = maze self.player = player self.observers = [] def attach(self, obs): self.observers.append(obs) def notify(self, event, data): for o in self.observers: o.update(event, data) def solve(self, strat): t0 = time.perf_counter() path, visited = strat.findPath(self.maze, self.maze.start_cell, self.maze.exit_cell) t1 = time.perf_counter() return SearchStats((t1 - t0) * 1000, visited, len(path)) # --- ЭТАП 5: OBSERVER & COMMAND --- class Player: def __init__(self, cell): self.current_cell = cell class MoveCommand: def __init__(self, player, dx, dy, maze): self.player = player self.dx = dx self.dy = dy self.maze = maze def execute(self): nx = self.player.current_cell.x + self.dx ny = self.player.current_cell.y + self.dy if nx >= 0 and nx < self.maze.width and ny >= 0 and ny < self.maze.height: target = self.maze.grid[ny][nx] if target.isPassable(): self.player.current_cell = target return True return False class ConsoleView: def update(self, event, data): print(f"[INFO] {event.upper()}: {data}") # --- ЗАПУСК --- if __name__ == "__main__": files = ["maze10-10.txt", "maze50-50.txt", "maze100-100.txt", "maze0.txt", "maze777.txt"] mode = input("Эксперимент (e) или игра (i)? ").lower() if mode == 'e': # Обновленная таблица с колонкой "Метод" print(f"{'Файл':<15} | {'Метод':<5} | {'Время(мс)':<10} | {'Посещено':<10} | {'Путь':<6}") print("-" * 58) for f in files: try: m = MazeBuilder().buildFromFile(f) # Словарь с нашими тремя методами strategies = { "BFS": BFSStrategy(), "DFS": DFSStrategy(), "A*": AStarStrategy() } # Запускаем каждый метод для текущего лабиринта for strat_name, strat_obj in strategies.items(): t_sum, v_sum, l_sum = 0, 0, 0 for _ in range(10): s = MazeSolver(m).solve(strat_obj) t_sum += s.time_ms v_sum += s.visited l_sum += s.length print(f"{f:<15} | {strat_name:<5} | {t_sum/10:<10.2f} | {v_sum/10:<10.1f} | {l_sum/10:<6.1f}") # Линия-разделитель между разными лабиринтами для удобства чтения print("-" * 58) except FileNotFoundError: print(f"{f:<15} | ОШИБКА: Файл не найден") print("-" * 58) elif mode == 'i': name = input("Имя файла: ") m = MazeBuilder().buildFromFile(name) p = Player(m.start_cell) s = MazeSolver(m, p) s.attach(ConsoleView()) while True: cmd = input("WASD (q-выход): ").lower() if cmd == 'q': break dx, dy = 0, 0 if cmd == 'w': dy = -1 elif cmd == 'a': dx = -1 elif cmd == 's': dy = 1 elif cmd == 'd': dx = 1 if dx != 0 or dy != 0: if MoveCommand(p, dx, dy, m).execute(): s.notify("move", f"Направление {cmd}, Координата ({p.current_cell.x}, {p.current_cell.y})") else: s.notify("error", "Стена!")