diff --git a/MininaVD/docs2/Report.ipynb b/MininaVD/docs2/Report.ipynb new file mode 100644 index 0000000..e05babc --- /dev/null +++ b/MininaVD/docs2/Report.ipynb @@ -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 +} diff --git a/MininaVD/docs2/data2/buildersMaze_builder.py b/MininaVD/docs2/data2/buildersMaze_builder.py new file mode 100644 index 0000000..e7c588a --- /dev/null +++ b/MininaVD/docs2/data2/buildersMaze_builder.py @@ -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 diff --git a/MininaVD/docs2/data2/buildersText_maze_builder.py b/MininaVD/docs2/data2/buildersText_maze_builder.py new file mode 100644 index 0000000..7780891 --- /dev/null +++ b/MininaVD/docs2/data2/buildersText_maze_builder.py @@ -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 diff --git a/MininaVD/docs2/data2/commandsCommand.py b/MininaVD/docs2/data2/commandsCommand.py new file mode 100644 index 0000000..c7d1fac --- /dev/null +++ b/MininaVD/docs2/data2/commandsCommand.py @@ -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 + diff --git a/MininaVD/docs2/data2/commandsMove_command.py b/MininaVD/docs2/data2/commandsMove_command.py new file mode 100644 index 0000000..b916987 --- /dev/null +++ b/MininaVD/docs2/data2/commandsMove_command.py @@ -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 + diff --git a/MininaVD/docs2/data2/commandsPlayer.py b/MininaVD/docs2/data2/commandsPlayer.py new file mode 100644 index 0000000..f78a9a7 --- /dev/null +++ b/MininaVD/docs2/data2/commandsPlayer.py @@ -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 + diff --git a/MininaVD/docs2/data2/experimentsBenchmark.py b/MininaVD/docs2/data2/experimentsBenchmark.py new file mode 100644 index 0000000..7f88121 --- /dev/null +++ b/MininaVD/docs2/data2/experimentsBenchmark.py @@ -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}") + + + diff --git a/MininaVD/docs2/data2/main.py b/MininaVD/docs2/data2/main.py new file mode 100644 index 0000000..cb8f41b --- /dev/null +++ b/MininaVD/docs2/data2/main.py @@ -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[ ]: + + + + diff --git a/MininaVD/docs2/data2/modelsCell.py b/MininaVD/docs2/data2/modelsCell.py new file mode 100644 index 0000000..e8c6b64 --- /dev/null +++ b/MininaVD/docs2/data2/modelsCell.py @@ -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 + diff --git a/MininaVD/docs2/data2/modelsMaze.py b/MininaVD/docs2/data2/modelsMaze.py new file mode 100644 index 0000000..43ae196 --- /dev/null +++ b/MininaVD/docs2/data2/modelsMaze.py @@ -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 + diff --git a/MininaVD/docs2/data2/solverMaze_solver.py b/MininaVD/docs2/data2/solverMaze_solver.py new file mode 100644 index 0000000..b95e911 --- /dev/null +++ b/MininaVD/docs2/data2/solverMaze_solver.py @@ -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 + diff --git a/MininaVD/docs2/data2/strategiesA_star_strategy.py b/MininaVD/docs2/data2/strategiesA_star_strategy.py new file mode 100644 index 0000000..71a288a --- /dev/null +++ b/MininaVD/docs2/data2/strategiesA_star_strategy.py @@ -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) + diff --git a/MininaVD/docs2/data2/strategiesBfs_strategy.py b/MininaVD/docs2/data2/strategiesBfs_strategy.py new file mode 100644 index 0000000..5602391 --- /dev/null +++ b/MininaVD/docs2/data2/strategiesBfs_strategy.py @@ -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) + diff --git a/MininaVD/docs2/data2/strategiesDfs_strategy.py b/MininaVD/docs2/data2/strategiesDfs_strategy.py new file mode 100644 index 0000000..74bee68 --- /dev/null +++ b/MininaVD/docs2/data2/strategiesDfs_strategy.py @@ -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) + diff --git a/MininaVD/docs2/data2/strategiesPathfinding_strategy.py b/MininaVD/docs2/data2/strategiesPathfinding_strategy.py new file mode 100644 index 0000000..579dea0 --- /dev/null +++ b/MininaVD/docs2/data2/strategiesPathfinding_strategy.py @@ -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 + diff --git a/MininaVD/docs2/data2/visualizationConsole_view.py b/MininaVD/docs2/data2/visualizationConsole_view.py new file mode 100644 index 0000000..088c34f --- /dev/null +++ b/MininaVD/docs2/data2/visualizationConsole_view.py @@ -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() + diff --git a/MininaVD/docs2/data2/visualizationObserver.py b/MininaVD/docs2/data2/visualizationObserver.py new file mode 100644 index 0000000..1f6b712 --- /dev/null +++ b/MininaVD/docs2/data2/visualizationObserver.py @@ -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 +