2026-rff_mp/Ezhovnd/maze_report.md
Ezhovnd b8c07ba140 Лабораторная работа №2
Лабораторная работа №2
2026-05-25 07:48:23 +00:00

15 KiB
Raw Blame History

Задание 2 — Поиск выхода из лабиринта (ООП + паттерны GoF)

1. Описание задачи

Разработать расширяемую программу поиска пути в лабиринте с возможностью смены алгоритма, визуализации и пошагового управления. Применить минимум 3 паттерна GoF.


2. Применённые паттерны GoF

2.1 Builder (Строитель) — builder.py

Проблема: Создание Maze — многошаговый процесс: чтение файла → парсинг символов → валидация (есть ли S и E) → расстановка координат. Если делать всё в конструкторе Maze — нарушение Single Responsibility.

Решение: Интерфейс MazeBuilder с методом build_from_file(). Реализации:

  • TextFileMazeBuilder — текстовый формат (#, пробел, S, E, W, N)
  • JsonMazeBuilder — JSON-формат (для демонстрации расширяемости)

Преимущество: Добавление нового формата (бинарный, XML) не требует изменения Maze, MazeSolver или стратегий.

2.2 Strategy (Стратегия) — strategy.py

Проблема: Алгоритмы поиска (BFS, DFS, A*, Dijkstra) взаимозаменяемы по интерфейсу, но различаются реализацией. Жёсткая связь с конкретным алгоритмом нарушает Open/Closed Principle.

Решение: Интерфейс PathFindingStrategy с методом find_path(maze, start, exit_). MazeSolver хранит ссылку на стратегию и вызывает strategy.find_path() — не зная, какой именно алгоритм работает. Метод set_strategy() меняет алгоритм в рантайме.

Преимущество: Новый алгоритм (IDA*, Bidirectional BFS) — это один новый класс без изменения остального кода.

2.3 Observer (Наблюдатель) — solver.py

Проблема: MazeSolver не должен знать о способе отображения. Консоль, GUI, файловый лог — разные задачи.

Решение: MazeSolver наследует Subject (хранит список _observers, метод notify()). При событиях (maze_loaded, path_found, no_path, player_moved) уведомляет всех наблюдателей. Реализации: ConsoleView, FileLogObserver.

Преимущество: GUI-интерфейс добавляется как новый Observer без изменения MazeSolver.

2.4 Command (Команда) — solver.py

Проблема: Пошаговое перемещение игрока должно поддерживать отмену (undo).

Решение: Интерфейс Command с методами execute() / undo(). MoveCommand хранит предыдущую позицию. CommandHistory — стек команд. Undo — pop() + cmd.undo().

Преимущество: Любое новое действие (телепортация, открытие двери) реализуется как отдельная команда, не меняя Player или MazeSolver.


3. Диаграмма классов (Mermaid)

classDiagram
    %% ── Model ──────────────────────────────────
    class Cell {
        +int x, y
        +str symbol
        +bool is_wall
        +bool is_start
        +bool is_exit
        +int weight
        +is_passable() bool
    }

    class Maze {
        +int width, height
        +Cell start, exit
        +get_cell(x,y) Cell
        +get_neighbors(cell) list
        +render(path, visited, player) str
    }
    Maze "1" *-- "many" Cell

    %% ── Builder ─────────────────────────────────
    class MazeBuilder {
        <<interface>>
        +build_from_file(filename) Maze
        +build_empty(w, h) Maze
    }
    class TextFileMazeBuilder {
        +build_from_file(filename) Maze
        +build_from_string(text) Maze
        +build_empty(w, h) Maze
    }
    class JsonMazeBuilder {
        +build_from_file(filename) Maze
        +build_empty(w, h) Maze
    }
    MazeBuilder <|.. TextFileMazeBuilder
    MazeBuilder <|.. JsonMazeBuilder
    TextFileMazeBuilder ..> Maze : creates
    JsonMazeBuilder ..> Maze : creates

    %% ── Strategy ────────────────────────────────
    class PathFindingStrategy {
        <<interface>>
        +find_path(maze, start, exit) tuple
        +name() str
    }
    class BFSStrategy { +find_path() tuple }
    class DFSStrategy { +find_path() tuple }
    class AStarStrategy { +find_path() tuple }
    class DijkstraStrategy { +find_path() tuple }
    PathFindingStrategy <|.. BFSStrategy
    PathFindingStrategy <|.. DFSStrategy
    PathFindingStrategy <|.. AStarStrategy
    PathFindingStrategy <|.. DijkstraStrategy

    %% ── Observer ────────────────────────────────
    class Observer {
        <<interface>>
        +update(event, payload)
    }
    class Subject {
        -observers list
        +add_observer(obs)
        +notify(event, payload)
    }
    class ConsoleView { +update(event, payload) }
    class FileLogObserver { +update(event, payload) }
    Observer <|.. ConsoleView
    Observer <|.. FileLogObserver
    Subject "1" o-- "many" Observer

    %% ── MazeSolver ──────────────────────────────
    class MazeSolver {
        -Maze maze
        -PathFindingStrategy strategy
        +set_strategy(strategy)
        +load_maze(maze)
        +solve() tuple
    }
    MazeSolver --|> Subject
    MazeSolver --> PathFindingStrategy : uses
    MazeSolver --> Maze : uses

    %% ── Command ─────────────────────────────────
    class Command {
        <<interface>>
        +execute()
        +undo()
    }
    class MoveCommand {
        -Player player
        -Cell target, previous
        +execute()
        +undo()
    }
    class CommandHistory {
        -stack list
        +execute(cmd)
        +undo()
    }
    class Player { +Cell position }
    Command <|.. MoveCommand
    CommandHistory o-- Command
    MoveCommand --> Player

4. Структура файлов

maze/
├── model.py       — Cell, Maze
├── builder.py     — MazeBuilder, TextFileMazeBuilder, JsonMazeBuilder
├── strategy.py    — PathFindingStrategy, BFS, DFS, A*, Dijkstra
├── solver.py      — Observer, Subject, ConsoleView, MazeSolver, Command, Player
├── generator.py   — Генератор лабиринтов (DFS-backtracker)
├── experiment.py  — Экспериментальная часть, запись CSV
├── main.py        — Интерактивная демонстрация
└── docs/
    └── data/
        ├── maze_results.csv
        ├── maze_benchmark.png
        ├── path_lengths.png
        └── mazes/          — текстовые файлы лабиринтов

5. Результаты экспериментов

5.1 Таблица результатов (N = 7 повторов, среднее)

Лабиринт Алгоритм Время (мс) Посещено Длина пути
Маленький 11×11 BFS 0.077 37 29
Маленький 11×11 DFS 0.059 29 29
Маленький 11×11 A* 0.111 29 29
Маленький 11×11 Dijkstra 0.115 37 29
Средний 51×51 BFS 2.228 755 405
Средний 51×51 DFS 0.935 423 405
Средний 51×51 A* 2.343 663 405
Средний 51×51 Dijkstra 2.528 755 405
Большой 101×101 BFS 3.299 1533 993
Большой 101×101 DFS 2.289 1061 993
Большой 101×101 A* 5.948 1515 993
Большой 101×101 Dijkstra 5.137 1533 993
Пустой 52×52 BFS 5.859 2500 99
Пустой 52×52 DFS 3.518 1275 1275
Пустой 52×52 A* 10.803 2500 99
Пустой 52×52 Dijkstra 10.227 2500 99
Без выхода 21×21 BFS 0.213 105 0
Без выхода 21×21 DFS 0.220 105 0
Без выхода 21×21 A* 0.361 105 0
Без выхода 21×21 Dijkstra 0.336 105 0
Взвешенный 51×51 BFS 0.722 335 221
Взвешенный 51×51 DFS 0.518 221 221
Взвешенный 51×51 A* 0.970 278 221
Взвешенный 51×51 Dijkstra 1.091 337 221

5.2 Графики

Benchmark

Path Lengths


6. Анализ алгоритмов

BFS (поиск в ширину)

  • Гарантирует кратчайший путь по количеству шагов.
  • Посещает все клетки на расстоянии k до нахождения выхода на расстоянии k+1.
  • На пустом лабиринте (максимум свободного пространства) посещает все 2500 клеток — это худший сценарий по памяти (хранит всю «волну»).
  • Практически эквивалентен Dijkstra для равновесных лабиринтов.

DFS (поиск в глубину)

  • Самый быстрый по времени на лабиринтах с длинными коридорами.
  • На правильно построенном лабиринте (DFS-backtracker) коридоры длинные → DFS «угадывает» направление и находит путь, посетив меньше клеток.
  • На пустом поле даёт огромный зигзагообразный путь (1275 клеток вместо оптимального 99) — классическая проблема DFS.
  • Не гарантирует оптимальность, зато экономит память (O(глубина) вместо O(ширина)).

A* (с манхэттенской эвристикой)

  • На лабиринтах с коридорами посещает меньше клеток, чем BFS, но при этом тоже гарантирует оптимальный путь.
  • На пустом поле ведёт себя хуже BFS по времени из-за накладных расходов на приоритетную очередь (heapq).
  • Манхэттенская эвристика работает хуже на лабиринтах с длинными обходами препятствий: оценка расстояния «по прямой» слишком оптимистична.
  • Выигрывает когда карта большая и путь очевиден по направлению (прямые коридоры к выходу).

Dijkstra

  • На равновесных лабиринтах идентичен BFS, но медленнее из-за приоритетной очереди.
  • Незаменим на взвешенных картах: учитывает клетки с весом 2 (песок) и 3 (болото) и находит минимальный суммарный вес пути, а не минимальное число шагов.
  • На взвешенном лабиринте Dijkstra и A* могут выдать более короткий (по весу) путь, чем BFS, при одинаковой длине в клетках.

Без выхода

  • Все алгоритмы обошли все 105 достижимых клеток и вернули пустой путь — корректная обработка.
  • Время идентично для всех алгоритмов (доминирует полный обход, а не структура поиска).

7. Выводы: ООП и паттерны

Что дали паттерны

Builder отделил детали парсинга от модели. Без него клиент сам читал бы файл и вручную собирал Maze — хрупкий, нерасширяемый код. С Builder: добавление JSON-формата заняло 10 строк.

Strategy позволил сравнить 4 алгоритма, не меняя MazeSolver. Переключение алгоритма — одна строка solver.set_strategy(new_algo). Без паттерна потребовался бы if/elif на 4 ветки в каждом методе.

Observer полностью развязал логику поиска и отображение. MazeSolver не знает, куда идут уведомления. Добавление FileLogObserver — новый класс из 5 строк, нулевые изменения в MazeSolver.

Command дал возможность реализовать undo за счёт хранения предыдущего состояния в объекте команды. Без паттерна — ручное сохранение/восстановление переменных.

Что было бы сложно без ООП и паттернов

Задача Без паттернов С паттернами
Добавить новый алгоритм Правка MazeSolver + if/elif Новый класс, наследующий интерфейс
Добавить формат файла Правка загрузчика Новый Builder
Добавить GUI Вставка GUI-кода в логику поиска Новый Observer
Отмена шага игрока Ручное сохранение переменной Command.undo()

Рекомендации по выбору алгоритма

Задача Рекомендация
Гарантированно кратчайший путь, равные веса BFS
Быстро найти какой-нибудь путь (поиск с препятствиями) DFS
Большая карта, путь по направлению к цели A*
Взвешенная карта (болото, песок, дорога) Dijkstra или A* с весовой эвристикой
Нужна гарантия оптимальности на взвешенном графе Dijkstra