2026-rff_mp/skorohodovsa/task_2/docs/source/task.md
2026-05-25 10:23:00 +03:00

13 KiB
Raw Blame History

Задание

Цель работы

Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 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. Экспериментальная часть (аналогично заданию со структурами данных)

Задача: Сравнить эффективность реализованных стратегий на лабиринтах разной сложности.

  1. Подготовка тестовых лабиринтов:

    • Маленький (10×10) с простым путём.
    • Средний (50×50) с тупиками.
    • Большой (100×100) с запутанной структурой.
    • «Пустой» лабиринт (без стен) для демонстрации максимальной производительности.
    • «Без выхода» чтобы проверить обработку отсутствия пути.
  2. Замеры:

    • Для каждого лабиринта и каждой стратегии запустить solve() 510 раз, усреднить время, количество посещённых клеток, длину пути.
    • Записать результаты в 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') для перерисовки.