import sys import os import time import csv from collections import deque import heapq import matplotlib.pyplot as plt import numpy as np # ========== Модель данных ========== class Tile: """Одна клетка лабиринта.""" def __init__(self, x, y): self._x = x self._y = y self._wall = False self._entry = False self._goal = False @property def x(self): return self._x @property def y(self): return self._y @property def is_wall(self): return self._wall @is_wall.setter def is_wall(self, value): self._wall = value @property def is_entry(self): return self._entry @is_entry.setter def is_entry(self, value): self._entry = value @property def is_goal(self): return self._goal @is_goal.setter def is_goal(self, value): self._goal = value def can_walk(self): """Можно ли встать на эту клетку.""" return not self._wall class Labyrinth: """Прямоугольный лабиринт.""" def __init__(self, width, height): self._width = width self._height = height self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)] self._start = None self._exit = None @property def width(self): return self._width @property def height(self): return self._height @property def start(self): return self._start @property def exit(self): return self._exit def tile_at(self, x, y): if 0 <= x < self._width and 0 <= y < self._height: return self._grid[y][x] return None def configure_tile(self, x, y, kind): tile = self.tile_at(x, y) if tile is None: return if kind == 'wall': tile.is_wall = True elif kind == 'entry': if self._start: self._start.is_entry = False tile.is_entry = True tile.is_wall = False self._start = tile elif kind == 'goal': if self._exit: self._exit.is_goal = False tile.is_goal = True tile.is_wall = False self._exit = tile elif kind == 'floor': tile.is_wall = False def neighbours(self, tile): """Соседние проходимые клетки.""" res = [] for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): nb = self.tile_at(tile.x + dx, tile.y + dy) if nb and nb.can_walk(): res.append(nb) return res # ========== Загрузка из файла ========== class LabyrinthBuilder: def build(self, filename): raise NotImplementedError class TextLabyrinthBuilder(LabyrinthBuilder): def build(self, filename): with open(filename, 'r', encoding='utf-8') as f: lines = [line.rstrip('\n') for line in f] h = len(lines) w = max(len(l) for l in lines) if h else 0 if h == 0 or w == 0: raise ValueError("Файл лабиринта пуст.") entries = exits = 0 lab = Labyrinth(w, h) for y, row in enumerate(lines): for x, ch in enumerate(row): if ch == '#': lab.configure_tile(x, y, 'wall') elif ch == 'S': lab.configure_tile(x, y, 'entry') entries += 1 elif ch == 'E': lab.configure_tile(x, y, 'goal') exits += 1 else: lab.configure_tile(x, y, 'floor') if entries != 1 or exits != 1: raise ValueError(f"Некорректный лабиринт: найдено S={entries}, E={exits}") return lab # ========== Алгоритмы поиска ========== class Pathfinder: def find_path(self, lab, start, goal): raise NotImplementedError def _build_path(self, preds, start, goal): path = [] cur = goal while cur is not None: path.append(cur) cur = preds.get(cur) path.reverse() return path @property def visited_count(self): return getattr(self, '_visited', 0) class BFS_Pathfinder(Pathfinder): def find_path(self, lab, start, goal): q = deque([start]) preds = {start: None} seen = {start} while q: cur = q.popleft() if cur == goal: self._visited = len(seen) return self._build_path(preds, start, goal) for nb in lab.neighbours(cur): if nb not in seen: seen.add(nb) preds[nb] = cur q.append(nb) self._visited = len(seen) return [] class DFS_Pathfinder(Pathfinder): def find_path(self, lab, start, goal): stack = [start] preds = {start: None} seen = {start} while stack: cur = stack.pop() if cur == goal: self._visited = len(seen) return self._build_path(preds, start, goal) for nb in lab.neighbours(cur): if nb not in seen: seen.add(nb) preds[nb] = cur stack.append(nb) self._visited = len(seen) return [] class AStar_Pathfinder(Pathfinder): def _heuristic(self, a, b): return abs(a.x - b.x) + abs(a.y - b.y) def find_path(self, lab, start, goal): heap = [] cnt = 0 f_start = self._heuristic(start, goal) heapq.heappush(heap, (f_start, cnt, start)) cnt += 1 preds = {} g = {start: 0} f = {start: f_start} seen = set() while heap: cur_f, _, cur = heapq.heappop(heap) seen.add(cur) if cur == goal: self._visited = len(seen) return self._build_path(preds, start, goal) if cur_f > f.get(cur, float('inf')): continue for nb in lab.neighbours(cur): tent_g = g[cur] + 1 if tent_g < g.get(nb, float('inf')): preds[nb] = cur g[nb] = tent_g new_f = tent_g + self._heuristic(nb, goal) f[nb] = new_f heapq.heappush(heap, (new_f, cnt, nb)) cnt += 1 self._visited = len(seen) return [] # ========== Интерактивный игрок ========== class Explorer: def __init__(self, start_tile, labyrinth): self._current = start_tile self._previous = None self._lab = labyrinth @property def current(self): return self._current def move(self, tile): if tile and tile.can_walk(): self._previous = self._current self._current = tile return True return False def undo(self): if self._previous: self._current, self._previous = self._previous, None return True return False class Action: def execute(self): raise NotImplementedError def undo(self): raise NotImplementedError class MoveAction(Action): def __init__(self, explorer, direction, lab): self._explorer = explorer self._dx, self._dy = direction self._lab = lab self._done = False def execute(self): nx = self._explorer.current.x + self._dx ny = self._explorer.current.y + self._dy target = self._lab.tile_at(nx, ny) if target and target.can_walk(): self._explorer.move(target) self._done = True return True return False def undo(self): if self._done: self._explorer.undo() self._done = False return True return False class GameObserver: def update(self, event, data): raise NotImplementedError class TerminalDisplay(GameObserver): def __init__(self, explorer=None): self._explorer = explorer self._last_path = None def update(self, event, data): if event == 'labyrinth_loaded': self._draw_lab(data) elif event == 'path_found': self._last_path = data self._show_path_info(data) elif event == 'player_moved': self._draw_with_player(data) def _draw_lab(self, lab): os.system('cls' if os.name == 'nt' else 'clear') print('=' * (lab.width * 2 + 4)) print(' ЛАБИРИНТ') print('=' * (lab.width * 2 + 4)) for y in range(lab.height): print(' ', end='') for x in range(lab.width): t = lab.tile_at(x, y) if t == lab.start: print('S', end=' ') elif t == lab.exit: print('E', end=' ') elif t.is_wall: print('#', end=' ') else: print('.', end=' ') print() print('=' * (lab.width * 2 + 4)) print(' S – вход E – выход # – стена . – пол') def _draw_with_player(self, lab): os.system('cls' if os.name == 'nt' else 'clear') print('=' * (lab.width * 2 + 4)) print(' ЛАБИРИНТ (P – игрок)') print('=' * (lab.width * 2 + 4)) for y in range(lab.height): print(' ', end='') for x in range(lab.width): t = lab.tile_at(x, y) if self._explorer and t == self._explorer.current: print('P', end=' ') elif t == lab.start: print('S', end=' ') elif t == lab.exit: print('E', end=' ') elif t.is_wall: print('#', end=' ') else: print('.', end=' ') print() print('=' * (lab.width * 2 + 4)) if self._explorer: print(f' Позиция: ({self._explorer.current.x}, {self._explorer.current.y})') def _show_path_info(self, path): if not path: print('\n Путь не найден!') else: print(f'\n Длина найденного пути: {len(path)} клеток.') class LabyrinthSolver: def __init__(self, lab): self._lab = lab self._strategy = None self._observers = [] def attach(self, obs): self._observers.append(obs) def _notify(self, event, data): for obs in self._observers: obs.update(event, data) def set_strategy(self, strategy): self._strategy = strategy def solve(self): if self._strategy is None: return None start_t = time.perf_counter() path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit) elapsed = (time.perf_counter() - start_t) * 1000 self._notify('path_found', path) return { 'time_ms': elapsed, 'visited': self._strategy.visited_count, 'length': len(path) } # ========== Эксперименты и визуализация ========== def run_benchmark(maze_file, strategy, runs=5): builder = TextLabyrinthBuilder() lab = builder.build(maze_file) total_t = total_v = total_l = 0 for _ in range(runs): solver = LabyrinthSolver(lab) solver.set_strategy(strategy) stats = solver.solve() if stats: total_t += stats['time_ms'] total_v += stats['visited'] total_l += stats['length'] return { 'time_ms': total_t / runs, 'visited_cells': total_v / runs, 'path_length': total_l / runs } def create_charts(results): mazes = sorted({r['maze'] for r in results}) strategies = ['BFS', 'DFS', 'AStar'] fig, axes = plt.subplots(1, 3, figsize=(15, 5)) x = np.arange(len(mazes)) width = 0.25 for i, strat in enumerate(strategies): times = [next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes] axes[0].bar(x + i*width, times, width, label=strat) axes[0].set_title('Время выполнения (мс)') axes[0].set_xticks(x + width) axes[0].set_xticklabels(mazes, rotation=30, ha='right') axes[0].legend() axes[0].grid(alpha=0.3) for i, strat in enumerate(strategies): visited = [next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes] axes[1].bar(x + i*width, visited, width, label=strat) axes[1].set_title('Посещено клеток') axes[1].set_xticks(x + width) axes[1].set_xticklabels(mazes, rotation=30, ha='right') axes[1].legend() axes[1].grid(alpha=0.3) for i, strat in enumerate(strategies): lengths = [next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes] axes[2].bar(x + i*width, lengths, width, label=strat) axes[2].set_title('Длина пути') axes[2].set_xticks(x + width) axes[2].set_xticklabels(mazes, rotation=30, ha='right') axes[2].legend() axes[2].grid(alpha=0.3) plt.tight_layout() plt.savefig('maze_performance.png', dpi=150, bbox_inches='tight') plt.show() # ========== Главный вход ========== if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == 'experiment': print('Запуск экспериментов...') maze_list = [ ('maze/maze1.txt', 'Small 10x6'), ('maze/maze10x10.txt', 'Medium 10x10'), ('maze/maze20x20.txt', 'Large 20x20'), ('maze/maze_empty.txt', 'Empty 15x15'), ('maze/maze_no_exit.txt', 'No exit 10x10') ] strategies = [ ('BFS', BFS_Pathfinder()), ('DFS', DFS_Pathfinder()), ('AStar', AStar_Pathfinder()) ] all_results = [] for file, name in maze_list: print(f'\nТестируем {name}...') for sname, strat in strategies: try: stats = run_benchmark(file, strat, runs=3) all_results.append({ 'maze': name, 'strategy': sname, 'time_ms': stats['time_ms'], 'visited_cells': stats['visited_cells'], 'path_length': stats['path_length'] }) print(f' {sname}: время={stats["time_ms"]:.3f}мс, клеток={stats["visited_cells"]:.0f}, длина={stats["path_length"]:.0f}') except Exception as e: print(f' {sname}: ошибка – {e}') with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['maze','strategy','time_ms','visited_cells','path_length']) writer.writeheader() writer.writerows(all_results) if all_results: create_charts(all_results) print('\nРезультаты сохранены в experiment_data.csv и maze_performance.png') else: # Интерактивная игра maze_file = 'maze/maze1.txt' if len(sys.argv) > 1: maze_file = sys.argv[1] builder = TextLabyrinthBuilder() labyrinth = builder.build(maze_file) player = Explorer(labyrinth.start, labyrinth) display = TerminalDisplay(player) display.update('labyrinth_loaded', labyrinth) solver = LabyrinthSolver(labyrinth) solver.attach(display) print('\n УПРАВЛЕНИЕ:') print(' H – влево J – вниз K – вверх L – вправо') print(' U – отменить ход Q – выход') print(' Автопоиск: B – BFS D – DFS A – A*') print('=' * 50) history = [] while True: cmd = input('\n Команда > ').lower().strip() if cmd == 'q': print('\n До свидания!') break elif cmd == 'b': solver.set_strategy(BFS_Pathfinder()) stats = solver.solve() print(f"\n BFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}") elif cmd == 'd': solver.set_strategy(DFS_Pathfinder()) stats = solver.solve() print(f"\n DFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}") elif cmd == 'a': solver.set_strategy(AStar_Pathfinder()) stats = solver.solve() print(f"\n A*: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}") elif cmd in ('h','j','k','l'): dirs = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)} action = MoveAction(player, dirs[cmd], labyrinth) if action.execute(): history.append(action) display.update('player_moved', labyrinth) if player.current == labyrinth.exit: print('\n ПОЗДРАВЛЯЕМ! ВЫ ВЫБРАЛИСЬ ИЗ ЛАБИРИНТА!') print(f' Всего ходов: {len(history)}') break else: print('\n Там стена!') elif cmd == 'u': if history: act = history.pop() act.undo() display.update('player_moved', labyrinth) print('\n Ход отменён') else: print('\n Нечего отменять') else: print('\n Неизвестная команда. Используйте H,J,K,L,U,Q,B,D,A')