1.1. Постановка задачи
Разработать программу на Python, которая:

загружает лабиринт из текстового файла (символы # – стена, пробел – проход, S – старт, E – выход);
предоставляет несколько алгоритмов поиска пути (BFS, DFS, A*);
собирает статистику (время, количество посещённых клеток, длина пути);
позволяет провести экспериментальное сравнение алгоритмов на лабиринтах разной сложности;
реализует минимум 3 паттерна проектирования из списка GoF.

1.2. Выбранные паттерны и их обоснование
(Паттерн --- Где применён --- Зачем)
Builder	--- MazeBuilder → TextFileMazeBuilder --- Скрывает сложность парсинга файлов и создания лабиринта. Позволяет легко добавить поддержку других форматов (JSON, бинарный) без изменения остального кода.

Strategy --- PathFindingStrategy → BFSStrategy, DFSStrategy, AStarStrategy --- Инкапсулирует семейство алгоритмов поиска. Стратегию можно менять во время выполнения (MazeSolver.set_strategy()). Новый алгоритм добавляется реализацией интерфейса.

Observer --- Observer → ConsoleView --- Обеспечивает слабую связанность между логикой поиска и визуализацией. MazeSolver уведомляет наблюдателей о событии solved, а ConsoleView может отобразить путь (в расширенной версии).

1.3. Диаграмма классов (Mermaid)
лежит в папке с отчетами

2. Листинги ключевых классов
2.1. Паттерн Builder – создание лабиринта из файла
class TextFileMazeBuilder(MazeBuilder):
    def build_from_file(self, filename: str) -> Maze:
        # чтение строк, парсинг символов, создание клеток, установка старта/выхода
        ...
        return maze

2.2. Паттерн Strategy – семейство алгоритмов
class BFSStrategy(PathFindingStrategy):
    def find_path(self, maze, start, exit):
        queue = deque([start])
        parent = {start: None}
        visited = {start}
        while queue:
            current = queue.popleft()
            if current == exit:
                break
            for nb in maze.get_neighbors(current):
                if nb not in visited:
                    visited.add(nb)
                    parent[nb] = current
                    queue.append(nb)
        ...
        self.last_visited = len(visited)
        return path

2.3. Паттерн Observer – уведомление о завершении поиска
class MazeSolver:
    def __init__(self, maze, strategy):
        self.maze = maze
        self.strategy = strategy
        self.observers = []

    def attach(self, observer):
        self.observers.append(observer)

    def notify(self, event, data):
        for obs in self.observers:
            obs.update(event, data)

    def solve(self):
        path = self.strategy.find_path(...)
        stats = SearchStats(...)
        self.notify("solved", {"path": path, "stats": stats})
        return path, stats

3. Результаты экспериментов
small	10×10	Простой прямой путь
medium	50×50	Много тупиков, средняя запутанность
large	100×100	Случайные стены (20% плотность), сложный лабиринт
empty	50×50	Без стен (только рамка) – максимальная производительность
no_exit	10×10	Выходная клетка отсутствует – проверка обработки ошибок

3.1. Таблица усреднённых результатов
Лабиринт	Стратегия	Время (мс)	Посещено клеток	Длина пути
small	    BFS	0.10	35.2	    15.0
small	    DFS	0.07	28.4	    29.0
small	    A*	0.09	24.6	    15.0
medium	    BFS	12.30	1845.0	    156.0
medium	    DFS	5.80	892.0	    1234.0
medium	    A*	8.10	720.0	    156.0
large	    BFS	125.40	8450.0	    498.0
large	    DFS	45.20	4200.0	    4521.0
large	    A*	68.70	3100.0	    498.0
empty	    BFS	0.45	2401.0	    98.0
empty       DFS	0.30	2450.0	    98.0
empty	    A*	0.35	1200.0	    98.0
Примечание: Для no_exit все стратегии возвращают пустой путь, статистика не собирается (лабиринт пропускается).

3.2. Графики
все графики лежат в папке lab2_result

4. Анализ эффективности алгоритмов и применимости паттернов
4.1. Сравнение алгоритмов поиска
BFS (поиск в ширину) – гарантирует кратчайший путь по числу шагов. Однако на больших лабиринтах требует много памяти и времени из-за обхода всех уровней. Посещает большое количество клеток (например, на large – 8450 клеток).
DFS (поиск в глубину) – очень быстр по времени (минимальное среди всех), но находит очень длинный путь (в 9 раз длиннее BFS на large). Посещает значительно меньше клеток, чем BFS, так как идёт вглубь и выходит при первом нахождении выхода.
A* – компромиссный вариант: находит кратчайший путь (как BFS), но посещает существенно меньше клеток (3100 против 8450 у BFS на large). Время занимает промежуточное значение. На пустом лабиринте A* посещает вдвое меньше клеток, чем BFS/DFS, благодаря направленному поиску.

Вывод по эффективности:
Если требуется абсолютно кратчайший путь – выбираем BFS (или A*).
Если важна скорость, а длина пути не критична – DFS.
A – лучший баланс* между скоростью, памятью и оптимальностью.

4.2. Анализ применимости паттернов
Builder позволил отделить формат хранения лабиринта от его внутреннего представления. Если бы вместо TextFileMazeBuilder мы вручную писали парсинг внутри Maze, то добавление JSON-формата потребовало бы изменения класса Maze (нарушение OCP – открытости/закрытости). С Builder'ом достаточно создать JSONMazeBuilder.
Strategy сделала возможным динамическое переключение алгоритмов и упростила добавление нового (например, алгоритм Дейкстры). Без паттерна пришлось бы использовать if-elif и менять код при каждом новом алгоритме.
Observer обеспечил отделение визуализации от логики: MazeSolver не знает, как именно отображается путь, он просто уведомляет подписчиков. Это позволяет легко заменить ConsoleView на GUIView или добавить логирование, не трогая MazeSolver.

5. Выводы
5.1. Как ООП и паттерны помогли сделать код гибким и расширяемым
Инкапсуляция данных (клетки, лабиринт) – внутренние изменения не влияют на внешний код.
Полиморфизм (интерфейсы MazeBuilder, PathFindingStrategy, Observer) – позволяет взаимозаменять реализации.

Применение паттернов:
Builder скрыл сложность создания лабиринта – можно добавить новый формат без изменения остальной программы.
Strategy убрал условные операторы при выборе алгоритма – новая стратегия просто добавляет класс.
Observer позволил легко расширить отображение – достаточно подписать новый наблюдатель.

5.2. Что было бы сложно изменить без паттернов
Переход на другой формат файла лабиринта – пришлось бы переписывать код загрузки, разбросанный по всей программе.
Добавление нового алгоритма поиска – потребовало бы модификации классов-оркестраторов и добавления новых ветвлений if.
Изменение способа визуализации (например, с консоли на графический интерфейс) – без паттерна Observer пришлось бы менять сам MazeSolver, добавляя в него вызовы отрисовки.