from abc import ABC, abstractmethod from collections import deque import heapq import time import csv import random import os BASE = os.path.dirname(os.path.abspath(__file__)) class Cell: def __init__(self, x, y): self.x = x self.y = y self.isWall = False self.isStart = False self.isExit = False def isPassable(self): return not self.isWall def __eq__(self, other): if other is None: return False return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y)) def __lt__(self, other): return (self.x, self.y) < (other.x, other.y) class Maze: def __init__(self, width, height): self.width = width self.height = height self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)] self.start = None self.exit = None def getCell(self, x, y): if 0 <= x < self.width and 0 <= y < self.height: return self.grid[x][y] return None def getNeighbors(self, cell): neighbors = [] for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]: n = self.getCell(cell.x + dx, cell.y + dy) if n and n.isPassable(): neighbors.append(n) return neighbors class MazeBuilder(ABC): @abstractmethod def buildFromFile(self, filename): pass class TextFileMazeBuilder(MazeBuilder): def buildFromFile(self, filename): with open(filename, "r", encoding="utf-8") as f: lines = [line.rstrip("\n\r") for line in f.readlines()] height = len(lines) width = len(lines[0]) if height > 0 else 0 for line in lines: if len(line) != width: raise ValueError("все строки должны быть одной длины") maze = Maze(width, height) for y in range(height): for x in range(width): ch = lines[y][x] cell = maze.getCell(x, y) if ch == "#": cell.isWall = True elif ch == " ": cell.isWall = False elif ch == "S": cell.isWall = False cell.isStart = True maze.start = cell elif ch == "E": cell.isWall = False cell.isExit = True maze.exit = cell else: raise ValueError(f"неизвестный символ: {ch}") if maze.start is None: raise ValueError("нет старта (S)") if maze.exit is None: raise ValueError("нет выхода (E)") return maze class PathFindingStrategy(ABC): @abstractmethod def findPath(self, maze, start, exit_cell): pass def _reconstruct(self, parent, exit_cell): path = [] curr = exit_cell while curr is not None: path.append(curr) curr = parent.get(curr) path.reverse() return path class BFSStrategy(PathFindingStrategy): def findPath(self, maze, start, exit_cell): if exit_cell is None: return [] queue = deque([start]) visited = {start} parent = {start: None} while queue: curr = queue.popleft() if curr == exit_cell: return self._reconstruct(parent, exit_cell) for n in maze.getNeighbors(curr): if n not in visited: visited.add(n) parent[n] = curr queue.append(n) return [] class DFSStrategy(PathFindingStrategy): def findPath(self, maze, start, exit_cell): if exit_cell is None: return [] stack = [start] visited = {start} parent = {start: None} while stack: curr = stack.pop() if curr == exit_cell: return self._reconstruct(parent, exit_cell) for n in maze.getNeighbors(curr): if n not in visited: visited.add(n) parent[n] = curr stack.append(n) return [] class AStarStrategy(PathFindingStrategy): def _heuristic(self, cell, exit_cell): return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) def findPath(self, maze, start, exit_cell): if exit_cell is None: return [] open_set = [] heapq.heappush(open_set, (0, start)) parent = {start: None} g_score = {start: 0} while open_set: curr = heapq.heappop(open_set)[1] if curr == exit_cell: return self._reconstruct(parent, exit_cell) for n in maze.getNeighbors(curr): new_g = g_score[curr] + 1 if n not in g_score or new_g < g_score[n]: g_score[n] = new_g parent[n] = curr f = new_g + self._heuristic(n, exit_cell) heapq.heappush(open_set, (f, n)) return [] class SearchStats: def __init__(self, time_ms, visited, path_len): self.time_ms = time_ms self.visited_cells = visited self.path_length = path_len class MazeSolver: def __init__(self, maze): self.maze = maze self.strategy = None self.observers = [] def setStrategy(self, strategy): self.strategy = strategy def attach(self, observer): self.observers.append(observer) def notify(self, event): for obs in self.observers: obs.update(event) def solve(self): if self.strategy is None: raise ValueError("стратегия не выбрана") start_time = time.perf_counter() path = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit) elapsed_ms = (time.perf_counter() - start_time) * 1000 stats = SearchStats(elapsed_ms, len(path), len(path)) self.notify({"type": "path_found", "maze": self.maze, "path": path, "stats": stats}) return path, stats class Observer(ABC): @abstractmethod def update(self, event): pass class ConsoleView(Observer): def update(self, event): if event["type"] == "path_found": stats = event["stats"] print(f"длина пути {stats.path_length}, время {stats.time_ms:.2f} мс") def save_maze(maze, filename): path = os.path.join(BASE, filename) with open(path, "w", encoding="utf-8") as f: for y in range(maze.height): line = "" for x in range(maze.width): cell = maze.getCell(x, y) if cell == maze.start: line += "S" elif cell == maze.exit: line += "E" elif cell.isWall: line += "#" else: line += " " f.write(line + "\n") def generate_with_walls(w, h, prob=0.3): maze = Maze(w, h) for x in range(w): for y in range(h): if random.random() < prob: maze.getCell(x, y).isWall = True maze.getCell(0, 0).isWall = False maze.getCell(w - 1, h - 1).isWall = False for x in range(w): maze.getCell(x, 0).isWall = False for y in range(h): maze.getCell(w - 1, y).isWall = False maze.getCell(0, 0).isStart = True maze.start = maze.getCell(0, 0) maze.getCell(w - 1, h - 1).isExit = True maze.exit = maze.getCell(w - 1, h - 1) return maze def generate_empty(w, h): maze = Maze(w, h) for x in range(w): for y in range(h): maze.getCell(x, y).isWall = False maze.getCell(0, 0).isStart = True maze.start = maze.getCell(0, 0) maze.getCell(w - 1, h - 1).isExit = True maze.exit = maze.getCell(w - 1, h - 1) return maze def generate_no_exit(w, h): maze = generate_with_walls(w, h, 0.3) exit_cell = maze.getCell(w - 1, h - 1) exit_cell.isWall = True exit_cell.isExit = False maze.exit = None return maze def run_experiment(maze, strategy_class, maze_name, repeats=5): times = [] path_lens = [] for _ in range(repeats): solver = MazeSolver(maze) solver.setStrategy(strategy_class()) path, stats = solver.solve() times.append(stats.time_ms) path_lens.append(len(path)) raw = strategy_class.__name__ strat_name = "A" if raw == "AStarStrategy" else raw.replace("Strategy", "") return { "лабиринт": maze_name, "стратегия": strat_name, "время_ср": sum(times) / repeats, "длина_пути_ср": sum(path_lens) / repeats, "путь_найден": any(l > 0 for l in path_lens), } def main(): mazes = [] small = generate_with_walls(10, 10, 0.2) save_maze(small, "maze_small.txt") mazes.append(("маленький 10x10", small)) medium = generate_with_walls(50, 50, 0.3) save_maze(medium, "maze_medium.txt") mazes.append(("средний 50x50", medium)) large = generate_with_walls(100, 100, 0.3) save_maze(large, "maze_large.txt") mazes.append(("большой 100x100", large)) empty = generate_empty(50, 50) save_maze(empty, "maze_empty.txt") mazes.append(("пустой 50x50", empty)) no_exit = generate_no_exit(20, 20) save_maze(no_exit, "maze_no_exit.txt") mazes.append(("без выхода 20x20", no_exit)) strategies = [BFSStrategy, DFSStrategy, AStarStrategy] results = [] for maze_name, maze in mazes: print(maze_name) for strat in strategies: res = run_experiment(maze, strat, maze_name) results.append(res) print(f" {strat.__name__}: {res['время_ср']:.2f} мс") csv_path = os.path.join(BASE, "resultslab.csv") with open(csv_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter( f, fieldnames=["лабиринт", "стратегия", "время_ср", "длина_пути_ср", "путь_найден"], delimiter=";", ) writer.writeheader() for row in results: row_ru = row.copy() row_ru["путь_найден"] = "да" if row["путь_найден"] else "нет" writer.writerow(row_ru) try: import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["Segoe UI", "Arial", "Tahoma", "DejaVu Sans"] plt.rcParams["axes.unicode_minus"] = False labyrinths = [] for r in results: if r["лабиринт"] not in labyrinths: labyrinths.append(r["лабиринт"]) fig, axes = plt.subplots(1, len(labyrinths), figsize=(4 * len(labyrinths), 4)) if len(labyrinths) == 1: axes = [axes] for idx, lab in enumerate(labyrinths): times = [] for s in ["BFS", "DFS", "A"]: for r in results: if r["лабиринт"] == lab and r["стратегия"] == s: times.append(r["время_ср"]) break axes[idx].bar(["BFS", "DFS", "A"], times, color=["#1a5632", "#0e5fb4", "#e67e22"]) axes[idx].set_title(lab) axes[idx].set_ylabel("мс") plt.tight_layout() plt.savefig(os.path.join(BASE, "maze_time_comparison.png")) plt.close() except ImportError: pass report_path = os.path.join(os.path.dirname(BASE), "report.md") with open(report_path, "w", encoding="utf-8-sig") as f: f.write("# Отчёт: поиск пути в лабиринте\n\n") f.write("Паттерны: Builder, Strategy, Observer\n\n") f.write("```mermaid\nclassDiagram\n") f.write("class MazeBuilder\nclass TextFileMazeBuilder\n") f.write("class PathFindingStrategy\nclass BFSStrategy\n") f.write("class DFSStrategy\nclass AStarStrategy\n") f.write("class MazeSolver\nclass Observer\nclass ConsoleView\n") f.write("MazeBuilder <|-- TextFileMazeBuilder\n") f.write("PathFindingStrategy <|-- BFSStrategy\n") f.write("PathFindingStrategy <|-- DFSStrategy\n") f.write("PathFindingStrategy <|-- AStarStrategy\n") f.write("Observer <|-- ConsoleView\n") f.write("MazeSolver --> PathFindingStrategy\n") f.write("```\n\n") f.write("| Лабиринт | Стратегия | Время (мс) | Длина пути | Найден |\n") f.write("| --- | --- | --- | --- | --- |\n") for r in results: found = "да" if r["путь_найден"] else "нет" f.write( f"| {r['лабиринт']} | {r['стратегия']} | {r['время_ср']:.2f} | " f"{r['длина_пути_ср']:.0f} | {found} |\n" ) f.write("\n![График](data/maze_time_comparison.png)\n\n") f.write("## Выводы\n\n") f.write("- BFS и A* находят кратчайший путь.\n") f.write("- DFS путь может быть длиннее.\n") f.write("- На пустом лабиринте алгоритмы работают быстрее всего.\n") f.write("- Без выхода все стратегии возвращают пустой путь.\n") print("Готово:", report_path) if __name__ == "__main__": main()