diff --git a/zverevem/lab2/docs/Report.ipynb b/zverevem/lab2/docs/Report.ipynb new file mode 100644 index 0000000..d7ea70a --- /dev/null +++ b/zverevem/lab2/docs/Report.ipynb @@ -0,0 +1,345 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9a863658", + "metadata": {}, + "source": [ + "# Отчёт: Поиск выхода из лабиринта (ООП + паттерны проектирования)\n", + "\n", + "## Цель работы\n", + "\n", + "Разработать гибкую расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма и экспериментального сравнения алгоритмов. Применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.\n", + "\n", + "---\n", + "\n", + "## Описание задачи и выбранных паттернов\n", + "\n", + "Программа решает задачу поиска пути в лабиринте, загружаемом из текстового файла. Лабиринт представляет собой сетку клеток, где `#` — стена, пробел — проход, `S` — старт, `E` — выход. Алгоритм поиска выбирается динамически, результаты выводятся через систему событий.\n", + "\n", + "В проекте применены паттерны Builder, Strategy, Observer и Command.\n", + "\n", + "### 1. Builder (Строитель) — `MazeBuilder.py`\n", + "\n", + "**Проблема:** построение объекта `Maze` из текстового файла включает несколько этапов: чтение файла, анализ символов, создание объектов клеток, определение стартовой и конечной позиции, формирование структуры лабиринта.\n", + "\n", + "**Решение:** создан абстрактный класс `MazeBuilder` с методом `build_from_file()`. Класс `TextFileMazeBuilder` реализует построение лабиринта из текстового файла.\n", + "\n", + "```python\n", + "class MazeBuilder(ABC):\n", + " @abstractmethod\n", + " def build_from_file(self, filename) -> Maze:\n", + " pass\n", + "```\n", + "\n", + "```python\n", + "class TextFileMazeBuilder(MazeBuilder):\n", + " def build_from_file(self, filename) -> Maze:\n", + "```\n", + "\n", + "Паттерн позволяет изолировать логику построения лабиринта от основной программы. При необходимости можно добавить другой формат загрузки без изменения клиентского кода.\n", + "\n", + "### 2. Strategy (Стратегия) — `FindingStrategy.py`\n", + "\n", + "**Проблема:** алгоритмы BFS, DFS и A* имеют разную реализацию, но используются одинаковым образом.\n", + "\n", + "**Решение:** создан интерфейс `PathFindingStrategy` с методом `find_path()`. Алгоритмы `BFSStrategy`, `DFSStrategy` и `AStarStrategy` реализуют общий интерфейс.\n", + "\n", + "```python\n", + "class PathFindingStrategy(ABC):\n", + " @abstractmethod\n", + " def find_path(self, maze, start, exit_cell):\n", + " pass\n", + "```\n", + "\n", + "Смена алгоритма выполняется динамически:\n", + "\n", + "```python\n", + "solver.set_strategy(BFSStrategy())\n", + "solver.solve()\n", + "\n", + "solver.set_strategy(AStarStrategy())\n", + "solver.solve()\n", + "```\n", + "\n", + "Паттерн Strategy позволяет добавлять новые алгоритмы поиска без изменения класса `MazeSolver`.\n", + "\n", + "### 3. Observer (Наблюдатель) — `MazeSolver.py`\n", + "\n", + "**Проблема:** необходимо уведомлять интерфейс о событиях поиска пути без жёсткой связи между логикой и выводом.\n", + "\n", + "**Решение:** реализован интерфейс `Observer` с методом `update()`. Класс `ConsoleView` подписывается на события `MazeSolver`.\n", + "\n", + "```python\n", + "class Observer(ABC):\n", + " @abstractmethod\n", + " def update(self, event, data=None):\n", + " pass\n", + "```\n", + "\n", + "```python\n", + "solver.add_observer(view)\n", + "```\n", + "\n", + "При нахождении пути вызывается уведомление:\n", + "\n", + "```python\n", + "self._notify('path_found', {\n", + " 'stats': stats,\n", + " 'strategy': type(self.strategy).__name__\n", + "})\n", + "```\n", + "\n", + "Паттерн позволяет отделить вывод информации от алгоритмов поиска.\n", + "\n", + "### 4. Command (Команда) — `MazeSolver.py`\n", + "\n", + "**Проблема:** необходимо реализовать возможность перемещения игрока с поддержкой отмены действий.\n", + "\n", + "**Решение:** создан интерфейс `Command` с методами `execute()` и `undo()`. Класс `MoveCommand` хранит предыдущее состояние игрока.\n", + "\n", + "```python\n", + "class Command(ABC):\n", + " @abstractmethod\n", + " def execute(self):\n", + " pass\n", + "\n", + " @abstractmethod\n", + " def undo(self):\n", + " pass\n", + "```\n", + "\n", + "```python\n", + "cmd.execute()\n", + "cmd.undo()\n", + "```\n", + "\n", + "Паттерн инкапсулирует действия в отдельные объекты и позволяет реализовать undo/redo.\n", + "\n", + "---\n", + "\n", + "## Диаграмма классов (Mermaid)\n", + "\n", + "```mermaid\n", + "classDiagram\n", + " class MazeBuilder {\n", + " <>\n", + " +build_from_file(filename) Maze\n", + " }\n", + "\n", + " class TextFileMazeBuilder {\n", + " +build_from_file(filename) Maze\n", + " }\n", + "\n", + " class Cell {\n", + " +x\n", + " +y\n", + " +is_wall\n", + " +is_start\n", + " +is_exit\n", + " +is_passable()\n", + " }\n", + "\n", + " class Maze {\n", + " +width\n", + " +height\n", + " +get_cell(x, y)\n", + " +get_neighbors(cell)\n", + " +render(path, player_pos)\n", + " }\n", + "\n", + " class PathFindingStrategy {\n", + " <>\n", + " +find_path(maze, start, exit)\n", + " }\n", + "\n", + " class BFSStrategy\n", + " class DFSStrategy\n", + " class AStarStrategy\n", + "\n", + " class MazeSolver {\n", + " +set_strategy(strategy)\n", + " +solve()\n", + " +add_observer(observer)\n", + " }\n", + "\n", + " class SearchStats\n", + "\n", + " class Observer {\n", + " <>\n", + " +update(event, data)\n", + " }\n", + "\n", + " class ConsoleView\n", + "\n", + " class Command {\n", + " <>\n", + " +execute()\n", + " +undo()\n", + " }\n", + "\n", + " class MoveCommand\n", + " class Player\n", + "\n", + " MazeBuilder <|.. TextFileMazeBuilder\n", + " Maze o-- Cell\n", + "\n", + " PathFindingStrategy <|.. BFSStrategy\n", + " PathFindingStrategy <|.. DFSStrategy\n", + " PathFindingStrategy <|.. AStarStrategy\n", + "\n", + " MazeSolver --> Maze\n", + " MazeSolver --> PathFindingStrategy\n", + " MazeSolver --> SearchStats\n", + "\n", + " Observer <|.. ConsoleView\n", + " MazeSolver --> Observer\n", + "\n", + " Command <|.. MoveCommand\n", + " MoveCommand --> Player\n", + "```\n", + "\n", + "---\n", + "\n", + "## Ключевые фрагменты реализации\n", + "\n", + "### Реализация BFS\n", + "\n", + "```python\n", + "class BFSStrategy(PathFindingStrategy):\n", + " def find_path(self, maze, start, exit_cell):\n", + " queue = deque([start])\n", + " came_from = {(start.x, start.y): None}\n", + "```\n", + "\n", + "Алгоритм BFS выполняет поиск в ширину и гарантирует нахождение кратчайшего пути.\n", + "\n", + "### Реализация DFS\n", + "\n", + "```python\n", + "class DFSStrategy(PathFindingStrategy):\n", + " def find_path(self, maze, start, exit_cell):\n", + " stack = [start]\n", + "```\n", + "\n", + "DFS использует стек и выполняет поиск в глубину.\n", + "\n", + "### Реализация A*\n", + "\n", + "```python\n", + "class AStarStrategy(PathFindingStrategy):\n", + "\n", + " def _heuristic(self, cell, goal):\n", + " return abs(cell.x - goal.x) + abs(cell.y - goal.y)\n", + "```\n", + "\n", + "A* использует манхэттенскую эвристику для направления поиска к цели.\n", + "\n", + "---\n", + "\n", + "## Экспериментальная часть\n", + "\n", + "### Параметры эксперимента\n", + "\n", + "| Параметр | Значение |\n", + "| ---------- | ------------------------------------ |\n", + "| Повторений | 7 |\n", + "| Алгоритмы | BFS, DFS, A* |\n", + "| Метрики | время, посещённые клетки, длина пути |\n", + "\n", + "### Тестовые лабиринты\n", + "\n", + "| Название | Размер |\n", + "| ------------- | ------- |\n", + "| small_10x10 | 10×10 |\n", + "| medium_50x50 | 50×50 |\n", + "| large_100x100 | 100×100 |\n", + "| open_50x50 | 50×50 |\n", + "| no_exit_20x20 | 20×20 |\n", + "\n", + "---\n", + "\n", + "## Результаты\n", + "\n", + "### Таблица результатов\n", + "\n", + "| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |\n", + "| ------------- | -------- | ---------- | --------------- | ---------- |\n", + "| small_10x10 | BFS | 0.094 | 54 | 15 |\n", + "| small_10x10 | DFS | 0.059 | 33 | 33 |\n", + "| small_10x10 | A* | 0.078 | 36 | 15 |\n", + "| medium_50x50 | BFS | 2.446 | 1639 | 95 |\n", + "| medium_50x50 | DFS | 1.480 | 1063 | 185 |\n", + "| medium_50x50 | A* | 1.528 | 588 | 95 |\n", + "| large_100x100 | BFS | 9.891 | 6564 | — |\n", + "| large_100x100 | DFS | 9.057 | 6564 | — |\n", + "| large_100x100 | A* | 17.578 | 6564 | — |\n", + "| open_50x50 | BFS | 3.296 | 2304 | 95 |\n", + "| open_50x50 | DFS | 1.830 | 1223 | 1129 |\n", + "| open_50x50 | A* | 5.566 | 2304 | 95 |\n", + "| no_exit_20x20 | BFS | 0.368 | 260 | — |\n", + "| no_exit_20x20 | DFS | 0.343 | 260 | — |\n", + "| no_exit_20x20 | A* | 0.607 | 260 | — |\n", + "\n", + "### Визуализация\n", + "\n", + "![Время выполнения](data/chart_время-мс.png)\n", + "\n", + "![Посещено клеток](data/chart_посещено-клеток.png)\n", + "\n", + "![Длина пути](data/chart_длина-пути.png)\n", + "\n", + "---\n", + "\n", + "## Анализ результатов\n", + "\n", + "### BFS\n", + "\n", + "Алгоритм BFS во всех случаях находит кратчайший путь. На лабиринте medium_50x50 длина найденного пути составила 95 шагов. Недостатком алгоритма является большое количество посещённых клеток.\n", + "\n", + "### DFS\n", + "\n", + "DFS выполняет поиск быстрее, однако найденный путь значительно длиннее. На open_50x50 длина пути составила 1129 шагов против 95 у BFS.\n", + "\n", + "### A*\n", + "\n", + "Алгоритм A* использует эвристику и уменьшает количество посещённых клеток. На medium_50x50 было посещено 588 клеток против 1639 у BFS.\n", + "\n", + "На открытом лабиринте преимущества эвристики снижаются, из-за чего время работы увеличивается.\n", + "\n", + "### Лабиринты без пути\n", + "\n", + "На large_100x100 и no_exit_20x20 путь найден не был. Все алгоритмы корректно завершили работу после обхода доступных клеток.\n", + "\n", + "---\n", + "\n", + "## Выводы\n", + "\n", + "В ходе работы была реализована объектно-ориентированная система поиска пути в лабиринте с применением паттернов проектирования GoF.\n", + "\n", + "Паттерн Strategy обеспечил возможность динамической смены алгоритма поиска. Builder отделил процесс создания лабиринта от логики приложения. Observer позволил реализовать систему уведомлений без жёстких зависимостей. Command обеспечил поддержку отмены действий.\n", + "\n", + "Экспериментальные результаты показали:\n", + "\n", + "* BFS гарантирует кратчайший путь;\n", + "* DFS работает быстрее, но может находить неоптимальные маршруты;\n", + "* A* наиболее эффективен на сложных лабиринтах благодаря эвристике.\n", + "\n", + "Архитектура программы получилась модульной и расширяемой. Добавление новых алгоритмов или способов загрузки лабиринтов возможно без изменения существующего кода.\n" + ] + }, + { + "cell_type": "markdown", + "id": "a9cbb6c6", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/zverevem/lab2/docs/data/FindingStrategy.py b/zverevem/lab2/docs/data/FindingStrategy.py new file mode 100644 index 0000000..a71c794 --- /dev/null +++ b/zverevem/lab2/docs/data/FindingStrategy.py @@ -0,0 +1,100 @@ +from abc import ABC, abstractmethod +from collections import deque +import heapq + + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze, start, exit_cell): + pass + + +def _reconstruct_path(came_from, start, exit_cell): + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get((current.x, current.y)) + path.reverse() + if path and path[0].x == start.x and path[0].y == start.y: + return path + return [] + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze, start, exit_cell): + queue = deque([start]) + came_from = {(start.x, start.y): None} + self.visited_count = 0 + + while queue: + current = queue.popleft() + self.visited_count += 1 + + if current.x == exit_cell.x and current.y == exit_cell.y: + return _reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + key = (neighbor.x, neighbor.y) + if key not in came_from: + came_from[key] = current + queue.append(neighbor) + + self.visited_count = len(came_from) + return [] # путь не найден + +class DFSStrategy(PathFindingStrategy): + + def find_path(self, maze, start, exit_cell): + stack = [start] + came_from = {(start.x, start.y): None} + self.visited_count = 0 + + while stack: + current = stack.pop() + self.visited_count += 1 + + if current.x == exit_cell.x and current.y == exit_cell.y: + return _reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + key = (neighbor.x, neighbor.y) + if key not in came_from: + came_from[key] = current + stack.append(neighbor) + + self.visited_count = len(came_from) + return [] + +class AStarStrategy(PathFindingStrategy): + + def _heuristic(self, cell, goal): + return abs(cell.x - goal.x) + abs(cell.y - goal.y) + + def find_path(self, maze, start, exit_cell): + # (f_score, счётчик для разрыва связей, клетка) + counter = 0 + open_set = [(0, counter, start)] + came_from = {(start.x, start.y): None} + g_score = {(start.x, start.y): 0} + self.visited_count = 0 + + while open_set: + _, _, current = heapq.heappop(open_set) + self.visited_count += 1 + + if current.x == exit_cell.x and current.y == exit_cell.y: + return _reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + key = (neighbor.x, neighbor.y) + tentative_g = g_score[(current.x, current.y)] + 1 + + if key not in g_score or tentative_g < g_score[key]: + g_score[key] = tentative_g + f = tentative_g + self._heuristic(neighbor, exit_cell) + counter += 1 + heapq.heappush(open_set, (f, counter, neighbor)) + came_from[key] = current + + self.visited_count = len(came_from) + return [] diff --git a/zverevem/lab2/docs/data/MazeBuilder.py b/zverevem/lab2/docs/data/MazeBuilder.py new file mode 100644 index 0000000..78abf85 --- /dev/null +++ b/zverevem/lab2/docs/data/MazeBuilder.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod +from maze_model import Cell, Maze + + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename) -> Maze: + with open(filename, 'r', encoding='utf-8') as f: + lines = f.read().splitlines() + + width = max(len(line) for line in lines) if lines else 0 + height = len(lines) + + cells = [] + start = None + exit_cell = None + + for y, line in enumerate(lines): + row = [] + line = line.ljust(width) + for x, char in enumerate(line): + is_wall = (char == '#') + is_start = (char == 'S') + is_exit = (char == 'E') + cell = Cell(x, y, is_wall=is_wall, + is_start=is_start, is_exit=is_exit) + if is_start: + start = cell + if is_exit: + exit_cell = cell + row.append(cell) + cells.append(row) + + if start is None: + raise ValueError("В файле лабиринта не найден старт (S)") + if exit_cell is None: + raise ValueError("В файле лабиринта не найден выход (E)") + + return Maze(width, height, cells, start, exit_cell) diff --git a/zverevem/lab2/docs/data/MazeModel.py b/zverevem/lab2/docs/data/MazeModel.py new file mode 100644 index 0000000..664ad01 --- /dev/null +++ b/zverevem/lab2/docs/data/MazeModel.py @@ -0,0 +1,62 @@ +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 + + def __repr__(self): + if self.is_wall: + return '#' + if self.is_start: + return 'S' + if self.is_exit: + return 'E' + return ' ' + + +class Maze: + def __init__(self, width, height, cells, start, exit_cell): + self.width = width + self.height = height + self._cells = cells + self.start = start + self.exit = 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): + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] + neighbors = [] + for dx, dy in directions: + neighbor = self.get_cell(cell.x + dx, cell.y + dy) + if neighbor is not None and neighbor.is_passable(): + neighbors.append(neighbor) + return neighbors + + def render(self, path=None, player_pos=None): + path_set = set((c.x, c.y) for c in path) if path else set() + + for row in self._cells: + line = '' + for cell in row: + if player_pos and cell.x == player_pos.x and cell.y == player_pos.y: + line += 'P' + elif cell.is_wall: + line += '#' + elif cell.is_start: + line += 'S' + elif cell.is_exit: + line += 'E' + elif (cell.x, cell.y) in path_set: + line += '.' + else: + line += ' ' + print(line) diff --git a/zverevem/lab2/docs/data/MazeSolver.py b/zverevem/lab2/docs/data/MazeSolver.py new file mode 100644 index 0000000..5347cf9 --- /dev/null +++ b/zverevem/lab2/docs/data/MazeSolver.py @@ -0,0 +1,121 @@ +import time +from abc import ABC, abstractmethod + +class SearchStats: + def __init__(self, time_ms, visited_cells, path_length, path): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + self.path = path + + def __repr__(self): + return (f"SearchStats(time={self.time_ms:.3f}ms, " + f"visited={self.visited_cells}, " + f"path_len={self.path_length})") + +class Observer(ABC): + + @abstractmethod + def update(self, event, data=None): + pass + + +class ConsoleView(Observer): + + def update(self, event, data=None): + if event == 'maze_loaded': + print(f"\n[ConsoleView] Лабиринт загружен: " + f"{data['width']}×{data['height']}") + + elif event == 'path_found': + stats = data['stats'] + strategy_name = data['strategy'] + if stats.path_length > 0: + print(f"\n[ConsoleView] [{strategy_name}] Путь найден! " + f"Длина: {stats.path_length}, " + f"Посещено клеток: {stats.visited_cells}, " + f"Время: {stats.time_ms:.3f} мс") + else: + print(f"\n[ConsoleView] [{strategy_name}] Путь не найден. " + f"Посещено клеток: {stats.visited_cells}") + + elif event == 'move': + print(f"[ConsoleView] Игрок переместился в " + f"({data['x']}, {data['y']})") + +class MazeSolver: + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self._observers = [] + + def set_strategy(self, strategy): + self.strategy = strategy + + def add_observer(self, observer): + self._observers.append(observer) + + def _notify(self, event, data=None): + for obs in self._observers: + obs.update(event, data) + + def solve(self): + if self.strategy is None: + raise RuntimeError("Стратегия не задана. Используйте set_strategy().") + + start = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end = time.perf_counter() + + stats = SearchStats( + time_ms=(end - start) * 1000, + visited_cells=getattr(self.strategy, 'visited_count', 0), + path_length=len(path), + path=path + ) + + self._notify('path_found', { + 'stats': stats, + 'strategy': type(self.strategy).__name__ + }) + + return stats + +class Command(ABC): + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass + + +class Player: + def __init__(self, start_cell): + self.current_cell = start_cell + + def move_to(self, cell): + self.current_cell = cell + + +class MoveCommand(Command): + def __init__(self, player, target_cell, observers=None): + self.player = player + self.target_cell = target_cell + self.previous_cell = None + self._observers = observers or [] + + def execute(self): + self.previous_cell = self.player.current_cell + self.player.move_to(self.target_cell) + for obs in self._observers: + obs.update('move', {'x': self.target_cell.x, + 'y': self.target_cell.y}) + + def undo(self): + if self.previous_cell is not None: + self.player.move_to(self.previous_cell) + for obs in self._observers: + obs.update('move', {'x': self.previous_cell.x, + 'y': self.previous_cell.y}) diff --git a/zverevem/lab2/docs/data/Results.py b/zverevem/lab2/docs/data/Results.py new file mode 100644 index 0000000..29dc2d6 --- /dev/null +++ b/zverevem/lab2/docs/data/Results.py @@ -0,0 +1,103 @@ +import csv +import os + +try: + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + HAS_MPL = True +except ImportError: + HAS_MPL = False + print(" matplotlib не установлен: pip install matplotlib\n") + +CSV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'results.csv') +OUT_DIR = os.path.dirname(os.path.abspath(__file__)) + +COLORS = {'BFS': '#4E9AF1', 'DFS': '#F4845F', 'A*': '#6BCB77'} +STRATEGIES = ['BFS', 'DFS', 'A*'] +METRICS = [ + ('время_мс', 'Среднее время (мс)'), + ('посещено_клеток', 'Посещено клеток'), + ('длина_пути', 'Длина пути (шагов)'), +] + + +def load_csv(path): + data = {} + with open(path, newline='', encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + key = (row['лабиринт'], row['стратегия']) + data[key] = { + 'время_мс': float(row['время_мс']), + 'посещено_клеток': float(row['посещено_клеток']), + 'длина_пути': float(row['длина_пути']), + } + return data + + +def get_mazes(data): + seen = [] + for (maze, _) in data: + if maze not in seen: + seen.append(maze) + return seen + + +def plot_by_metric(data): + mazes = get_mazes(data) + x = range(len(mazes)) + w = 0.25 + + for metric_key, metric_label in METRICS: + fig, ax = plt.subplots(figsize=(12, 5)) + fig.suptitle(f'{metric_label} по лабиринтам', fontweight='bold') + + for i, strat in enumerate(STRATEGIES): + vals = [data.get((m, strat), {}).get(metric_key, 0) for m in mazes] + offset = [xi + (i - 1) * w for xi in x] + bars = ax.bar(offset, vals, width=w, + label=strat, color=COLORS[strat], edgecolor='white') + for bar, val in zip(bars, vals): + if val > 0: + ax.text(bar.get_x() + bar.get_width() / 2, + bar.get_height() + max(vals) * 0.01, + f'{val:.1f}', ha='center', va='bottom', fontsize=7) + + ax.set_xticks(list(x)) + ax.set_xticklabels(mazes, rotation=15, ha='right', fontsize=9) + ax.set_ylabel(metric_label) + ax.legend() + ax.grid(axis='y', alpha=0.3) + + safe = metric_key.replace('_', '-') + out = os.path.join(OUT_DIR, f'chart_{safe}.png') + plt.tight_layout() + plt.savefig(out, dpi=150, bbox_inches='tight') + print(f" График сохранён: {out}") + plt.show() + + +def print_table(data): + print(f"\n{'Лабиринт':<20} {'Алгоритм':<6} " + f"{'Время мс':>10} {'Посещено':>10} {'Путь':>6}") + print('-' * 56) + for (maze, strat), vals in sorted(data.items()): + print(f"{maze:<20} {strat:<6} " + f"{vals['время_мс']:>10.3f} " + f"{vals['посещено_клеток']:>10.0f} " + f"{vals['длина_пути']:>6.0f}") + + +if __name__ == '__main__': + if not os.path.exists(CSV_PATH): + print(f" Файл не найден: {CSV_PATH}") + print(" Сначала запустите: python benchmark.py") + exit(1) + + data = load_csv(CSV_PATH) + print_table(data) + + if HAS_MPL: + plot_by_metric(data) + else: + print("\n Установите matplotlib: pip install matplotlib") diff --git a/zverevem/lab2/docs/data/Standard.py b/zverevem/lab2/docs/data/Standard.py new file mode 100644 index 0000000..a6c0b21 --- /dev/null +++ b/zverevem/lab2/docs/data/Standard.py @@ -0,0 +1,153 @@ +import time +import csv +import os +import random + +from maze_builder import TextFileMazeBuilder +from maze_solver import MazeSolver +from maze_strategies import BFSStrategy, DFSStrategy, AStarStrategy + +REPEATS = 7 +OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__)) +CSV_PATH = os.path.join(OUTPUT_DIR, 'results.csv') + +STRATEGIES = { + 'BFS': BFSStrategy, + 'DFS': DFSStrategy, + 'A*': AStarStrategy, +} + +MAZES = [ + ('small_10x10', 'maze_small.txt'), + ('medium_50x50', 'maze_medium.txt'), + ('large_100x100', 'maze_large.txt'), + ('open_50x50', 'maze_open.txt'), + ('no_exit_20x20', 'maze_no_exit.txt'), +] + +def _make_grid(width, height, density=0.0, has_exit=True, seed=42): + + rng = random.Random(seed) + grid = [] + for y in range(height): + row = [] + for x in range(width): + on_border = (x == 0 or x == width - 1 or y == 0 or y == height - 1) + row.append('#' if on_border else ' ') + grid.append(row) + + for y in range(1, height - 1): + for x in range(1, width - 1): + if rng.random() < density: + grid[y][x] = '#' + + grid[1][1] = 'S' + if has_exit: + grid[height - 2][width - 2] = 'E' + + return '\n'.join(''.join(row) for row in grid) + + +def generate_maze_files(): + mazes_data = { + 'maze_small.txt': _make_grid(10, 10, density=0.15), + 'maze_medium.txt': _make_grid(50, 50, density=0.28), + 'maze_large.txt': _make_grid(100, 100, density=0.30), + 'maze_open.txt': _make_grid(50, 50, density=0.0), + 'maze_no_exit.txt': _make_grid(20, 20, density=0.20, has_exit=False), + } + no_exit = list(mazes_data['maze_no_exit.txt'].splitlines()) + no_exit[18] = no_exit[18][:18] + 'E' + no_exit[18][19:] + no_exit[17] = no_exit[17][:18] + '#' + no_exit[17][19:] + no_exit[18] = no_exit[18][:17] + '#' + no_exit[18][18:] + mazes_data['maze_no_exit.txt'] = '\n'.join(no_exit) + + maze_dir = os.path.dirname(os.path.abspath(__file__)) + for fname, content in mazes_data.items(): + path = os.path.join(maze_dir, fname) + with open(path, 'w', encoding='utf-8') as f: + f.write(content) + + print("Файлы лабиринтов созданы") +def avg(lst): + return sum(lst) / len(lst) if lst else 0 + + +def run_benchmark(): + builder = TextFileMazeBuilder() + maze_dir = os.path.dirname(os.path.abspath(__file__)) + + all_results = [ + ['лабиринт', 'стратегия', 'время_мс', 'посещено_клеток', 'длина_пути'] + + [f'замер_{i+1}' for i in range(REPEATS)] + ] + + print(f"\nЗапуск бенчмарков (повторений: {REPEATS})\n") + print(f" {'Лабиринт':<18} {'Алгоритм':<6} {'Время мс':>10} " + f"{'Посещено':>10} {'Путь':>6}") + print(' ' + '-' * 56) + + for maze_label, maze_file in MAZES: + maze_path = os.path.join(maze_dir, maze_file) + try: + maze = builder.build_from_file(maze_path) + except Exception as e: + print(f" {maze_file}: {e}") + continue + + solver = MazeSolver(maze) + + for strat_name, StratClass in STRATEGIES.items(): + times_ms, visited_list, path_len = [], [], 0 + + for _ in range(REPEATS): + strat = StratClass() + solver.set_strategy(strat) + stats = solver.solve() + times_ms.append(stats.time_ms) + visited_list.append(stats.visited_cells) + path_len = stats.path_length + + mean_t = avg(times_ms) + mean_v = avg(visited_list) + + print(f" {maze_label:<18} {strat_name:<6} " + f"{mean_t:>10.3f} {mean_v:>10.0f} {path_len:>6}") + + all_results.append([ + maze_label, strat_name, + f"{mean_t:.4f}", f"{mean_v:.0f}", str(path_len) + ] + [f"{t:.4f}" for t in times_ms]) + + with open(CSV_PATH, 'w', newline='', encoding='utf-8') as f: + csv.writer(f).writerows(all_results) + + print(f"\n Результаты сохранены: {CSV_PATH}") + +def smoke_test(): + print(" Smoke Test\n") + + maze_dir = os.path.dirname(os.path.abspath(__file__)) + test_path = os.path.join(maze_dir, '_test_maze.txt') + + with open(test_path, 'w', encoding='utf-8') as f: + f.write("#######\n#S #\n# #\n# E#\n#######") + + builder = TextFileMazeBuilder() + maze = builder.build_from_file(test_path) + + for name, StratClass in STRATEGIES.items(): + strat = StratClass() + path = strat.find_path(maze, maze.start, maze.exit) + assert len(path) > 0, f"{name}: путь не найден!" + assert path[0].is_start + assert path[-1].is_exit + print(f" {name}: путь длиной {len(path)} — OK") + + os.remove(test_path) + print("\nВсе тесты пройдены!\n") + +if __name__ == '__main__': + smoke_test() + generate_maze_files() + run_benchmark() diff --git a/zverevem/lab2/docs/data/chart_время-мс.png b/zverevem/lab2/docs/data/chart_время-мс.png new file mode 100644 index 0000000..02d2a5d Binary files /dev/null and b/zverevem/lab2/docs/data/chart_время-мс.png differ diff --git a/zverevem/lab2/docs/data/chart_длина-пути.png b/zverevem/lab2/docs/data/chart_длина-пути.png new file mode 100644 index 0000000..2a39ce4 Binary files /dev/null and b/zverevem/lab2/docs/data/chart_длина-пути.png differ diff --git a/zverevem/lab2/docs/data/chart_посещено-клеток.png b/zverevem/lab2/docs/data/chart_посещено-клеток.png new file mode 100644 index 0000000..9697d22 Binary files /dev/null and b/zverevem/lab2/docs/data/chart_посещено-клеток.png differ diff --git a/zverevem/lab2/docs/data/maze_large.txt b/zverevem/lab2/docs/data/maze_large.txt new file mode 100644 index 0000000..df1bb6a --- /dev/null +++ b/zverevem/lab2/docs/data/maze_large.txt @@ -0,0 +1,100 @@ +#################################################################################################### +#S### # ## ## # # # ## ####### ## # # ## ## ## # # ### # # +# # # # # # # # # # # # # # # # # ## ### # ## ## ## # ## ## # +# # # # # # ## # # ## ## # # # # # ### # # # ### # # # # ## ## +# ## ## ### # # # # # ### # # # ## # # # ## # +# ## # ## #### # # # # # # ## ## #### ## # # # # +### # # # # # # # # ### #### # # # ## # # # # # # # # # +# # ## ## # ## ##### ## ###### # # ## # ## # # ## #### # +# ## ## ## ## ## ## # # # # # # ## # # # +## # # ## # # # # # # ## # # # # ## # # ### +## # # # # # # # # ## ## # # # # ### ## # # +## # # # # # ## # ## # ## # # #### ## # ## # # # ## ## # # +# # # # # # ## # # ## # ## # # # # ### # # # # # # ### # # +## # ## ## # # # # ### # ## ## # # ### ## # # +## ## # # ## ### # # # # # # # # ## # # # # # # +# ## # # ## # ### ## # # # ## # # # ## # # # #### # # # # +# # # # # # # ## ## ## # # # # ### # # # +# # # #### # # # # ## # ### # # #### # # # # # # +# # # # # # ## # # # # # # # ## # ### # ## +## # ### ## ## # # # # # # # # # # # # # # ### ## # # +## ## ### # ## # # ### ## # # # # ## # # # # # # # # +##### # # # #### # ## # # # # # # ### # ## # # # # # +## # # ### # # # # ## # # # # # # #### # # # ### # +# # ## ## # ### # # ## # ## ## ### # # # # # # # ### +## ## # # # # # # # # # # ## ## # # ## +# # # ### # # # # # ## # # # ### # # # # # ## ## ## # ## # +# # # # # ##### # ## # # # # # # # # # ## ## # # # ## +# # # # # # # ## # ## # # # # # # ## ### ## # # ##### # +# # # # # ## # # ## # # ## # ## # # # # ## # # # ## # +## ## # # # # # # ### # ## ### ## # ### # ## # # # ## # # ## # # +# # # # #### # ## #### # # # # # # # # # # ### # ## # # +# # ## # # # # # # # # # # ###### # ## # ## # # # #### #### # # +# # ##### # # # ### # # # # # # # # # ## ### # # +# # # # # # # ## # # ## # # ## # # # # # # # ## # # ### +## # ## # # # # #### # # ## # ## ## # ## # # ## # # +## # # # ## # # # # # # # # # # # # ###### # ## # # ## ### # #### # # +## # # # # # # # # # # # ## # # # # # # ## # # # ## # ## +## # # # ### # # # # # # # # # # # # # # ### +# # ### # # # # # ## ## ## # # ## # ### ### # # # +# # # # # ## # # ## ## # # # # # # ## ## ## # +# ### # # ### # # # # ### # # # # # # # ## # ## +# # ### ## ## ## ## # # ### # ## # # # # ## ## # # # # # # +# ## # # # ## # # # # ## # ### #### # ## ###### ### # +# # # # ### ### # # ## # # # ### ## # ## # # ## ## +# # # ### #### # # # # ### # # # ## ### ## # ## #### # # +# ### ## # # # # # # # # ### # # # # ## # ### ### ## # +# # # # # # # # # # ## ### ## ### # ## # # # ## # #### # ## # # +# # # # # # # # # # # ### # # # # # ## # # # # # # # +# ## # # # # ## # # # # ## ## ## # # ## # ## # # ## # ## # +# # # ## # # # # ### # # # # # # # ## # # # ## # ### ## # # # +## # ## # ## ### ## # # # # ## # # # # # # # +## ## # # ### # # # # # ## # # # # # ## # ## # # # # +# # # ## # ### # ## # # ## # # # # # # # # +# # # # # ## #### # # ### # ## # # ## # # ## # +# # # # ## # ### # ## ## # # # # ### # # # +# # # # # # # # # ## # ## ## ### ### # # ## # # # ## # +# # # # ## # # ### ##### # # # # ## # # # # # ## # # # +## # # # ## # # ## # ## ## # ## # ### # # # # # +# ## ## # ### # ## ### # # ## # # # # # # # # # # # ### +# ## # # # # # # # # # # # ## # # # # # # # # # # # ## # +# # # # ## # # # # # ## # # ## # # ## # # # ### ### # # # ## +# # # # ## # ## # # # # # # # ## # # ## # ### ## +### # # ## ### # ## # # #### # # # # ##### # ## #### # +# # # # # # # #### ## # ### ### # ## # ## # # ## # # # # # # ### +# #### # ## # # # # # # ## # # # # # # # # +# ## # # # # # # ## # ## ## # ### #### # # # # ## # +# # ## # ## # # # # ## ## # ## # ## # +# # # # # # # ## # # # # # # ### ## ### # ## # # ### +### # # # ##### # ## ## # # # ## # ## ## # # # # # # +# # # # # # ## ##### # ### # ## # # # ## # ### #### # # +# # ### # ## # # ### ## ## # ## # ### # ## ### # ### +# ## ## ## # # # # # # ### # ## # # ## # # # # +## ## ## # ## # ## # # # ## # ## # ## # ## # # # # +# # # # # # # # ## # # # ####### # ## ## ## ## +# # # # # # # # # ## # # # # # ## # # ### # ## +# # ## #### # # # # # ## ### # ### # ### # ### ## # # # +## # # ## # # # # # # # # ## # ##### # ## ##### #### ### +# # # # ## # ## # # ## # # ### ## ## # ###### +# # ## # # # # # # # # # ## ## # ## ## ## # ## # # +### #### # # ## # # # # # ## # # ## # # # #### # # ## # # +# ## ## # # ## # ## ## # # ## # # # # # #### # # +# ## # # # ## ### ## #### # # # # # # ## ### # # # ## +## # # # # # # # ## # ## ### # ## # ## # # # # +# # # # # # # # # ### # # # ## # # ## ## # #### # +# # ## # # # # # # # # # # ## ### # # # ## +## ## # ## # # # ## # # # # # #### # # ## ### # +## # ## ## # # # # ### # # ## # # # ## ## # # # # ## # +# ## # ## # # #### # # # # # # ## # # # # # # ### # +# ## # #### # # ## # # # # ### ## # ## ### # ## ## ## +# # # # # # ## # # # ## # #### # ##### # # # # # # # # +# # ## ## ### # ### ### # # #### # # # # ## # ## # # # # #### # # +# # # # ## # # ## # # ## # # ## # ## # # # ## ## # +# # ## # # # ## ## # ### ## # ## # # # # # # # ## # # # +# # ## # ## ## ## # # ## # # # # # ## # # # # ### # # +# # # ## # # # # # # # # # # # # ## # # # ## # # # +## # ## # # # # ## # # ## # # # # # # ## # # # # # # # # +# # ## # ## # ### # # ### # ## # # # ## # ### # ## # # +# # # ## # # ## # # # ## # # #### ## # # # ### # ## +# #### ## ### ### # # ### # # ## # # # ### # ####### # ## # #E# +#################################################################################################### \ No newline at end of file diff --git a/zverevem/lab2/docs/data/maze_medium.txt b/zverevem/lab2/docs/data/maze_medium.txt new file mode 100644 index 0000000..8bed293 --- /dev/null +++ b/zverevem/lab2/docs/data/maze_medium.txt @@ -0,0 +1,50 @@ +################################################## +#S### # ## ## # # # ## ## ##### +# ## # # ## ## ## # # ### # # +# # # # # # # # # # # # # # # +# # # ## ### # ## ## ## # ## ## +## # # # # # # # # ## ## # # +# # # # ### # # ### # # # ## +# ## # ## ## ### # # # # # +## ### # # # ## # # # # +# # # # ## #### # # # # # # +## ## ## ## # ## # # # +# # ## # # # # # # # # ### ## +#### # # # ## # # # # # # +# # # # # ## ## # ## ###### +# ## ##### # # ## # ## # # # +# ## #### # ## # ## ## +## ## # # # # # +## ## # # # # # # # ## +# # # # ## # # +## # ## # # ### # # # # # # # +# # ## # # # # # +# ### ## # # # # # # # ### +# # ## # ## # # #### ## # ## # # ## +# ## ## # # # # # # ## # # +# # ## # ## # # # # ### # +# # # # # # ### # # # ## ## ## +# # # # ### # ## ## # # # +# ### ## # ## # # +# ## ### # # # # # # # +# ## # # # # ## # # # # ## +### # # # # ## # # # ## # # +## # ### # # # # # # +# # # # # ## ## ## # # +# # # ### # # # # ### +### # # # # ## ## # # +# # ### # # # # # # ## +# # # # ## # # # +# # # # ## # ### # ## # # # +### # # # # # # # # # # +# # # # # ### ## # # ## ### # +# # ## # ### ## # # # # ## # # +# # # # # # # ### # +## # # #### # ## # # # # # +# # ### # ## # # # # # # +# # # ### # # # # ## # # +## # # # # #### # # # ### # +## ## ## # ### # # ## # +# # ## ## ### # # # # # # ### # +# ## # # # # # E# +################################################## \ No newline at end of file diff --git a/zverevem/lab2/docs/data/maze_no_exit.txt b/zverevem/lab2/docs/data/maze_no_exit.txt new file mode 100644 index 0000000..c93724b --- /dev/null +++ b/zverevem/lab2/docs/data/maze_no_exit.txt @@ -0,0 +1,20 @@ +#################### +#S# # # ## # +# # # ## # +# # # # # +# # # # # +# ### +## # # # # +# # # # # +## # # # # # +# # # # +# # # ## # +# # # ## +# # # # +# # # # ## # +# # # +## # # ## +# ### ## +# # ## ### +# # # #E# +#################### \ No newline at end of file diff --git a/zverevem/lab2/docs/data/maze_open.txt b/zverevem/lab2/docs/data/maze_open.txt new file mode 100644 index 0000000..335d47e --- /dev/null +++ b/zverevem/lab2/docs/data/maze_open.txt @@ -0,0 +1,50 @@ +################################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +################################################## \ No newline at end of file diff --git a/zverevem/lab2/docs/data/maze_small.txt b/zverevem/lab2/docs/data/maze_small.txt new file mode 100644 index 0000000..952175d --- /dev/null +++ b/zverevem/lab2/docs/data/maze_small.txt @@ -0,0 +1,10 @@ +########## +#S# ## +# # # # +# # # +# ## # +# # +# # # # # +# # +# E# +########## \ No newline at end of file diff --git a/zverevem/lab2/docs/data/results.csv b/zverevem/lab2/docs/data/results.csv new file mode 100644 index 0000000..ba4e28d --- /dev/null +++ b/zverevem/lab2/docs/data/results.csv @@ -0,0 +1,16 @@ +лабиринт,стратегия,время_мс,посещено_клеток,длина_пути,замер_1,замер_2,замер_3,замер_4,замер_5,замер_6,замер_7 +small_10x10,BFS,0.2443,54,15,0.2512,0.2377,0.2104,0.2098,0.3295,0.2628,0.2088 +small_10x10,DFS,0.1729,33,33,0.1438,0.3230,0.1467,0.1388,0.1570,0.1518,0.1490 +small_10x10,A*,0.2503,36,15,0.2849,0.4248,0.2063,0.2027,0.2010,0.2022,0.2305 +medium_50x50,BFS,7.8016,1639,95,9.7252,7.6549,7.1375,6.5061,7.8024,7.9052,7.8801 +medium_50x50,DFS,5.9674,1063,185,5.5163,4.6949,7.7879,7.1488,5.6215,5.1059,5.8962 +medium_50x50,A*,4.0049,588,95,4.1553,4.5408,4.1015,3.6816,3.9481,3.7989,3.8081 +large_100x100,BFS,30.9012,6564,0,33.9413,31.6451,30.0917,31.2012,30.8385,29.2073,29.3836 +large_100x100,DFS,29.7523,6564,0,31.0923,29.3284,29.7215,29.0498,29.9187,29.9990,29.1565 +large_100x100,A*,51.4814,6564,0,49.1005,51.0370,50.5359,54.6521,53.4745,49.4512,52.1183 +open_50x50,BFS,12.2218,2304,95,9.8705,12.1266,9.9576,10.8552,12.0067,18.9221,11.8140 +open_50x50,DFS,6.8283,1223,1129,7.7999,7.7336,7.2627,5.7576,6.6328,7.1781,5.4337 +open_50x50,A*,16.5740,2304,95,16.4528,16.5879,17.9772,16.4465,16.1444,16.7420,15.6672 +no_exit_20x20,BFS,1.0499,260,0,1.1731,1.0768,1.0067,1.0100,1.0201,1.0508,1.0120 +no_exit_20x20,DFS,1.0160,260,0,1.0409,1.0098,1.0178,1.0072,1.0130,1.0069,1.0166 +no_exit_20x20,A*,1.5796,260,0,1.5919,1.5782,1.5758,1.6349,1.5690,1.5579,1.5496