13 KiB
Задание
Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
Общая схема приложения (пример)
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 {
<<interface>>
+buildFromFile(filename): Maze
}
class TextFileMazeBuilder {
+buildFromFile(filename): Maze
}
class PathFindingStrategy {
<<interface>>
+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 {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Direction dir
-Cell previousCell
+execute()
+undo()
}
class Player {
-Cell currentCell
+moveTo(cell)
}
class Observer {
<<interface>>
+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. Экспериментальная часть (аналогично заданию со структурами данных)
Задача: Сравнить эффективность реализованных стратегий на лабиринтах разной сложности.
-
Подготовка тестовых лабиринтов:
- Маленький (10×10) с простым путём.
- Средний (50×50) с тупиками.
- Большой (100×100) с запутанной структурой.
- «Пустой» лабиринт (без стен) – для демонстрации максимальной производительности.
- «Без выхода» – чтобы проверить обработку отсутствия пути.
-
Замеры:
- Для каждого лабиринта и каждой стратегии запустить
solve()5–10 раз, усреднить время, количество посещённых клеток, длину пути. - Записать результаты в CSV:
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути.
- Для каждого лабиринта и каждой стратегии запустить
-
Анализ:
- Построить графики для каждого лабиринта.
- Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
-
Дополнительное задание: Реализовать взвешенные клетки (например, болото – вес 3, песок – вес 2, асфальт – вес 1) и сравнить Дейкстру с A* на взвешенном графе.
Этап 7. Отчёт
Структура отчёта:
- Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
- Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
- Результаты экспериментов (таблицы, графики).
- Анализ эффективности алгоритмов и применимости паттернов.
- Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.
Советы
- Для A* самая простая эвристика:
abs(x1 - x2) + abs(y1 - y2). - При поиске пути надо хранить предшественников (
parentдля каждой посещённой клетки), чтобы восстановить путь. - Для BFS/DFS используй
deque(очередь) иlist(стек). - Визуализацию в консоли можно сделать с помощью
os.system('cls' if os.name == 'nt' else 'clear')для перерисовки.