import time import random import csv import os from collections import deque import heapq import matplotlib.pyplot as plt import numpy as np class Cell: def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False): self.x = x self.y = y self.is_wall = is_wall self.is_start = is_start self.is_exit = is_exit def is_passable(self): return not self.is_wall 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_cell = None self.exit_cell = None def get_cell(self, x, y): if 0 <= x < self.width and 0 <= y < self.height: return self.grid[x][y] return None def get_neighbors(self, cell): neighbors = [] for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]: 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: def build_from_file(self, filename): raise NotImplementedError class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename): with open(filename, 'r', encoding='utf-8') as f: lines = f.readlines() lines = [line.rstrip('\n') for line in lines if line.strip() != ''] if not lines: raise ValueError("Файл пуст") 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): if x >= width: break cell = maze.get_cell(x, y) if ch == '#': cell.is_wall = True elif ch == 'S': cell.is_start = True maze.start_cell = cell elif ch == 'E': cell.is_exit = True maze.exit_cell = cell if maze.start_cell is None or maze.exit_cell is None: raise ValueError("В лабиринте должны быть S и E") return maze class PathFindingStrategy: def find_path(self, maze, start, exit): raise NotImplementedError class BFSStrategy(PathFindingStrategy): def find_path(self, maze, start, exit): if start == exit: return [start] queue = deque([start]) visited = {start} parent = {start: None} while queue: current = queue.popleft() if current == exit: break for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parent[neighbor] = current queue.append(neighbor) if exit not in parent: return [] path = [] step = exit while step is not None: path.append(step) step = parent[step] path.reverse() return path class DFSStrategy(PathFindingStrategy): def find_path(self, maze, start, exit): if start == exit: return [start] stack = [start] visited = {start} parent = {start: None} while stack: current = stack.pop() if current == exit: break for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) parent[neighbor] = current stack.append(neighbor) if exit not in parent: return [] path = [] step = exit while step is not None: path.append(step) step = parent[step] path.reverse() return path class AStarStrategy(PathFindingStrategy): def heuristic(self, a, b): return abs(a.x - b.x) + abs(a.y - b.y) def find_path(self, maze, start, exit): if start == exit: return [start] open_set = [] heapq.heappush(open_set, (0, id(start), start)) came_from = {} g_score = {start: 0} f_score = {start: self.heuristic(start, exit)} while open_set: _, _, current = heapq.heappop(open_set) if current == exit: path = [] step = current while step is not None: path.append(step) step = came_from.get(step) path.reverse() return path for neighbor in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if neighbor not in g_score or tentative_g < g_score[neighbor]: came_from[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + self.heuristic(neighbor, exit) heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor)) return [] class MazeSolver: def __init__(self, maze, strategy=None): self.maze = maze self.strategy = strategy self.observers = [] # для Observer def set_strategy(self, strategy): self.strategy = strategy def attach(self, observer): self.observers.append(observer) def detach(self, observer): self.observers.remove(observer) def notify(self, event): for obs in self.observers: obs.update(event) def solve(self): if self.strategy is None: raise ValueError("Стратегия не установлена") start = self.maze.start_cell exit_cell = self.maze.exit_cell if start is None or exit_cell is None: raise ValueError("Лабиринт не содержит старта или выхода") self.notify("Поиск начат") start_time = time.perf_counter() path = self.strategy.find_path(self.maze, start, exit_cell) end_time = time.perf_counter() elapsed_ms = (end_time - start_time) * 1000 self.notify("Поиск завершён") return path, elapsed_ms class Observer: def update(self, event): raise NotImplementedError class ConsoleView(Observer): def __init__(self, maze): self.maze = maze def update(self, event): if event == "Поиск начат": print("=== Поиск начат ===") elif event == "Поиск завершён": print("=== Поиск завершён ===") def render(self, path=None): path_set = set(path) if 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_wall: row += '#' elif cell.is_start: row += 'S' elif cell.is_exit: row += 'E' elif cell in path_set: row += '*' else: row += ' ' print(row) print() class Command: def execute(self): raise NotImplementedError def undo(self): raise NotImplementedError class MoveCommand(Command): def __init__(self, player, dx, dy): self.player = player self.dx = dx self.dy = dy self.prev_x = player.x self.prev_y = player.y def execute(self): new_x = self.player.x + self.dx new_y = self.player.y + self.dy maze = self.player.maze cell = maze.get_cell(new_x, new_y) if cell and cell.is_passable(): self.player.x = new_x self.player.y = new_y self.player.current_cell = cell return True return False def undo(self): self.player.x = self.prev_x self.player.y = self.prev_y self.player.current_cell = self.player.maze.get_cell(self.prev_x, self.prev_y) class Player: def __init__(self, maze, start_cell): self.maze = maze self.x = start_cell.x self.y = start_cell.y self.current_cell = start_cell # EEEEEEEEEKSPERIMENTY def generate_empty_maze(width, height): maze = Maze(width, height) start = maze.get_cell(0, 0) exit_cell = maze.get_cell(width-1, height-1) start.is_start = True exit_cell.is_exit = True maze.start_cell = start maze.exit_cell = exit_cell return maze def generate_random_maze(width, height, wall_prob=0.3): maze = Maze(width, height) for x in range(width): for y in range(height): cell = maze.get_cell(x, y) if random.random() < wall_prob: cell.is_wall = True start = maze.get_cell(0, 0) exit_cell = maze.get_cell(width-1, height-1) start.is_wall = False start.is_start = True exit_cell.is_wall = False exit_cell.is_exit = True maze.start_cell = start maze.exit_cell = exit_cell return maze def generate_maze_with_dead_ends(width, height): maze = Maze(width, height) for x in range(width): for y in range(height): maze.get_cell(x, y).is_wall = True x, y = 0, 0 while x < width and y < height: cell = maze.get_cell(x, y) cell.is_wall = False if x == width-1 and y == height-1: break if y+1 < height and (x == width-1 or random.choice([True, False])): y += 1 else: x += 1 start = maze.get_cell(0, 0) exit_cell = maze.get_cell(width-1, height-1) start.is_start = True exit_cell.is_exit = True maze.start_cell = start maze.exit_cell = exit_cell return maze def generate_maze_no_exit(width, height): maze = generate_random_maze(width, height, 0.2) exit_cell = maze.get_cell(width-1, height-1) for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]: nx, ny = exit_cell.x + dx, exit_cell.y + dy neighbor = maze.get_cell(nx, ny) if neighbor: neighbor.is_wall = True start = maze.get_cell(0, 0) start.is_wall = False start.is_start = True maze.start_cell = start maze.exit_cell = exit_cell return maze def run_experiment(): os.makedirs("results", exist_ok=True) maze_generators = [ ("empty_10x10", lambda: generate_empty_maze(10, 10)), ("empty_50x50", lambda: generate_empty_maze(50, 50)), ("empty_100x100", lambda: generate_empty_maze(100, 100)), ("random_10x10", lambda: generate_random_maze(10, 10, 0.3)), ("random_50x50", lambda: generate_random_maze(50, 50, 0.3)), ("random_100x100", lambda: generate_random_maze(100, 100, 0.3)), ("dead_ends_10x10", lambda: generate_maze_with_dead_ends(10, 10)), ("dead_ends_50x50", lambda: generate_maze_with_dead_ends(50, 50)), ("dead_ends_100x100", lambda: generate_maze_with_dead_ends(100, 100)), ("no_exit_10x10", lambda: generate_maze_no_exit(10, 10)), ("no_exit_50x50", lambda: generate_maze_no_exit(50, 50)), ] strategies = [ ("BFS", BFSStrategy()), ("DFS", DFSStrategy()), ("AStar", AStarStrategy()) ] repeats = 5 all_results = [] for maze_name, gen_func in maze_generators: print(f"Тестирование лабиринта: {maze_name}") maze = gen_func() solver = MazeSolver(maze) for strat_name, strat in strategies: solver.set_strategy(strat) total_time = 0 total_path_len = 0 path = [] for rep in range(repeats): path, elapsed_ms = solver.solve() total_time += elapsed_ms total_path_len += len(path) if path else 0 avg_time = total_time / repeats avg_len = total_path_len / repeats all_results.append({ "Maze": maze_name, "Strategy": strat_name, "AvgTime_ms": avg_time, "AvgPathLen": avg_len, "PathFound": len(path) > 0 if path else False }) print(f" {strat_name}: время {avg_time:.3f} мс, длина пути {avg_len:.1f}") # Сохраняем CSV csv_path = "results/experiment_results.csv" with open(csv_path, 'w', newline='', encoding='utf-8') as f: fieldnames = ["Maze", "Strategy", "AvgTime_ms", "AvgPathLen", "PathFound"] writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(all_results) print(f"Результаты сохранены в {csv_path}") # Построение графика maze_names = sorted(set(r["Maze"] for r in all_results)) strategy_names = ["BFS", "DFS", "AStar"] data = {maze: {s: None for s in strategy_names} for maze in maze_names} for r in all_results: data[r["Maze"]][r["Strategy"]] = r["AvgTime_ms"] fig, ax = plt.subplots(figsize=(14, 6)) x = np.arange(len(maze_names)) width = 0.25 colors = ['skyblue', 'lightgreen', 'salmon'] for i, strat in enumerate(strategy_names): times = [data[maze][strat] if data[maze][strat] is not None else 0 for maze in maze_names] ax.bar(x + i*width, times, width, label=strat, color=colors[i]) ax.set_xlabel('Лабиринт') ax.set_ylabel('Среднее время (мс)') ax.set_title('Сравнение стратегий поиска пути') ax.set_xticks(x + width) ax.set_xticklabels(maze_names, rotation=45, ha='right') ax.legend() plt.tight_layout() plt.savefig("results/performance.png", dpi=150) plt.show() print("График сохранён в results/performance.png") if __name__ == '__main__': run_experiment()