2026-rff_mp/ivanchenkoam/maze_project/report.py

662 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Генерация отчёта в формате 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",
"**Группа:** [Номер группы]\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",
" <<interface>>\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",
" <<interface>>\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",
" <<interface>>\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",
" <<interface>>\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")