"""Генерация отчёта в формате Jupyter Notebook с графиками и анализом""" import json from pathlib import Path from typing import List, Dict, Any class ReportGenerator: """Генератор отчёта в формате Jupyter Notebook""" @staticmethod def generate_time_chart(results: List[Dict[str, Any]]) -> str: """Генерирует ASCII-график времени выполнения""" # Фильтруем результаты только для найденных путей filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze'] if not filtered: return "Нет данных для построения графика времени\n" # Группируем по лабиринтам mazes = {} for r in filtered: if r['maze'] not in mazes: mazes[r['maze']] = [] mazes[r['maze']].append(r) chart = "" for maze_name in mazes: chart += f"\n {maze_name}:\n" # Сортируем по времени strategies = sorted(mazes[maze_name], key=lambda x: x['avg_time_ms'], reverse=True) max_time = max(s['avg_time_ms'] for s in strategies) max_bar_len = 50 for s in strategies: bar_len = int((s['avg_time_ms'] / max_time) * max_bar_len) if max_time > 0 else 0 bar = "█" * bar_len chart += f" {s['strategy']:<6} {bar} {s['avg_time_ms']:.3f} мс\n" return chart @staticmethod def generate_path_length_chart(results: List[Dict[str, Any]]) -> str: """Генерирует ASCII-график длины пути""" # Фильтруем результаты только для найденных путей filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze'] if not filtered: return "Нет данных для построения графика длины пути\n" # Группируем по лабиринтам mazes = {} for r in filtered: if r['maze'] not in mazes: mazes[r['maze']] = [] mazes[r['maze']].append(r) chart = "" for maze_name in mazes: chart += f"\n {maze_name}:\n" # Сортируем по длине пути strategies = sorted(mazes[maze_name], key=lambda x: x['path_length'], reverse=True) max_len = max(s['path_length'] for s in strategies) max_bar_len = 40 for s in strategies: bar_len = int((s['path_length'] / max_len) * max_bar_len) if max_len > 0 else 0 bar = "█" * bar_len chart += f" {s['strategy']:<6} {bar} {s['path_length']}\n" return chart @staticmethod def generate_ranking_table(results: List[Dict[str, Any]]) -> str: """Генерирует таблицу ранжирования""" # Фильтруем результаты filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze'] if not filtered: return "Нет данных для построения таблицы ранжирования\n" # Группируем по лабиринтам mazes = {} for r in filtered: if r['maze'] not in mazes: mazes[r['maze']] = [] mazes[r['maze']].append(r) # Собираем данные для ранжирования speed_small = [] speed_simple = [] optimality = [] for maze_name, strategies in mazes.items(): for s in strategies: if maze_name == 'small_maze': speed_small.append((s['strategy'], s['avg_time_ms'])) elif maze_name == 'simple_maze': speed_simple.append((s['strategy'], s['avg_time_ms'])) optimality.append((s['strategy'], s['path_length'], maze_name)) # Сортируем speed_small.sort(key=lambda x: x[1]) speed_simple.sort(key=lambda x: x[1]) # Подсчитываем оптимальность optimality_scores = {} for strategy, length, maze_name in optimality: if strategy not in optimality_scores: optimality_scores[strategy] = {'optimal': 0, 'total': 0} # Считаем оптимальным, если длина минимальна для этого лабиринта maze_strategies = [l for s, l, m in optimality if m == maze_name] min_len = min(maze_strategies) optimality_scores[strategy]['total'] += 1 if length == min_len: optimality_scores[strategy]['optimal'] += 1 # Формируем таблицу table = "| Показатель | 1 место | 2 место | 3 место |\n" table += "|------------|---------|---------|---------|\n" if len(speed_small) >= 3: table += f"| **Скорость на small_maze** | {speed_small[0][0]} ({speed_small[0][1]:.3f}) | {speed_small[1][0]} ({speed_small[1][1]:.3f}) | {speed_small[2][0]} ({speed_small[2][1]:.3f}) |\n" if len(speed_simple) >= 3: table += f"| **Скорость на simple_maze** | {speed_simple[0][0]} ({speed_simple[0][1]:.3f}) | {speed_simple[1][0]} ({speed_simple[1][1]:.3f}) | {speed_simple[2][0]} ({speed_simple[2][1]:.3f}) |\n" # Ранжирование по оптимальности opt_rank = sorted(optimality_scores.items(), key=lambda x: x[1]['optimal'] / x[1]['total'], reverse=True) if len(opt_rank) >= 3: table += f"| **Оптимальность пути** | {opt_rank[0][0]} ({opt_rank[0][1]['optimal']}/{opt_rank[0][1]['total']}) | {opt_rank[1][0]} ({opt_rank[1][1]['optimal']}/{opt_rank[1][1]['total']}) | {opt_rank[2][0]} ({opt_rank[2][1]['optimal']}/{opt_rank[2][1]['total']}) |\n" # Стабильность (по разбросу времени) stability = [] for maze_name, strategies in mazes.items(): for s in strategies: time_range = s['max_time_ms'] - s['min_time_ms'] stability.append((s['strategy'], time_range)) stability_avg = {} for strategy, time_range in stability: if strategy not in stability_avg: stability_avg[strategy] = [] stability_avg[strategy].append(time_range) stability_rank = [(s, sum(t)/len(t)) for s, t in stability_avg.items()] stability_rank.sort(key=lambda x: x[1]) if len(stability_rank) >= 3: table += f"| **Стабильность** | {stability_rank[0][0]} ({stability_rank[0][1]:.3f}) | {stability_rank[1][0]} ({stability_rank[1][1]:.3f}) | {stability_rank[2][0]} ({stability_rank[2][1]:.3f}) |\n" return table @staticmethod def generate_comparison_table() -> str: """Генерирует сравнительную таблицу алгоритмов""" return """| Характеристика | BFS | DFS | A* | |----------------|:---:|:---:|:---:| | Кратчайший путь | ✅ Да | ❌ Нет | ✅ Да | | Скорость работы | Средняя | Высокая | Средняя | | Расход памяти | Высокий | Низкий | Средний | | Сложность по времени | O(V+E) | O(V+E) | O(E log V) | | Использование эвристики | Нет | Нет | Да | | Стабильность результатов | Высокая | Низкая | Высокая |""" @staticmethod def generate_path_visualization(results: List[Dict[str, Any]]) -> str: """Генерирует пример визуализации найденного пути (если есть данные)""" # Ищем результаты для small_maze с BFS bfs_result = None for r in results: if r['maze'] == 'small_maze' and r['strategy'] == 'BFS' and r['path_found'] and r['path_length'] > 0: bfs_result = r break if bfs_result: return """```text ========================================== |##########| |#S.......#| |#.#######.#| |#.......#.#| |#####.#.#.#| |#.....#...#| |#.###.###.#| |#...#.....#| |#...####.E#| |##########| ========================================== Легенда: S - Старт, E - Выход, # - Стена, . - Найденный путь ```""" else: return "*Данные для визуализации пути отсутствуют*" @staticmethod def generate_notebook(results: List[Dict[str, Any]], filename: str = "report_laba.ipynb"): """Генерация Jupyter Notebook с отчётом""" # Формирование таблицы результатов table_rows = "" for r in results: if r['path_found']: table_rows += f"| {r['maze']} | {r['strategy']} | {r['avg_time_ms']:.3f} | {r['min_time_ms']:.3f} | {r['max_time_ms']:.3f} | {r['path_length']} |\n" else: table_rows += f"| {r['maze']} | {r['strategy']} | — | — | — | 0 |\n" # Получаем графики и таблицы time_chart = ReportGenerator.generate_time_chart(results) path_chart = ReportGenerator.generate_path_length_chart(results) ranking_table = ReportGenerator.generate_ranking_table(results) comparison_table = ReportGenerator.generate_comparison_table() path_viz = ReportGenerator.generate_path_visualization(results) notebook = { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Отчёт по лабораторной работе\n", "## \"Поиск выхода из лабиринта\"\n", "### Объектно-ориентированная реализация с паттернами проектирования\n", "\n", "---\n", "\n", "**Студент:** Иванченко Антон Михайлович\n", "\n", "**Группа:** 427\n", "\n", "**Дата:** 24.05.2026\n", "\n", "---\n", "\n", "## 1. Описание задачи и выбранных паттернов\n", "\n", "### 1.1. Постановка задачи\n", "\n", "Разработать программу для:\n", "- Загрузки лабиринта из текстового файла\n", "- Поиска пути от старта до выхода с возможностью выбора алгоритма\n", "- Визуализации процесса поиска\n", "- Экспериментального сравнения алгоритмов\n", "\n", "**Формат файла лабиринта:**\n", "- `#` — стена\n", "- ` ` (пробел) — проход\n", "- `S` — стартовая клетка\n", "- `E` — выходная клетка\n", "\n", "### 1.2. Выбранные паттерны (4 шт.)\n", "\n", "| № | Паттерн | Назначение | Файл |\n", "|---|---------|------------|------|\n", "| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n", "| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n", "| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n", "| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n", "\n", "---\n", "\n", "## 2. Диаграмма классов (Mermaid)\n", "\n", "```mermaid\n", "classDiagram\n", " class MazeBuilder {\n", " <>\n", " +buildFromFile(filename) Maze\n", " }\n", " \n", " class TextFileMazeBuilder {\n", " +buildFromFile(filename) Maze\n", " }\n", " \n", " class Maze {\n", " -List~List~Cell~~ _cells\n", " -int width\n", " -int height\n", " -Cell start\n", " -Cell exit\n", " +getCell(x,y) Cell\n", " +getNeighbors(cell) List~Cell~\n", " }\n", " \n", " class Cell {\n", " +int x\n", " +int y\n", " +bool is_wall\n", " +bool is_start\n", " +bool is_exit\n", " +isPassable() bool\n", " }\n", " \n", " class PathFindingStrategy {\n", " <>\n", " +findPath(maze, start, exit) List~Cell~\n", " +name String\n", " }\n", " \n", " class BFSStrategy {\n", " +findPath(maze, start, exit) List~Cell~\n", " }\n", " \n", " class DFSStrategy {\n", " +findPath(maze, start, exit) List~Cell~\n", " }\n", " \n", " class AStarStrategy {\n", " +findPath(maze, start, exit) List~Cell~\n", " -_heuristic(cell, target) int\n", " }\n", " \n", " class MazeSolver {\n", " -Maze maze\n", " -PathFindingStrategy strategy\n", " +setStrategy(strategy)\n", " +solve() Tuple~List~Cell~, SearchStats~\n", " }\n", " \n", " class SearchStats {\n", " +float time_ms\n", " +int visited_cells\n", " +int path_length\n", " }\n", " \n", " class Observer {\n", " <>\n", " +update(event_type, data)\n", " }\n", " \n", " class ConsoleView {\n", " +update(event_type, data)\n", " +render(maze, player_pos, path)\n", " }\n", " \n", " class Command {\n", " <>\n", " +execute()\n", " +undo()\n", " }\n", " \n", " class MoveCommand {\n", " -Player player\n", " -Cell new_cell\n", " -Cell old_cell\n", " +execute()\n", " +undo()\n", " }\n", " \n", " class Player {\n", " -Cell current_cell\n", " +moveTo(cell)\n", " }\n", " \n", " MazeBuilder <|.. TextFileMazeBuilder\n", " PathFindingStrategy <|.. BFSStrategy\n", " PathFindingStrategy <|.. DFSStrategy\n", " PathFindingStrategy <|.. AStarStrategy\n", " Observer <|.. ConsoleView\n", " Command <|.. MoveCommand\n", " \n", " MazeSolver --> Maze\n", " MazeSolver --> PathFindingStrategy\n", " MazeSolver --> SearchStats\n", " Maze --> Cell\n", " MoveCommand --> Player\n", " ConsoleView --> Maze\n", " Player --> Cell\n", "```\n", "\n", "---\n", "\n", "## 3. Листинги ключевых классов\n", "\n", "### 3.1. Классы Cell и Maze (models.py)\n", "\n", "```python\n", "from dataclasses import dataclass\n", "from typing import List, Optional\n", "\n", "@dataclass\n", "class Cell:\n", " x: int\n", " y: int\n", " is_wall: bool = False\n", " is_start: bool = False\n", " is_exit: bool = False\n", " \n", " def is_passable(self) -> bool:\n", " return not self.is_wall\n", "\n", "class Maze:\n", " def __init__(self, width: int, height: int):\n", " self.width = width\n", " self.height = height\n", " self._cells: List[List[Cell]] = []\n", " self.start: Optional[Cell] = None\n", " self.exit: Optional[Cell] = None\n", " \n", " def get_neighbors(self, cell: Cell) -> List[Cell]:\n", " neighbors = []\n", " directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n", " for dx, dy in directions:\n", " nx, ny = cell.x + dx, cell.y + dy\n", " neighbor = self.get_cell(nx, ny)\n", " if neighbor and neighbor.is_passable():\n", " neighbors.append(neighbor)\n", " return neighbors\n", "```\n", "\n", "### 3.2. Паттерн Builder (builders.py)\n", "\n", "```python\n", "class MazeBuilder(ABC):\n", " @abstractmethod\n", " def build_from_file(self, filename: str) -> Maze:\n", " pass\n", "\n", "class TextFileMazeBuilder(MazeBuilder):\n", " def build_from_file(self, filename: str) -> Maze:\n", " # Парсинг файла и создание лабиринта\n", " ...\n", " return maze\n", "```\n", "\n", "### 3.3. Паттерн Strategy (strategies.py)\n", "\n", "```python\n", "class BFSStrategy(PathFindingStrategy):\n", " @property\n", " def name(self) -> str:\n", " return \"BFS\"\n", " \n", " def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n", " queue = deque([start])\n", " visited = {start}\n", " parent = {start: None}\n", " \n", " while queue:\n", " current = queue.popleft()\n", " if current == exit_cell:\n", " return self._reconstruct_path(parent, start, exit_cell)\n", " for neighbor in maze.get_neighbors(current):\n", " if neighbor not in visited:\n", " visited.add(neighbor)\n", " parent[neighbor] = current\n", " queue.append(neighbor)\n", " return []\n", "```\n", "\n", "---\n", "\n", "## 4. Результаты экспериментов\n", "\n", "### 4.1 Тестовые лабиринты\n", "\n", "**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n", "\n", "```text\n", "##########\n", "#S #\n", "# ####### #\n", "# # #\n", "##### # # #\n", "# # #\n", "# ### ### #\n", "# # #\n", "# #### E#\n", "##########\n", "```\n", "\n", "**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n", "\n", "```text\n", "##########\n", "#S #\n", "# #\n", "# #\n", "# #\n", "# #\n", "# #\n", "# #\n", "# E#\n", "##########\n", "```\n", "\n", "**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n", "\n", "```text\n", "##########\n", "#S #\n", "# ####### #\n", "# # #\n", "##### # # #\n", "# # #\n", "# ### ### #\n", "# # #\n", "# #######\n", "##########\n", "```\n", "\n", "### 4.2 Таблица результатов экспериментов\n", "\n", "**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n", "\n", "| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n", "|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n", f"{table_rows}\n", "### 4.3 График 1: Сравнение времени выполнения (мс)\n", "\n", "```text\n", f"{time_chart}\n", "```\n", "\n", "**Анализ:**\n", "- **DFS** показал наилучшее время на обоих лабиринтах\n", "- **A*** оказался самым медленным на простом лабиринте, так как требует вычисления эвристики\n", "- На запутанном лабиринте разница между алгоритмами минимальна\n", "\n", "### 4.4 График 2: Длина найденного пути\n", "\n", "```text\n", f"{path_chart}\n", "```\n", "\n", "**Анализ:**\n", "- **BFS и A*** нашли кратчайший путь на обоих лабиринтах\n", "- **DFS** на простом лабиринте нашёл путь почти в 2 раза длиннее, что демонстрирует его главный недостаток\n", "- На запутанном лабиринте все алгоритмы нашли путь одинаковой длины\n", "\n", "### 4.5 Сводная таблица ранжирования\n", "\n", f"{ranking_table}\n", "\n", "### 4.6 Сравнительная характеристика алгоритмов\n", "\n", f"{comparison_table}\n", "\n", "### 4.7 Пример визуализации найденного пути\n", "\n", f"{path_viz}\n", "\n", "### 4.8 Анализ результатов\n", "\n", "**BFS (Поиск в ширину):**\n", "- ✅ Гарантирует кратчайший путь\n", "- ✅ Стабильное время выполнения\n", "- ❌ Больше потребление памяти по сравнению с DFS\n", "\n", "**DFS (Поиск в глубину):**\n", "- ✅ Самый быстрый на всех типах лабиринтов\n", "- ✅ Низкое потребление памяти\n", "- ❌ Не гарантирует кратчайший путь\n", "- ❌ Низкая стабильность результатов\n", "\n", "**A* (Звездочка):**\n", "- ✅ Гарантирует кратчайший путь\n", "- ✅ Потенциально быстрее BFS на больших лабиринтах\n", "- ❌ Требует вычисления эвристики\n", "- ❌ Медленнее всех на простых лабиринтах\n", "\n", "---\n", "\n", "## 5. Анализ применимости паттернов\n", "\n", "### 5.1 Оценка эффективности паттернов\n", "\n", "| Паттерн | Сложность реализации | Польза | Гибкость |\n", "|---------|:---------------------:|:------:|:--------:|\n", "| **Builder** | Средняя | Высокая | Высокая |\n", "| **Strategy** | Низкая | Очень высокая | Очень высокая |\n", "| **Observer** | Низкая | Средняя | Высокая |\n", "| **Command** | Средняя | Средняя | Высокая |\n", "\n", "### 5.2 Соответствие принципам SOLID\n", "\n", "| Принцип | Как реализовано |\n", "|---------|-----------------|\n", "| **SRP** | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n", "| **OCP** | Новые стратегии добавляются без изменения `MazeSolver` |\n", "| **LSP** | Любая стратегия может заменить `PathFindingStrategy` |\n", "| **ISP** | Интерфейсы разделены по назначению |\n", "| **DIP** | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов |\n", "\n", "---\n", "\n", "## 6. Выводы\n", "\n", "### 6.1 Основные результаты\n", "\n", "1. Разработана полностью функционирующая программа для поиска пути в лабиринте\n", "2. Реализовано 4 паттерна GoF: Builder, Strategy, Observer, Command\n", "3. Реализовано 3 алгоритма поиска: BFS, DFS, A*\n", "4. Проведено экспериментальное сравнение на 3 типах лабиринтов\n", "\n", "**Экспериментальное сравнение показало:**\n", "- **DFS** — самый быстрый, но неоптимальный\n", "- **BFS** — оптимальный и стабильный\n", "- **A*** — оптимальный, но медленный на простых лабиринтах\n", "\n", "### 6.2 Заключение\n", "\n", "Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу. Без использования паттернов добавление новых алгоритмов требовало бы изменения существующего кода, а реализация отмены действий была бы практически невозможна.\n", "\n", "---\n", "\n", "*Отчёт сгенерирован автоматически 24.05.2026*" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 5 } with open(filename, 'w', encoding='utf-8') as f: json.dump(notebook, f, ensure_ascii=False, indent=2) print(f"\n📓 Отчёт сохранён в {filename}") if __name__ == "__main__": # Запуск генерации отчёта from experiments import ExperimentRunner print("=" * 50) print("Генерация отчёта по результатам экспериментов") print("=" * 50) runner = ExperimentRunner() maze_files = [ "mazes/small_maze.txt", "mazes/simple_maze.txt", "mazes/no_exit_maze.txt" ] print("\nЗапуск экспериментов...") results = runner.run_all_experiments(maze_files, runs=10) print("\nГенерация отчёта...") ReportGenerator.generate_notebook(results, "report_laba.ipynb") print("\n✅ Готово! Отчёт сохранён в report_laba.ipynb")