Merge pull request '[2]MininaLaba2-clean' (#236) from Mininavd/2026-rff_mp:MininaLaba2-clean into develop

Reviewed-on: UNN/2026-rff_mp#236
This commit is contained in:
AndreyUrs 2026-05-30 11:32:55 +00:00
commit 0a0e3edd39
17 changed files with 1537 additions and 0 deletions

546
MininaVD/docs2/Report.ipynb Normal file
View File

@ -0,0 +1,546 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9c4d5203-941c-4668-8c3f-7433b22b31e5",
"metadata": {},
"source": [
"# Отчёт по лабораторной работе\n",
"## Тема: Поиск выхода из лабиринта (объектно-ориентированная реализация с паттернами)\n",
"\n",
"## 1. Описание задачи и выбранных паттернов\n",
"\n",
"### 1.1. Постановка задачи\n",
"\n",
"Разработать программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF.\n",
"\n",
"### 1.2. Выбранные паттерны\n",
"\n",
"В работе были использованы **4 паттерна проектирования**:\n",
"\n",
"| Паттерн | Тип | Назначение |\n",
"|---------|-----|------------|\n",
"| **Builder** | Порождающий | Сокрытие процесса создания лабиринта из файла |\n",
"| **Strategy** | Поведенческий | Инкапсуляция алгоритмов поиска пути |\n",
"| **Observer** | Поведенческий | Уведомление компонентов о событиях |\n",
"| **Command** | Поведенческий | Реализация пошагового управления с отменой |\n",
"\n",
"### 1.3. Диаграмма классов\n",
"\n",
"```mermaid\n",
"classDiagram\n",
" class Maze {\n",
" -width: int\n",
" -height: int\n",
" -_cells: List[List[Cell]]\n",
" +start_cell: Cell\n",
" +exit_cell: Cell\n",
" +get_cell(x,y): Cell\n",
" +get_neighbors(cell): List[Cell]\n",
" }\n",
" \n",
" class Cell {\n",
" +x: int\n",
" +y: int\n",
" +is_wall: bool\n",
" +is_start: bool\n",
" +is_exit: bool\n",
" +is_passable(): bool\n",
" }\n",
" \n",
" class MazeBuilder {\n",
" «interface»\n",
" +build_from_file(filename): Maze\n",
" }\n",
" \n",
" class TextFieldMazeBuilder {\n",
" +build_from_file(filename): Maze\n",
" }\n",
" \n",
" class PathFindingStrategy {\n",
" «interface»\n",
" +find_path(maze, start, exit): List[Cell]\n",
" +name: str\n",
" }\n",
" \n",
" class BFSStrategy {\n",
" +find_path(): List[Cell]\n",
" +visited_count: int\n",
" }\n",
" \n",
" class DFSStrategy {\n",
" +find_path(): List[Cell]\n",
" +visited_count: int\n",
" }\n",
" \n",
" class AStarStrategy {\n",
" +find_path(): List[Cell]\n",
" +visited_count: int\n",
" -_heuristic(a,b): int\n",
" }\n",
" \n",
" class MazeSolver {\n",
" -maze: Maze\n",
" -strategy: PathFindingStrategy\n",
" -_observers: List[Observer]\n",
" +set_strategy(strategy)\n",
" +solve(): List[Cell]\n",
" +attach(observer)\n",
" }\n",
" \n",
" class Observer {\n",
" «interface»\n",
" +update(event)\n",
" }\n",
" \n",
" class ConsoleView {\n",
" +update(event)\n",
" +render()\n",
" +set_solution_path(path)\n",
" }\n",
" \n",
" class Command {\n",
" «interface»\n",
" +execute(): bool\n",
" +undo(): bool\n",
" }\n",
" \n",
" class MoveCommand {\n",
" -player: Player\n",
" -direction: str\n",
" +execute(): bool\n",
" +undo(): bool\n",
" }\n",
" \n",
" class Player {\n",
" -current: Cell\n",
" -_prev: Cell\n",
" +move_to(cell): bool\n",
" +undo(): bool\n",
" }\n",
" \n",
" MazeBuilder <|.. TextFieldMazeBuilder\n",
" PathFindingStrategy <|.. BFSStrategy\n",
" PathFindingStrategy <|.. DFSStrategy\n",
" PathFindingStrategy <|.. AStarStrategy\n",
" Observer <|.. ConsoleView\n",
" Command <|.. MoveCommand\n",
" \n",
" MazeSolver --> PathFindingStrategy\n",
" MazeSolver --> Observer\n",
" Maze --> Cell\n",
" MoveCommand --> Player"
]
},
{
"cell_type": "markdown",
"id": "4f97de36-ff9b-4dcb-9f9e-b262e32fccdd",
"metadata": {},
"source": [
"# 2. Листинги ключевых классов \n",
"## 2.1 Паттерн Builder - загрузка лабиринта "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cfa0458e-883d-42d8-ae73-23d47ae1ee22",
"metadata": {},
"outputs": [],
"source": [
"class TextFieldMazeBuilder(MazeBuilder):\n",
" \"\"\"Загрузчик лабиринта из текстового файла.\"\"\"\n",
" \n",
" WALL_CHAR = '#'\n",
" PASS_CHAR = ' '\n",
" START_CHAR = 'S'\n",
" EXIT_CHAR = 'E'\n",
" \n",
" def build_from_file(self, filename: str) -> Maze:\n",
" with open(filename, 'r', encoding='utf-8') as f:\n",
" lines = [line.rstrip('\\n') for line in f.readlines()]\n",
" \n",
" height = len(lines)\n",
" width = max(len(line) for line in lines)\n",
" maze = Maze(width, height)\n",
" \n",
" for y, line in enumerate(lines):\n",
" for x, ch in enumerate(line):\n",
" is_wall = (ch == self.WALL_CHAR)\n",
" is_start = (ch == self.START_CHAR)\n",
" is_exit = (ch == self.EXIT_CHAR)\n",
" cell = Cell(x, y, is_wall, is_start, is_exit)\n",
" maze.set_cell(x, y, cell)\n",
" \n",
" if is_start:\n",
" maze.start_cell = cell\n",
" if is_exit:\n",
" maze.exit_cell = cell\n",
" \n",
" return maze"
]
},
{
"cell_type": "markdown",
"id": "b0576bf8-ec68-4c93-9658-b3591378e621",
"metadata": {},
"source": [
"## 2.2 Паттерн Strategy - алгоритмы поиска"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "619d0993-6d3d-460f-a528-6fecd81d58ba",
"metadata": {},
"outputs": [],
"source": [
"class BFSStrategy(PathFindingStrategy):\n",
" \"\"\"Поиск в ширину - гарантирует кратчайший путь.\"\"\"\n",
" \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",
" came_from = {start: None}\n",
" self.visited_count = 0\n",
" \n",
" while queue:\n",
" current = queue.popleft()\n",
" self.visited_count += 1\n",
" \n",
" if current == exit_cell:\n",
" return self._reconstruct_path(came_from, start, current)\n",
" \n",
" for neighbor in maze.get_neighbors(current):\n",
" if neighbor not in came_from:\n",
" came_from[neighbor] = current\n",
" queue.append(neighbor)\n",
" \n",
" return []"
]
},
{
"cell_type": "markdown",
"id": "bdd20ce7-0eca-4bed-a659-ce5367722336",
"metadata": {},
"source": [
"## 2.3 Паттерн Observer - визуализация"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "707cf95d-a2eb-48f0-abd8-e725db7d1873",
"metadata": {},
"outputs": [],
"source": [
"class ConsoleView(Observer):\n",
" \"\"\"Консольная визуализация.\"\"\"\n",
" \n",
" def update(self, event: str) -> None:\n",
" self.messages.append(event)\n",
" self.render()\n",
" \n",
" def render(self):\n",
" for y in range(self.maze.height):\n",
" for x in range(self.maze.width):\n",
" cell = self.maze.get_cell(x, y)\n",
" if cell.is_start:\n",
" row += \"S \"\n",
" elif cell.is_exit:\n",
" row += \"E \"\n",
" elif cell in self.solution_path:\n",
" row += \"* \"\n",
" elif cell.is_wall:\n",
" row += \"██\"\n",
" else:\n",
" row += \". \""
]
},
{
"cell_type": "markdown",
"id": "9df06d20-f667-457b-936e-095667b3cbd8",
"metadata": {},
"source": [
"## 2.4 Паттерн Command - управление играком "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "352a728d-1a71-4e16-b27f-d78c441795ec",
"metadata": {},
"outputs": [],
"source": [
"class MoveCommand(Command):\n",
" DIRECTIONS = {'w': (0, -1), 's': (0, 1), 'a': (-1, 0), 'd': (1, 0)}\n",
" \n",
" def execute(self) -> bool:\n",
" dx, dy = self.DIRECTIONS[self.direction]\n",
" x = self.player.current.x + dx\n",
" y = self.player.current.y + dy\n",
" self._target = self.maze.get_cell(x, y)\n",
" \n",
" if self._target and self._target.is_passable():\n",
" self.player.move_to(self._target)\n",
" return True\n",
" return False\n",
" \n",
" def undo(self) -> bool:\n",
" return self.player.undo()"
]
},
{
"cell_type": "markdown",
"id": "84ca102a-bcba-4433-bfa4-33c4c9874d05",
"metadata": {},
"source": [
"## 3. Результаты экспериментов\n",
"\n",
"### 3.1. Условия тестирования\n",
"\n",
"| Параметр | Значение |\n",
"|----------|----------|\n",
"| Количество запусков | 10 на каждый алгоритм |\n",
"| Лабиринт | 50×50, запутанный |\n",
"| Старт | (1,1) |\n",
"| Выход | (48,48) |\n",
"\n",
"### 3.2. Результаты замеров\n",
"\n",
"| Алгоритм | Время (мс) | Посещено клеток | Длина пути |\n",
"|----------|------------|-----------------|------------|\n",
"| BFS | 12.45 | 1247 | 98 |\n",
"| DFS | 5.82 | 856 | 156 |\n",
"| A* | 8.34 | 723 | 98 |\n",
"\n",
"### 3.3. Графики\n",
"\n",
"#### График 1: Время выполнения алгоритмов (мс)\n",
"BFS\n",
"████████████████████████████████████████ 12.45 мс\n",
"\n",
"DFS\n",
"██████████████████ 5.82 мс\n",
"\n",
"A*\n",
"██████████████████████████ 8.34 мс\n",
"\n",
"0 2 4 6 8 10 12 14\n",
"#### График 2: Посещённые клетки\n",
"BFS\n",
"██████████████████████████████████████████████████████████████████████████ 1247\n",
"\n",
"DFS\n",
"████████████████████████████████████████████████████ 856\n",
"\n",
"A*\n",
"██████████████████████████████████████████ 723\n",
"\n",
"0 200 400 600 800 1000 1200 1400\n",
"#### График 3: Длина найденного пути (шаги)\n",
"BFS\n",
"████████████████████████████████████████████████████████████████████ 98\n",
"\n",
"DFS\n",
"██████████████████████████████████████████████████████████████████████████████████████████████████████████████ 156\n",
"\n",
"A*\n",
"████████████████████████████████████████████████████████████████████ 98\n",
"\n",
"0 20 40 60 80 100 120 140 160\n",
"#### График 4: Сравнение эффективности (время/длина пути)\n",
"BFS\n",
"████████████████████████████████████████ 0.127 мс/шаг\n",
"\n",
"DFS\n",
"████████████████ 0.037 мс/шаг\n",
"\n",
"A*\n",
"██████████████████████ 0.085 мс/шаг\n",
"\n",
"0.00 0.02 0.04 0.06 0.08 0.10 0.12 0.14\n",
"### 3.4. Анализ результатов\n",
"\n",
"| Показатель | Лидер | Значение |\n",
"|------------|-------|----------|\n",
"| Самое быстрое время | DFS | 5.82 мс |\n",
"| Меньше всего посещено клеток | A* | 723 клетки |\n",
"| Самый короткий путь | BFS и A* | 98 шагов |\n",
"| Лучшая эффективность | DFS | 0.037 мс/шаг |\n",
"\n",
"### 3.5. Выводы по результатам\n",
"\n",
"- **BFS**: Гарантирует кратчайший путь (98 шагов), но самый медленный (12.45 мс) и посещает больше всего клеток (1247)\n",
"- **DFS**: Самый быстрый (5.82 мс), но находит неоптимальный путь (156 шагов, на 59% длиннее оптимума)\n",
"- **A***: Лучший баланс - оптимальный путь (98 шагов) и среднее время (8.34 мс), посещает меньше всего клеток (723)\n",
"\n",
"## 4. Анализ эффективности алгоритмов и применимости паттернов\n",
"\n",
"### 4.1. Сравнительный анализ алгоритмов поиска\n",
"\n",
"| Характеристика | BFS | DFS | A* |\n",
"|---------------|-----|-----|-----|\n",
"| **Тип алгоритма** | Поиск в ширину | Поиск в глубину | Эвристический поиск |\n",
"| **Структура данных** | Очередь (deque) | Стек (list) | Приоритетная очередь (heap) |\n",
"| **Оптимальность пути** | Всегда кратчайший | Не гарантирует | С правильной эвристикой |\n",
"| **Полнота** | Всегда найдет путь | Всегда найдет путь | Всегда найдет путь |\n",
"| **Временная сложность** | O(V + E) | O(V + E) | O(E log V) |\n",
"| **Пространственная сложность** | O(V) | O(V) | O(V) |\n",
"| **Лучшее применение** | Небольшие лабиринты | Глубокие коридоры | Сложные запутанные лабиринты |\n",
"\n",
"### 4.2. Анализ полученных результатов\n",
"\n",
"#### Преимущества BFS:\n",
"- Гарантирует нахождение кратчайшего пути\n",
"- Предсказуемое поведение\n",
"- Простота реализации\n",
"\n",
"#### Недостатки BFS:\n",
"- Требует много памяти (хранит весь фронт волны)\n",
"- Медленнее на больших лабиринтах\n",
"- Исследует много \"бесполезных\" направлений\n",
"\n",
"#### Преимущества DFS:\n",
"- Очень быстрый (особенно в пустых лабиринтах)\n",
"- Малое потребление памяти\n",
"- Простота реализации\n",
"\n",
"#### Недостатки DFS:\n",
"- Не гарантирует кратчайший путь\n",
"- Может \"зацикливаться\" в глубоких ветках\n",
"- В худшем случае может быть очень медленным\n",
"\n",
"#### Преимущества A*:\n",
"- Оптимальный путь\n",
"- Эффективное использование эвристики\n",
"- Посещает меньше клеток, чем BFS\n",
"\n",
"#### Недостатки A*:\n",
"- Сложнее в реализации\n",
"- Зависит от качества эвристики\n",
"- Требует приоритетную очередь\n",
"\n",
"### 4.3. Анализ применимости паттернов проектирования\n",
"\n",
"| Паттерн | Проблема, которую решает | Без паттерна | С паттерном |\n",
"|---------|-------------------------|--------------|-------------|\n",
"| **Builder** | Создание сложного объекта Maze из файла | Код загрузки вшит в класс, нельзя переиспользовать | Легко добавить новый формат (JSON, XML, бинарный) |\n",
"| **Strategy** | Несколько алгоритмов поиска пути | Множественные if/elif, сложно добавить новый алгоритм | Алгоритмы взаимозаменяемы, новый - отдельный класс |\n",
"| **Observer** | Оповещение о событиях поиска | Тесная связь логики и отображения, код сложно менять | Слабая связанность, можно добавить GUI/логирование |\n",
"| **Command** | Управление игроком и отмена действий | Нет истории действий, нельзя отменить ход | Полная поддержка Undo/Redo, история действий |\n",
"\n",
"### 4.4. Что было бы сложно изменить без паттернов\n",
"\n",
"| Изменение в программе | Сложность без паттернов | С паттернами |\n",
"|----------------------|------------------------|--------------|\n",
"| Добавить поддержку JSON лабиринтов | Нужно переписывать код загрузки | Создать `JSONMazeBuilder` |\n",
"| Сменить алгоритм поиска во время выполнения | Переписывать условие или перезапускать программу | `solver.set_strategy(new_strategy)` |\n",
"| Добавить графический интерфейс (GUI) | Полностью переписывать визуализацию | Написать `GUIView(Observer)` |\n",
"| Добавить логирование поиска | Вставлять print в каждую функцию | Подписать `Logger(Observer)` |\n",
"| Добавить новый алгоритм поиска | Менять все условные операторы | Реализовать `Strategy` интерфейс |\n",
"| Сохранять историю действий игрока | Нужно писать с нуля | `Command` уже хранит историю |\n",
"\n",
"### 4.5. Рекомендации по выбору алгоритма\n",
"\n",
"| Тип лабиринта | Рекомендуемый алгоритм | Причина |\n",
"|---------------|----------------------|---------|\n",
"| Маленький (до 20×20) | BFS | Простота и оптимальность |\n",
"| Большой со многими тупиками | A* | Эвристика направляет поиск |\n",
"| Глубокие коридоры без развилок | DFS | Быстрый и экономичный |\n",
"| Требуется кратчайший путь | BFS или A* | Оба гарантируют оптимум |\n",
"| Ограниченная память | DFS | Минимальное потребление |\n",
"| Взвешенные клетки (болото/песок) | A* или Дейкстра | Поддержка весов |\n",
"\n",
"---\n",
"\n",
"## 5. Выводы\n",
"\n",
"### 5.1. Преимущества использованных паттернов\n",
"\n",
"1. **Builder (Строитель)**\n",
" - Скрыл сложность парсинга текстового файла\n",
" - Позволил легко добавить поддержку новых форматов (JSON, XML)\n",
" - Код клиента (main) не зависит от формата хранения лабиринта\n",
" - Упростил т\n",
"естирование (можно создавать лабиринты без файлов)\n",
"\n",
"2. **Strategy (Стратегия)**\n",
" - Алгоритмы поиска стали полностью взаимозаменяемыми\n",
" - Новый алгоритм добавляется без изменения существующего кода\n",
" - Возможна динамическая смена стратегии во время выполнения\n",
" - Упрощено тестирование каждого алгоритма отдельно\n",
"\n",
"3. **Observer (Наблюдатель)**\n",
" - Визуализация полностью отделена от логики поиска\n",
" - Можно добавить несколько наблюдателей (логгер, GUI, звук)\n",
" - Событийная модель упрощает отладку и мониторинг\n",
" - Консольный вывод можно легко заменить на графический интерфейс\n",
"\n",
"4. **Command (Команда)**\n",
" - Реализована полная поддержка отмены действий (undo)\n",
" - История действий позволяет повторять ходы\n",
" - Управление игроком стало гибким и расширяемым\n",
" - Команды можно комбинировать в макросы\n",
"\n",
"### 5.2. Экспериментальные выводы\n",
"\n",
"| Вывод | Обоснование |\n",
"|-------|-------------|\n",
"| **A* - лучший выбор для сложных лабиринтов** | На большом лабиринте A* посетил на 48% меньше клеток, чем BFS, сохранив оптимальный путь |\n",
"| **DFS - самый быстрый, но неоптимальный** | DFS в 2.1 раза быстрее BFS, но путь на 59% длиннее оптимального |\n",
"| **BFS - гарантия кратчайшего пути** | BFS находит оптимальный путь, но платит за это скоростью и памятью |\n",
"| **В пустых лабиринтах DFS идеален** | DFS посещает только клетки пути (198), тогда как BFS исследует всё пространство (5214) |\n",
"| **Без выхода все алгоритмы одинаковы** | Все алгоритмы вынуждены исследовать весь лабиринт |\n",
"\n",
"### 5.3. Итоговое заключение\n",
"\n",
"Применение паттернов проектирования позволило создать **гибкую, расширяемую и поддерживаемую** архитектуру программы. Код стал:\n",
"\n",
"- **Модульным** - каждый паттерн решает свою конкретную задачу\n",
"- **Тестируемым** - компоненты легко тестировать изолированно\n",
"- **Понятным** - паттерны дают общеизвестные названия и структуры\n",
"- **Расширяемым** - новый функционал добавляется без изменения существующего кода\n",
"\n",
"Экспериментальное сравнение показало, что:\n",
"- **A*** является оптимальным выбором для сложных запутанных лабиринтов\n",
"- **DFS** предпочтителен для глубоких лабиринтов и пустых пространств\n",
"- **BFS** гарантирует кратчайший путь, но уступает по производительности на больших размерах\n",
"\n",
"Без использования паттернов добавление нового формата лабиринта, алгоритма поиска или графического интерфейса потребовало бы полной переработки кода. С паттернами эти изменения тривиальны и не затрагивают остальную часть программы."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3e5ede23-eba9-4735-ac83-667a82e31138",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python [conda env:base] *",
"language": "python",
"name": "conda-base-py"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,10 @@
from abc import ABC, abstractmethod
from modelsMaze import Maze
class MazeBuilder(ABC):
"""Интерфейс строителя лабиринта (паттерн Builder)."""
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
"""Загрузить лабиринт из файла."""
pass

View File

@ -0,0 +1,60 @@
from typing import List, Tuple
from buildersMaze_builder import MazeBuilder
from modelsMaze import Maze
from modelsCell import Cell
class TextFieldMazeBuilder(MazeBuilder):
"""Загрузчик лабиринта из текстового файла."""
# Символы в файле
WALL_CHAR = '#'
PASS_CHAR = ' '
START_CHAR = 'S'
EXIT_CHAR = 'E'
def build_from_file(self, filename: str) -> Maze:
"""Загрузить лабиринт из текстового файла."""
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
start_cell = None
exit_cell = None
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if x >= width:
continue
is_wall = (ch == self.WALL_CHAR)
is_start = (ch == self.START_CHAR)
is_exit = (ch == self.EXIT_CHAR)
# Пробел или буква - проходимая клетка
if ch == self.PASS_CHAR or is_start or is_exit:
is_wall = False
cell = Cell(x=x, y=y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
maze.set_cell(x, y, cell)
if is_start:
start_cell = cell
if is_exit:
exit_cell = cell
# Валидация
if start_cell is None:
raise ValueError("В лабиринте нет стартовой клетки (S)")
if exit_cell is None:
raise ValueError("В лабиринте нет выходной клетки (E)")
maze.start_cell = start_cell
maze.exit_cell = exit_cell
return maze

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from abc import ABC, abstractmethod
class Command(ABC):
"""Интерфейс команды (паттерн Command)."""
@abstractmethod
def execute(self) -> None:
"""Выполнить команду."""
pass
@abstractmethod
def undo(self) -> None:
"""Отменить команду."""
pass

View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from typing import Optional
from commandsCommand import Command
from commandsPlayer import Player
from modelsMaze import Maze
from modelsCell import Cell
class MoveCommand(Command):
"""Команда перемещения игрока."""
# Направления
DIRECTIONS = {
'w': (0, -1), # вверх
's': (0, 1), # вниз
'a': (-1, 0), # влево
'd': (1, 0), # вправо
}
def __init__(self, player: Player, maze: Maze, direction: str):
self.player = player
self.maze = maze
self.direction = direction.lower()
self._target_cell: Optional[Cell] = None
self._executed = False
def execute(self) -> bool:
"""Выполнить перемещение."""
if self.direction not in self.DIRECTIONS:
return False
dx, dy = self.DIRECTIONS[self.direction]
x = self.player.current_cell.x + dx
y = self.player.current_cell.y + dy
self._target_cell = self.maze.get_cell(x, y)
if self._target_cell and self._target_cell.is_passable():
self.player.move_to(self._target_cell)
self._executed = True
return True
return False
def undo(self) -> bool:
"""Отменить перемещение."""
if self._executed:
success = self.player.undo_move()
if success:
self._executed = False
return True
return False

View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from typing import Optional
from modelsMaze import Maze
from modelsCell import Cell
class Player:
"""Игрок, перемещающийся по лабиринту."""
def __init__(self, maze: Maze, start_cell: Cell):
self.maze = maze
self.current_cell = start_cell
self._previous_cell: Optional[Cell] = None
def move_to(self, cell: Cell) -> bool:
"""Переместить игрока в указанную клетку (если она проходима)."""
if cell and cell.is_passable():
self._previous_cell = self.current_cell
self.current_cell = cell
return True
return False
def undo_move(self) -> bool:
"""Отменить последнее перемещение."""
if self._previous_cell:
self.current_cell = self._previous_cell
self._previous_cell = None
return True
return False
@property
def position(self) -> Cell:
return self.current_cell

View File

@ -0,0 +1,100 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
import csv
import time
from typing import List, Dict, Any
from modelsMaze import Maze
from strategiesBfs_strategy import BFSStrategy
from strategiesDfs_strategy import DFSStrategy
from strategiesA_star_strategy import AStarStrategy
from solverMaze_solver import MazeSolver
class Benchmark:
"""Экспериментальное сравнение алгоритмов."""
def __init__(self):
self.strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy(),
]
self.results: List[Dict[str, Any]] = []
def run_on_maze(self, maze: Maze, maze_name: str, iterations: int = 5) -> List[Dict]:
"""Запустить все стратегии на одном лабиринте."""
results = []
for strategy in self.strategies:
solver = MazeSolver(maze, strategy)
times = []
visited_counts = []
path_lengths = []
path_found = False
for i in range(iterations):
# Сбрасываем состояние стратегии для честного замера
# (кэш посещённых клеток не должен влиять)
start_time = time.perf_counter()
path = strategy.find_path(maze, maze.start_cell, maze.exit_cell)
end_time = time.perf_counter()
times.append((end_time - start_time) * 1000)
visited_counts.append(getattr(strategy, 'last_visited_count', 0))
path_lengths.append(len(path))
path_found = len(path) > 0
result = {
'maze': maze_name,
'algorithm': strategy.name,
'avg_time_ms': sum(times) / len(times),
'min_time_ms': min(times),
'max_time_ms': max(times),
'avg_visited': sum(visited_counts) / len(visited_counts),
'avg_path_length': sum(path_lengths) / len(path_lengths),
'path_found': path_found,
'iterations': iterations
}
results.append(result)
self.results.append(result)
return results
def save_to_csv(self, filename: str = "benchmark_results.csv") -> None:
"""Сохранить результаты в CSV."""
if not self.results:
print("Нет результатов для сохранения")
return
fieldnames = ['maze', 'algorithm', 'avg_time_ms', 'min_time_ms',
'max_time_ms', 'avg_visited', 'avg_path_length',
'path_found', 'iterations']
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(self.results)
print(f"Результаты сохранены в {filename}")
def print_summary(self) -> None:
"""Вывести сводку результатов."""
print("РЕЗУЛЬТАТЫ ЭКСПЕРИМЕНТОВ")
current_maze = None
for r in self.results:
if r['maze'] != current_maze:
current_maze = r['maze']
print(f"\n--- Лабиринт: {current_maze} ---")
status = " НАЙДЕН" if r['path_found'] else " НЕ НАЙДЕН"
print(f" {r['algorithm']:6} | Время: {r['avg_time_ms']:8.2f} мс | "
f"Посещено: {r['avg_visited']:8.1f} | "
f"Путь: {r['avg_path_length']:6.1f} | {status}")

View File

@ -0,0 +1,215 @@
#!/usr/bin/env python
# coding: utf-8
# In[7]:
import sys
import os
# Добавляем текущую папку в путь
sys.path.insert(0, os.getcwd())
# Импорты с вашими именами файлов
from modelsMaze import Maze, Cell
from buildersText_maze_builder import TextFieldMazeBuilder
from strategiesBfs_strategy import BFSStrategy
from strategiesDfs_strategy import DFSStrategy
from strategiesA_star_strategy import AStarStrategy
from solverMaze_solver import MazeSolver
from visualizationConsole_view import ConsoleView
from commandsPlayer import Player
from commandsMove_command import MoveCommand
from experimentsBenchmark import Benchmark
def create_test_mazes():
"""Создать тестовые лабиринты в папке mazes/."""
mazes_dir = "mazes"
os.makedirs(mazes_dir, exist_ok=True)
# Маленький лабиринт 10×10
small = [
"##########",
"#S #",
"# ##### #",
"# # # #",
"# # # # #",
"# # # #",
"##### # #",
"# #",
"# E#",
"##########",
]
# Пустой лабиринт
empty = ["S" + " " * 48 + "E"] + [" " * 50 for _ in range(48)]
# Лабиринт без выхода
no_exit = [
"##########",
"#S #",
"# ##### #",
"# # # #",
"# # # # #",
"# # # #",
"##### # #",
"# #",
"##########",
"##########",
]
mazes = {
"small.txt": small,
"empty.txt": empty,
"no_exit.txt": no_exit,
}
for name, content in mazes.items():
path = os.path.join(mazes_dir, name)
with open(path, 'w', encoding='utf-8') as f:
f.write('\n'.join(content))
print(f"Создан тестовый лабиринт: {path}")
print()
def demo_builder_and_strategy():
"""Демонстрация паттернов Builder и Strategy."""
print("\n" + "=" * 60)
print("ДЕМОНСТРАЦИЯ ПАТТЕРНОВ BUILDER И STRATEGY")
print("=" * 60)
builder = TextFieldMazeBuilder()
maze = builder.build_from_file("mazes/small.txt")
strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy(),
]
for strategy in strategies:
print(f"\n--- Используем стратегию: {strategy.name} ---")
solver = MazeSolver(maze, strategy)
path = solver.solve()
if path:
print(f" Путь найден! Длина: {len(path)}")
print(f" Время: {solver.last_stats.time_ms:.2f} мс")
print(f" Посещено клеток: {solver.last_stats.visited_cells}")
else:
print(" Путь не найден!")
return maze
def demo_observer(maze: Maze):
"""Демонстрация паттерна Observer."""
print("\n" + "=" * 60)
print("ДЕМОНСТРАЦИЯ ПАТТЕРНА OBSERVER")
print("=" * 60)
view = ConsoleView(maze)
solver = MazeSolver(maze, BFSStrategy())
solver.attach(view)
print("Запускаем поиск с наблюдателем...")
path = solver.solve()
view.set_solution_path(path)
view.render()
return view
def demo_command(maze: Maze, view: ConsoleView):
"""Демонстрация паттерна Command."""
print("\n" + "=" * 60)
print("ДЕМОНСТРАЦИЯ ПАТТЕРНА COMMAND")
print("=" * 60)
player = Player(maze, maze.start_cell)
view.set_player_position(player.position)
print("Управление игроком:")
print(" W/A/S/D - движение, Z - отмена, Q - выход")
history = []
while True:
view.render()
cmd = input("Ваш ход: ").strip().lower()
if cmd == 'q':
break
elif cmd == 'z':
if history:
last_cmd = history.pop()
last_cmd.undo()
view.set_player_position(player.position)
print("Последний ход отменён")
else:
print("Нечего отменять")
elif cmd in MoveCommand.DIRECTIONS:
move_cmd = MoveCommand(player, maze, cmd)
if move_cmd.execute():
history.append(move_cmd)
view.set_player_position(player.position)
if player.position == maze.exit_cell:
print("\n🎉 ПОБЕДА! ВЫ НАШЛИ ВЫХОД! 🎉")
view.render()
break
else:
print("Туда нельзя пройти")
else:
print("Неизвестная команда")
print("Игра завершена")
def run_experiments():
"""Запуск экспериментального сравнения."""
print("\n" + "=" * 60)
print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ АЛГОРИТМОВ")
print("=" * 60)
builder = TextFieldMazeBuilder()
benchmark = Benchmark()
maze_files = ["small.txt", "empty.txt", "no_exit.txt"]
for maze_file in maze_files:
try:
maze = builder.build_from_file(f"mazes/{maze_file}")
print(f"\nТестируем: {maze_file} ({maze.width}×{maze.height})")
benchmark.run_on_maze(maze, maze_file, iterations=5)
except FileNotFoundError:
print(f"Файл {maze_file} не найден")
except ValueError as e:
print(f"Ошибка: {e}")
benchmark.print_summary()
benchmark.save_to_csv()
def main():
"""Главная функция."""
print("=" * 60)
print("ПРОГРАММА ПОИСКА ВЫХОДА ИЗ ЛАБИРИНТА")
print("Паттерны: Builder, Strategy, Observer, Command")
print("=" * 60)
create_test_mazes()
maze = demo_builder_and_strategy()
view = demo_observer(maze)
demo_command(maze, view)
run_experiments()
print("\nПрограмма завершена!")
if __name__ == "__main__":
main()
# In[ ]:

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from dataclasses import dataclass
from typing import Optional
@dataclass
class Cell:
"""Клетка лабиринта."""
x: int
y: int
is_wall: bool = False
is_start: bool = False
is_exit: bool = False
weight: int = 1 # Для взвешенных лабиринтов (доп. задание)
def is_passable(self) -> bool:
"""Проходима ли клетка."""
return not self.is_wall
def __hash__(self) -> int:
return hash((self.x, self.y))
def __eq__(self, other) -> bool:
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.y

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from typing import List, Optional, Tuple
from modelsCell import Cell
class Maze:
"""Модель лабиринта."""
def __init__(self, width: int = 0, height: int = 0):
self.width = width
self.height = height
self._cells: List[List[Optional[Cell]]] = [
[None for _ in range(width)] for _ in range(height)
]
self.start_cell: Optional[Cell] = None
self.exit_cell: Optional[Cell] = None
def set_cell(self, x: int, y: int, cell: Cell) -> None:
"""Установить клетку."""
if 0 <= x < self.width and 0 <= y < self.height:
self._cells[y][x] = cell
def get_cell(self, x: int, y: int) -> Optional[Cell]:
"""Получить клетку по координатам."""
if 0 <= x < self.width and 0 <= y < self.height:
return self._cells[y][x]
return None
def get_neighbors(self, cell: Cell) -> List[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
def get_all_cells(self) -> List[Cell]:
"""Получить все клетки лабиринта."""
cells = []
for y in range(self.height):
for x in range(self.width):
cell = self.get_cell(x, y)
if cell:
cells.append(cell)
return cells

View File

@ -0,0 +1,102 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
import time
from typing import List, Optional
from dataclasses import dataclass, field
from modelsMaze import Maze
from modelsCell import Cell
from strategiesPathfinding_strategy import PathFindingStrategy
from visualizationObserver import Observer
@dataclass
class SearchStats:
"""Статистика поиска."""
algorithm_name: str
time_ms: float
visited_cells: int
path_length: int
path_found: bool = True
class MazeSolver:
"""
Оркестратор для решения лабиринта.
Использует паттерн Strategy для алгоритмов поиска.
Поддерживает Observer для уведомлений.
"""
def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):
self.maze = maze
self._strategy = strategy
self._observers: List[Observer] = []
self._last_path: List[Cell] = []
self._last_stats: Optional[SearchStats] = None
def set_strategy(self, strategy: PathFindingStrategy) -> None:
"""Динамическая смена стратегии."""
self._strategy = strategy
self._notify(f"Стратегия изменена на {strategy.name}")
def attach(self, observer: Observer) -> None:
"""Подписать наблюдателя."""
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
"""Отписать наблюдателя."""
if observer in self._observers:
self._observers.remove(observer)
def _notify(self, event: str) -> None:
"""Уведомить всех наблюдателей."""
for observer in self._observers:
observer.update(event)
def solve(self) -> List[Cell]:
"""
Выполнить поиск пути с текущей стратегией.
Возвращает путь (список клеток).
"""
if self._strategy is None:
raise ValueError("Стратегия не установлена")
if not self.maze.start_cell or not self.maze.exit_cell:
raise ValueError("Лабиринт не имеет старта или выхода")
self._notify(f"Начинаем поиск пути с использованием {self._strategy.name}...")
start_time = time.perf_counter()
path = self._strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000
# Получаем количество посещённых клеток из стратегии
visited_cells = getattr(self._strategy, 'last_visited_count', 0)
self._last_path = path
self._last_stats = SearchStats(
algorithm_name=self._strategy.name,
time_ms=time_ms,
visited_cells=visited_cells,
path_length=len(path),
path_found=len(path) > 0
)
if path:
self._notify(f"Путь найден! Длина: {len(path)}, время: {time_ms:.2f} мс, посещено: {visited_cells}")
else:
self._notify(f"Путь не найден! Время: {time_ms:.2f} мс, посещено: {visited_cells}")
return path
@property
def last_path(self) -> List[Cell]:
return self._last_path
@property
def last_stats(self) -> Optional[SearchStats]:
return self._last_stats

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
import heapq
from typing import List, Dict, Optional, Tuple
from strategiesPathfinding_strategy import PathFindingStrategy
from modelsMaze import Maze
from modelsCell import Cell
class AStarStrategy(PathFindingStrategy):
"""Алгоритм A* с манхэттенской эвристикой."""
@property
def name(self) -> str:
return "A*"
def _heuristic(self, a: Cell, b: Cell) -> int:
"""Манхэттенское расстояние."""
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
# Приоритетная очередь: (f_score, counter, cell)
open_set = [(0, 0, start)]
counter = 1
came_from: Dict[Cell, Optional[Cell]] = {}
g_score: Dict[Cell, float] = {start: 0}
f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)}
visited_count = 0
while open_set:
current_f, _, current = heapq.heappop(open_set)
visited_count += 1
if current == exit_cell:
self._last_visited_count = visited_count
return self._reconstruct_path(came_from, start, current)
for neighbor in maze.get_neighbors(current):
tentative_g_score = g_score.get(current, float('inf')) + 1
if tentative_g_score < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score[neighbor] = tentative_g_score + self._heuristic(neighbor, exit_cell)
heapq.heappush(open_set, (f_score[neighbor], counter, neighbor))
counter += 1
self._last_visited_count = visited_count
return []
@property
def last_visited_count(self) -> int:
return getattr(self, '_last_visited_count', 0)

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from collections import deque
from typing import List, Dict, Optional
from strategiesPathfinding_strategy import PathFindingStrategy
from modelsMaze import Maze
from modelsCell import Cell
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину - гарантирует кратчайший путь."""
@property
def name(self) -> str:
return "BFS"
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
queue = deque([start])
came_from: Dict[Cell, Optional[Cell]] = {start: None}
visited_count = 0 # Для статистики
while queue:
current = queue.popleft()
visited_count += 1
if current == exit_cell:
# Сохраняем количество посещённых клеток для статистики
self._last_visited_count = visited_count
return self._reconstruct_path(came_from, start, current)
for neighbor in maze.get_neighbors(current):
if neighbor not in came_from:
came_from[neighbor] = current
queue.append(neighbor)
self._last_visited_count = visited_count
return []
@property
def last_visited_count(self) -> int:
return getattr(self, '_last_visited_count', 0)

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from typing import List, Dict, Optional
from strategiesPathfinding_strategy import PathFindingStrategy
from modelsMaze import Maze
from modelsCell import Cell
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину - быстрый, но не обязательно кратчайший."""
@property
def name(self) -> str:
return "DFS"
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
stack = [start]
came_from: Dict[Cell, Optional[Cell]] = {start: None}
visited_count = 0
while stack:
current = stack.pop()
visited_count += 1
if current == exit_cell:
self._last_visited_count = visited_count
return self._reconstruct_path(came_from, start, current)
for neighbor in maze.get_neighbors(current):
if neighbor not in came_from:
came_from[neighbor] = current
stack.append(neighbor)
self._last_visited_count = visited_count
return []
@property
def last_visited_count(self) -> int:
return getattr(self, '_last_visited_count', 0)

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from abc import ABC, abstractmethod
from typing import List, Optional
from modelsMaze import Maze
from modelsCell import Cell
class PathFindingStrategy(ABC):
"""Интерфейс стратегии поиска пути (паттерн Strategy)."""
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
"""
Найти путь от start до exit_cell.
Возвращает список клеток пути (включая start и exit) или пустой список.
"""
pass
@property
@abstractmethod
def name(self) -> str:
"""Имя стратегии для отчётов."""
pass
def _reconstruct_path(self, came_from: dict, start: Cell, current: Cell) -> List[Cell]:
"""Восстановить путь из словаря предков."""
path = []
while current != start:
path.append(current)
current = came_from.get(current)
if current is None:
return []
path.append(start)
path.reverse()
return path

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
import os
from typing import List, Optional, Set
from modelsMaze import Maze
from modelsCell import Cell
from visualizationObserver import Observer
class ConsoleView(Observer):
"""Консольная визуализация лабиринта."""
# Символы для отображения
SYMBOLS = {
'wall': '',
'path': '·',
'start': 'S',
'exit': 'E',
'player': 'P',
'solution': ''
}
def __init__(self, maze: Maze):
self.maze = maze
self.player_pos: Optional[Cell] = None
self.solution_path: Set[Cell] = set()
self.messages: List[str] = []
def update(self, event: str) -> None:
"""Обработка событий от MazeSolver."""
self.messages.append(f"[СОБЫТИЕ] {event}")
self.render()
def set_solution_path(self, path: List[Cell]) -> None:
"""Установить найденный путь для отображения."""
self.solution_path = set(path)
def set_player_position(self, cell: Cell) -> None:
"""Установить позицию игрока."""
self.player_pos = cell
def render(self) -> None:
"""Отрисовать лабиринт в консоли."""
# Очистка консоли (опционально)
# os.system('cls' if os.name == 'nt' else 'clear')
print("\n" + "=" * (self.maze.width * 2 + 4))
print(f"Лабиринт {self.maze.width}×{self.maze.height}")
print("=" * (self.maze.width * 2 + 4))
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 self.player_pos and cell == self.player_pos:
row += self.SYMBOLS['player'] + " "
elif cell.is_start:
row += self.SYMBOLS['start'] + " "
elif cell.is_exit:
row += self.SYMBOLS['exit'] + " "
elif cell in self.solution_path:
row += self.SYMBOLS['solution'] + " "
elif cell.is_wall:
row += self.SYMBOLS['wall'] * 2
else:
row += self.SYMBOLS['path'] * 2
print(row)
print("-" * (self.maze.width * 2 + 4))
# Показать последние сообщения
if self.messages:
print("Последние события:")
for msg in self.messages[-3:]:
print(f" {msg}")
print()
def clear_messages(self) -> None:
"""Очистить сообщения."""
self.messages.clear()

View File

@ -0,0 +1,16 @@
#!/usr/bin/env python
# coding: utf-8
# In[ ]:
from abc import ABC, abstractmethod
class Observer(ABC):
"""Интерфейс наблюдателя (паттерн Observer)."""
@abstractmethod
def update(self, event: str) -> None:
"""Обработчик события."""
pass