import time import csv import heapq from collections import deque from abc import ABC, abstractmethod import matplotlib.pyplot as plt import pandas as pd from dataclasses import dataclass import os class Cell: """Клетка лабиринта""" def __init__(self, x, y, is_wall=False): self.x = x self.y = y self.is_wall = is_wall self.is_start = False self.is_exit = False def is_passable(self): return not self.is_wall class Maze: """Лабиринт""" def __init__(self, width, height): self.width = width self.height = height self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)] self.start = None self.exit = None def get_cell(self, x, y): if 0 <= x < self.width and 0 <= y < self.height: return self.cells[y][x] return None def get_neighbors(self, cell): neighbors = [] for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]: nx, ny = cell.x + dx, cell.y + dy nb = self.get_cell(nx, ny) if nb and nb.is_passable(): neighbors.append(nb) return neighbors def __str__(self): result = "" for y in range(self.height): for x in range(self.width): cell = self.get_cell(x, y) if cell is None: result += "?" elif cell.is_wall: result += "#" elif cell.is_start: result += "S" elif cell.is_exit: result += "E" else: result += " " result += "\n" return result class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename): pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename): with open(filename, 'r', encoding='utf-8') as f: lines = [line.rstrip('\n') for line in f.readlines()] height = len(lines) width = max(len(line) for line in lines) maze = Maze(width, height) for y, line in enumerate(lines): for x, ch in enumerate(line): cell = maze.get_cell(x, y) if ch == '#': cell.is_wall = True elif ch == 'S': cell.is_start = True maze.start = cell elif ch == 'E': cell.is_exit = True maze.exit = cell else: cell.is_wall = False return maze class PathFindingStrategy(ABC): @abstractmethod def find_path(self, maze, start, exit): pass class BFSStrategy(PathFindingStrategy): """Поиск в ширину""" def find_path(self, maze, start, exit): visited = set() if start == exit: return [start], 1 queue = deque([start]) visited.add(start) parent = {start: None} while queue: current = queue.popleft() for nb in maze.get_neighbors(current): if nb not in visited: visited.add(nb) parent[nb] = current if nb == exit: path = [] node = nb while node is not None: path.append(node) node = parent[node] path.reverse() return path, len(visited) queue.append(nb) return [], len(visited) class DFSStrategy(PathFindingStrategy): """Поиск в глубину""" def find_path(self, maze, start, exit): visited = set() stack = [(start, [start])] while stack: current, path = stack.pop() if current == exit: return path, len(visited) visited.add(current) for nb in maze.get_neighbors(current): if nb not in visited: stack.append((nb, path + [nb])) return [], len(visited) class AStarStrategy(PathFindingStrategy): """Алгоритм A""" def heuristic(self, cell, exit): return abs(cell.x - exit.x) + abs(cell.y - exit.y) def find_path(self, maze, start, exit): open_set = [] counter = 0 heapq.heappush(open_set, (0, counter, start)) counter += 1 came_from = {} g_score = {start: 0} f_score = {start: self.heuristic(start, exit)} visited = set() while open_set: _, _, current = heapq.heappop(open_set) visited.add(current) if current == exit: path = [] node = current while node in came_from: path.append(node) node = came_from[node] path.append(start) path.reverse() return path, len(visited) for nb in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if tentative_g < g_score.get(nb, float('inf')): came_from[nb] = current g_score[nb] = tentative_g f = tentative_g + self.heuristic(nb, exit) heapq.heappush(open_set, (f, counter, nb)) counter += 1 return [], len(visited) @dataclass class SearchStats: time_ms: float visited_cells: int path_length: int algorithm: str class MazeSolver: def __init__(self, maze, strategy): self.maze = maze self.strategy = strategy def set_strategy(self, strategy): self.strategy = strategy def solve(self): if self.maze.start is None or self.maze.exit is None: raise ValueError("Лабиринт не имеет старта или выхода") start_time = time.perf_counter() path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() stats = SearchStats( time_ms=(end_time - start_time) * 1000, visited_cells=visited, path_length=len(path), algorithm=self.strategy.__class__.__name__ ) return path, stats class Observer(ABC): @abstractmethod def update(self, event_type, data=None): pass class ConsoleLogger(Observer): def update(self, event_type, data=None): if event_type == "search_start": print(f"[LOG] Поиск пути начат") elif event_type == "path_found": print(f"[LOG] Путь найден! Длина: {data}") elif event_type == "no_path": print("[LOG] Путь не найден") elif event_type == "step": print(f"[LOG] Шаг: {data}") class MazeSolverWithObserver(MazeSolver): def __init__(self, maze, strategy, observers=None): super().__init__(maze, strategy) self.observers = observers if observers else [] def attach(self, observer): self.observers.append(observer) def detach(self, observer): self.observers.remove(observer) def notify(self, event_type, data=None): for obs in self.observers: obs.update(event_type, data) def solve(self): if self.maze.start is None or self.maze.exit is None: raise ValueError("Лабиринт не имеет старта или выхода") self.notify("search_start") start_time = time.perf_counter() path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() if path: self.notify("path_found", len(path)) else: self.notify("no_path") stats = SearchStats( time_ms=(end_time - start_time) * 1000, visited_cells=visited, path_length=len(path), algorithm=self.strategy.__class__.__name__ ) return path, stats class Command(ABC): @abstractmethod def execute(self): pass @abstractmethod def undo(self): pass class MoveCommand(Command): def __init__(self, player, direction, maze): self.player = player self.direction = direction self.maze = maze self.prev_pos = None def execute(self): self.prev_pos = self.player.current_cell dx, dy = self.direction nx, ny = self.player.current_cell.x + dx, self.player.current_cell.y + dy new_cell = self.maze.get_cell(nx, ny) if new_cell and new_cell.is_passable(): self.player.current_cell = new_cell return True return False def undo(self): if self.prev_pos: self.player.current_cell = self.prev_pos return True return False class Player: def __init__(self, start_cell): self.current_cell = start_cell def interactive_move_demo(maze, path): """Демонстрация движения с отменой последнего шага""" if not path: print("Путь не найден, демонстрация движения невозможна.") return player = Player(maze.start) command_history = [] print("\n Интерактивное движение по найденному пути") print("Текущая позиция: старт") for step, cell in enumerate(path): if cell == maze.start: continue prev = path[step-1] dx = cell.x - prev.x dy = cell.y - prev.y cmd = MoveCommand(player, (dx, dy), maze) cmd.execute() command_history.append(cmd) print(f"Шаг {step}: перемещение на ({dx},{dy}), позиция ({player.current_cell.x},{player.current_cell.y})") if cell == maze.exit: print("Достигнут выход!") break if command_history: print("\nДемонстрация отмены последнего шага") cmd = command_history[-1] cmd.undo() print(f"Отменён последний шаг, позиция: ({player.current_cell.x},{player.current_cell.y})") def test_single_maze(filename, strategies, repeats=5): """Тестирование одного лабиринта с разными стратегиями""" builder = TextFileMazeBuilder() maze = builder.build_from_file(filename) results = [] for strategy in strategies: solver = MazeSolver(maze, strategy) times = [] visits = [] lengths = [] for _ in range(repeats): _, stats = solver.solve() times.append(stats.time_ms) visits.append(stats.visited_cells) lengths.append(stats.path_length) results.append({ 'algorithm': strategy.__class__.__name__, 'avg_time_ms': sum(times) / repeats, 'avg_visited': sum(visits) / repeats, 'avg_path_len': sum(lengths) / repeats }) return results def save_maze_to_file(maze, filename): """Сохранение лабиринта в файл""" os.makedirs(os.path.dirname(filename), exist_ok=True) with open(filename, 'w', encoding='utf-8') as f: for y in range(maze.height): line = "" for x in range(maze.width): cell = maze.get_cell(x, y) if cell.is_wall: line += "#" elif cell.is_start: line += "S" elif cell.is_exit: line += "E" else: line += " " f.write(line + "\n") def create_test_mazes(): """Создание тестовых лабиринтов""" os.makedirs("mazes", exist_ok=True) # 1. Простой лабиринт 10x10 (tiny.txt) maze1 = Maze(10, 10) for y in range(10): for x in range(10): is_start = (x == 0 and y == 0) is_exit = (x == 9 and y == 0) is_wall = False if y == 1 and x not in [0, 1, 9]: is_wall = True if y == 2 and x not in [9]: is_wall = True if y == 3 and x not in [0, 9]: is_wall = True if y == 4 and x not in [0, 1, 9]: is_wall = True if y == 5 and x not in [9]: is_wall = True if y == 6 and x not in [0, 9]: is_wall = True if y == 7 and x not in [9]: is_wall = True if y == 8 and x not in [0, 9]: is_wall = True cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze1.cells[y][x] = cell if is_start: maze1.start = cell if is_exit: maze1.exit = cell save_maze_to_file(maze1, "mazes/tiny.txt") # 2. Средний лабиринт 15x15 (medium.txt) maze2 = Maze(15, 15) for y in range(15): for x in range(15): is_start = (x == 0 and y == 0) is_exit = (x == 14 and y == 14) is_wall = (x % 3 == 1 and y % 2 == 0) and not is_start and not is_exit cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze2.cells[y][x] = cell if is_start: maze2.start = cell if is_exit: maze2.exit = cell save_maze_to_file(maze2, "mazes/medium.txt") # 3. Большой лабиринт 30x30 (large.txt) maze3 = Maze(30, 30) for y in range(30): for x in range(30): is_start = (x == 0 and y == 0) is_exit = (x == 29 and y == 29) is_wall = (x % 2 == 0 and y % 3 == 0) and not is_start and not is_exit cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze3.cells[y][x] = cell if is_start: maze3.start = cell if is_exit: maze3.exit = cell save_maze_to_file(maze3, "mazes/large.txt") # 4. Пустой лабиринт 15x15 (empty.txt) maze4 = Maze(15, 15) for y in range(15): for x in range(15): is_start = (x == 0 and y == 0) is_exit = (x == 14 and y == 14) cell = Cell(x, y, is_wall=False) cell.is_start = is_start cell.is_exit = is_exit maze4.cells[y][x] = cell if is_start: maze4.start = cell if is_exit: maze4.exit = cell save_maze_to_file(maze4, "mazes/empty.txt") # 5. Лабиринт без выхода 10x10 (no_exit.txt) maze5 = Maze(10, 10) for y in range(10): for x in range(10): is_start = (x == 0 and y == 0) is_exit = (x == 9 and y == 9) is_wall = (x > 0 and y > 0) and not is_start cell = Cell(x, y, is_wall=is_wall) cell.is_start = is_start cell.is_exit = is_exit maze5.cells[y][x] = cell if is_start: maze5.start = cell if is_exit: maze5.exit = cell save_maze_to_file(maze5, "mazes/no_exit.txt") def print_analysis(): """Вывод анализа эффективности алгоритмов""" print(" АНАЛИЗ ЭФФЕКТИВНОСТИ АЛГОРИТМОВ ПОИСКА ПУТИ") print(""" BFS (Поиск в ширину): - Всегда находит КРАТЧАЙШИЙ путь - Сложность O(V+E) - Много памяти (очередь) - Лучший выбор для поиска минимального пути DFS (Поиск в глубину): - НЕ гарантирует кратчайший путь - Сложность O(V+E) - Мало памяти - Быстрый, но путь может быть очень длинным - Хорош для проверки существования пути A* (Алгоритм с эвристикой): - Находит КРАТЧАЙШИЙ путь (при допустимой эвристике) - Эвристика: манхэттенское расстояние |x1-x2| + |y1-y2| - Быстрее BFS благодаря целенаправленному поиску - Лучший выбор для больших запутанных лабиринтов """) print(""" ВЛИЯНИЕ ТИПА ЛАБИРИНТА: Простой лабиринт (tiny.txt): - Все алгоритмы работают быстро - Разница в скорости незначительна - BFS и A* находят оптимальный путь - DFS может найти более длинный путь Средний лабиринт (medium.txt): - A* начинает показывать преимущество - BFS исследует больше клеток - DFS может заблудиться в тупиках Большой лабиринт (large.txt): - A* значительно быстрее BFS - DFS сильно проигрывает на запутанных лабиринтах Пустой лабиринт (empty.txt): - A* значительно быстрее BFS - DFS быстро уходит вглубь, но путь неоптимальный Лабиринт без выхода (no_exit.txt): - Все алгоритмы обходят все достижимые клетки - Возвращают пустой путь """) print(""" ВЫВОДЫ ПО ПАТТЕРНАМ: BUILDER: - Легко добавить новый формат - Код загрузки не смешивается с логикой лабиринта STRATEGY: - Алгоритмы можно менять во время выполнения - Легко добавить новый алгоритм - Код не дублируется OBSERVER: - Отделяет визуализацию от логики - Легко добавить GUI или логирование - Наблюдателей можно добавлять динамически COMMAND: - Позволяет выполнять и отменять действия - Удобно для пошагового управления - История команд позволяет сохранять/загружать состояние """) def main(): print("ЛАБОРАТОРНАЯ РАБОТА №2: ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА") print("Паттерны: Builder, Strategy, Observer, Command") # Создание тестовых лабиринтов print("\n1. СОЗДАНИЕ ТЕСТОВЫХ ЛАБИРИНТОВ...") create_test_mazes() print(" Созданы лабиринты: tiny, medium, large, empty, no_exit") # Список файлов лабиринтов maze_files = [ "mazes/tiny.txt", "mazes/medium.txt", "mazes/large.txt", "mazes/empty.txt", "mazes/no_exit.txt" ] strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()] all_results = [] # Демонстрация Observer и Command на первом лабиринте print("\n2. ДЕМОНСТРАЦИЯ РАБОТЫ ПРОГРАММЫ") builder = TextFileMazeBuilder() maze = builder.build_from_file("mazes/tiny.txt") print("Лабиринт tiny.txt:") print(maze) logger = ConsoleLogger() solver_with_observer = MazeSolverWithObserver(maze, strategies[0], observers=[logger]) path, _ = solver_with_observer.solve() interactive_move_demo(maze, path) # Эксперименты print("3. ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ АЛГОРИТМОВ") for maze_file in maze_files: try: results = test_single_maze(maze_file, strategies) for r in results: r['maze'] = maze_file all_results.append(r) print(f"\n{maze_file}:") for r in results: print(f" {r['algorithm']}: {r['avg_time_ms']:.3f} мс, " f"посещено {r['avg_visited']:.1f}, путь {r['avg_path_len']:.1f}") except Exception as e: print(f"Ошибка при обработке {maze_file}: {e}") # Сохранение CSV if all_results: os.makedirs("results", exist_ok=True) with open('results/all_results.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited', 'avg_path_len']) writer.writeheader() writer.writerows(all_results) print("\nРезультаты сохранены в results/all_results.csv") # Построение графиков для каждого лабиринта df = pd.DataFrame(all_results) for maze in df['maze'].unique(): subset = df[df['maze'] == maze] plt.figure(figsize=(8, 5)) bars = plt.bar(subset['algorithm'], subset['avg_time_ms'], color=['blue', 'green', 'red']) plt.title(f'Сравнение алгоритмов на лабиринте {maze}') plt.ylabel('Среднее время (мс)') plt.xlabel('Алгоритм') for bar, val in zip(bars, subset['avg_time_ms']): plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, f'{val:.3f}', ha='center', va='bottom', fontsize=9) plt.tight_layout() filename = f'results/plot_{maze.replace("/", "_")}.png' plt.savefig(filename) plt.close() print(f" Сохранён график: {filename}") # Сводный график plt.figure(figsize=(12, 6)) for alg in df['algorithm'].unique(): subset = df[df['algorithm'] == alg] plt.plot(subset['maze'], subset['avg_time_ms'], marker='o', linewidth=2, markersize=8, label=alg) plt.xlabel('Лабиринт') plt.ylabel('Среднее время (мс)') plt.title('Сравнение эффективности алгоритмов на разных лабиринтах') plt.legend() plt.grid(True, alpha=0.3) plt.xticks(rotation=45) plt.tight_layout() plt.savefig('results/summary_comparison.png') plt.show() print("\nГрафики сохранены в папке results/") print(" - plot_*.png - графики для каждого лабиринта") print(" - summary_comparison.png - сводный график") print_analysis() print("ЭКСПЕРИМЕНТ ЗАВЕРШЁН") if __name__ == "__main__": main()