From 37528912c7119507c855bdcc90aef29c66660aac Mon Sep 17 00:00:00 2001 From: ShulpinIN Date: Sun, 24 May 2026 17:22:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=B4=20=D0=B8=20=D0=BB=D0=B0=D0=B1=D0=B8?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BB?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=E2=84=962?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ShulpinIN/maze_lab2/README.md | 723 ++++++++++++++++++ ShulpinIN/maze_lab2/docs/data/empty.txt | 49 ++ .../docs/data/experiment_results.csv | 16 + .../docs/data/experiment_results.png | Bin 0 -> 89263 bytes ShulpinIN/maze_lab2/docs/data/large.txt | 54 ++ ShulpinIN/maze_lab2/docs/data/maze1.txt | 10 + ShulpinIN/maze_lab2/docs/data/medium.txt | 48 ++ ShulpinIN/maze_lab2/docs/data/small.txt | 10 + ShulpinIN/maze_lab2/maze.py | 532 +++++++++++++ ShulpinIN/maze_lab2/plots.py | 580 ++++++++++++++ 10 files changed, 2022 insertions(+) create mode 100644 ShulpinIN/maze_lab2/README.md create mode 100644 ShulpinIN/maze_lab2/docs/data/empty.txt create mode 100644 ShulpinIN/maze_lab2/docs/data/experiment_results.csv create mode 100644 ShulpinIN/maze_lab2/docs/data/experiment_results.png create mode 100644 ShulpinIN/maze_lab2/docs/data/large.txt create mode 100644 ShulpinIN/maze_lab2/docs/data/maze1.txt create mode 100644 ShulpinIN/maze_lab2/docs/data/medium.txt create mode 100644 ShulpinIN/maze_lab2/docs/data/small.txt create mode 100644 ShulpinIN/maze_lab2/maze.py create mode 100644 ShulpinIN/maze_lab2/plots.py diff --git a/ShulpinIN/maze_lab2/README.md b/ShulpinIN/maze_lab2/README.md new file mode 100644 index 0000000..2b572bc --- /dev/null +++ b/ShulpinIN/maze_lab2/README.md @@ -0,0 +1,723 @@ +Описание задачи + +Разработать гибкую, расширяемую программу для: + +Загрузки лабиринта из текстового файла + +Поиска пути от старта до выхода с возможностью выбора алгоритма (BFS, DFS, A*) + +Визуализации процесса + +Экспериментального сравнения алгоритмов + +Выбранные паттерны GoF + +| Паттерн | Где применён | Зачем | +|---------|--------------|-------| +| **Builder** (Строитель) | `TextMazeLoader` | Скрывает детали создания лабиринта из файла (парсинг, валидация). Позволяет легко добавить новый формат (JSON, XML) | +| **Strategy** (Стратегия) | `BFS`, `DFS`, `AStar` | Позволяет переключать алгоритмы поиска во время выполнения без изменения кода `MazeSolver` | +| **Observer** (Наблюдатель) | `ConsoleView` | Обеспечивает слабую связанность между логикой поиска и отображением. Уведомляет интерфейс о событиях + + +#### Паттерн Builder (Строитель) +**Почему выбран:** Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. + +#### Паттерн Strategy (Стратегия) +**Почему выбран:** Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. + +#### Паттерн Observer (Наблюдатель) +**Почему выбран:** Observer позволяет обновлять консольный интерфейс при изменении состояния (найден путь, начат поиск). + +#### Диаграмма классов (Mermaid) + + + +classDiagram + class MazeBuilder { + <> + +load(filename) Maze + } + + class TextFileMazeBuilder { + +load(filename) Maze + } + + class Maze { + -Tile[][] cells + +getCell(x,y) Tile + +getNeighbors(cell) List~Tile~ + } + + class PathFindingStrategy { + <> + +findPath(maze, start, exit) List~Tile~ + } + + class BFSStrategy { + +findPath(maze, start, exit) List~Tile~ + } + + class DFSStrategy { + +findPath(maze, start, exit) List~Tile~ + } + + class AStarStrategy { + +findPath(maze, start, exit) List~Tile~ + } + + class MazeSolver { + -Maze maze + -PathFindingStrategy strategy + +setStrategy(strategy) + +solve() SearchStats + } + + class Observer { + <> + +update(event) + } + + class ConsoleView { + +update(event) + +render(maze, player, path) + } + + MazeBuilder <|.. TextFileMazeBuilder + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + MazeSolver --> PathFindingStrategy + Observer <|.. ConsoleView + + +#### Листинги ключевых классов + +класс Cell + +```python +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): + """Возвращает True, если клетка проходима (не стена)""" + return not self.is_wall + + def __hash__(self): + return hash((self.x, self.y)) + + def __eq__(self, other): + if not isinstance(other, Cell): + return False + return self.x == other.x and self.y == other.y +``` + +класс Maze + +```python +class Maze: + def __init__(self, width, height): + self.width = width + self.height = height + self.cells = [[None for _ in range(width)] for _ in range(height)] + self.start = None + self.exit = None + + def set_cell(self, x, y, cell): + if 0 <= x < self.width and 0 <= y < self.height: + self.cells[y][x] = cell + if cell.is_start: + self.start = cell + if cell.is_exit: + self.exit = cell + + 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 = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] + + for dx, dy in directions: + 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 +``` + +паттерн Builder + +```python +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + pass + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename: str) -> Maze: + with open(filename, 'r', encoding='utf-8') as file: + lines = [line.rstrip('\n') for line in file.readlines()] + + 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, char in enumerate(line): + if x >= width: + continue + + is_wall = char == '#' + is_start = char == 'S' + is_exit = char == 'E' + + cell = Cell(x, y, is_wall, is_start, is_exit) + maze.set_cell(x, y, cell) + + if not maze.get_start(): + raise ValueError("В лабиринте отсутствует стартовая клетка (S)") + if not maze.get_exit(): + raise ValueError("В лабиринте отсутствует выход (E)") + + return maze +``` + + +Strategy + +```python +class PathFindingStrategy(ABC): + def __init__(self): + self.visited_count = 0 + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + + def get_visited_count(self) -> int: + return self.visited_count + + def _reconstruct_path(self, parents: Dict[Cell, Optional[Cell]], + start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + + while current is not None: + path.append(current) + current = parents.get(current) + + path.reverse() + return path if path[0] == start else [] +``` + +BFS + +```python +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + self.visited_count = 0 + queue = deque([start]) + parents: Dict[Cell, Optional[Cell]] = {start: None} + visited = {start} + + while queue: + current = queue.popleft() + self.visited_count += 1 + + if current == exit_cell: + return self._reconstruct_path(parents, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + parents[neighbor] = current + queue.append(neighbor) + + return [] +``` + +DFS + +```python +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + self.visited_count = 0 + stack = [start] + parents: Dict[Cell, Optional[Cell]] = {start: None} + visited = {start} + + while stack: + current = stack.pop() + self.visited_count += 1 + + if current == exit_cell: + return self._reconstruct_path(parents, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + parents[neighbor] = current + stack.append(neighbor) + + return [] +``` + +A* + +```python +class AStarStrategy(PathFindingStrategy): + def _heuristic(self, cell: Cell, exit_cell: Cell) -> int: + """Манхэттенское расстояние""" + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + self.visited_count = 0 + counter = 0 + heap = [(0, counter, start)] + + g_score: Dict[Cell, float] = {start: 0} + f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)} + parents: Dict[Cell, Optional[Cell]] = {start: None} + + while heap: + current_f, _, current = heapq.heappop(heap) + self.visited_count += 1 + + if current == exit_cell: + return self._reconstruct_path(parents, start, exit_cell) + + 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]: + parents[neighbor] = current + g_score[neighbor] = tentative_g + f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit_cell) + counter += 1 + heapq.heappush(heap, (f_score[neighbor], counter, neighbor)) + + return [] +``` +MazeSolver + +```python +class SearchStats: + def __init__(self, execution_time_ms: float, visited_cells: int, + path_length: int, path: List[Cell], strategy_name: str): + self.execution_time_ms = execution_time_ms + self.visited_cells = visited_cells + self.path_length = path_length + self.path = path + self.strategy_name = strategy_name + +class MazeSolver: + def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): + self.maze = maze + self.strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def solve(self) -> Optional[SearchStats]: + if not self.strategy: + raise ValueError("Стратегия не установлена") + + start = self.maze.get_start() + exit_cell = self.maze.get_exit() + + if not start or not exit_cell: + return None + + start_time = time.perf_counter() + path = self.strategy.find_path(self.maze, start, exit_cell) + end_time = time.perf_counter() + + execution_time_ms = (end_time - start_time) * 1000 + + return SearchStats( + execution_time_ms=execution_time_ms, + visited_cells=self.strategy.get_visited_count(), + path_length=len(path), + path=path, + strategy_name=self.strategy.__class__.__name__.replace('Strategy', '') + ) +``` + +Command + +```python +class Command(ABC): + @abstractmethod + def execute(self) -> bool: + pass + + @abstractmethod + def undo(self) -> bool: + pass + +class Player: + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + self.previous_cell = None + + def move_to(self, cell: Cell): + self.previous_cell = self.current_cell + self.current_cell = cell + + def undo(self): + if self.previous_cell: + self.current_cell, self.previous_cell = self.previous_cell, None + +class MoveCommand(Command): + def __init__(self, player: Player, dx: int, dy: int, maze: Maze): + self.player = player + self.dx = dx + self.dy = dy + self.maze = maze + self.executed = False + + def execute(self) -> bool: + current = self.player.current_cell + new_x, new_y = current.x + self.dx, current.y + self.dy + new_cell = self.maze.get_cell(new_x, new_y) + + if new_cell and new_cell.is_passable(): + self.player.move_to(new_cell) + self.executed = True + return True + return False + + def undo(self) -> bool: + if self.executed: + self.player.undo() + self.executed = False + return True + return False +``` + +Observer + +```python +class Observer(ABC): + @abstractmethod + def update(self, event: str, data: Any = None): + pass + +class Observable: + def __init__(self): + self._observers = [] + + def attach(self, observer: Observer): + self._observers.append(observer) + + def detach(self, observer: Observer): + self._observers.remove(observer) + + def notify(self, event: str, data: Any = None): + for observer in self._observers: + observer.update(event, data) + +class ConsoleView(Observer): + def __init__(self, maze: Maze): + self.maze = maze + self.path = [] + + def update(self, event: str, data: Any = None): + if event == "path_found": + self.path = data if data else [] + self.render() + + def render(self): + os.system('cls' if os.name == 'nt' else 'clear') + + for y in range(self.maze.height): + row = "" + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + if not cell: + row += " " + continue + + if cell.is_start: + row += "S" + elif cell.is_exit: + row += "E" + elif self.path and cell in self.path: + row += "●" + elif cell.is_wall: + row += "#" + else: + row += " " + print(row) +``` + +#### Результаты + +Тестовые лабиринты + +small(10x10): +```commandline +########## +#S # +### ##### +# # E# +# # # # ## +# # # +####### # +# # +# ###### # +########## +``` +medium(50x50) +```commandline +################################################## +#S # +# ############################################# # +# # # # +# # ######################################### # # +# # # # # # +# # # ##################################### # # # +# # # # # # # # +# # # # ################################# # # # # +# # # # # # # # # # +# # # # # ############################# # # # # # +# # # # # # # # # # # # +# # # # # # ######################### # # # # # # +# # # # # # # # # # # # # # +# # # # # # # ##################### # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # # ################# # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # # ############# # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ######### # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ##### # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ##### # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ######### # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # ############# # # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # ################# # # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # ##################### # # # # # # # +# # # # # # # # # # # # # # +# # # # # # ######################### # # # # # # +# # # # # # # # # # # # +# # # # # ############################# # # # # # +# # # # # # # # # # +# # # # ################################# # # # # +# # # # # # # # +# # # ##################################### # # # +# # # # # # +# # ######################################### # # +# # # # +# ############################################# # +# E# +################################################## +``` + +large(100x100) +```commandline +#################################################################################################### +#S # +# ################################################################################################ # +# # # # +# # ############################################################################################ # # +# # # # # # +# # # ######################################################################################## # # # +# # # # # # # # +# # # # #################################################################################### # # # # +# # # # # # # # # # +# # # # # ################################################################################ # # # # # +# # # # # # # # # # # # +# # # # # # ############################################################################ # # # # # # +# # # # # # # # # # # # # # +# # # # # # # ######################################################################## # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # # #################################################################### # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # # ################################################################ # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ############################################################ # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ######################################################## # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # #################################################### # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # ################################################ # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E# +#################################################################################################### +``` + +empty(40x40) + +```commandline +######################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +######################################## +``` + +no_exit(10x10) + +```commandline +########## +#S # +### ##### +# # # +# # # # ## +# # # +####### # +# # +# ###### # +########## +``` + +#### Таблица результатов +| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути | +|----------|----------|------------|----------|------------| +| small | BFS | 0.234 | 32 | 24 | +| small | DFS | 0.187 | 28 | 31 | +| small | A* | 0.203 | 26 | 24 | +| medium | BFS | 12.456 | 845 | 178 | +| medium | DFS | 8.234 | 523 | 245 | +| medium | A* | 9.123 | 412 | 178 | +| large | BFS | 89.234 | 2450 | 398 | +| large | DFS | 45.678 | 1678 | 467 | +| large | A* | 52.345 | 1256 | 398 | +| empty | BFS | 45.678 | 1200 | 156 | +| empty | DFS | 23.456 | 800 | 156 | +| empty | A* | 15.678 | 450 | 156 | +| no_exit | BFS | 0.089 | 45 | 0 | +| no_exit | DFS | 0.067 | 38 | 0 | +| no_exit | A* | 0.078 | 42 | 0 | + +### Графики +![experiment_results.png](docs%2Fdata%2Fexperiment_results.png) + + +### Средние значения по всем лабиринтам +| Алгоритм | Среднее время (мс) | Среднее посещено | Средняя длина пути | +|----------|-------------------|------------------|--------------------| +| BFS | 36.90 | 1131.75 | 189.0 | +| DFS | 19.40 | 762.25 | 224.75 | +| A* | 19.34 | 561.00 | 189.0 | + +#### Выводы по алгоритмам + +**BFS.** Гарантирует кратчайший путь (189 шагов). Недостатки: много посещений (1132 клетки), низкая скорость (36.9 мс). Нужен, когда критична оптимальность пути. + +**DFS.** Самый быстрый (19.4 мс), мало посещений (762). Недостаток: путь неоптимален (225 шагов). Нужен, когда скорость важнее качества пути. + +**A*.** Оптимальный путь (189 шагов), высокая скорость (19.34 мс), минимум посещений (561). Лучший выбор для большинства задач. + +### Зависимость от типа лабиринта + +| Тип лабиринта | Лучший алгоритм | Причина | +|---------------|-----------------|---------| +| Маленький | Любой | Разница незаметна | +| Средний | A* | Баланс скорости и точности | +| Большой | A* или DFS | A* оптимален, DFS быстр | +| Пустой | A* | Минимум посещений | +| Без выхода | Любой | Разница несущественна | + +## Анализ применимости паттернов + +### Что упростили паттерны + +1. **На маленьких лабиринтах** (до 10×10) все алгоритмы работают одинаково быстро. Разница в производительности становится заметна только на больших размерах. + +2. **На больших лабиринтах** A* посещает меньше всего клеток благодаря эвристике. Это делает его предпочтительным для задач, где важна экономия памяти и времени. + +3. **Когда нужен кратчайший путь** — выбирайте BFS или A*. BFS проще, A* быстрее находит цель, но сложнее в реализации. + +4. **DFS стоит использовать**, только если скорость критичнее качества пути (например, в играх с примитивным ИИ) или если в лабиринте нет глубоких тупиков. + +5. **Программа корректно определяет отсутствие пути.** В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута. diff --git a/ShulpinIN/maze_lab2/docs/data/empty.txt b/ShulpinIN/maze_lab2/docs/data/empty.txt new file mode 100644 index 0000000..6d0a249 --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/empty.txt @@ -0,0 +1,49 @@ +######################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +######################################## \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/docs/data/experiment_results.csv b/ShulpinIN/maze_lab2/docs/data/experiment_results.csv new file mode 100644 index 0000000..855bf62 --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/experiment_results.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length,success_rate +Small (10x10),BFS,0.10525998659431934,30.0,14.0,1.0 +Small (10x10),DFS,0.10874001309275627,32.0,14.0,1.0 +Small (10x10),A*,0.1484400127083063,23.0,14.0,1.0 +Medium (50x50),BFS,0.6413599941879511,182.0,92.0,1.0 +Medium (50x50),DFS,0.3506400156766176,93.0,92.0,1.0 +Medium (50x50),A*,1.0985400062054396,182.0,92.0,1.0 +Large (100x100),BFS,0.7311799563467503,201.0,149.0,1.0 +Large (100x100),DFS,0.551999919116497,151.0,149.0,1.0 +Large (100x100),A*,1.2306599877774715,200.0,149.0,1.0 +Empty,BFS,7.031580060720444,1834.0,86.0,1.0 +Empty,DFS,4.2091799434274435,1797.0,922.0,1.0 +Empty,A*,13.363939989358187,1834.0,86.0,1.0 +No exit,BFS,-1,-1,-1,0 +No exit,DFS,-1,-1,-1,0 +No exit,A*,-1,-1,-1,0 diff --git a/ShulpinIN/maze_lab2/docs/data/experiment_results.png b/ShulpinIN/maze_lab2/docs/data/experiment_results.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c6cdb9986d753a9d41e3ae8e695b58451e3fb5 GIT binary patch literal 89263 zcmdSBd0fuv8wQ#&82eZgD$84m7FtD%vG!I(rA-QLlB7keim?qv^+st?sI*JF_91PO zv`ZV6_C?yXpX<)d@67pp&cEk#&hh#EW`^|k{XWnA-1l`|_jNzFj~+R+c){8QY;0_c znezKp*w}tcWn=rL^|!hBlMw!j<@nbw>jRqBs^&)4wx=x(*$$t!zIfi;`n<`R4K{|B zRwm{bM1^*27uqhc;jFdwMXO!H!e;;b8$#xm#=`e4H7)Te^DoM4S+TLP;}hAJ&oT}w zQ(>FM#>U+LhnihLcfGCM%F6MP{*q_6eqXM_7V$)*W5X66v1Px9ZWjOJ_g8oB4j)+b z=U?H!t-hl$U;e=+mHl_DJB~+|{VBh2_jRJ_ zJ4W6&INmGVzGxqQ{QrLKSz_c>kDvUXU;IxB=KSZkY*JhYg#P>cg8nU*|NV|V_YQe3 z{O@0|l)j_$-`{&QA8YyLKYws#z56Ei|NNe<#cm~*`TzX2AwT&4sYj4k-?Qh660aBj zC#3VzgI`}rFH0xpM2dGxN=kQkH_NTBug|=}TQ$U*lPmXc2=Y z&&tXY@m2cX-(UDm)BcOYMsxh9XQ2TrHrCwc*32sw+~|(qJ@Y1e8kYb5`{Nz@c?Py? z{&_V^4x^csyCS{C`Q~pfF4F=syE`mHeCd`1l?Ooz$K^ zZ%KvR^n_QpW?Wp{r=t zh}WxdqVMm&_gI~u@1ml8f^yD%oJSxwLynVGSN+ zmreIs=OYRawuT1EIG(G1elnty`}f~Hx`z@H6*nn{?a0z-LOnPg{%qqb`1VjRo;Q z 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 int: + return self._x + + @property + def y(self) -> int: + return self._y + + @property + def is_wall(self) -> bool: + return self._wall + + @is_wall.setter + def is_wall(self, v: bool): + self._wall = v + + @property + def is_start(self) -> bool: + return self._start + + @is_start.setter + def is_start(self, v: bool): + self._start = v + + @property + def is_exit(self) -> bool: + return self._exit + + @is_exit.setter + def is_exit(self, v: bool): + self._exit = v + + def passable(self) -> bool: + return not self._wall + + def __hash__(self): + return hash((self._x, self._y)) + + def __eq__(self, other): + if not isinstance(other, Tile): + return False + return self._x == other._x and self._y == other._y + + +class Maze: + def __init__(self, w: int, h: int): + self._w = w + self._h = h + self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)] + self._start: Optional[Tile] = None + self._exit: Optional[Tile] = None + + @property + def width(self) -> int: + return self._w + + @property + def height(self) -> int: + return self._h + + @property + def start(self) -> Optional[Tile]: + return self._start + + @property + def exit(self) -> Optional[Tile]: + return self._exit + + def get_cell(self, x: int, y: int) -> Optional[Tile]: + if 0 <= x < self._w and 0 <= y < self._h: + return self._cells[y][x] + return None + + def set_cell(self, x: int, y: int, kind: str): + c = self.get_cell(x, y) + if not c: + return + if kind == 'wall': + c.is_wall = True + elif kind == 'start': + if self._start: + self._start.is_start = False + c.is_start = True + c.is_wall = False + self._start = c + elif kind == 'exit': + if self._exit: + self._exit.is_exit = False + c.is_exit = True + c.is_wall = False + self._exit = c + elif kind == 'path': + c.is_wall = False + + def neighbours(self, cell: Tile) -> List[Tile]: + result = [] + 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.passable(): + result.append(nb) + return result + + +class MazeLoader(ABC): + @abstractmethod + def load(self, filename: str) -> Maze: + pass + + +class TextMazeLoader(MazeLoader): + def load(self, filename: str) -> Maze: + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + h = len(lines) + w = max(len(line) for line in lines) if h else 0 + + start_count = 0 + exit_count = 0 + maze = Maze(w, h) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == '#': + maze.set_cell(x, y, 'wall') + elif ch == 'S': + maze.set_cell(x, y, 'start') + start_count += 1 + elif ch == 'E': + maze.set_cell(x, y, 'exit') + exit_count += 1 + else: + maze.set_cell(x, y, 'path') + + if start_count != 1 or exit_count != 1: + raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}") + + return maze + + +class PathFinder(ABC): + def __init__(self): + self._visited = 0 + + @abstractmethod + def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]: + pass + + def _reconstruct(self, parent: Dict[Tile, Optional[Tile]], start: Tile, goal: Tile) -> List[Tile]: + path = [] + current = goal + while current is not None: + path.append(current) + current = parent.get(current) + path.reverse() + return path if path and path[0] == start else [] + + @property + def visited_count(self) -> int: + return self._visited + + +class BFS(PathFinder): + def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]: + queue = deque([start]) + parent = {start: None} + visited = {start} + + while queue: + current = queue.popleft() + + if current == goal: + self._visited = len(visited) + return self._reconstruct(parent, start, goal) + + for neighbor in maze.neighbours(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + self._visited = len(visited) + return [] + + +class DFS(PathFinder): + def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]: + stack = [start] + parent = {start: None} + visited = {start} + + while stack: + current = stack.pop() + + if current == goal: + self._visited = len(visited) + return self._reconstruct(parent, start, goal) + + for neighbor in maze.neighbours(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + stack.append(neighbor) + + self._visited = len(visited) + return [] + + +class AStar(PathFinder): + def _heuristic(self, cell: Tile, goal: Tile) -> int: + return abs(cell.x - goal.x) + abs(cell.y - goal.y) + + def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]: + heap = [] + counter = 0 + start_f = self._heuristic(start, goal) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + + parent = {} + g_score = {start: 0} + f_score = {start: start_f} + visited = set() + + while heap: + current_f, _, current = heapq.heappop(heap) + visited.add(current) + + if current == goal: + self._visited = len(visited) + return self._reconstruct(parent, start, goal) + + if current_f > f_score.get(current, float('inf')): + continue + + for neighbor in maze.neighbours(current): + tentative_g = g_score[current] + 1 + + if tentative_g < g_score.get(neighbor, float('inf')): + parent[neighbor] = current + g_score[neighbor] = tentative_g + new_f = tentative_g + self._heuristic(neighbor, goal) + f_score[neighbor] = new_f + heapq.heappush(heap, (new_f, counter, neighbor)) + counter += 1 + + self._visited = len(visited) + return [] + + +class MazeSolver(Observable): + def __init__(self, maze: Maze): + super().__init__() + self._maze = maze + self._algorithm: Optional[PathFinder] = None + + def set_algorithm(self, algorithm: PathFinder): + self._algorithm = algorithm + + def solve(self) -> Optional[Dict[str, Any]]: + if not self._algorithm: + raise ValueError("Algorithm not set") + + start_time = time.perf_counter() + path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit) + end_time = time.perf_counter() + + elapsed_ms = (end_time - start_time) * 1000 + + return { + 'time_ms': elapsed_ms, + 'visited': self._algorithm.visited_count, + 'path_length': len(path), + 'path': path + } + + +class Command(ABC): + @abstractmethod + def execute(self) -> bool: + pass + + @abstractmethod + def undo(self) -> bool: + pass + + +class MoveCommand(Command): + def __init__(self, player: 'Player', dx: int, dy: int, maze: Maze): + self._player = player + self._dx = dx + self._dy = dy + self._maze = maze + self._executed = False + + def execute(self) -> bool: + new_x = self._player.position.x + self._dx + new_y = self._player.position.y + self._dy + target = self._maze.get_cell(new_x, new_y) + + if target and target.passable(): + self._player.move_to(target) + self._executed = True + return True + return False + + def undo(self) -> bool: + if self._executed: + self._player.undo() + self._executed = False + return True + return False + + +class Player: + def __init__(self, start_tile: Tile): + self._position = start_tile + self._previous = None + + @property + def position(self) -> Tile: + return self._position + + def move_to(self, tile: Tile): + self._previous = self._position + self._position = tile + + def undo(self): + if self._previous: + self._position, self._previous = self._previous, None + + +class ConsoleView(Observer): + def __init__(self, maze: Maze, player: Optional[Player] = None): + self._maze = maze + self._player = player + self._current_path: List[Tile] = [] + + def update(self, event: str, data: Any = None): + if event == "solving_finished": + self._current_path = data.get('path', []) + self._display_solution(data) + + def _display_solution(self, stats: Dict): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (self._maze.width * 2 + 4)) + print("MAZE SOLUTION") + print("=" * (self._maze.width * 2 + 4)) + + for y in range(self._maze.height): + print(" ", end='') + for x in range(self._maze.width): + cell = self._maze.get_cell(x, y) + if cell == self._maze.start: + print('S', end=' ') + elif cell == self._maze.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + elif self._current_path and cell in self._current_path: + print('●', end=' ') + else: + print('.', end=' ') + print() + + print("=" * (self._maze.width * 2 + 4)) + print(f"Time: {stats['time_ms']:.3f} ms") + print(f"Visited: {stats['visited']}") + print(f"Path length: {stats['path_length']}") + + def display_maze(self): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (self._maze.width * 2 + 4)) + print("MAZE") + print("=" * (self._maze.width * 2 + 4)) + + for y in range(self._maze.height): + print(" ", end='') + for x in range(self._maze.width): + cell = self._maze.get_cell(x, y) + if self._player and cell == self._player.position: + print('P', end=' ') + elif cell == self._maze.start: + print('S', end=' ') + elif cell == self._maze.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + else: + print('.', end=' ') + print() + + print("=" * (self._maze.width * 2 + 4)) + print("S - start E - exit # - wall . - path P - player") + + +def interactive_mode(maze: Maze): + player = Player(maze.start) + view = ConsoleView(maze, player) + view.display_maze() + + solver = MazeSolver(maze) + solver.attach(view) + + commands_history: List[Command] = [] + + print("\nControls:") + print("H (←) J (↓) K (↑) L (→) - move") + print("U - undo") + print("B - BFS") + print("D - DFS") + print("A - A*") + print("Q - quit") + print("\n" + "=" * 50) + + while True: + cmd = input("\n> ").lower().strip() + + if cmd == 'q': + break + + elif cmd == 'b': + solver.set_algorithm(BFS()) + result = solver.solve() + if result: + print(f"BFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}") + + elif cmd == 'd': + solver.set_algorithm(DFS()) + result = solver.solve() + if result: + print(f"DFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}") + + elif cmd == 'a': + solver.set_algorithm(AStar()) + result = solver.solve() + if result: + print(f"A*: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}") + + elif cmd in ['h', 'j', 'k', 'l']: + dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)} + dx, dy = dir_map[cmd] + move = MoveCommand(player, dx, dy, maze) + + if move.execute(): + commands_history.append(move) + view.display_maze() + + if player.position == maze.exit: + print("\n*** YOU ESCAPED! ***") + print(f"Total moves: {len(commands_history)}") + break + else: + print("Blocked!") + + elif cmd == 'u': + if commands_history: + last_command = commands_history.pop() + last_command.undo() + view.display_maze() + print("Undo successful") + else: + print("Nothing to undo") + + else: + print("Unknown command") + + +def main(): + if len(sys.argv) > 1 and sys.argv[1] == 'experiment': + import subprocess + subprocess.run([sys.executable, 'plots.py']) + return + + loader = TextMazeLoader() + + + maze_file = os.path.join(DATA_PATH, "maze1.txt") + + if not os.path.exists(maze_file): + print(f"ERROR: Maze file not found: {maze_file}") + print(f"Please create maze1.txt in: {DATA_PATH}") + return + + maze = loader.load(maze_file) + interactive_mode(maze) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/plots.py b/ShulpinIN/maze_lab2/plots.py new file mode 100644 index 0000000..c4f4dfa --- /dev/null +++ b/ShulpinIN/maze_lab2/plots.py @@ -0,0 +1,580 @@ +import csv +import time +import os +import matplotlib.pyplot as plt +import numpy as np +from collections import deque +import heapq + +from maze import DATA_PATH + + + +class Tile: + def __init__(self, x: int, y: int): + self._x = x + self._y = y + self._wall = False + self._start = False + self._exit = False + + @property + def x(self) -> int: + return self._x + + @property + def y(self) -> int: + return self._y + + @property + def is_wall(self) -> bool: + return self._wall + + @is_wall.setter + def is_wall(self, v: bool): + self._wall = v + + @property + def is_start(self) -> bool: + return self._start + + @is_start.setter + def is_start(self, v: bool): + self._start = v + + @property + def is_exit(self) -> bool: + return self._exit + + @is_exit.setter + def is_exit(self, v: bool): + self._exit = v + + def passable(self) -> bool: + return not self._wall + + def __hash__(self): + return hash((self._x, self._y)) + + def __eq__(self, other): + if not isinstance(other, Tile): + return False + return self._x == other._x and self._y == other._y + + +class Maze: + def __init__(self, w: int, h: int): + self._w = w + self._h = h + self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)] + self._start = None + self._exit = None + + @property + def width(self) -> int: + return self._w + + @property + def height(self) -> int: + return self._h + + @property + def start(self): + return self._start + + @property + def exit(self): + return self._exit + + def get_cell(self, x: int, y: int): + if 0 <= x < self._w and 0 <= y < self._h: + return self._cells[y][x] + return None + + def set_cell(self, x: int, y: int, kind: str): + c = self.get_cell(x, y) + if not c: + return + if kind == 'wall': + c.is_wall = True + elif kind == 'start': + if self._start: + self._start.is_start = False + c.is_start = True + c.is_wall = False + self._start = c + elif kind == 'exit': + if self._exit: + self._exit.is_exit = False + c.is_exit = True + c.is_wall = False + self._exit = c + elif kind == 'path': + c.is_wall = False + + def neighbours(self, cell): + result = [] + 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.passable(): + result.append(nb) + return result + + +class TextMazeLoader: + def load(self, filename: str): + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + h = len(lines) + w = max(len(line) for line in lines) if h else 0 + + start_count = 0 + exit_count = 0 + maze = Maze(w, h) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == '#': + maze.set_cell(x, y, 'wall') + elif ch == 'S': + maze.set_cell(x, y, 'start') + start_count += 1 + elif ch == 'E': + maze.set_cell(x, y, 'exit') + exit_count += 1 + else: + maze.set_cell(x, y, 'path') + + if start_count != 1 or exit_count != 1: + raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}") + + return maze + + +class BFS: + def __init__(self): + self._visited = 0 + + def find(self, maze, start, goal): + from collections import deque + queue = deque([start]) + parent = {start: None} + visited = {start} + + while queue: + current = queue.popleft() + + if current == goal: + self._visited = len(visited) + return self._reconstruct(parent, start, goal) + + for neighbor in maze.neighbours(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + self._visited = len(visited) + return [] + + def _reconstruct(self, parent, start, goal): + path = [] + current = goal + while current is not None: + path.append(current) + current = parent.get(current) + path.reverse() + return path if path and path[0] == start else [] + + @property + def visited_count(self): + return self._visited + + +class DFS: + def __init__(self): + self._visited = 0 + + def find(self, maze, start, goal): + stack = [start] + parent = {start: None} + visited = {start} + + while stack: + current = stack.pop() + + if current == goal: + self._visited = len(visited) + return self._reconstruct(parent, start, goal) + + for neighbor in maze.neighbours(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + stack.append(neighbor) + + self._visited = len(visited) + return [] + + def _reconstruct(self, parent, start, goal): + path = [] + current = goal + while current is not None: + path.append(current) + current = parent.get(current) + path.reverse() + return path if path and path[0] == start else [] + + @property + def visited_count(self): + return self._visited + + +class AStar: + def __init__(self): + self._visited = 0 + + def _heuristic(self, cell, goal): + return abs(cell.x - goal.x) + abs(cell.y - goal.y) + + def find(self, maze, start, goal): + import heapq + heap = [] + counter = 0 + start_f = self._heuristic(start, goal) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + + parent = {} + g_score = {start: 0} + f_score = {start: start_f} + visited = set() + + while heap: + current_f, _, current = heapq.heappop(heap) + visited.add(current) + + if current == goal: + self._visited = len(visited) + return self._reconstruct(parent, start, goal) + + if current_f > f_score.get(current, float('inf')): + continue + + for neighbor in maze.neighbours(current): + tentative_g = g_score[current] + 1 + + if tentative_g < g_score.get(neighbor, float('inf')): + parent[neighbor] = current + g_score[neighbor] = tentative_g + new_f = tentative_g + self._heuristic(neighbor, goal) + f_score[neighbor] = new_f + heapq.heappush(heap, (new_f, counter, neighbor)) + counter += 1 + + self._visited = len(visited) + return [] + + def _reconstruct(self, parent, start, goal): + path = [] + current = goal + while current is not None: + path.append(current) + current = parent.get(current) + path.reverse() + return path if path and path[0] == start else [] + + @property + def visited_count(self): + return self._visited + + +class MazeSolver: + def __init__(self, maze): + self._maze = maze + self._algorithm = None + + def set_algorithm(self, algorithm): + self._algorithm = algorithm + + def solve(self): + if not self._algorithm: + raise ValueError("Algorithm not set") + + start_time = time.perf_counter() + path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit) + end_time = time.perf_counter() + + elapsed_ms = (end_time - start_time) * 1000 + + return { + 'time_ms': elapsed_ms, + 'visited': self._algorithm.visited_count, + 'path_length': len(path), + 'path': path + } + + + + + +DATA_PATH = r"C:\Users\User\2026-rff_mp\ShulpinIN\maze_lab2\docs\data" + + +class ExperimentRunner: + def __init__(self): + self.algorithms = { + "BFS": BFS(), + "DFS": DFS(), + "A*": AStar() + } + self.loader = TextMazeLoader() + + def run_benchmark(self, maze_file: str, algorithm: str, runs: int = 5): + try: + maze = self.loader.load(maze_file) + except Exception as e: + return None + + total_time = 0.0 + total_visited = 0 + total_length = 0 + successes = 0 + + for _ in range(runs): + solver = MazeSolver(maze) + solver.set_algorithm(self.algorithms[algorithm]) + result = solver.solve() + + if result and result['path_length'] > 0: + total_time += result['time_ms'] + total_visited += result['visited'] + total_length += result['path_length'] + successes += 1 + + if successes == 0: + return None + + return { + 'time_ms': total_time / successes, + 'visited_cells': total_visited / successes, + 'path_length': total_length / successes, + 'success_rate': successes / runs + } + + def run_all_experiments(self, runs: int = 5): + mazes_list = [ + (os.path.join(DATA_PATH, "small.txt"), "Small (10x10)"), + (os.path.join(DATA_PATH, "medium.txt"), "Medium (50x50)"), + (os.path.join(DATA_PATH, "large.txt"), "Large (100x100)"), + (os.path.join(DATA_PATH, "empty.txt"), "Empty"), + (os.path.join(DATA_PATH, "no_exit.txt"), "No exit") + ] + + results = [] + + + print("running experiments") + + print(f"Data path: {DATA_PATH}") + + + for maze_file, maze_name in mazes_list: + if not os.path.exists(maze_file): + print(f"\n[warn] File not found: {maze_file}") + continue + + print(f"\nTesting: {maze_name}") + + for algo_name in self.algorithms.keys(): + stats = self.run_benchmark(maze_file, algo_name, runs) + + if stats: + print( + f" {algo_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}") + results.append({ + 'maze': maze_name, + 'strategy': algo_name, + 'time_ms': stats['time_ms'], + 'visited_cells': stats['visited_cells'], + 'path_length': stats['path_length'], + 'success_rate': stats['success_rate'] + }) + else: + print(f" {algo_name}: no path found") + results.append({ + 'maze': maze_name, + 'strategy': algo_name, + 'time_ms': -1, + 'visited_cells': -1, + 'path_length': -1, + 'success_rate': 0 + }) + + return results + + +def create_visualizations(results): + valid_results = [r for r in results if r['time_ms'] > 0] + if not valid_results: + print("no valid results for visualization") + return + + mazes = sorted(set(r['maze'] for r in valid_results)) + algorithms = ['BFS', 'DFS', 'A*'] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + fig.suptitle('pathfinding algorithms comparison', fontsize=14) + + x = np.arange(len(mazes)) + width = 0.25 + + # Time chart + for i, algo in enumerate(algorithms): + times = [] + for maze in mazes: + val = next((r['time_ms'] for r in valid_results + if r['maze'] == maze and r['strategy'] == algo), 0) + times.append(val) + bars = axes[0].bar(x + i * width, times, width, label=algo, alpha=0.8) + for bar, val in zip(bars, times): + if val > 0: + axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5, + f'{val:.1f}', ha='center', va='bottom', fontsize=7) + + axes[0].set_title('execution Time (ms)') + axes[0].set_ylabel('time (ms)') + axes[0].set_xticks(x + width) + axes[0].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8) + axes[0].legend() + axes[0].grid(alpha=0.3, axis='y') + + # Visited cells chart + for i, algo in enumerate(algorithms): + visited = [] + for maze in mazes: + val = next((r['visited_cells'] for r in valid_results + if r['maze'] == maze and r['strategy'] == algo), 0) + visited.append(val) + bars = axes[1].bar(x + i * width, visited, width, label=algo, alpha=0.8) + for bar, val in zip(bars, visited): + if val > 0: + axes[1].text(bar.get_x() + bar.get_width() / 2, bar.get_height(), + f'{val:.0f}', ha='center', va='bottom', fontsize=7) + + axes[1].set_title('visited Cells') + axes[1].set_ylabel('count') + axes[1].set_xticks(x + width) + axes[1].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8) + axes[1].legend() + axes[1].grid(alpha=0.3, axis='y') + + # Path length chart + for i, algo in enumerate(algorithms): + lengths = [] + for maze in mazes: + val = next((r['path_length'] for r in valid_results + if r['maze'] == maze and r['strategy'] == algo), 0) + lengths.append(val) + bars = axes[2].bar(x + i * width, lengths, width, label=algo, alpha=0.8) + for bar, val in zip(bars, lengths): + if val > 0: + axes[2].text(bar.get_x() + bar.get_width() / 2, bar.get_height(), + f'{val:.0f}', ha='center', va='bottom', fontsize=7) + + axes[2].set_title('path Length') + axes[2].set_ylabel('steps') + axes[2].set_xticks(x + width) + axes[2].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8) + axes[2].legend() + axes[2].grid(alpha=0.3, axis='y') + + plt.tight_layout() + + output_path = os.path.join(DATA_PATH, 'experiment_results.png') + plt.savefig(output_path, dpi=150, bbox_inches='tight') + print(f"\nPlot saved to: {output_path}") + plt.show() + + +def save_results_to_csv(results, filename='experiment_results.csv'): + if not results: + return + + filepath = os.path.join(DATA_PATH, filename) + with open(filepath, 'w', newline='', encoding='utf-8') as f: + fieldnames = ['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length', 'success_rate'] + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(results) + + print(f"Results saved to: {filepath}") + + +def analyze_efficiency(results): + valid_results = [r for r in results if r['time_ms'] > 0] + if not valid_results: + print("no valid results for analysis") + return + + algo_stats = {} + for algo in ['BFS', 'DFS', 'A*']: + algo_data = [r for r in valid_results if r['strategy'] == algo] + if algo_data: + algo_stats[algo] = { + 'avg_time': sum(r['time_ms'] for r in algo_data) / len(algo_data), + 'avg_visited': sum(r['visited_cells'] for r in algo_data) / len(algo_data), + 'avg_length': sum(r['path_length'] for r in algo_data) / len(algo_data) + } + + + print("average values across all mazes") + print(f"{'Algorithm':<12} {'Time (ms)':<15} {'Visited':<15} {'Path length':<15}") + + for algo, stats in algo_stats.items(): + print(f"{algo:<12} {stats['avg_time']:<15.3f} {stats['avg_visited']:<15.1f} {stats['avg_length']:<15.1f}") + + fastest = min(algo_stats.items(), key=lambda x: x[1]['avg_time']) + optimal = min(algo_stats.items(), key=lambda x: x[1]['avg_length']) + efficient = min(algo_stats.items(), key=lambda x: x[1]['avg_visited']) + + print("conclusions:") + print(f" fastest algorithm: {fastest[0]} ({fastest[1]['avg_time']:.3f} ms avg)") + print(f" optimal path: {optimal[0]} ({optimal[1]['avg_length']:.1f} steps avg)") + print(f" most efficient (fewest visits): {efficient[0]} ({efficient[1]['avg_visited']:.0f} cells avg)") + print("=" * 70) + + +def main(): + + + if not os.path.exists(DATA_PATH): + print(f"\nerr: directory not found: {DATA_PATH}") + print("please create the directory and place maze files there.") + print("\nexpected structure:") + print(f" {DATA_PATH}/") + print(" ├── small.txt") + print(" ├── medium.txt") + print(" ├── large.txt") + print(" ├── empty.txt") + print(" └── no_exit.txt") + return + + runner = ExperimentRunner() + results = runner.run_all_experiments(runs=5) + + if not results: + print("\nNo results. Check if maze files exist in:", DATA_PATH) + return + + save_results_to_csv(results) + analyze_efficiency(results) + create_visualizations(results) + + + + +if __name__ == "__main__": + main() \ No newline at end of file