# Задание ## Цель работы Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. ## Общая схема приложения (пример) ```{mermaid} classDiagram class Maze { -Cell[] cells -int width, height -Cell start -Cell exit +getCell(x,y): Cell +getNeighbors(cell): List~Cell~ } class Cell { -int x, y -bool isWall -bool isStart -bool isExit +isPassable(): bool } class MazeBuilder { <> +buildFromFile(filename): Maze } class TextFileMazeBuilder { +buildFromFile(filename): Maze } class PathFindingStrategy { <> +findPath(maze, start, exit): List~Cell~ } class BFSStrategy class DFSStrategy class AStarStrategy class DijkstraStrategy class SearchStats { +timeMs: float +visitedCells: int +pathLength: int } class MazeSolver { -Maze maze -PathFindingStrategy strategy +setStrategy(strategy) +solve(): SearchStats } class Command { <> +execute() +undo() } class MoveCommand { -Player player -Direction dir -Cell previousCell +execute() +undo() } class Player { -Cell currentCell +moveTo(cell) } class Observer { <> +update(event) } class ConsoleView { +update(event) +render(maze, player, path) } MazeBuilder <|.. TextFileMazeBuilder MazeBuilder --> Maze : creates PathFindingStrategy <|.. BFSStrategy PathFindingStrategy <|.. DFSStrategy PathFindingStrategy <|.. AStarStrategy PathFindingStrategy <|.. DijkstraStrategy MazeSolver --> PathFindingStrategy : uses MazeSolver --> Maze : uses Command <|.. MoveCommand MoveCommand --> Player Player --> Cell Observer <|.. ConsoleView MazeSolver --> Observer : notifies ``` ## Выполнение ### Этап 1. Модель лабиринта (без паттернов, просто классы) **Задача:** Создать классы `Cell` и `Maze`, которые представляют карту лабиринта. - `Cell` хранит координаты (x, y), флаги `isWall`, `isStart`, `isExit`, метод `isPassable()` (возвращает `True` для прохода, если не стена). - `Maze` хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: `getCell(x, y)`, `getNeighbors(cell)` – возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена). **Результат:** Лабиринт можно создать вручную в коде, но загрузку пока не делаем. ### Этап 2. Загрузка лабиринта из файла – применение паттерна **Builder** **Задача:** Реализовать загрузку лабиринта из текстового файла, где `#` – стена, ` ` (пробел) – проход, `S` – старт, `E` – выход. - Создать интерфейс `MazeBuilder` с методом `buildFromFile(filename)`. - Реализовать класс `TextFileMazeBuilder`, который читает файл, парсит символы, создаёт объекты `Cell`, задаёт координаты и флаги, после чего возвращает готовый `Maze`. Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию `MazeBuilder`. ### Этап 3. Стратегии поиска пути – паттерн **Strategy** **Задача:** Реализовать семейство алгоритмов поиска пути от старта до выхода. - Создать интерфейс `PathFindingStrategy` с методом `findPath(maze, start, exit)`, возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет. - Реализовать минимум 3 стратегии: - **BFS** (поиск в ширину) – гарантирует кратчайший путь по количеству шагов. - **DFS** (поиск в глубину) – быстрый, но не обязательно кратчайший. - **A*** (с эвристикой, например, манхэттенское расстояние) – компромисс между скоростью и оптимальностью. - (Опционально) **Дейкстра** – полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS. Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A* – приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток. Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс. ### Этап 4. Класс-оркестратор – **MazeSolver** (использует Strategy) **Задача:** Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику. - `MazeSolver` содержит поля `maze` и `strategy`. - Метод `setStrategy(strategy)` для динамической смены алгоритма. - Метод `solve()` вызывает `strategy.findPath(...)` и возвращает объект `SearchStats` (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути). - Для замера времени используйте `time.perf_counter()` до и после вызова стратегии. ### Этап 5. Визуализация и пошаговое управление – паттерны **Observer** и **Command** (по желанию) **5.1. Наблюдатель (Observer)** – обновление консольного интерфейса. - Создать интерфейс `Observer` с методом `update(event)`, где `event` может быть строкой или объектом с типом события (`"path_found"`, `"move"`, `"maze_loaded"`). - Реализовать класс `ConsoleView`, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод `render(maze, player_position, path)` рисует карту в консоли. - `MazeSolver` (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния. **5.2. Команда (Command)** – для пошагового перемещения игрока по найденному пути (или ручного управления). - Создать интерфейс `Command` с методами `execute()` и `undo()`. - Реализовать `MoveCommand`, который принимает игрока (`Player`), направление и изменяет его позицию, сохраняя предыдущую для отмены. - Создать класс `Player`, хранящий текущую клетку. - Консольное меню позволяет вводить команды (W/A/S/D), выполнять `MoveCommand`, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн. *Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command – для демонстрации undo при ручном исследовании лабиринта.* ### Этап 6. Экспериментальная часть (аналогично заданию со структурами данных) **Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности. 1. **Подготовка тестовых лабиринтов:** - Маленький (10×10) с простым путём. - Средний (50×50) с тупиками. - Большой (100×100) с запутанной структурой. - «Пустой» лабиринт (без стен) – для демонстрации максимальной производительности. - «Без выхода» – чтобы проверить обработку отсутствия пути. 2. **Замеры:** - Для каждого лабиринта и каждой стратегии запустить `solve()` 5–10 раз, усреднить время, количество посещённых клеток, длину пути. - Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`. 3. **Анализ:** - Построить графики для каждого лабиринта. - Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях). 4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A* на взвешенном графе. ### Этап 7. Отчёт **Структура отчёта:** 1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid). 2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий. 3. Результаты экспериментов (таблицы, графики). 4. Анализ эффективности алгоритмов и применимости паттернов. 5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них. ## Советы - Для A* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`. - При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь. - Для BFS/DFS используй `deque` (очередь) и `list` (стек). - Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки.