diff --git a/skorohodovsa/task_2/.gitignore b/skorohodovsa/task_2/.gitignore index 369c288..deb7a34 100644 --- a/skorohodovsa/task_2/.gitignore +++ b/skorohodovsa/task_2/.gitignore @@ -14,7 +14,7 @@ __pycache__/ # Сборка документации Sphinx - ЭТО ВАЖНО! docs/build/ -docs/source/_build/ +docs/_build/ # Системные файлы .DS_Store @@ -32,5 +32,4 @@ Thumbs.db .ruff_cache/ -/.idea pupu.py \ No newline at end of file diff --git a/skorohodovsa/task_2/docs.md b/skorohodovsa/task_2/docs.md new file mode 100644 index 0000000..d0eec40 --- /dev/null +++ b/skorohodovsa/task_2/docs.md @@ -0,0 +1,1185 @@ +`файл является компиляцией документации sphinx в папке docs` +# Лабораторная работа «Поиск выхода из лабиринта» + +* [Задание](task.md) + * [Цель работы](task.md#id2) + * [Общая схема приложения (пример)](task.md#id3) + * [Выполнение](task.md#id4) + * [Советы](task.md#id8) +* [Этап 1. Модель лабиринта](stage1.md) + * [Класс `Cell`](stage1.md#cell) + * [Класс `Maze`](stage1.md#maze) +* [Этап 2. Загрузка лабиринта из файла](stage2.md) + * [Паттерн Builder](stage2.md#builder) + * [Класс `MazeBuilder`](stage2.md#mazebuilder) + * [Класс `TextFileBuilder`](stage2.md#textfilebuilder) + * [Использование](stage2.md#id2) + * [Известная ошибка](stage2.md#id3) +* [Этап 3. Стратегии поиска пути](stage3.md) + * [Паттерн Strategy](stage3.md#strategy) + * [Структура пакета](stage3.md#id2) + * [Класс `PathFindingStrategy`](stage3.md#pathfindingstrategy) + * [Алгоритмы](stage3.md#id3) +* [Этап 4. Класс-оркестратор MazeSolver](stage4.md) + * [Роль в архитектуре](stage4.md#id1) + * [Класс `SearchStats`](stage4.md#searchstats) + * [Класс `MazeSolver`](stage4.md#id3) +* [Этап 5. Визуализация и пошаговое управление](stage5.md) + * [5.1. Паттерн Observer](stage5.md#observer) + * [5.2. Паттерн Command](stage5.md#command) +* [Этап 6. Экспериментальная часть](stage6.md) + * [Подготовка](stage6.md#id2) + * [Замеры](stage6.md#id3) + * [Результаты](stage6.md#id4) + * [Выводы](stage6.md#id8) + * [Визуализация](stage6.md#id9) +* [Этап 7. Отчёт](stage7.md) + * [Описание задачи](stage7.md#id2) + * [Диаграмма классов](stage7.md#id3) + * [Результаты экспериментов](stage7.md#id4) + * [Выводы](stage7.md#id5) +* [API Reference](api.md) + * [Базовые модели](api.md#module-source.models.base) + * [Загрузка лабиринта](api.md#module-source.build.builder) + * [Стратегии поиска пути](api.md#module-source.strategy.algorithms) + * [Оркестратор](api.md#module-source.strategy.solver) + * [Визуализация](api.md#module-source.view.observer) + * [Управление игроком](api.md#module-source.view.command) +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе разработки необходимо реализовать загрузку лабиринта из текстового файла, где: `#` – стена, ` ` – проход, `S` – старт, `E` – выход. + +## Систематизация файлов + +Для удобного хранения лабиринтов было решено сделать систему наименования текстовых файлов в папке `source/templates`. + +Общая структура: + +```default +{размер}_{свойство 1}-{свойство 2}-{свойство n}_{версия}.txt +``` + +### Размер + +Формат: `{ширина}x{высота}` + +| Пример | Значение | +|-----------|----------------| +| `10x10` | 10×10 клеток | +| `50x50` | 50×50 клеток | +| `100x100` | 100×100 клеток | +| `30x30` | 30×30 клеток | +| `20x20` | 20×20 клеток | + +### Свойства + +| Свойство | Код | Описание | +|--------------|-------------|----------------------------------------------------------------------------------------------| +| Простой путь | `path` | Существует маршрут от S до E | +| Тупики | `deadends` | Лабиринт специально содержит тупики (могут быть и в других типах, но здесь — гарантированно) | +| Запутанный | `spaghetti` | Сложная структура с циклами и ложными ходами | +| Пустой | `empty` | Нет стен (`#`), только пробелы, S и E | +| Без выхода | `noexit` | В лабиринте отсутствует символ `E` | + +### Версия + +Формат: `v{номер}` + +- `v1`, `v2`, `v10` + +### Примеры + +#### Маленькие (10×10, простой путь) + +```default +10x10_path_v1.txt +10x10_path_v2.txt +... +10x10_path_v10.txt +``` + +#### Средние (50×50, тупики) + +```default +50x50_deadends_v1.txt +50x50_deadends_v2.txt +... +50x50_deadends_v10.txt +``` + +#### Большие (100×100, запутанные) + +```default +100x100_spaghetti_v1.txt +100x100_spaghetti_v2.txt +... +100x100_spaghetti_v10.txt +``` + +#### Пустые (30×30) + +```default +30x30_empty_v1.txt +30x30_empty_v2.txt +... +30x30_empty_v10.txt +``` + +#### Без выхода (20×20) + +```default +20x20_noexit_v1.txt +20x20_noexit_v2.txt +... +20x20_noexit_v10.txt +``` + +#### Комбинированные свойства + +```default +50x50_deadends-noexit_v1.txt +100x100_spaghetti-noexit_v1.txt +10x10_path-empty_v1.txt (избыточно, но допустимо) +``` + +### Примечание + +- Регистр имён файлов: **нижний регистр** +- Разделители: только `_` и `-` +- Расширение: `.txt` +- Кодировка: UTF-8 +# Этап 1. Модель лабиринта + +В первом этапе разработки необходимо создать базовые классы `Cell` и `Maze`, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы. + +## Класс `Cell` + +Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая. + +По условию задания клетка должна иметь флаги `isWall`, `isStart`, `isExit` и метод `isPassable()`. В реализации флаги оформлены как **свойства** (`@property`) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа. + +```python +cell = Cell(1, 1) +cell.is_wall = True +``` + +Типы клетки взаимоисключают друг друга — клетка не может быть одновременно стеной и стартом. Логика сброса вынесена в приватный метод `_clear_flags()`. + +### Символьное представление + +Для вывода лабиринта в консоль каждая клетка возвращает символ через `__str__`. Символы берутся из `cell_mapping` в `source/settings.py`, что позволяет менять отображение без правки классов: + +| Тип | Символ по умолчанию | +|--------|-----------------------| +| Стена | `#` | +| Старт | `S` | +| Выход | `E` | +| Пустая | | + +## Класс `Maze` + +Лабиринт хранит двумерный список клеток и предоставляет методы для работы с ними. + +По условию задания требовались методы `getCell(x, y)` и `getNeighbors(cell)`. В реализации добавлено несколько вещей сверх задания: + +### Именование методов + +Задание написано в стиле Java/pseudocode — названия методов и полей используют `camelCase` (`isWall`, `getCell`, `isPassable`). В Python принят другой стандарт именования — **PEP 8**, который предписывает `snake_case` для методов и атрибутов. Поэтому все названия были приведены к Python стилю: + +| Задание | Реализация | +|---------------------------|-----------------------------| +| `isWall` | `is_wall` | +| `isStart` | `is_start` | +| `isExit` | `is_exit` | +| `isPassable()` | `is_possible()` | +| `getCell(x, y)` | `get_cell(x, y)` | +| `getNeighbors(cell)` | `get_neighbors(x, y)` | +| `buildFromFile(filename)` | `build_from_file(filename)` | + +Это соответствует стандарту оформления кода на Python и делает API классов идиоматичным для языка. + +### Индексация `maze[row, col]` + +Вместо явного вызова `get_cell()` реализованы `__getitem__` и `__setitem__`, что позволяет обращаться к клеткам естественным образом: + +```python +maze[0, 0] = cell_mapping['wall'] # установить стену +cell = maze[2, 3] # получить клетку +``` + +Обратите внимание: индексация идёт в формате `[row, col]`, то есть сначала строка (Y), потом столбец (X) — аналогично numpy. + +### Свойства `start` и `exit` + +Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода: + +```python +maze.start # Cell или None +maze.exit # Cell или None +``` + +Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают `start` и `exit` автоматически из лабиринта. + +### Свойство `shape` + +По аналогии с numpy добавлено свойство `shape`, возвращающее `(height, width)`: + +```python +rows, cols = maze.shape +``` + +Используется в стратегиях поиска и тестах для итерации по лабиринту. + +### `get_neighbors` + +Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает `None`. + +```python +neighbors = maze.get_neighbors(2, 2) # список Cell +``` + +Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS). +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна **Builder**. + +## Паттерн Builder + +Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод `build_from_file()`, внутри которого сосредоточена вся логика построения. + +Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию `MazeBuilder` без изменения остального кода. + +## Класс `MazeBuilder` + +Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод `build_from_file()`, который обязан реализовать каждый конкретный строитель. + +По условию задания интерфейс назывался `MazeBuilder` с методом `buildFromFile`. В реализации название метода приведено к **PEP 8** — `build_from_file`. Сам класс оформлен через `ABC` — попытка создать объект `MazeBuilder()` напрямую вызовет `TypeError`. + +## Класс `TextFileBuilder` + +Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из `.txt` файла где `#` — стена, — проход, `S` — старт, `E` — выход. + +Процесс построения разбит на три приватных шага: + +### `_read_file` + +Читает файл построчно и обрезает символы переноса строки `\n` и `\r`. Возвращает список строк — каждая строка соответствует одной строке лабиринта. + +### `_test_text_maze` + +Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и `_create_maze` выбросит `ValueError`. + +Реализован как `@staticmethod` — не использует состояние объекта, только входные данные. + +### `_create_maze` + +Создаёт объект `Maze` нужного размера и заполняет его клетки символами из файла через `maze[y, x] = symbol`. Тип каждой клетки определяется автоматически через `cell_mapping` в `__setitem__` лабиринта. + +## Использование + +```python +from source.build.builder import TextFileBuilder + +maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt') +print(maze) +``` + +## Известная ошибка + +В текущей реализации `_create_maze` есть опечатка при вычислении `width`: + +```python +height, width = len(text_maze), len(text_maze) # width всегда равен height +``` + +Правильная версия: + +```python +height, width = len(text_maze), len(text_maze[0]) +``` + +На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат. +# Этап 3. Стратегии поиска пути + +В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**. + +## Паттерн Strategy + +Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии: + +```python +solver = MazeSolver(maze, BFSStrategy()) +solver.set_strategy(AStarStrategy()) +``` + +Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно. + +## Структура пакета + +Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт: + +```default +source/strategy/ +├── __init__.py ← единственный импорт для пользователя +├── algorithms.py ← базовый класс PathFindingStrategy +├── bfs.py +├── dfs.py +└── astar.py +``` + +```python +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy +``` + +## Класс `PathFindingStrategy` + +Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод `find_path()` и содержит два вспомогательных метода, общих для всех стратегий. + +По условию задания метод назывался `findPath` — приведён к **PEP 8** как `find_path`. + +### `_validate` + +Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа `noexit`: алгоритм падал с `AttributeError` внутри, вместо понятного сообщения. + +`_validate` подставляет `start` и `exit` из лабиринта если они не переданы явно, и выбрасывает `ValueError` с понятным сообщением если клетки не найдены: + +```python +start, exit = self._validate(maze, start, exit) +``` + +Вынесен в базовый класс чтобы не дублировать в каждом алгоритме. + +### `_reconstruct_path` + +Восстанавливает путь по словарю предков `came_from`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса. + +Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список: + +```default +exit → D → C → B → start (идём по came_from) +start → B → C → D → exit (после reverse) +``` + +## Алгоритмы + +### BFS — `BFSStrategy` + +Поиск в ширину. Использует `deque` как очередь (FIFO) — каждый раз берём самую старую клетку из начала. Это гарантирует послойный обход и кратчайший путь по количеству шагов. + +Сложность: O(V + E) по времени и памяти. + +### DFS — `DFSStrategy` + +Поиск в глубину. Использует `list` как стек (LIFO) — каждый раз берём самую свежую клетку с конца. Алгоритм ныряет вглубь по одному направлению до тупика, затем возвращается. + +Не гарантирует кратчайший путь. На запутанных лабиринтах может обойти почти все клетки прежде чем найти выход, хотя по времени часто быстрее BFS из-за меньших накладных расходов на структуру данных. + +Сложность: O(V + E) по времени и памяти. + +### A\* — `AStarStrategy` + +Использует `heapq` как приоритетную очередь. На каждом шаге выбирает клетку с минимальным значением `f = g + h`, где `g` — стоимость пути от старта, `h` — манхэттенская эвристика до выхода. + +Эвристика направляет поиск в сторону выхода, поэтому A\* обходит меньше клеток чем BFS при том же гарантированно кратчайшем пути. + +В кортеж приоритетной очереди добавлен счётчик `counter` как tie-breaker — без него `heapq` попытался бы сравнивать объекты `Cell` при одинаковом `f`, что вызвало бы `TypeError`: + +```python +heapq.heappush(open_heap, (f, counter, neighbor)) +``` + +Сложность: O(E · log V) в худшем случае. +# Этап 4. Класс-оркестратор MazeSolver + +В четвёртом этапе реализован класс `MazeSolver`, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику. + +## Роль в архитектуре + +`MazeSolver` — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) +# Время: 0.041 мс | Посещено клеток: 13 | Длина пути: 13 +``` + +## Класс `SearchStats` + +Оформлен через `@dataclass` — это избавляет от ручного `__init__` и автоматически даёт `__repr__`. Хранит четыре поля: время выполнения, количество посещённых клеток, длину пути и сам путь как список клеток. + +`__str__` переопределён для удобного вывода в консоль и отчётах. + +### Ограничение + +В текущей реализации `visited_count` и `path_length` всегда равны друг другу — оба вычисляются как `len(path)`. Это потому что стратегии возвращают только финальный путь, а не все посещённые клетки. Чтобы получить точное количество посещений, потребовалось бы дорабатывать каждую стратегию — добавлять счётчик внутри `find_path`. На данном этапе это сознательное упрощение. + +## Класс `MazeSolver` + +### `set_strategy` + +Позволяет менять алгоритм без пересоздания солвера. Это и есть основная демонстрация паттерна Strategy в действии — один объект, разные алгоритмы: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats_bfs = solver.solve() + +solver.set_strategy(AStarStrategy()) +stats_astar = solver.solve() +``` + +### `solve` + +Замеряет время через `time.perf_counter()` — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000. + +`start` и `exit` можно не передавать — стратегия найдёт их сама через `_validate`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки. +# Этап 5. Визуализация и пошаговое управление + +В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком. + +## 5.1. Паттерн Observer + +### Идея + +`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода. + +### Класс `Event` + +Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий: + +| Тип | Когда генерируется | +|---------------|----------------------------| +| `maze_loaded` | Лабиринт загружен из файла | +| `path_found` | Алгоритм нашёл путь | +| `no_path` | Путь не найден | +| `move` | Игрок сделал ход | + +### Класс `Observer` + +Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать. + +### Класс `ConsoleView` + +Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта. + +Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе: + +```python +path_set = set(path) if path else set() +``` + +Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики: + +```python +PLAYER_SYMBOL = "P" +PATH_SYMBOL = "·" +``` + +## 5.2. Паттерн Command + +### Идея + +Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной. + +### Класс `Player` + +Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке. + +### Класс `Command` + +Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю. + +### Класс `MoveCommand` + +Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`. + +Направления вынесены в словарь `DIRECTIONS` на уровне модуля: + +```python +DIRECTIONS = { + "w": (0, -1), # вверх + "s": (0, 1), # вниз + "a": (-1, 0), # влево + "d": (1, 0), # вправо +} +``` + +### Класс `CommandHistory` + +Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`. + +Пример игрового цикла: + +```python +cmd = MoveCommand(player, 'd', maze) +if cmd.execute(): + history.push(cmd) # добавляем только успешный ход + +history.undo() # отмена последнего хода +``` +# Этап 6. Экспериментальная часть + +В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (`practice/main.ipynb`). + +## Подготовка + +Лабиринты загружаются из папки `source/templates` автоматически — все файлы считываются через `os.listdir` и передаются в `TextFileBuilder`. Стратегии собраны в словарь для удобной итерации: + +```python +strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), +} +``` + +## Замеры + +Каждая пара лабиринт + стратегия запускается **10 раз**, результаты усредняются. Это сглаживает разброс из-за кэширования и фоновой активности системы. + +Лабиринты типа `noexit` пропускаются автоматически — стратегия выбрасывает `ValueError`, который перехватывается через `try/except`, и выполнение продолжается. + +Результаты собираются в список словарей и затем преобразуются в `DataFrame` через pandas. + +## Результаты + +### 10×10 (простой путь) + +На маленьких лабиринтах все три алгоритма показывают практически одинаковое время (~0.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A\* давала преимущество. + +### 50×50 (тупики) + +BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A\* показывает время между BFS и DFS. + +### 100×100 (запутанный, spaghetti) + +Наиболее показательные результаты: + +| Стратегия | Время (мс) | Длина пути | +|-------------|--------------|--------------| +| BFS | ~9 | ~210 | +| DFS | ~7 | ~2200 | +| A\* | ~8 | ~210 | + +DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A\* находят кратчайший путь, A\* при этом чуть быстрее за счёт эвристики. + +### 30×30 (пустой) + +Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A\*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных. + +A\* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет. + +## Выводы + +- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо. +- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути. +- **A**\* — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь. + +## Визуализация + +![[results.png]] +# Этап 7. Отчёт + +## Описание задачи + +Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF: + +| Паттерн | Где применён | +|--------------|-----------------------------------------------| +| **Builder** | `TextFileBuilder` | +| **Strategy** | `BFSStrategy`, `DFSStrategy`, `AStarStrategy` | +| **Observer** | `ConsoleView` | +| **Command** | `MoveCommand`, `CommandHistory` | + +## Диаграмма классов + +## Результаты экспериментов + +| Лабиринт | Быстрее всех | Кратчайший путь | +|-------------------|----------------|-------------------| +| 10×10 path | все одинаково | все одинаково | +| 50×50 deadends | BFS | BFS = A\* | +| 100×100 spaghetti | DFS | BFS = A\* | +| 30×30 empty | DFS | BFS = A\* | + +- **BFS** — надёжный выбор, всегда кратчайший путь. +- **DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток. +- **A\*** — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`. + +## Выводы + +Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода. +# Задание + +## Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +## Общая схема приложения (пример) + +## Выполнение + +### Этап 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')` для перерисовки. + +# API Reference + +## Базовые модели + +### *class* source.models.base.Cell(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Базовые классы: `object` + +Представляет одну клетку поля лабиринта. + +Каждая клетка хранит свои координаты и один из четырёх возможных +типов: стена, старт, выход или пустая клетка. Типы взаимоисключают +друг друга: установка одного автоматически сбрасывает остальные. + +#### \_\_init_\_(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Инициализирует клетку с заданными координатами и типом. + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. + * **is_wall** – Если True — клетка является стеной. + * **is_start** – Если True — клетка является стартом. + * **is_exit** – Если True — клетка является выходом. + +#### *property* is_exit *: bool* + +True, если клетка является выходом из лабиринта. + +#### is_possible() → bool + +Проверяет, можно ли переместиться в эту клетку. + +* **Результат:** + True, если клетка не является стеной, иначе False. + +#### *property* is_start *: bool* + +True, если клетка является стартовой позицией. + +#### *property* is_wall *: bool* + +True, если клетка является стеной. + +### *class* source.models.base.Maze(size: tuple[int, int] = (10, 10)) + +Базовые классы: `object` + +Представляет двумерный лабиринт из клеток Cell. + +Лабиринт хранится как список списков клеток. Доступ к отдельным +клеткам и их изменение возможны через индексацию вида maze[row, col]. + +#### \_\_init_\_(size: tuple[int, int] = (10, 10)) + +Создаёт пустой лабиринт заданного размера. + +* **Параметры:** + **size** – Кортеж (width, height) — ширина и высота лабиринта в клетках. + +#### *property* exit *: [Cell](#source.models.base.Cell) | None* + +#### get_cell(x: int, y: int) → [Cell](#source.models.base.Cell) | None + +Возвращает клетку по координатам или None, если координаты вне границ. + +* **Параметры:** + * **x** – Координата по оси X. + * **y** – Координата по оси Y. +* **Результат:** + Объект Cell, если координаты корректны, иначе None. + +#### get_neighbors(x: int, y: int) → list[[Cell](#source.models.base.Cell)] | None + +Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево). + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. +* **Результат:** + Список проходимых соседних клеток, или None если (x, y) вне границ. + +#### *property* shape *: tuple[int, int]* + +#### *property* start *: [Cell](#source.models.base.Cell) | None* + +## Загрузка лабиринта + +### *class* source.build.builder.MazeBuilder + +Базовые классы: `ABC` + +#### *abstractmethod* build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +### *class* source.build.builder.TextFileBuilder + +Базовые классы: [`MazeBuilder`](#source.build.builder.MazeBuilder) + +#### build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +## Стратегии поиска пути + +### *class* source.strategy.algorithms.PathFindingStrategy + +Базовые классы: `ABC` + +Интерфейс стратегии поиска пути в лабиринте. + +#### *abstractmethod* find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.bfs.BFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в ширину (Breadth-First Search). + +Гарантирует кратчайший путь по количеству шагов. +Сложность: O(V + E) по времени и памяти. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.dfs.DFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в глубину (Depth-First Search). + +Находит путь, но не гарантирует кратчайший. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.astar.AStarStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Алгоритм A\* с манхэттенской эвристикой. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + +## Оркестратор + +### *class* source.strategy.solver.MazeSolver(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) + +Базовые классы: `object` + +Оркестратор поиска пути в лабиринте. + +Принимает лабиринт и стратегию поиска, выполняет поиск +и возвращает результат вместе со статистикой выполнения. + +### Пример + +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) + +solver.set_strategy(AStarStrategy()) +stats = solver.solve() + +#### \_\_init_\_(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Инициализирует солвер с лабиринтом и стратегией поиска. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **strategy** – Стратегия поиска пути. + +#### set_strategy(strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Заменяет текущую стратегию поиска. + +* **Параметры:** + **strategy** – Новая стратегия поиска пути. + +#### solve(start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → [SearchStats](#source.strategy.solver.SearchStats) + +Выполняет поиск пути и собирает статистику. + +Если start или exit не переданы явно, стратегия найдёт +их самостоятельно по флагам is_start / is_exit в лабиринте. + +* **Параметры:** + * **start** – Стартовая клетка (опционально). + * **exit** – Конечная клетка (опционально). +* **Результат:** + Объект SearchStats с временем выполнения, количеством + посещённых клеток и длиной найденного пути. + +### *class* source.strategy.solver.SearchStats(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) + +Базовые классы: `object` + +Статистика выполнения поиска пути. + +#### elapsed_ms + +Время выполнения в миллисекундах. + +* **Type:** + float + +#### visited_count + +Количество посещённых клеток. + +* **Type:** + int + +#### path_length + +Длина найденного пути (0 если путь не найден). + +* **Type:** + int + +#### path + +Найденный путь — список клеток от старта до выхода. + +* **Type:** + list[[source.models.base.Cell](#source.models.base.Cell)] + +#### \_\_init_\_(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) → None + +#### elapsed_ms *: float* + +#### path *: list[[Cell](#source.models.base.Cell)]* + +#### path_length *: int* + +#### visited_count *: int* + +## Визуализация + +### *class* source.view.observer.ConsoleView + +Базовые классы: [`Observer`](#source.view.observer.Observer) + +Отображает состояние лабиринта и события в консоли. + +#### PATH_SYMBOL *= '·'* + +#### PLAYER_SYMBOL *= 'P'* + +#### render(maze: [Maze](#source.models.base.Maze), player: [Cell](#source.models.base.Cell) | None = None, path: list[[Cell](#source.models.base.Cell)] | None = None) → None + +Рисует лабиринт в консоли. + +Путь отмечается символом „·“, позиция игрока — „P“. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **player** – Текущая клетка игрока (опционально). + * **path** – Список клеток найденного пути (опционально). + +#### update(event: [Event](#source.view.observer.Event)) → None + +Реагирует на события и выводит информацию в консоль. + +* **Параметры:** + **event** – Объект события. + +### *class* source.view.observer.Event(type: str, payload: dict = None) + +Базовые классы: `object` + +Событие, передаваемое наблюдателям. + +#### type + +Тип события („maze_loaded“, „path_found“, „move“, „no_path“). + +* **Type:** + str + +#### payload + +Дополнительные данные события. + +* **Type:** + dict + +#### \_\_init_\_(type: str, payload: dict = None) → None + +#### payload *: dict* *= None* + +#### type *: str* + +### *class* source.view.observer.Observer + +Базовые классы: `ABC` + +Интерфейс наблюдателя за событиями лабиринта. + +#### *abstractmethod* update(event: [Event](#source.view.observer.Event)) → None + +Обрабатывает входящее событие. + +* **Параметры:** + **event** – Объект события с типом и данными. + +## Управление игроком + +### *class* source.view.command.Command + +Базовые классы: `ABC` + +Интерфейс команды с поддержкой отмены. + +#### *abstractmethod* execute() → bool + +Выполняет команду. + +* **Результат:** + True если команда выполнена успешно, False иначе. + +#### *abstractmethod* undo() → None + +Отменяет команду, восстанавливая предыдущее состояние. + +### *class* source.view.command.CommandHistory + +Базовые классы: `object` + +Хранит историю выполненных команд и позволяет отменять их. + +### Пример + +history = CommandHistory() +cmd = MoveCommand(player, „w“, maze) +if cmd.execute(): + +> history.push(cmd) + +history.undo() # отменяет последний успешный ход + +#### \_\_init_\_() → None + +#### clear() → None + +Очищает историю команд. + +#### push(command: [Command](#source.view.command.Command)) → None + +Добавляет выполненную команду в историю. + +* **Параметры:** + **command** – Успешно выполненная команда. + +#### undo() → bool + +Отменяет последнюю команду из истории. + +* **Результат:** + True если отмена выполнена, False если история пуста. + +### *class* source.view.command.MoveCommand(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) + +Базовые классы: [`Command`](#source.view.command.Command) + +Перемещает игрока в заданном направлении. + +Сохраняет предыдущую клетку для возможности отмены хода. + +#### \_\_init_\_(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) → None + +Инициализирует команду перемещения. + +* **Параметры:** + * **player** – Объект игрока. + * **direction** – Направление („w“, „a“, „s“, „d“). + * **maze** – Объект лабиринта для проверки проходимости. +* **Исключение:** + **ValueError** – Если направление не распознано. + +#### execute() → bool + +Перемещает игрока если целевая клетка проходима. + +* **Результат:** + True если перемещение выполнено, False если клетка непроходима. + +#### undo() → None + +Возвращает игрока на предыдущую клетку. + +### *class* source.view.command.Player(cell: [Cell](#source.models.base.Cell)) + +Базовые классы: `object` + +Хранит текущее положение игрока в лабиринте. + +#### cell + +Текущая клетка игрока. + +#### \_\_init_\_(cell: [Cell](#source.models.base.Cell)) → None + +Инициализирует игрока на заданной клетке. + +* **Параметры:** + **cell** – Начальная клетка игрока. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/_build/markdown/api.md b/skorohodovsa/task_2/docs/_build/markdown/api.md new file mode 100644 index 0000000..3fcd9a9 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/api.md @@ -0,0 +1,470 @@ +# API Reference + +## Базовые модели + +### *class* source.models.base.Cell(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Базовые классы: `object` + +Представляет одну клетку поля лабиринта. + +Каждая клетка хранит свои координаты и один из четырёх возможных +типов: стена, старт, выход или пустая клетка. Типы взаимоисключают +друг друга: установка одного автоматически сбрасывает остальные. + +#### \_\_init_\_(x: int, y: int, is_wall: bool = False, is_start: bool = False, is_exit: bool = False) + +Инициализирует клетку с заданными координатами и типом. + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. + * **is_wall** – Если True — клетка является стеной. + * **is_start** – Если True — клетка является стартом. + * **is_exit** – Если True — клетка является выходом. + +#### *property* is_exit *: bool* + +True, если клетка является выходом из лабиринта. + +#### is_possible() → bool + +Проверяет, можно ли переместиться в эту клетку. + +* **Результат:** + True, если клетка не является стеной, иначе False. + +#### *property* is_start *: bool* + +True, если клетка является стартовой позицией. + +#### *property* is_wall *: bool* + +True, если клетка является стеной. + +### *class* source.models.base.Maze(size: tuple[int, int] = (10, 10)) + +Базовые классы: `object` + +Представляет двумерный лабиринт из клеток Cell. + +Лабиринт хранится как список списков клеток. Доступ к отдельным +клеткам и их изменение возможны через индексацию вида maze[row, col]. + +#### \_\_init_\_(size: tuple[int, int] = (10, 10)) + +Создаёт пустой лабиринт заданного размера. + +* **Параметры:** + **size** – Кортеж (width, height) — ширина и высота лабиринта в клетках. + +#### *property* exit *: [Cell](#source.models.base.Cell) | None* + +#### get_cell(x: int, y: int) → [Cell](#source.models.base.Cell) | None + +Возвращает клетку по координатам или None, если координаты вне границ. + +* **Параметры:** + * **x** – Координата по оси X. + * **y** – Координата по оси Y. +* **Результат:** + Объект Cell, если координаты корректны, иначе None. + +#### get_neighbors(x: int, y: int) → list[[Cell](#source.models.base.Cell)] | None + +Возвращает список проходимых соседей клетки (вверх, вправо, вниз, влево). + +* **Параметры:** + * **x** – Координата клетки по оси X. + * **y** – Координата клетки по оси Y. +* **Результат:** + Список проходимых соседних клеток, или None если (x, y) вне границ. + +#### *property* shape *: tuple[int, int]* + +#### *property* start *: [Cell](#source.models.base.Cell) | None* + +## Загрузка лабиринта + +### *class* source.build.builder.MazeBuilder + +Базовые классы: `ABC` + +#### *abstractmethod* build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +### *class* source.build.builder.TextFileBuilder + +Базовые классы: [`MazeBuilder`](#source.build.builder.MazeBuilder) + +#### build_from_file(filename: str) → [Maze](#source.models.base.Maze) + +Возвращает объект лабиринта по указанному пути файлу. + +* **Параметры:** + **filename** (*str*) – Путь к файлу +* **Исключение:** + **TypeError** – Если введен путь файла с нерассмотренным расширением +* **Результат:** + Объект лабиринта +* **Тип результата:** + [Maze](#source.models.base.Maze) + +## Стратегии поиска пути + +### *class* source.strategy.algorithms.PathFindingStrategy + +Базовые классы: `ABC` + +Интерфейс стратегии поиска пути в лабиринте. + +#### *abstractmethod* find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.bfs.BFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в ширину (Breadth-First Search). + +Гарантирует кратчайший путь по количеству шагов. +Сложность: O(V + E) по времени и памяти. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.dfs.DFSStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Поиск в глубину (Depth-First Search). + +Находит путь, но не гарантирует кратчайший. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + + + +### *class* source.strategy.astar.AStarStrategy + +Базовые классы: [`PathFindingStrategy`](#source.strategy.algorithms.PathFindingStrategy) + +Алгоритм A\* с манхэттенской эвристикой. + +#### find_path(maze: [Maze](#source.models.base.Maze), start: [Cell](#source.models.base.Cell) | None = None, exit: [Cell](#source.models.base.Cell) | None = None) → list[[Cell](#source.models.base.Cell)] + +Найти путь от start до exit. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **start** – Стартовая клетка. + * **exit** – Целевая клетка. +* **Результат:** + Список клеток пути (от start до exit включительно). + Пустой список, если путь не найден. + +## Оркестратор + +### *class* source.strategy.solver.MazeSolver(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) + +Базовые классы: `object` + +Оркестратор поиска пути в лабиринте. + +Принимает лабиринт и стратегию поиска, выполняет поиск +и возвращает результат вместе со статистикой выполнения. + +### Пример + +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) + +solver.set_strategy(AStarStrategy()) +stats = solver.solve() + +#### \_\_init_\_(maze: [Maze](#source.models.base.Maze), strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Инициализирует солвер с лабиринтом и стратегией поиска. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **strategy** – Стратегия поиска пути. + +#### set_strategy(strategy: [PathFindingStrategy](#source.strategy.algorithms.PathFindingStrategy)) → None + +Заменяет текущую стратегию поиска. + +* **Параметры:** + **strategy** – Новая стратегия поиска пути. + +#### solve(start: [Cell](#source.models.base.Cell) = None, exit: [Cell](#source.models.base.Cell) = None) → [SearchStats](#source.strategy.solver.SearchStats) + +Выполняет поиск пути и собирает статистику. + +Если start или exit не переданы явно, стратегия найдёт +их самостоятельно по флагам is_start / is_exit в лабиринте. + +* **Параметры:** + * **start** – Стартовая клетка (опционально). + * **exit** – Конечная клетка (опционально). +* **Результат:** + Объект SearchStats с временем выполнения, количеством + посещённых клеток и длиной найденного пути. + +### *class* source.strategy.solver.SearchStats(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) + +Базовые классы: `object` + +Статистика выполнения поиска пути. + +#### elapsed_ms + +Время выполнения в миллисекундах. + +* **Type:** + float + +#### visited_count + +Количество посещённых клеток. + +* **Type:** + int + +#### path_length + +Длина найденного пути (0 если путь не найден). + +* **Type:** + int + +#### path + +Найденный путь — список клеток от старта до выхода. + +* **Type:** + list[[source.models.base.Cell](#source.models.base.Cell)] + +#### \_\_init_\_(elapsed_ms: float, visited_count: int, path_length: int, path: list[[Cell](#source.models.base.Cell)]) → None + +#### elapsed_ms *: float* + +#### path *: list[[Cell](#source.models.base.Cell)]* + +#### path_length *: int* + +#### visited_count *: int* + +## Визуализация + +### *class* source.view.observer.ConsoleView + +Базовые классы: [`Observer`](#source.view.observer.Observer) + +Отображает состояние лабиринта и события в консоли. + +#### PATH_SYMBOL *= '·'* + +#### PLAYER_SYMBOL *= 'P'* + +#### render(maze: [Maze](#source.models.base.Maze), player: [Cell](#source.models.base.Cell) | None = None, path: list[[Cell](#source.models.base.Cell)] | None = None) → None + +Рисует лабиринт в консоли. + +Путь отмечается символом „·“, позиция игрока — „P“. + +* **Параметры:** + * **maze** – Объект лабиринта. + * **player** – Текущая клетка игрока (опционально). + * **path** – Список клеток найденного пути (опционально). + +#### update(event: [Event](#source.view.observer.Event)) → None + +Реагирует на события и выводит информацию в консоль. + +* **Параметры:** + **event** – Объект события. + +### *class* source.view.observer.Event(type: str, payload: dict = None) + +Базовые классы: `object` + +Событие, передаваемое наблюдателям. + +#### type + +Тип события („maze_loaded“, „path_found“, „move“, „no_path“). + +* **Type:** + str + +#### payload + +Дополнительные данные события. + +* **Type:** + dict + +#### \_\_init_\_(type: str, payload: dict = None) → None + +#### payload *: dict* *= None* + +#### type *: str* + +### *class* source.view.observer.Observer + +Базовые классы: `ABC` + +Интерфейс наблюдателя за событиями лабиринта. + +#### *abstractmethod* update(event: [Event](#source.view.observer.Event)) → None + +Обрабатывает входящее событие. + +* **Параметры:** + **event** – Объект события с типом и данными. + +## Управление игроком + +### *class* source.view.command.Command + +Базовые классы: `ABC` + +Интерфейс команды с поддержкой отмены. + +#### *abstractmethod* execute() → bool + +Выполняет команду. + +* **Результат:** + True если команда выполнена успешно, False иначе. + +#### *abstractmethod* undo() → None + +Отменяет команду, восстанавливая предыдущее состояние. + +### *class* source.view.command.CommandHistory + +Базовые классы: `object` + +Хранит историю выполненных команд и позволяет отменять их. + +### Пример + +history = CommandHistory() +cmd = MoveCommand(player, „w“, maze) +if cmd.execute(): + +> history.push(cmd) + +history.undo() # отменяет последний успешный ход + +#### \_\_init_\_() → None + +#### clear() → None + +Очищает историю команд. + +#### push(command: [Command](#source.view.command.Command)) → None + +Добавляет выполненную команду в историю. + +* **Параметры:** + **command** – Успешно выполненная команда. + +#### undo() → bool + +Отменяет последнюю команду из истории. + +* **Результат:** + True если отмена выполнена, False если история пуста. + +### *class* source.view.command.MoveCommand(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) + +Базовые классы: [`Command`](#source.view.command.Command) + +Перемещает игрока в заданном направлении. + +Сохраняет предыдущую клетку для возможности отмены хода. + +#### \_\_init_\_(player: [Player](#source.view.command.Player), direction: str, maze: [Maze](#source.models.base.Maze)) → None + +Инициализирует команду перемещения. + +* **Параметры:** + * **player** – Объект игрока. + * **direction** – Направление („w“, „a“, „s“, „d“). + * **maze** – Объект лабиринта для проверки проходимости. +* **Исключение:** + **ValueError** – Если направление не распознано. + +#### execute() → bool + +Перемещает игрока если целевая клетка проходима. + +* **Результат:** + True если перемещение выполнено, False если клетка непроходима. + +#### undo() → None + +Возвращает игрока на предыдущую клетку. + +### *class* source.view.command.Player(cell: [Cell](#source.models.base.Cell)) + +Базовые классы: `object` + +Хранит текущее положение игрока в лабиринте. + +#### cell + +Текущая клетка игрока. + +#### \_\_init_\_(cell: [Cell](#source.models.base.Cell)) → None + +Инициализирует игрока на заданной клетке. + +* **Параметры:** + **cell** – Начальная клетка игрока. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage1.md b/skorohodovsa/task_2/docs/_build/markdown/stage1.md new file mode 100644 index 0000000..7597841 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage1.md @@ -0,0 +1,91 @@ +# Этап 1. Модель лабиринта + +В первом этапе разработки необходимо создать базовые классы `Cell` и `Maze`, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы. + +## Класс `Cell` + +Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая. + +По условию задания клетка должна иметь флаги `isWall`, `isStart`, `isExit` и метод `isPassable()`. В реализации флаги оформлены как **свойства** (`@property`) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа. + +```python +cell = Cell(1, 1) +cell.is_wall = True +``` + +Типы клетки взаимоисключают друг друга — клетка не может быть одновременно стеной и стартом. Логика сброса вынесена в приватный метод `_clear_flags()`. + +### Символьное представление + +Для вывода лабиринта в консоль каждая клетка возвращает символ через `__str__`. Символы берутся из `cell_mapping` в `source/settings.py`, что позволяет менять отображение без правки классов: + +| Тип | Символ по умолчанию | +|--------|-----------------------| +| Стена | `#` | +| Старт | `S` | +| Выход | `E` | +| Пустая | | + +## Класс `Maze` + +Лабиринт хранит двумерный список клеток и предоставляет методы для работы с ними. + +По условию задания требовались методы `getCell(x, y)` и `getNeighbors(cell)`. В реализации добавлено несколько вещей сверх задания: + +### Именование методов + +Задание написано в стиле Java/pseudocode — названия методов и полей используют `camelCase` (`isWall`, `getCell`, `isPassable`). В Python принят другой стандарт именования — **PEP 8**, который предписывает `snake_case` для методов и атрибутов. Поэтому все названия были приведены к Python стилю: + +| Задание | Реализация | +|---------------------------|-----------------------------| +| `isWall` | `is_wall` | +| `isStart` | `is_start` | +| `isExit` | `is_exit` | +| `isPassable()` | `is_possible()` | +| `getCell(x, y)` | `get_cell(x, y)` | +| `getNeighbors(cell)` | `get_neighbors(x, y)` | +| `buildFromFile(filename)` | `build_from_file(filename)` | + +Это соответствует стандарту оформления кода на Python и делает API классов идиоматичным для языка. + +### Индексация `maze[row, col]` + +Вместо явного вызова `get_cell()` реализованы `__getitem__` и `__setitem__`, что позволяет обращаться к клеткам естественным образом: + +```python +maze[0, 0] = cell_mapping['wall'] # установить стену +cell = maze[2, 3] # получить клетку +``` + +Обратите внимание: индексация идёт в формате `[row, col]`, то есть сначала строка (Y), потом столбец (X) — аналогично numpy. + +### Свойства `start` и `exit` + +Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода: + +```python +maze.start # Cell или None +maze.exit # Cell или None +``` + +Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают `start` и `exit` автоматически из лабиринта. + +### Свойство `shape` + +По аналогии с numpy добавлено свойство `shape`, возвращающее `(height, width)`: + +```python +rows, cols = maze.shape +``` + +Используется в стратегиях поиска и тестах для итерации по лабиринту. + +### `get_neighbors` + +Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает `None`. + +```python +neighbors = maze.get_neighbors(2, 2) # список Cell +``` + +Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS). diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage2.md b/skorohodovsa/task_2/docs/_build/markdown/stage2.md new file mode 100644 index 0000000..c605d04 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage2.md @@ -0,0 +1,60 @@ +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна **Builder**. + +## Паттерн Builder + +Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод `build_from_file()`, внутри которого сосредоточена вся логика построения. + +Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию `MazeBuilder` без изменения остального кода. + +## Класс `MazeBuilder` + +Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод `build_from_file()`, который обязан реализовать каждый конкретный строитель. + +По условию задания интерфейс назывался `MazeBuilder` с методом `buildFromFile`. В реализации название метода приведено к **PEP 8** — `build_from_file`. Сам класс оформлен через `ABC` — попытка создать объект `MazeBuilder()` напрямую вызовет `TypeError`. + +## Класс `TextFileBuilder` + +Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из `.txt` файла где `#` — стена, — проход, `S` — старт, `E` — выход. + +Процесс построения разбит на три приватных шага: + +### `_read_file` + +Читает файл построчно и обрезает символы переноса строки `\n` и `\r`. Возвращает список строк — каждая строка соответствует одной строке лабиринта. + +### `_test_text_maze` + +Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и `_create_maze` выбросит `ValueError`. + +Реализован как `@staticmethod` — не использует состояние объекта, только входные данные. + +### `_create_maze` + +Создаёт объект `Maze` нужного размера и заполняет его клетки символами из файла через `maze[y, x] = symbol`. Тип каждой клетки определяется автоматически через `cell_mapping` в `__setitem__` лабиринта. + +## Использование + +```python +from source.build.builder import TextFileBuilder + +maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt') +print(maze) +``` + +## Известная ошибка + +В текущей реализации `_create_maze` есть опечатка при вычислении `width`: + +```python +height, width = len(text_maze), len(text_maze) # width всегда равен height +``` + +Правильная версия: + +```python +height, width = len(text_maze), len(text_maze[0]) +``` + +На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage3.md b/skorohodovsa/task_2/docs/_build/markdown/stage3.md new file mode 100644 index 0000000..8a52b27 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage3.md @@ -0,0 +1,90 @@ +# Этап 3. Стратегии поиска пути + +В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**. + +## Паттерн Strategy + +Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии: + +```python +solver = MazeSolver(maze, BFSStrategy()) +solver.set_strategy(AStarStrategy()) +``` + +Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно. + +## Структура пакета + +Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт: + +```default +source/strategy/ +├── __init__.py ← единственный импорт для пользователя +├── algorithms.py ← базовый класс PathFindingStrategy +├── bfs.py +├── dfs.py +└── astar.py +``` + +```python +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy +``` + +## Класс `PathFindingStrategy` + +Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод `find_path()` и содержит два вспомогательных метода, общих для всех стратегий. + +По условию задания метод назывался `findPath` — приведён к **PEP 8** как `find_path`. + +### `_validate` + +Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа `noexit`: алгоритм падал с `AttributeError` внутри, вместо понятного сообщения. + +`_validate` подставляет `start` и `exit` из лабиринта если они не переданы явно, и выбрасывает `ValueError` с понятным сообщением если клетки не найдены: + +```python +start, exit = self._validate(maze, start, exit) +``` + +Вынесен в базовый класс чтобы не дублировать в каждом алгоритме. + +### `_reconstruct_path` + +Восстанавливает путь по словарю предков `came_from`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса. + +Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список: + +```default +exit → D → C → B → start (идём по came_from) +start → B → C → D → exit (после reverse) +``` + +## Алгоритмы + +### BFS — `BFSStrategy` + +Поиск в ширину. Использует `deque` как очередь (FIFO) — каждый раз берём самую старую клетку из начала. Это гарантирует послойный обход и кратчайший путь по количеству шагов. + +Сложность: O(V + E) по времени и памяти. + +### DFS — `DFSStrategy` + +Поиск в глубину. Использует `list` как стек (LIFO) — каждый раз берём самую свежую клетку с конца. Алгоритм ныряет вглубь по одному направлению до тупика, затем возвращается. + +Не гарантирует кратчайший путь. На запутанных лабиринтах может обойти почти все клетки прежде чем найти выход, хотя по времени часто быстрее BFS из-за меньших накладных расходов на структуру данных. + +Сложность: O(V + E) по времени и памяти. + +### A\* — `AStarStrategy` + +Использует `heapq` как приоритетную очередь. На каждом шаге выбирает клетку с минимальным значением `f = g + h`, где `g` — стоимость пути от старта, `h` — манхэттенская эвристика до выхода. + +Эвристика направляет поиск в сторону выхода, поэтому A\* обходит меньше клеток чем BFS при том же гарантированно кратчайшем пути. + +В кортеж приоритетной очереди добавлен счётчик `counter` как tie-breaker — без него `heapq` попытался бы сравнивать объекты `Cell` при одинаковом `f`, что вызвало бы `TypeError`: + +```python +heapq.heappush(open_heap, (f, counter, neighbor)) +``` + +Сложность: O(E · log V) в худшем случае. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage4.md b/skorohodovsa/task_2/docs/_build/markdown/stage4.md new file mode 100644 index 0000000..cc800b4 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage4.md @@ -0,0 +1,44 @@ +# Этап 4. Класс-оркестратор MazeSolver + +В четвёртом этапе реализован класс `MazeSolver`, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику. + +## Роль в архитектуре + +`MazeSolver` — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) +# Время: 0.041 мс | Посещено клеток: 13 | Длина пути: 13 +``` + +## Класс `SearchStats` + +Оформлен через `@dataclass` — это избавляет от ручного `__init__` и автоматически даёт `__repr__`. Хранит четыре поля: время выполнения, количество посещённых клеток, длину пути и сам путь как список клеток. + +`__str__` переопределён для удобного вывода в консоль и отчётах. + +### Ограничение + +В текущей реализации `visited_count` и `path_length` всегда равны друг другу — оба вычисляются как `len(path)`. Это потому что стратегии возвращают только финальный путь, а не все посещённые клетки. Чтобы получить точное количество посещений, потребовалось бы дорабатывать каждую стратегию — добавлять счётчик внутри `find_path`. На данном этапе это сознательное упрощение. + +## Класс `MazeSolver` + +### `set_strategy` + +Позволяет менять алгоритм без пересоздания солвера. Это и есть основная демонстрация паттерна Strategy в действии — один объект, разные алгоритмы: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats_bfs = solver.solve() + +solver.set_strategy(AStarStrategy()) +stats_astar = solver.solve() +``` + +### `solve` + +Замеряет время через `time.perf_counter()` — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000. + +`start` и `exit` можно не передавать — стратегия найдёт их сама через `_validate`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки. diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage5.md b/skorohodovsa/task_2/docs/_build/markdown/stage5.md new file mode 100644 index 0000000..0ce2cc4 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage5.md @@ -0,0 +1,84 @@ +# Этап 5. Визуализация и пошаговое управление + +В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком. + +## 5.1. Паттерн Observer + +### Идея + +`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода. + +### Класс `Event` + +Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий: + +| Тип | Когда генерируется | +|---------------|----------------------------| +| `maze_loaded` | Лабиринт загружен из файла | +| `path_found` | Алгоритм нашёл путь | +| `no_path` | Путь не найден | +| `move` | Игрок сделал ход | + +### Класс `Observer` + +Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать. + +### Класс `ConsoleView` + +Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта. + +Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе: + +```python +path_set = set(path) if path else set() +``` + +Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики: + +```python +PLAYER_SYMBOL = "P" +PATH_SYMBOL = "·" +``` + +## 5.2. Паттерн Command + +### Идея + +Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной. + +### Класс `Player` + +Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке. + +### Класс `Command` + +Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю. + +### Класс `MoveCommand` + +Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`. + +Направления вынесены в словарь `DIRECTIONS` на уровне модуля: + +```python +DIRECTIONS = { + "w": (0, -1), # вверх + "s": (0, 1), # вниз + "a": (-1, 0), # влево + "d": (1, 0), # вправо +} +``` + +### Класс `CommandHistory` + +Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`. + +Пример игрового цикла: + +```python +cmd = MoveCommand(player, 'd', maze) +if cmd.execute(): + history.push(cmd) # добавляем только успешный ход + +history.undo() # отмена последнего хода +``` diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage6.md b/skorohodovsa/task_2/docs/_build/markdown/stage6.md new file mode 100644 index 0000000..4399a8b --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage6.md @@ -0,0 +1,61 @@ +# Этап 6. Экспериментальная часть + +В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (`practice/main.ipynb`). + +## Подготовка + +Лабиринты загружаются из папки `source/templates` автоматически — все файлы считываются через `os.listdir` и передаются в `TextFileBuilder`. Стратегии собраны в словарь для удобной итерации: + +```python +strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), +} +``` + +## Замеры + +Каждая пара лабиринт + стратегия запускается **10 раз**, результаты усредняются. Это сглаживает разброс из-за кэширования и фоновой активности системы. + +Лабиринты типа `noexit` пропускаются автоматически — стратегия выбрасывает `ValueError`, который перехватывается через `try/except`, и выполнение продолжается. + +Результаты собираются в список словарей и затем преобразуются в `DataFrame` через pandas. + +## Результаты + +### 10×10 (простой путь) + +На маленьких лабиринтах все три алгоритма показывают практически одинаковое время (~0.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A\* давала преимущество. + +### 50×50 (тупики) + +BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A\* показывает время между BFS и DFS. + +### 100×100 (запутанный, spaghetti) + +Наиболее показательные результаты: + +| Стратегия | Время (мс) | Длина пути | +|-------------|--------------|--------------| +| BFS | ~9 | ~210 | +| DFS | ~7 | ~2200 | +| A\* | ~8 | ~210 | + +DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A\* находят кратчайший путь, A\* при этом чуть быстрее за счёт эвристики. + +### 30×30 (пустой) + +Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A\*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных. + +A\* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет. + +## Выводы + +- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо. +- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути. +- **A**\* — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь. + +## Визуализация + +![[results.png]] diff --git a/skorohodovsa/task_2/docs/_build/markdown/stage7.md b/skorohodovsa/task_2/docs/_build/markdown/stage7.md new file mode 100644 index 0000000..9072b69 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/stage7.md @@ -0,0 +1,33 @@ +# Этап 7. Отчёт + +## Описание задачи + +Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF: + +| Паттерн | Где применён | +|--------------|-----------------------------------------------| +| **Builder** | `TextFileBuilder` | +| **Strategy** | `BFSStrategy`, `DFSStrategy`, `AStarStrategy` | +| **Observer** | `ConsoleView` | +| **Command** | `MoveCommand`, `CommandHistory` | + +## Диаграмма классов + +## Результаты экспериментов + +| Лабиринт | Быстрее всех | Кратчайший путь | +|-------------------|----------------|-------------------| +| 10×10 path | все одинаково | все одинаково | +| 50×50 deadends | BFS | BFS = A\* | +| 100×100 spaghetti | DFS | BFS = A\* | +| 30×30 empty | DFS | BFS = A\* | + +**BFS** — надёжный выбор, всегда кратчайший путь.
+\\\\ +**DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток.
+\\\\ +**A**\* — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`. + +## Выводы + +Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода. diff --git a/skorohodovsa/task_2/docs/_build/markdown/task.md b/skorohodovsa/task_2/docs/_build/markdown/task.md new file mode 100644 index 0000000..f30ed53 --- /dev/null +++ b/skorohodovsa/task_2/docs/_build/markdown/task.md @@ -0,0 +1,103 @@ +# Задание + +## Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +## Общая схема приложения (пример) + +## Выполнение + +### Этап 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')` для перерисовки. diff --git a/skorohodovsa/task_2/docs/source/api.md b/skorohodovsa/task_2/docs/source/api.md index 58caf5f..cf2f512 100644 --- a/skorohodovsa/task_2/docs/source/api.md +++ b/skorohodovsa/task_2/docs/source/api.md @@ -2,16 +2,25 @@ ## Базовые модели -````{eval-rst} +```{eval-rst} .. automodule:: source.models.base :members: :undoc-members: :show-inheritance: -```` +``` + +## Загрузка лабиринта + +```{eval-rst} +.. automodule:: source.build.builder + :members: + :undoc-members: + :show-inheritance: +``` ## Стратегии поиска пути -````{eval-rst} +```{eval-rst} .. automodule:: source.strategy.algorithms :members: :undoc-members: @@ -31,4 +40,31 @@ :members: :undoc-members: :show-inheritance: -```` \ No newline at end of file +``` + +## Оркестратор + +```{eval-rst} +.. automodule:: source.strategy.solver + :members: + :undoc-members: + :show-inheritance: +``` + +## Визуализация + +```{eval-rst} +.. automodule:: source.view.observer + :members: + :undoc-members: + :show-inheritance: +``` + +## Управление игроком + +```{eval-rst} +.. automodule:: source.view.command + :members: + :undoc-members: + :show-inheritance: + ``` \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/conf.py b/skorohodovsa/task_2/docs/source/conf.py index 4eeb0f1..8fcfe48 100644 --- a/skorohodovsa/task_2/docs/source/conf.py +++ b/skorohodovsa/task_2/docs/source/conf.py @@ -35,6 +35,7 @@ extensions = [ "sphinx.ext.mathjax", "sphinx_new_tab_link", "sphinx.ext.autosummary", + "sphinxcontrib.mermaid", ] autosummary_generate = True diff --git a/skorohodovsa/task_2/docs/source/index.md b/skorohodovsa/task_2/docs/source/index.md index a1f0950..0d882ed 100644 --- a/skorohodovsa/task_2/docs/source/index.md +++ b/skorohodovsa/task_2/docs/source/index.md @@ -1,6 +1,15 @@ # Лабораторная работа "Поиск выхода из лабиринта" + :::{toctree} :maxdepth: 2 -naming_maze -api \ No newline at end of file +task +stage1 +stage2 +stage3 +stage4 +stage5 +stage6 +stage7 +api + diff --git a/skorohodovsa/task_2/docs/source/naming_maze.md b/skorohodovsa/task_2/docs/source/naming_maze.md deleted file mode 100644 index 11b4e13..0000000 --- a/skorohodovsa/task_2/docs/source/naming_maze.md +++ /dev/null @@ -1,95 +0,0 @@ -# Этап 2. Загрузка лабиринта из файла - -Во втором этапе разработки необходимо реализовать загрузку лабиринта из текстового файла, где: `#` – стена, ` ` – проход, `S` – старт, `E` – выход. - -## Систематизация файлов - -Для удобного хранения лабиринтов было решено сделать систему наименования текстовых файлов в папке `source/templates`. - -Общая структура: - -``` -{размер}_{свойство 1}-{свойство 2}-{свойство n}_{версия}.txt -``` - -### Размер - -Формат: `{ширина}x{высота}` - -| Пример | Значение | -|--------|----------| -| `10x10` | 10×10 клеток | -| `50x50` | 50×50 клеток | -| `100x100` | 100×100 клеток | -| `30x30` | 30×30 клеток | -| `20x20` | 20×20 клеток | -### Свойства -| Свойство | Код | Описание | -| ------------ | ----------- | -------------------------------------------------------------------------------------------- | -| Простой путь | `path` | Существует маршрут от S до E | -| Тупики | `deadends` | Лабиринт специально содержит тупики (могут быть и в других типах, но здесь — гарантированно) | -| Запутанный | `spaghetti` | Сложная структура с циклами и ложными ходами | -| Пустой | `empty` | Нет стен (`#`), только пробелы, S и E | -| Без выхода | `noexit` | В лабиринте отсутствует символ `E` | -### Версия - -Формат: `v{номер}` -- `v1`, `v2`, `v10` -### Примеры -#### Маленькие (10×10, простой путь) - -``` -10x10_path_v1.txt -10x10_path_v2.txt -... -10x10_path_v10.txt -``` -#### Средние (50×50, тупики) - -``` -50x50_deadends_v1.txt -50x50_deadends_v2.txt -... -50x50_deadends_v10.txt -``` -#### Большие (100×100, запутанные) - -``` -100x100_spaghetti_v1.txt -100x100_spaghetti_v2.txt -... -100x100_spaghetti_v10.txt -``` - -#### Пустые (30×30) - -``` -30x30_empty_v1.txt -30x30_empty_v2.txt -... -30x30_empty_v10.txt -``` - -#### Без выхода (20×20) - -``` -20x20_noexit_v1.txt -20x20_noexit_v2.txt -... -20x20_noexit_v10.txt -``` - -#### Комбинированные свойства - -``` -50x50_deadends-noexit_v1.txt -100x100_spaghetti-noexit_v1.txt -10x10_path-empty_v1.txt (избыточно, но допустимо) -``` - -### Примечание - -- Регистр имён файлов: **нижний регистр** -- Разделители: только `_` и `-` -- Расширение: `.txt` -- Кодировка: UTF-8 \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage1.md b/skorohodovsa/task_2/docs/source/stage1.md new file mode 100644 index 0000000..0365c1c --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage1.md @@ -0,0 +1,91 @@ +# Этап 1. Модель лабиринта + +В первом этапе разработки необходимо создать базовые классы `Cell` и `Maze`, которые представляют карту лабиринта. Паттерны на этом этапе не применяются — только чистые классы. + +## Класс `Cell` + +Клетка — минимальная единица лабиринта. Хранит координаты и тип: стена, старт, выход или пустая. + +По условию задания клетка должна иметь флаги `isWall`, `isStart`, `isExit` и метод `isPassable()`. В реализации флаги оформлены как **свойства** (`@property`) с сеттерами — это позволяет автоматически сбрасывать остальные флаги при установке нового типа. + +```python +cell = Cell(1, 1) +cell.is_wall = True +``` + +Типы клетки взаимоисключают друг друга — клетка не может быть одновременно стеной и стартом. Логика сброса вынесена в приватный метод `_clear_flags()`. + +### Символьное представление + +Для вывода лабиринта в консоль каждая клетка возвращает символ через `__str__`. Символы берутся из `cell_mapping` в `source/settings.py`, что позволяет менять отображение без правки классов: + +|Тип|Символ по умолчанию| +|---|---| +|Стена|`#`| +|Старт|`S`| +|Выход|`E`| +|Пустая|| + +## Класс `Maze` + +Лабиринт хранит двумерный список клеток и предоставляет методы для работы с ними. + +По условию задания требовались методы `getCell(x, y)` и `getNeighbors(cell)`. В реализации добавлено несколько вещей сверх задания: + +### Именование методов + +Задание написано в стиле Java/pseudocode — названия методов и полей используют `camelCase` (`isWall`, `getCell`, `isPassable`). В Python принят другой стандарт именования — **PEP 8**, который предписывает `snake_case` для методов и атрибутов. Поэтому все названия были приведены к Python стилю: + +|Задание|Реализация| +|---|---| +|`isWall`|`is_wall`| +|`isStart`|`is_start`| +|`isExit`|`is_exit`| +|`isPassable()`|`is_possible()`| +|`getCell(x, y)`|`get_cell(x, y)`| +|`getNeighbors(cell)`|`get_neighbors(x, y)`| +|`buildFromFile(filename)`|`build_from_file(filename)`| + +Это соответствует стандарту оформления кода на Python и делает API классов идиоматичным для языка. + +### Индексация `maze[row, col]` + +Вместо явного вызова `get_cell()` реализованы `__getitem__` и `__setitem__`, что позволяет обращаться к клеткам естественным образом: + +```python +maze[0, 0] = cell_mapping['wall'] # установить стену +cell = maze[2, 3] # получить клетку +``` + +Обратите внимание: индексация идёт в формате `[row, col]`, то есть сначала строка (Y), потом столбец (X) — аналогично numpy. + +### Свойства `start` и `exit` + +Добавлены свойства для быстрого получения стартовой и выходной клетки без ручного обхода: + +```python +maze.start # Cell или None +maze.exit # Cell или None +``` + +Это оказалось необходимым при реализации алгоритмов поиска пути — стратегии получают `start` и `exit` автоматически из лабиринта. + +### Свойство `shape` + +По аналогии с numpy добавлено свойство `shape`, возвращающее `(height, width)`: + +```python +rows, cols = maze.shape +``` + +Используется в стратегиях поиска и тестах для итерации по лабиринту. + +### `get_neighbors` + +Метод возвращает список проходимых соседей клетки по четырём направлениям. Стены и клетки за границей лабиринта исключаются автоматически. Если переданные координаты вне границ — возвращает `None`. + +```python +neighbors = maze.get_neighbors(2, 2) # список Cell +``` + +Направления обхода: вниз → вправо → вверх → влево (порядок влияет на поведение DFS). \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage2.md b/skorohodovsa/task_2/docs/source/stage2.md new file mode 100644 index 0000000..db3d870 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage2.md @@ -0,0 +1,60 @@ +# Этап 2. Загрузка лабиринта из файла + +Во втором этапе реализована загрузка лабиринта из текстового файла с применением паттерна **Builder**. + +## Паттерн Builder + +Процесс создания лабиринта из файла включает несколько шагов: чтение файла, валидацию структуры, парсинг символов и заполнение клеток. Builder скрывает эти детали от клиента — снаружи виден только один метод `build_from_file()`, внутри которого сосредоточена вся логика построения. + +Дополнительное преимущество: в будущем можно легко добавить новый формат (например, JSON или бинарный) через новую реализацию `MazeBuilder` без изменения остального кода. + +## Класс `MazeBuilder` + +Абстрактный базовый класс — интерфейс паттерна Builder. Объявляет единственный метод `build_from_file()`, который обязан реализовать каждый конкретный строитель. + +По условию задания интерфейс назывался `MazeBuilder` с методом `buildFromFile`. В реализации название метода приведено к **PEP 8** — `build_from_file`. Сам класс оформлен через `ABC` — попытка создать объект `MazeBuilder()` напрямую вызовет `TypeError`. + +## Класс `TextFileBuilder` + +Конкретная реализация строителя для текстовых файлов. Загружает лабиринт из `.txt` файла где `#` — стена, — проход, `S` — старт, `E` — выход. + +Процесс построения разбит на три приватных шага: + +### `_read_file` + +Читает файл построчно и обрезает символы переноса строки `\n` и `\r`. Возвращает список строк — каждая строка соответствует одной строке лабиринта. + +### `_test_text_maze` + +Валидирует структуру: проверяет что все строки одинаковой длины. Если нет — лабиринт некорректен и `_create_maze` выбросит `ValueError`. + +Реализован как `@staticmethod` — не использует состояние объекта, только входные данные. + +### `_create_maze` + +Создаёт объект `Maze` нужного размера и заполняет его клетки символами из файла через `maze[y, x] = symbol`. Тип каждой клетки определяется автоматически через `cell_mapping` в `__setitem__` лабиринта. + +## Использование + +```python +from source.build.builder import TextFileBuilder + +maze = TextFileBuilder().build_from_file('source/templates/10x10_path_v1.txt') +print(maze) +``` + +## Известная ошибка + +В текущей реализации `_create_maze` есть опечатка при вычислении `width`: + +```python +height, width = len(text_maze), len(text_maze) # width всегда равен height +``` + +Правильная версия: + +```python +height, width = len(text_maze), len(text_maze[0]) +``` + +На квадратных лабиринтах (10×10, 50×50) это не проявляется, но на прямоугольных даст некорректный результат. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage3.md b/skorohodovsa/task_2/docs/source/stage3.md new file mode 100644 index 0000000..63b9dc3 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage3.md @@ -0,0 +1,90 @@ +# Этап 3. Стратегии поиска пути + +В третьем этапе реализованы алгоритмы поиска пути с применением паттерна **Strategy**. + +## Паттерн Strategy + +Все три алгоритма реализуют один интерфейс `PathFindingStrategy`. Это позволяет переключать алгоритм в любой момент без изменения кода клиента — достаточно передать другой объект стратегии: + +```python +solver = MazeSolver(maze, BFSStrategy()) +solver.set_strategy(AStarStrategy()) +``` + +Новый алгоритм добавляется реализацией интерфейса — остальной код трогать не нужно. + +## Структура пакета + +Стратегии разбиты по отдельным файлам, а `__init__.py` собирает всё в один импорт: + +``` +source/strategy/ +├── __init__.py ← единственный импорт для пользователя +├── algorithms.py ← базовый класс PathFindingStrategy +├── bfs.py +├── dfs.py +└── astar.py +``` + +```python +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy +``` + +## Класс `PathFindingStrategy` + +Абстрактный базовый класс — интерфейс паттерна. Объявляет абстрактный метод `find_path()` и содержит два вспомогательных метода, общих для всех стратегий. + +По условию задания метод назывался `findPath` — приведён к **PEP 8** как `find_path`. + +### `_validate` + +Добавлен в процессе разработки — изначально в задании не было требования к обработке отсутствия старта или выхода. Проблема проявилась при тестировании лабиринтов типа `noexit`: алгоритм падал с `AttributeError` внутри, вместо понятного сообщения. + +`_validate` подставляет `start` и `exit` из лабиринта если они не переданы явно, и выбрасывает `ValueError` с понятным сообщением если клетки не найдены: + +```python +start, exit = self._validate(maze, start, exit) +``` + +Вынесен в базовый класс чтобы не дублировать в каждом алгоритме. + +### `_reconstruct_path` + +Восстанавливает путь по словарю предков `came_from`. Все три алгоритма строят этот словарь одинаково — `{клетка: откуда_пришли}` — поэтому восстановление вынесено в общий метод базового класса. + +Алгоритм идёт от выхода к старту по цепочке предков, затем разворачивает список: + +``` +exit → D → C → B → start (идём по came_from) +start → B → C → D → exit (после reverse) +``` + +## Алгоритмы + +### BFS — `BFSStrategy` + +Поиск в ширину. Использует `deque` как очередь (FIFO) — каждый раз берём самую старую клетку из начала. Это гарантирует послойный обход и кратчайший путь по количеству шагов. + +Сложность: O(V + E) по времени и памяти. + +### DFS — `DFSStrategy` + +Поиск в глубину. Использует `list` как стек (LIFO) — каждый раз берём самую свежую клетку с конца. Алгоритм ныряет вглубь по одному направлению до тупика, затем возвращается. + +Не гарантирует кратчайший путь. На запутанных лабиринтах может обойти почти все клетки прежде чем найти выход, хотя по времени часто быстрее BFS из-за меньших накладных расходов на структуру данных. + +Сложность: O(V + E) по времени и памяти. + +### A* — `AStarStrategy` + +Использует `heapq` как приоритетную очередь. На каждом шаге выбирает клетку с минимальным значением `f = g + h`, где `g` — стоимость пути от старта, `h` — манхэттенская эвристика до выхода. + +Эвристика направляет поиск в сторону выхода, поэтому A* обходит меньше клеток чем BFS при том же гарантированно кратчайшем пути. + +В кортеж приоритетной очереди добавлен счётчик `counter` как tie-breaker — без него `heapq` попытался бы сравнивать объекты `Cell` при одинаковом `f`, что вызвало бы `TypeError`: + +```python +heapq.heappush(open_heap, (f, counter, neighbor)) +``` + +Сложность: O(E · log V) в худшем случае. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage4.md b/skorohodovsa/task_2/docs/source/stage4.md new file mode 100644 index 0000000..dc1dd66 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage4.md @@ -0,0 +1,44 @@ +# Этап 4. Класс-оркестратор MazeSolver + +В четвёртом этапе реализован класс `MazeSolver`, который объединяет лабиринт и стратегию поиска, выполняет поиск и собирает статистику. + +## Роль в архитектуре + +`MazeSolver` — точка входа для клиентского кода. Он не знает деталей ни одного алгоритма и не работает напрямую с клетками лабиринта — только делегирует задачу стратегии и замеряет время: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats = solver.solve() +print(stats) +# Время: 0.041 мс | Посещено клеток: 13 | Длина пути: 13 +``` + +## Класс `SearchStats` + +Оформлен через `@dataclass` — это избавляет от ручного `__init__` и автоматически даёт `__repr__`. Хранит четыре поля: время выполнения, количество посещённых клеток, длину пути и сам путь как список клеток. + +`__str__` переопределён для удобного вывода в консоль и отчётах. + +### Ограничение + +В текущей реализации `visited_count` и `path_length` всегда равны друг другу — оба вычисляются как `len(path)`. Это потому что стратегии возвращают только финальный путь, а не все посещённые клетки. Чтобы получить точное количество посещений, потребовалось бы дорабатывать каждую стратегию — добавлять счётчик внутри `find_path`. На данном этапе это сознательное упрощение. + +## Класс `MazeSolver` + +### `set_strategy` + +Позволяет менять алгоритм без пересоздания солвера. Это и есть основная демонстрация паттерна Strategy в действии — один объект, разные алгоритмы: + +```python +solver = MazeSolver(maze, BFSStrategy()) +stats_bfs = solver.solve() + +solver.set_strategy(AStarStrategy()) +stats_astar = solver.solve() +``` + +### `solve` + +Замеряет время через `time.perf_counter()` — самый точный таймер в Python для коротких интервалов, не зависящий от системных часов. Результат переводится в миллисекунды умножением на 1000. + +`start` и `exit` можно не передавать — стратегия найдёт их сама через `_validate`. Явная передача нужна только если хочется запустить поиск не от стандартного старта, а от произвольной клетки. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage5.md b/skorohodovsa/task_2/docs/source/stage5.md new file mode 100644 index 0000000..ecca7cd --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage5.md @@ -0,0 +1,84 @@ +# Этап 5. Визуализация и пошаговое управление + +В пятом этапе реализованы два паттерна: **Observer** для отображения событий и **Command** для пошагового управления игроком. + +## 5.1. Паттерн Observer + +### Идея + +`MazeSolver` и игровой цикл не знают как именно отображать происходящее — они просто генерируют события. Наблюдатели подписываются на эти события и реагируют по своему усмотрению. Это позволяет в будущем добавить, например, `FileLogger` или графический интерфейс без изменения основного кода. + +### Класс `Event` + +Оформлен через `@dataclass`. Хранит тип события строкой и словарь `payload` с дополнительными данными. Поддерживаются четыре типа событий: + +|Тип|Когда генерируется| +|---|---| +|`maze_loaded`|Лабиринт загружен из файла| +|`path_found`|Алгоритм нашёл путь| +|`no_path`|Путь не найден| +|`move`|Игрок сделал ход| + +### Класс `Observer` + +Абстрактный базовый класс с единственным методом `update(event)`. Любой наблюдатель обязан его реализовать. + +### Класс `ConsoleView` + +Конкретная реализация наблюдателя. Обрабатывает события через `match/case` и вызывает `render()` для перерисовки лабиринта. + +Метод `render()` принимает лабиринт, опциональную позицию игрока и опциональный путь. Путь преобразуется в `set` для быстрой проверки принадлежности клетки — это O(1) вместо O(n) при каждом обходе: + +```python +path_set = set(path) if path else set() +``` + +Лабиринт обрамляется рамкой из `+` и `─` для читаемости в консоли. Символы игрока и пути вынесены в константы класса — легко поменять без правки логики: + +```python +PLAYER_SYMBOL = "P" +PATH_SYMBOL = "·" +``` + +## 5.2. Паттерн Command + +### Идея + +Каждое перемещение игрока оборачивается в объект `MoveCommand`. Это позволяет сохранить предыдущее состояние и отменить ход — реализация `undo` становится тривиальной. + +### Класс `Player` + +Простой контейнер для текущей клетки игрока. Намеренно минималистичный — вся логика перемещения и проверок находится в команде, а не в игроке. + +### Класс `Command` + +Абстрактный интерфейс с двумя методами: `execute()` и `undo()`. `execute()` возвращает `bool` — это отличие от классического варианта паттерна, где команды не возвращают значений. Возврат `False` нужен чтобы не добавлять неуспешный ход в историю. + +### Класс `MoveCommand` + +Хранит ссылку на игрока, направление и лабиринт. При `execute()` проверяет проходимость целевой клетки, сохраняет текущую в `_prev_cell` и перемещает игрока. При `undo()` восстанавливает `_prev_cell`. + +Направления вынесены в словарь `DIRECTIONS` на уровне модуля: + +```python +DIRECTIONS = { + "w": (0, -1), # вверх + "s": (0, 1), # вниз + "a": (-1, 0), # влево + "d": (1, 0), # вправо +} +``` + +### Класс `CommandHistory` + +Стек выполненных команд. Хранит только успешные ходы — неуспешные (`execute()` вернул `False`) в историю не добавляются. `undo()` снимает последнюю команду со стека и вызывает её `undo()`. + +Пример игрового цикла: + +```python +cmd = MoveCommand(player, 'd', maze) +if cmd.execute(): + history.push(cmd) # добавляем только успешный ход + +history.undo() # отмена последнего хода +``` \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage6.md b/skorohodovsa/task_2/docs/source/stage6.md new file mode 100644 index 0000000..410842c --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage6.md @@ -0,0 +1,63 @@ +# Этап 6. Экспериментальная часть + +В шестом этапе проведено сравнение эффективности трёх стратегий поиска пути на лабиринтах разной сложности. Эксперимент реализован в Jupyter Notebook (`practice/main.ipynb`). + +## Подготовка + +Лабиринты загружаются из папки `source/templates` автоматически — все файлы считываются через `os.listdir` и передаются в `TextFileBuilder`. Стратегии собраны в словарь для удобной итерации: + +```python +strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), +} +``` + +## Замеры + +Каждая пара лабиринт + стратегия запускается **10 раз**, результаты усредняются. Это сглаживает разброс из-за кэширования и фоновой активности системы. + +Лабиринты типа `noexit` пропускаются автоматически — стратегия выбрасывает `ValueError`, который перехватывается через `try/except`, и выполнение продолжается. + +Результаты собираются в список словарей и затем преобразуются в `DataFrame` через pandas. + +## Результаты + +### 10×10 (простой путь) + +На маленьких лабиринтах все три алгоритма показывают практически одинаковое время (~0.03–0.07 мс) и одинаковую длину пути. Разница незначительна — лабиринт слишком мал чтобы эвристика A* давала преимущество. + +### 50×50 (тупики) + +BFS стабильно быстрее DFS по времени при одинаковой длине пути. DFS заходит в каждый тупик до конца и тратит время на возврат, хотя финальный путь совпадает. A* показывает время между BFS и DFS. + +### 100×100 (запутанный, spaghetti) + +Наиболее показательные результаты: + +|Стратегия|Время (мс)|Длина пути| +|---|---|---| +|BFS|~9|~210| +|DFS|~7|~2200| +|A*|~8|~210| + +DFS быстрее по времени, но находит путь в 10 раз длиннее — обходит почти весь лабиринт. BFS и A* находят кратчайший путь, A* при этом чуть быстрее за счёт эвристики. + +### 30×30 (пустой) + +Неожиданный результат: DFS быстрее всех (~0.73 мс против 1.1 у BFS и 2.0 у A*), хотя находит путь из 379 клеток против 55 у BFS. На пустом поле без стен DFS сразу уходит вглубь без возвратов, тогда как BFS строит очередь и обходит клетки волнами во все стороны — это накладные расходы на структуру данных. + +A* на пустом лабиринте медленнее всех — накладные расходы на `heapq` и вычисление эвристики не окупаются когда препятствий нет. + +## Выводы + +- **BFS** — надёжный выбор по умолчанию. Всегда находит кратчайший путь, время предсказуемо. + +- **DFS** — быстрый по времени, но путь непредсказуем. На запутанных лабиринтах может пройти весь граф. Подходит когда важна скорость, а не оптимальность пути. + +- **A*** — лучший выбор для больших лабиринтов с препятствиями. Находит кратчайший путь быстрее BFS за счёт эвристики. На простых или пустых лабиринтах проигрывает из-за накладных расходов на приоритетную очередь. + +## Визуализация + +![[results.png]] \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/stage7.md b/skorohodovsa/task_2/docs/source/stage7.md new file mode 100644 index 0000000..ceaa5a9 --- /dev/null +++ b/skorohodovsa/task_2/docs/source/stage7.md @@ -0,0 +1,81 @@ +# Этап 7. Отчёт + +## Описание задачи + +Разработать программу для загрузки лабиринта из файла, поиска пути с выбором алгоритма и сравнения их эффективности. Применены четыре паттерна GoF: + +|Паттерн|Где применён| +|---|---| +|**Builder**|`TextFileBuilder`| +|**Strategy**|`BFSStrategy`, `DFSStrategy`, `AStarStrategy`| +|**Observer**|`ConsoleView`| +|**Command**|`MoveCommand`, `CommandHistory`| + +## Диаграмма классов + +```{mermaid} +classDiagram + class Cell { + +int x, y + +bool is_wall, is_start, is_exit + +is_possible() bool + } + class Maze { + +get_cell(x, y) Cell + +get_neighbors(x, y) list + +start, exit, shape + } + class MazeBuilder { <> } + class TextFileBuilder + class PathFindingStrategy { + <> + +find_path(maze, start, exit) list + } + class BFSStrategy + class DFSStrategy + class AStarStrategy + class MazeSolver { + +set_strategy(strategy) + +solve() SearchStats + } + class ConsoleView { + +update(event) + +render(maze, player, path) + } + class MoveCommand { + +execute() bool + +undo() + } + class CommandHistory { + +push(command) + +undo() bool + } + + Maze *-- Cell + MazeBuilder <|-- TextFileBuilder + TextFileBuilder ..> Maze : creates + PathFindingStrategy <|-- BFSStrategy + PathFindingStrategy <|-- DFSStrategy + PathFindingStrategy <|-- AStarStrategy + MazeSolver --> PathFindingStrategy + MazeSolver --> Maze + MoveCommand --> Maze + CommandHistory --> MoveCommand +``` + +## Результаты экспериментов + +| Лабиринт | Быстрее всех | Кратчайший путь | +| ----------------- | ------------- | --------------- | +| 10×10 path | все одинаково | все одинаково | +| 50×50 deadends | BFS | BFS = A* | +| 100×100 spaghetti | DFS | BFS = A* | +| 30×30 empty | DFS | BFS = A* | + +**BFS** — надёжный выбор, всегда кратчайший путь. +**DFS** — быстрый, но путь длиннее. На 100×100 обошёл в 10 раз больше клеток. +**A*** — лучший на больших лабиринтах с препятствиями, проигрывает на простых из-за накладных расходов на `heapq`. + +## Выводы + +Паттерны сделали код расширяемым: новый алгоритм — один класс, новый формат файла — один класс, новый способ отображения — один класс. Без паттернов каждое такое изменение потребовало бы правки существующего кода. \ No newline at end of file diff --git a/skorohodovsa/task_2/docs/source/task.md b/skorohodovsa/task_2/docs/source/task.md new file mode 100644 index 0000000..0c9274c --- /dev/null +++ b/skorohodovsa/task_2/docs/source/task.md @@ -0,0 +1,183 @@ +# Задание + +## Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 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')` для перерисовки. \ No newline at end of file diff --git a/skorohodovsa/task_2/practice/main.ipynb b/skorohodovsa/task_2/practice/main.ipynb new file mode 100644 index 0000000..0f49667 --- /dev/null +++ b/skorohodovsa/task_2/practice/main.ipynb @@ -0,0 +1,937 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b41a67fc", + "metadata": {}, + "source": [ + "# Экспериментальная часть " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3986182c", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "sys.path.insert(0, os.path.abspath(\"..\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c304f83d", + "metadata": {}, + "outputs": [], + "source": [ + "from source.build.builder import TextFileBuilder\n", + "from source.models.base import Maze\n", + "from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy\n", + "from source.strategy.solver import MazeSolver, SearchStats" + ] + }, + { + "cell_type": "markdown", + "id": "f4d32c9b", + "metadata": {}, + "source": [ + "**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4233a72f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "filenames: list[str] = os.listdir(\"../source/templates\")\n", + "\n", + "list_maze: list[Maze] = [\n", + " TextFileBuilder().build_from_file(filename=\"../source/templates/\" + filename)\n", + " for filename in filenames\n", + "]\n", + "\n", + "list_maze" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8c9592fb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "50" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(filenames)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "04e5e6a6", + "metadata": {}, + "outputs": [], + "source": [ + "strategies = {\n", + " \"BFS\": BFSStrategy(),\n", + " \"DFS\": DFSStrategy(),\n", + " \"A*\": AStarStrategy(),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "3ccf351f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v1.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v1.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v1.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v10.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v10.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v10.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v2.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v2.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v2.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v3.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v3.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v3.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v4.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v4.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v4.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v5.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v5.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v5.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v6.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v6.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v6.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v7.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v7.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v7.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v8.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v8.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v8.txt | A*\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v9.txt | BFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v9.txt | DFS\n", + "Выходная клетка не найдена в лабиринте | 20x20_noexit_v9.txt | A*\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'лабиринт': '100x100_spaghetti_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 9.698449999996228,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.488289999992048,\n", + " 'посещено_клеток': 2129.0,\n", + " 'длина_пути': 2129.0},\n", + " {'лабиринт': '100x100_spaghetti_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 7.629470000074434,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.57915999999932,\n", + " 'посещено_клеток': 207.0,\n", + " 'длина_пути': 207.0},\n", + " {'лабиринт': '100x100_spaghetti_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 8.580700000084107,\n", + " 'посещено_клеток': 2489.0,\n", + " 'длина_пути': 2489.0},\n", + " {'лабиринт': '100x100_spaghetti_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 8.167220000041198,\n", + " 'посещено_клеток': 207.0,\n", + " 'длина_пути': 207.0},\n", + " {'лабиринт': '100x100_spaghetti_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.6136500000066,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.336869999950068,\n", + " 'посещено_клеток': 2063.0,\n", + " 'длина_пути': 2063.0},\n", + " {'лабиринт': '100x100_spaghetti_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 10.417360000155895,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.563159999994241,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 5.783009999913702,\n", + " 'посещено_клеток': 2107.0,\n", + " 'длина_пути': 2107.0},\n", + " {'лабиринт': '100x100_spaghetti_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 6.953359999988606,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.79095000000234,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.422280000060709,\n", + " 'посещено_клеток': 2409.0,\n", + " 'длина_пути': 2409.0},\n", + " {'лабиринт': '100x100_spaghetti_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 6.4153799999985495,\n", + " 'посещено_клеток': 205.0,\n", + " 'длина_пути': 205.0},\n", + " {'лабиринт': '100x100_spaghetti_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.864429999994172,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.053909999922325,\n", + " 'посещено_клеток': 2071.0,\n", + " 'длина_пути': 2071.0},\n", + " {'лабиринт': '100x100_spaghetti_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 10.017580000112503,\n", + " 'посещено_клеток': 217.0,\n", + " 'длина_пути': 217.0},\n", + " {'лабиринт': '100x100_spaghetti_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 9.399200000052588,\n", + " 'посещено_клеток': 243.0,\n", + " 'длина_пути': 243.0},\n", + " {'лабиринт': '100x100_spaghetti_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.747049999989031,\n", + " 'посещено_клеток': 1869.0,\n", + " 'длина_пути': 1869.0},\n", + " {'лабиринт': '100x100_spaghetti_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 14.121079999995345,\n", + " 'посещено_клеток': 243.0,\n", + " 'длина_пути': 243.0},\n", + " {'лабиринт': '100x100_spaghetti_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.773490000021411,\n", + " 'посещено_клеток': 211.0,\n", + " 'длина_пути': 211.0},\n", + " {'лабиринт': '100x100_spaghetti_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.66454999995949,\n", + " 'посещено_клеток': 2283.0,\n", + " 'длина_пути': 2283.0},\n", + " {'лабиринт': '100x100_spaghetti_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 7.22499999997126,\n", + " 'посещено_клеток': 211.0,\n", + " 'длина_пути': 211.0},\n", + " {'лабиринт': '100x100_spaghetti_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 8.687369999961447,\n", + " 'посещено_клеток': 221.0,\n", + " 'длина_пути': 221.0},\n", + " {'лабиринт': '100x100_spaghetti_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 7.4791699999423145,\n", + " 'посещено_клеток': 2473.0,\n", + " 'длина_пути': 2473.0},\n", + " {'лабиринт': '100x100_spaghetti_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 10.324829999944996,\n", + " 'посещено_клеток': 221.0,\n", + " 'длина_пути': 221.0},\n", + " {'лабиринт': '100x100_spaghetti_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 9.33376999992106,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '100x100_spaghetti_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 6.118089999927179,\n", + " 'посещено_клеток': 1939.0,\n", + " 'длина_пути': 1939.0},\n", + " {'лабиринт': '100x100_spaghetti_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 7.4974299999212235,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '10x10_path_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.03240000014557154,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.03378000001248438,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.03725999999915075,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.030790000027991482,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.035979999984192546,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.03738000000339525,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.035089999892079504,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.045300000101633486,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04377000000204134,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.035430000025371555,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.055879999990793294,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04907000002276618,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.053650000018024,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.09142999997493462,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.09646000012253353,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.035020000041185995,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.03366999999343534,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.037369999972725054,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.03128999992441095,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.03374999996594852,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.037149999934626976,\n", + " 'посещено_клеток': 13.0,\n", + " 'длина_пути': 13.0},\n", + " {'лабиринт': '10x10_path_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.040439999975205865,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.05008999996789498,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04825999994864105,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.04994999994778482,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.058220000073561096,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.08468999990327575,\n", + " 'посещено_клеток': 29.0,\n", + " 'длина_пути': 29.0},\n", + " {'лабиринт': '10x10_path_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.048520000018470455,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.04457000009097101,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '10x10_path_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.04779000005328271,\n", + " 'посещено_клеток': 17.0,\n", + " 'длина_пути': 17.0},\n", + " {'лабиринт': '30x30_empty_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.3075400000161608,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7962099999531347,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 3.0833899998924608,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.6444799998680537,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.1289500000657426,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 3.0003100000158156,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.5947500000493164,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0904399999617453,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.242679999972097,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.3086099999327416,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0075400000459922,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.17491000003065,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.5689699999711593,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0746400001153233,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.2863700000471,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.3352300000406103,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.8594300000368094,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.264869999999064,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.158610000175031,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7807300000422401,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.158290000170382,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.1674999999286229,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7544099999449827,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.252279999993334,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.248879999957353,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7802099999480561,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.2779600000831124,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.201050000054238,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '30x30_empty_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 0.7864399999107263,\n", + " 'посещено_клеток': 379.0,\n", + " 'длина_пути': 379.0},\n", + " {'лабиринт': '30x30_empty_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.155060000040976,\n", + " 'посещено_клеток': 55.0,\n", + " 'длина_пути': 55.0},\n", + " {'лабиринт': '50x50_deadends_v1.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.8954199999825505,\n", + " 'посещено_клеток': 729.0,\n", + " 'длина_пути': 729.0},\n", + " {'лабиринт': '50x50_deadends_v1.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8011099999966973,\n", + " 'посещено_клеток': 729.0,\n", + " 'длина_пути': 729.0},\n", + " {'лабиринт': '50x50_deadends_v1.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.820240000028207,\n", + " 'посещено_клеток': 729.0,\n", + " 'длина_пути': 729.0},\n", + " {'лабиринт': '50x50_deadends_v10.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.7890000001225417,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v10.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.94999999994252,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v10.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.0044500000731205,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v2.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.0780099999919912,\n", + " 'посещено_клеток': 249.0,\n", + " 'длина_пути': 249.0},\n", + " {'лабиринт': '50x50_deadends_v2.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8470900000920665,\n", + " 'посещено_клеток': 249.0,\n", + " 'длина_пути': 249.0},\n", + " {'лабиринт': '50x50_deadends_v2.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.1555500000213215,\n", + " 'посещено_клеток': 249.0,\n", + " 'длина_пути': 249.0},\n", + " {'лабиринт': '50x50_deadends_v3.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.9816600000704057,\n", + " 'посещено_клеток': 297.0,\n", + " 'длина_пути': 297.0},\n", + " {'лабиринт': '50x50_deadends_v3.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8496700000014243,\n", + " 'посещено_клеток': 297.0,\n", + " 'длина_пути': 297.0},\n", + " {'лабиринт': '50x50_deadends_v3.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.18772000005265,\n", + " 'посещено_клеток': 297.0,\n", + " 'длина_пути': 297.0},\n", + " {'лабиринт': '50x50_deadends_v4.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.5539699999862933,\n", + " 'посещено_клеток': 413.0,\n", + " 'длина_пути': 413.0},\n", + " {'лабиринт': '50x50_deadends_v4.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.6492100000050414,\n", + " 'посещено_клеток': 413.0,\n", + " 'длина_пути': 413.0},\n", + " {'лабиринт': '50x50_deadends_v4.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.9135399998958746,\n", + " 'посещено_клеток': 413.0,\n", + " 'длина_пути': 413.0},\n", + " {'лабиринт': '50x50_deadends_v5.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.1528699998507363,\n", + " 'посещено_клеток': 309.0,\n", + " 'длина_пути': 309.0},\n", + " {'лабиринт': '50x50_deadends_v5.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.7570300000897987,\n", + " 'посещено_клеток': 309.0,\n", + " 'длина_пути': 309.0},\n", + " {'лабиринт': '50x50_deadends_v5.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.199769999993805,\n", + " 'посещено_клеток': 309.0,\n", + " 'длина_пути': 309.0},\n", + " {'лабиринт': '50x50_deadends_v6.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.0987699999986944,\n", + " 'посещено_клеток': 337.0,\n", + " 'длина_пути': 337.0},\n", + " {'лабиринт': '50x50_deadends_v6.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.9475800000236632,\n", + " 'посещено_клеток': 337.0,\n", + " 'длина_пути': 337.0},\n", + " {'лабиринт': '50x50_deadends_v6.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.3809099999434693,\n", + " 'посещено_клеток': 337.0,\n", + " 'длина_пути': 337.0},\n", + " {'лабиринт': '50x50_deadends_v7.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.8687199999258155,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v7.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.8890300000293792,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v7.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 1.1017699999683828,\n", + " 'посещено_клеток': 261.0,\n", + " 'длина_пути': 261.0},\n", + " {'лабиринт': '50x50_deadends_v8.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 1.7002200001115853,\n", + " 'посещено_клеток': 565.0,\n", + " 'длина_пути': 565.0},\n", + " {'лабиринт': '50x50_deadends_v8.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.7665699999724893,\n", + " 'посещено_клеток': 565.0,\n", + " 'длина_пути': 565.0},\n", + " {'лабиринт': '50x50_deadends_v8.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 2.3602600000231178,\n", + " 'посещено_клеток': 565.0,\n", + " 'длина_пути': 565.0},\n", + " {'лабиринт': '50x50_deadends_v9.txt',\n", + " 'стратегия': 'BFS',\n", + " 'время_мс': 0.7835800000066229,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '50x50_deadends_v9.txt',\n", + " 'стратегия': 'DFS',\n", + " 'время_мс': 1.0580000001027656,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0},\n", + " {'лабиринт': '50x50_deadends_v9.txt',\n", + " 'стратегия': 'A*',\n", + " 'время_мс': 0.8593499999733467,\n", + " 'посещено_клеток': 209.0,\n", + " 'длина_пути': 209.0}]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RUNS = 10\n", + "results = []\n", + "\n", + "\n", + "for filename, maze in zip(filenames, list_maze):\n", + " for strategy_name, strategy in strategies.items():\n", + " try:\n", + " solver = MazeSolver(maze, strategy)\n", + "\n", + " times, visited, lengths = [], [], []\n", + " for _ in range(RUNS):\n", + " stats = solver.solve()\n", + " times.append(stats.elapsed_ms)\n", + " visited.append(stats.visited_count)\n", + " lengths.append(stats.path_length)\n", + "\n", + " results.append(\n", + " {\n", + " \"лабиринт\": filename,\n", + " \"стратегия\": strategy_name,\n", + " \"время_мс\": sum(times) / RUNS,\n", + " \"посещено_клеток\": sum(visited) / RUNS,\n", + " \"длина_пути\": sum(lengths) / RUNS,\n", + " }\n", + " )\n", + " except Exception as ex:\n", + " print(ex, filename, strategy_name, sep=\" | \")\n", + "\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "f6cfb407", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "120" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(results)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "c6d14f8d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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999/yfr7smXL6v+lcsvKMaYAAAAAAADgjrNnz6p9+/bp10ePHlUjRozQg6A3btz4in/bsWNHlT9/flW3bl1VsGBB9c8//6g33nhDP81PxpsKNyqmAAAAAAAAPGzu3Ll6rCiROXNmVbp0aTV9+nT98LkrqV+/vho/frwaPXq0Onz4sMqZM6eukFqwYMFVPdUvVFRMAQAAAAAAJOLrd5tEfblMnDhRT0lxHCfosubNm+spUhhjCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAiIjYyYQEAAAAAAKLbtgHNjcYr/uoX1/w3bdu2VZMmTdKvY2NjVY4cOVSFChVU69at9bKYmP/XJqlo0aJq586d8f62QIEC6u+//9avv/zySzV48GC1ceNGdenSJVW4cGF15513qqFDh6pwosUUAAAAAACAh919993qn3/+UTt27FDffvutqlOnjurSpYtq1KiRunDhgv/3XnvtNf17vmn16tV6/oIFC1SrVq1U8+bN1fLly9XKlSvVgAED1Pnz58P+2WkxBQAAAAAA4GFxcXEqb968/lZQVapUUdWqVVP16tVTEydOVE888YReljlzZv/vBfr6669VzZo11UsvveSfV7JkSdW0adOwf3ZaTAEAAAAAAFimbt26qmLFimrGjBlX/F2prPrjjz/U77//rkyLSMXU4sWLVePGjVX+/PlVqlSp1MyZM/3LpJlY9+7dVfny5VXGjBn17zz22GNq7969kfioAAAAAAAAnlS6dGndvc9H6lsyZcrkn4YPH67nP/vss+qWW27RdTEyFtWDDz6oxo8fr86ePWtnxdSpU6d0rd3IkSMTLDt9+rRatWqV6t27t/5favb+/PNPdd9990XiowIAAAAAAHiS4zi6QZCPdNVbs2aNf5KGQEIaBs2ZM0dt3bpV9erVS1davfDCC6pq1aq6nsa6MaYaNmyop8RkzZpVzZ8/P968ESNG6MLYtWuXHhUeAAAAAAAASZMn7BUrVsz/c86cOVWJEiWC/v7111+vJxmT6tVXX9XjTH322WeqXbt2KkUPfn7s2DFdw5ctW7agvyPNywKbmB0/flz/L484lAlAeKRS/1f7fjm2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VZs3b070ySZuGThwoDp27JgKNzl4nz592oo4NuViaj0zlY+pXEzEuf/++9WhQ4eULfszeepXTEyMFXFsysXUumZTLibjmDhPt+1YY1McU7mYuh60KR9TZWYLuvIBgIft3btXd32SAcoB1rPo2WbYNgEgOvebNsWx7VhjWz64elRMAQD8pGXUvn379Ou8efPGe8qQ1+3fv1+dPXs2rOMXyZOZnnnmGZUzZ04VTnLSJk+v8RppPRDusgEiSVr+FipUSMXG2tEp4cKFC9bkImRoXXkaGIJ/33/88Ue884CyZct68ngDwFsYY8rjF1lujskjj4cdNmyY6tmzp57ktcxz299//61OnjyZ6IWWG32KDx8+rBYuXKiOHDnivxAaPHiwLquNGzeqcJKxBKTJZjhPqCS3Dz/8UM2ePVuXmVvfSWBz8yVLlqiHH35Y3XbbbeqRRx5RS5cuDTmGPPp1586dygQpG+kS+PPPP+uff/jhB3XPPfeou+++W33wwQeuxfnvv//U+PHj9dgrDRs2VPfee6969tln1YIFC5TJk8hdu3aF/D7jxo3TJ585cuTQ/we+/uijj5SbY4nJuBstW7ZMUE6yDroxHseJEyf0eit322TMknPnzunKonz58ukxMu644w51/PjxkGLI318+SaXegAED1LZt2/zzQjVt2jT9+X1GjBih80qXLp2u4AnXuGyyXs2fP19/999//71rTerz5Mmj6tWrp6ZMmaIrCcNFPq98D5cuXdI/Sywpy08//VQfO9106tQpfez67LPP1PTp09XKlSv1vtqEv/76y7UxeWT7e+utt3T3I3nEvUzy+u2331YHDx5Ubg6i/Mknn6hvvvkm3rrtK0u31mlZf/v27av3/0K+I9lPS3lNmDBBhUupUqXCeh4grQkkLzlGv/jii2rTpk2uvO/cuXPV+vXr9WvZbl5//XXdtS4uLk4VLFhQDRo0KOT1unHjxurjjz/Wx85wku1dyub222/X53/ijTfeUJkyZVKZM2dWDz30kCv7Z7F27VrdTUeOXenTp1cZM2ZU5cuXV71793YtxtXug0Ih33mvXr1Urly5VOXKlfW2IpO8zp07t87Htz/1ksuPXcuWLdNl5db5czDt2rXT22q4yOeX/Uw4h1n4999/9fWGfPdyjuhWLDlGmnLgwAF9DPB9djn+y3FO9me+/Z0b5HxDurvK/kaOmTI8iant3yoRfSYgQrJmzRonJiYm5FLcv3+/U6tWLSdVqlROkSJFnKpVq+pJXss8WSa/E6q9e/c6t9xyi/7MqVOndh599FHnxIkT/uX79u0LOZ9ly5Y5WbNm1Z87e/bszooVK5xixYo5N9xwg3P99dc76dOnd1auXBlyLsOGDUt0krx69uzp/zlUDRs2dP7991/9+vDhw86tt96qc8uVK5cuq9KlSzsHDhwIOY58319//bV+PXPmTP3e9913n9O9e3fn/vvvd9KkSeNfnlzyuaV86tev73z66afO2bNnnXAYM2aMExsb69x0001OlixZnI8//tjJnDmz88QTTzhPP/20XgeGDh0acpwtW7bobSR37txOoUKFdH733nuv/o4kzxYtWhh5fLQb+4G33nrLyZAhg9OjRw9n4cKFzoYNG/Qkr2V9zpgxo/P222+H/Fllm5A4zzzzjPPII484adOmdd58801X9wGic+fOetsYPny4foRvkyZNnHLlyjk//fST8+OPPzply5Z1XnnllZBiyOdMbJL1IPD/UMl7+Pa/48ePd9KlS+f06dPHmTNnjvPGG2/o7+bDDz90pcx82/ju3bt1+cl6nCdPHv1/+fLlnb///jvkOFIud999t/7uZR8tcVevXu24ae3atU6+fPl02cn3vmvXLv2/lFWmTJl03OXLl4cc5+LFi85LL72k1+nA7993LJ01a5bjlfMAKQ8plwIFCjht2rRxXn75ZT3J64IFCzo5cuRwfvvtN1fiZMuWTe+bZV9cokQJ5/fff3d9HyD7fTkOVKlSRX/nEyZM0HHlOPD444/r9W/69OkhxZBjY2KTfH45zvl+DpWUk+84/8cff+hzHCk3OcbIdirrn6zzoSpVqpSzePFi/Vr2y9ddd53z3nvvOd9++60+Zsq+YNCgQSHFkG1DvhfJoUOHDvocLRyef/55J3/+/M4LL7zglClTxunUqZNTuHBh55NPPnGmTJmiy+/ZZ58NOc7cuXP199O8eXN9TJPvQvZpcu4kMeS8859//nG8sB+QfZmcW8o51Pbt253Tp0/rSV6PHTtWn+vIPiFU586d07GkbOSa4KOPPoq33K19gFxz1KxZUx+/br/9dufIkSP6HM23jy5ZsqT+nVDJtpfYJOfNX375pf/nUAwePFh/F+LChQt6vZZ9mJSTbE/t2rXT5Roq2V/5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iCBPYnLrBN73VLHLH0seOD+cJ6Ru5xOpGMShzEyuA25eyMuTxaQJsjz1J7Hp6aefDlsu8tQiubMkd5jkiXDyZEivxrEpF5MVbSbzsaHSUKxevdp54YUXnAIFCuinJYVKjvPyZLHAJz8FTrIsnGXmdj6RjGNTLqbi2JSLyThChhBwY18j+175zJc/BdI3ycV1uPebbuWSkuLYlIttcUzlYhMqpjxE7sB07NjRfwdGHrksj3ANlTzi9qOPPtKPnE5smjNnTlg2qnDlYzoGcSgzk+tAOC7k5fHZo0aNSvIk2819wOUtSxo0aKBblpw4ccK1GKbi2JSLyQoj0/nYUGnoa5FZtmxZfSdWHnU+btw4599//w35vaXySVpHm9oHhDsf03FsysVUHJtyMRnHN9CybK9NmjTRN5bkMfXdu3cP6T1lOALphmy6S384crE9jk252BbHVC62omLKA3r06KFPGtOmTevce++9zpQpU1ztsy4XBPJEkaQORnI31Sv5mIpBHMrM5DoQzgv55557Tl9MJzXwce3atR23yP6katWq+iRYHq4QLibi2JSLyQojE/nYVGl466236otCufv69ttvO3///bfjpubNm+vueqbOA8Kdj8k4NuViKo5NuZiMM3fuXD2Is3Sry5Ejh/PUU0+59qS8AQMG6BbSwezatctp27at44VcbI1jUy62xTGVi+2omPIAuYsRzqd8yIBtH3/8cdDlR44c0c14vZKPqRjEocxMrgOmKiZMuNqnl0klnzyiOprj2JSLyfXMRD42VRq+8sorYX3qp7x3UgO1njt3Treg9ko+JuPYlIupODblYjKODKzcokULZ+bMmXqb9DJTudgUx6ZcbItj07YZSVRMWUSeALJ3717HFibyMVVmxKHMQl0HTFVMRBPpEvnXX39ZEccruUTbehZKPrZVGkbTemaKV7abaIlhWxybcnEjjgysfDUGDhzoHD16NNldkUwwkYttcWzKxbY4pnKxXYyCNRYvXqz++++/ZP/9Tz/9pGzKJ1piEIcyc2MduOGGG67q955++mm1f//+ZMUoUaKE6tevn9q8ebOKBnLzxJY4XsnFxHpmKh9TuURTmYX6/b/xxhtq+/btKlp4ZbuJlhi2xbEpFzfiZM6c+ap+780331RHjhxJVozcuXOrtm3bqvnz56tLly6pcDGRi21xbMrFtjimcrEdFVPwq1u3ripWrJh65ZVX1IYNGygZwINCOfF95pln1Jw5c1SZMmXULbfcooYNG6b27dvn6ueDHUxdyJnglYtSE6ZPn64rqGvUqKFGjRqlDh06FOmPBMDgvmbSpEnq1KlTqkmTJqpAgQKqa9euasWKFRH7DmzbP1Ohm7LjeOE8IJKomILf3r171QsvvKB+/PFHVa5cOVWpUiX19ttvq7///ptSAlKA559/Xv32229q48aN6p577lEjR45UhQoVUg0aNFCTJ0+O9McDEGZr165V69atU7Vr11bvvPOOyp8/v7r33nvVlClT1OnTpyl/wHL333+/rqCW1p3SukNuVFerVk2VLFlSvfbaa5H+eAAsRsUU/HLmzKk6d+6sfv75Z/XXX3+pFi1a6DsnRYsW1a2pAKQMcgLav39/3aVvyZIl6uDBg6pdu3aR/lgADLjxxhv1Bem2bdvUwoUL9TmAtJrImzcv5Q+kENI1SY773333na6szpgxoz4vAIBwoWIKiZIufT169FCDBg1S5cuX162oAKQcy5cv1xejcvdUKqikohpAdEuVKpWr7ycXo+nTp1dp06ZV58+fV17PJ5JxbMrFVBybcjEZxw1nzpxR06ZNU02bNlVVqlTR4+K89NJLkf5YACxGxRQSkBZTnTp1Uvny5VMPPfSQ7tYn484AsJtUQPXt21e3mKpZs6bu0jd48GDdpP/TTz81/nmKFCmi0qRJY0Ucm3IxybZ8vDB+hQx+PmDAAN1y6uabb1arV6/WLSUiMd4cY4tQZqxnZs2bN0+1adNG5cmTR3Xs2FH/L62mdu7cqW9WA0C4xIbtnWGcDFqeI0eOZP99z5499cWnjDV155136oGPZfDDDBkyKC/mEy0xiEOZmVwHQrmQL126tB70XAZBf/DBB/UJaTidO3dOHThwIMGTfwoXLqz///333z0Tx6ZcTFYYRUM+Xqg0lC51derUueLvffvtt3rA4uSSsWRknLkKFSrobjytW7cO6f0inY+JODblYiqOTbmYjHO1brvtNt3SMTmklXSjRo30uJIy1mSkbwyEkktKjWNTLrbFMZWLV6VyGB4+Ks2aNUs1bNhQHxDkdVLuu+8+V2JKC4mHH35YtWzZUo835bV8TJUZcSizSGyfV3shH4otW7aoG2644Yq/N3XqVJ2XdPNJbpzHH39c/fLLL/Hmy+FIujpcvHgxWe8biTg25WJqPTOdT7hzMREnLi5OFSxYUFcWSWsGeShBOLz66qv6PKBs2bIqnEzlYyKOTbmYimNTLibjrFq1Sp93yBAb4quvvlITJkzQ22u/fv10l9tQnThxQo8vdSXSeqpDhw4qW7ZsUZuLbXFsysW2OKZysZ5UTCH6pEqVytm/f7//dbApJibG+Ge75557nL1790ZdPqbKjDiUWSS2z82bNzu1atXS7xk4RWI/kDlzZuevv/5K9t/XqFHDuf32251vvvnGWb16tbNmzZp4k1tMxLEpF5PrmYl8TOViIs7Bgwed9957z6lYsaITGxvrNGjQwPnss8+cs2fPOpEQ6j7AVD4m4tiUi6k4NuViMs7NN9/sfP755/q1bH/p0qVzWrdu7ZQoUcLp0qWL46V9gKlcbIpjUy62xYmmbdPLqJjCNcuUKVNIByMA0VsxYWIfkCFDBmfjxo2ufqZIxbEpF5PrmYl8bKs09Fm5cqXTuXNn57rrrtPTs88+67l9QCTyMRHHplxMxbEpl3DHyZIli7N161b9etCgQboCTPz0009OwYIFHS/tA0zlYlMcm3KxLU40bZteRsWUB0yaNMk5c+ZMgvlyJ0aWmRbqwchEPqbKjDiUmal1wFTFhIl9gNxZWrJkiaufKVJxbMrF5HpmIh/bKg0D7dmzx+nbt68TFxfnZMyY0UmdOrVutfX777978gaVqXxMxLEpF1NxbMolnHGklZK00BT169d3hg4dql/v3LlTt9AwKdR9gKlcbIpjUy62xYmmbdPLqJjyAOkK4Os2FOjQoUMR6coX6sHIRD6myow4lJmpdcBUxUS49gHHjh3zTwsWLHCqV6/uLFy4UJdT4DKZQmEijk25mFzPTOdjW6XhuXPnnOnTpzsNGzbU3YWqVavmfPjhh87Jkyed7du3Ow8//LBTpkwZxysVU6byMRHHplxMxbEpF1Nx6tSp4zz22GPO5MmTnTRp0jhbtmzR8xctWuQUKVLEMSnUfYCpXGyKY1MutsWJpm3Ty6iY8gAZp+LAgQMJ5kvT4OzZsxv/PKEejEzkY6rMiEOZhXMdiETFRLj2Ab7xdi4ff8ftMXlMxLEpF5PrmYl8bK009HUNypEjhx6vYv369Ql+559//tHl54XzAFP5mIhjUy6m4tiUi8k4a9eudcqVK6e7DfXr1y9efBnPxqRQ9wGmcrEpjk252BYnmrZNL4uN9ODrCK5y5cr66UQy1atXT8XG/t/XJU8s2r59u7r77rs9U4Qm8jFVZsShzEysA/K0G3l/H7mZILFMPJXNbfI4bVvi2JSLyfXMRD6mcjG9bW7YsEG9//77qlmzZvoJYImRp+maWmcCc4/mfEzEsSkXU3FsysVknAoVKqj169cnmP/222+r1KlTKy8xlYtNcWzKxbY4Nm2bkUTFVBRr2rSp/n/NmjXqrrvuUpkyZfIvk8dOFi1aVDVv3lx5hYl8TJUZcSgzE+uAqYvMa1WkSBH9WNxrcccdd/hf79q1Sz9O+/KLW7mQ3717d0ifzUQcm3IxuZ6ZyMe2SkOfvn37qho1asSrABcXLlxQv/zyi7r99tv1ssAyDif5nryQj4k4NuViKo5NuZiME0y6dOmUabfddptKnz69Z3OxKY5NudgWJxLbppfpNqWR/hBI2qRJk9SDDz4Y9C6MaQMHDlQdO3bUd4yjNR9TZUYcyszUOnClC/nChQu7FuvcuXPqwIED6tKlS/HmuxVD7h79888/Knfu3PHmHz58WM9zq/WXiTg25WJyPTORj6lcTMQx9f1LhVudOnWu+Hs//fSTuuWWW5K937Npu7EpF1NxbMol3HGyZ89+1S0Ujxw5okK1atUqfeOpfPny+uevvvpKTZgwQZUtW1b169dP33iL9lxsimNTLrbFMb1tpgS0mPKA/v37q0aNGiU4Afz3339VlSpV1LZt25L93rNmzVINGzbUByF5nZT77rtP/9+zZ08VrfmYjEEcyszkOlCsWLFET3zlYCfL3DjB3rJli3r88cf1Hd5wdhf0vd/lTp486erdJRNxbMrF1HpmKh9TuZiIE6y85MI3Y8aMyi3S/bhgwYKqXbt2qk2bNrrCLTG1atXyRD4m4tiUi6k4NuUS7jhDhw6N935vvPGGbqVdvXp1PW/p0qVq3rx5qnfv3soNTz/9tOrRo4eumJLzF7nxdv/996vp06er06dPx/s80ZqLTXFsysW2OKa3zZSAFlMeEBMTo/bt25fgpHf//v36TuzZs2ddeW95HYybF6XhzMdkDOJQZqbXAXnPXLlyxZu/c+dOfSfz1KlTIceoWbOm7m4gJ6X58uVLcKJdsWLFkN6/W7du+v9hw4apJ598UmXIkMG/TPYvy5Yt03eef/7556iPY1MuJtczk/mY2GbCHUfGrPG1WpBKo8AKcCmvdevWqVKlSqm5c+cqNxw6dEh9/PHHuiXoH3/8oerWravat2+vuy6H0lLCdD4m4tiUi6k4NuViMo6PDA8gLRo7d+4cb/6IESPU999/r2bOnBlyjKxZs+pWU9dff70aPHiw+uGHH/TFteyTpZIq1K7jJnOxLY5NudgWx1QutqPFVBQLbMEkBwU5WAQe8BYsWKDHsQlFYFedy7vteDEfEzGIQ5mZXAd8F/JSSSR3XRK7kK9UqZJyg4yXtXLlSlW6dGkVDqtXr/bfXZZBIgMvdOW1VHy9+OKLnohjUy4m1zMT+ZjKxUQc335Fyitz5szxxnSR8qpWrZqu4HOLDND8/PPP60kuTqULT6dOnfT00EMP6UqqUCqoTeVjIo5NuZiKY1MuJuMEnmtIZdHlpFJMbii5QXLxXQ/IBbW0CBfSelIqrr2Ui21xbMrFtjimcrFepB8LiODksbK+R2f7XvumtGnTOiVLlnS+/vpr14pw0qRJzpkzZxLMP3v2rF7mhXxMlRlxKDNT60Dt2rX1JO9bo0YN/88yNWjQwHnqqaeczZs3O264+eabnSVLljjh1rZtW+fYsWNWxLElF5PrWbjzMZWLyTKTx0+fPHnSMW3Pnj1O3759nbi4OCdjxoxO6tSpnVq1ajm///67J/IxEcemXEzFsSkXk3EKFy7svPPOOwnmyzxZ5oY6deo4jz32mDN58mQnTZo0zpYtW/T8RYsWOUWKFHG8lIttcWzKxbY4pnKxHRVTHlC0aFHn4MGDYY8jF9j79+9PMP/QoUN6mZfyMVVmxKHMTK0D4bqQl/f0TQsWLHCqV6/uLFy4UG/3gctMVL4g8kxVtJlgS6VhIDlGL168WE+JHa/dcO7cOWf69OlOw4YNndjYWKdatWrOhx9+qC+8t2/f7jz88MNOmTJlPJOPqTg25WIqjk25mIgzYcIEXTncqFEj5/XXX9eTvJbtVJa5Ye3atU65cuWcLFmy6Ao3n86dOzutW7d2vJSLbXFsysW2OKZysR1jTHnMmTNnwvboyWDjZKxdu1b3mw3HEwXCmY/JGMShzEyuA25v94FjSSU2iKvbg5+LFStWqGnTpuknmslTAAPNmDHDU3FsysUk2/IJpxMnTujudJ9++ql/O5SxuFq1aqVGjhwZrytxKJ599lk1depUvc0/+uij6oknnlDlypWL9zsypl7+/PlD6v5vKh8TcWzKxVQcm3IxGUdIF+Hhw4erjRs36p/LlCmjnnvuOXXrrbeqcJ/fSE7ysCSv5WJTHJtysS1OpLZNq0S6ZgxXdvHiRee1115z8ufPr2tj//rrLz2/V69ezrhx40IuwkqVKjmVK1fWraLKly+vX/umChUqOJkzZ3ZatGjhmXxMxSAOZWZyHRC//fab89JLLzmtWrVy7r///nhTcknz/Kud3DJ16lTdRUDuJkm3R/lfuj5mzZpVtz7xUhybcgnnehapfEzkYiJOy5YtnRtuuMGZO3euvwWjvC5VqpSO6Za6des6U6ZMSbRbv8/58+dD3h+YysdEHJtyMRXHplxMxgEAm1Ex5QH9+/d3ihcv7nzyySdO+vTp/Re+n376qW5iHyppqiuTjJPx4osv+n+W6c0339QnqTLOlFfyMRWDOJSZyXXAxIX8zp07nUuXLiWYL/NkmVukAnzEiBH6daZMmXSZSYwnn3zS6dOnj6fi2JSLyQojE/nYVGmYIUOGRMd/ky5DsswtP/74o654upzMk2VuMZWPiTg25WIqjk25mIzjuxn2559/6niyTQZOyZUtWzYne/bsVzVFey62x7EpF9vimMrFZlRMecD111/vfP/99/FO4MXGjRv1wcQtEydOTPIuqZfyMVVmxKHMTK0DJi7kTY0zJyfqMlaNyJEjh7Nu3Tr9esOGDU7evHk9FcemXExWgJnIx6ZKw0KFCvnL6PLxYAoUKOC4xdQ+wFQ+JuLYlIupODblYjLO0qVLnWLFiiX60JVQtk85//dN7777rq6AevDBB51hw4bpSV7LvPfeey/qc7E5jk252BbHVC62o2LKA9KlS+fs2LEjwYXvH3/8oZ+S4xbZoOTk83JHjx7Vy7yUj6kyIw5lZmodMHEhLwfQAwcOJJgv+bl511dO1H2fXy7qpVWm+OWXX/SAq16KY1MuJivATORjU6Xh2LFjnfr16zv//POPf568lqf/jRkzxnFLsH2A3AWWbv1uMZWPiTg25WIqjk25mIxTsWJFPbSG7Fvk3Pzff/+NN7mhWbNmzvvvv59gvsxr0qSJ46VcbItjUy62xTGVi+1iIz3GFa6sbNmyasmSJapIkSLx5n/++eeqcuXKrhXhjh07Eh3c+OzZs2rPnj2eysdUmRGHMjO1DmTPnl0PsCoKFCigfv/9d1W+fHn177//qtOnT4f03t26ddP/ywDnvXv3VhkyZPAvk32CDOhYqVIl5Zbbb79dzZ8/X3/+Fi1aqC5duqgffvhBz6tXr56n4tiUS7jXM9P5mMrFRJzRo0errVu3qsKFC+tJyKDxcXFx6uDBg2rs2LH+3121atU1v3+zZs38+4C2bdvq9w3cB6xbt07VqFFDuSXc+ZiMY1MupuLYlIvJOFu2bNHnFiVKlFDhMm/ePDV48OAE8++++27Vo0cP1+KYyMW2ODblYlscU7nYjoopD+jTp49q06aNrhySp+DI04r+/PNPNXnyZDV79uyQ33/WrFnxDkiBTw+RE9IFCxaookWLKq/kYyoGcSgzk+tAOC/kV69erf+XVrTr169XadOm9S+T1xUrVlQvvviicsuIESP0E37Eq6++qp/y88svv6jmzZurXr16eSqOTbmYrAAzkY9NlYZNmzZV4eQ77ss+IHPmzCp9+vTx9gHVqlVTTz75pGvxwp2PyTg25WIqjk25mIwjT/eSCrBwXvxed9116quvvlIvvPBCvPkyT5Z5KRfb4tiUi21xTOVivUg32cLVkQEUpZlwrly59ADLNWvWdObNm+dK8QX2gb28X6wM5CqDuH799deeycdkDOJQZqbWgcOHDzt79uzxD7A4cOBAp3Hjxk63bt2cI0eOuBJDBmqWpwkh5TKxntmWi01lJg89OXnyZKQ/BoBEzJgxwylbtqwzYcIEZ8WKFXoMq8DJDfLe8oRheYjD66+/rid5HRsbq5d5KRfb4tiUi21xTOViu1TyT6QrxxAdihUrpn777TeVM2fOSH8UAJaT1phffvml2rhxo79LZJMmTVRsbKzn4tiUi0m25WPKyZMndevMQFmyZHE1xoEDB3TLT1GqVCmVO3du5eV8TMWxKRdTcWzKJdxxYmJiEsyT7rdyKSf/JzYcR3JI9/3hw4f7981lypRRzz33nG4V4hZTudgUx6ZcbItjKhfbUTHlIefOndMni5cf8Hz92d0kXSzSpUunvJ6PqTIjDmVmYh0wcSG/YsUKNW3aND0+huQUSLopuuGPP/5Q9913n9q3b5++6BWbN29WuXLlUl9//bUqV66cZ+LYlIvJ9cxUPrZUGm7fvl117txZLVq0yN8FUrh90itjZXXq1El9+umn/vdMnTq1atWqlRo5cmS8rv5eyMdEHJtyMRXHplxMxtm5c2eSyy8f6zKamcrFpjg25WJbHJu2zUiiYsoDZEC1xx9/XI+/EcjtA55cUA8YMECNGTNG7d+/X18kFC9eXA+GLGNMtW/f3jP5mCoz4lBmptYBExfycjH62GOPqbvuukt99913qkGDBjqG7A/uv/9+NWHCBBcyUap69er6c0+aNEkPHC2OHj2qB12WgWIvL8tojmNTLiYrjEzkY1OlYc2aNfU+RcavypMnj963BLrjjjuUG6QCSsace//99/V3JJYuXarjygMQZB/hBlP5mIhjUy6m4tiUi8k4psj1gIyXk9jNNhlTDwDCItJ9CXFlNWrUcG6//Xbnm2++cVavXu2sWbMm3uSW/v37O8WLF3c++eQTPU6O77H3n376qVOtWjVP5WOqzIhDmZlaB2QblHFrAseskdf33XefU716dVdilC9f3hkxYoR+nSlTJr0PuHTpkvPkk086ffr0cdySLl065/fff08wf/369XqZl+LYlIup9cxUPqZyMREnY8aMzqZNm5xwy5Ahg7NkyZJEx9GTZW4xlY+JODblYiqOTbmYjCMmT56szzvy5cvn7NixQ88bMmSIM3PmTFfef+nSpU6xYsUSHXdW5nkpFxvj2JSLbXFM5WIzKqY8QE4GN27cGPY4119/vfP999/HuygVEjtbtmyeysdUmRGHMjO1Dpi4kJdctm/frl/nyJHDWbdunX69YcMGJ2/evI5bKlSo4CxYsCDBfJlXrlw5T8WxKReTFWAm8rGp0rB27drO/PnznXArVKiQf7sPJIO3FihQwLU4pvIxEcemXEzFsSkXk3FGjRrl5MyZ03njjTfi3UCWAZflM7ihYsWKTosWLfRx/+jRo86///4bb/JSLrbFsSkX2+KYysV2VEx5wM0335zoHUy3yQm0r4Y3sGLqjz/+0HeDvJSPqTIjDmVmah0wcSEvF56+i1JpPTVlyhT9+pdffnGyZMniuGXOnDnOjTfe6EyfPt3ZvXu3nuS1xJRl8mRA3xTtcWzKxWQFmIl8bKo03Lp1q37y58SJE8P6xJ+xY8fqOP/8849/nrxu0KCBM2bMGNfimMrHRBybcjEVx6ZcTMYpU6aM8+WXXyY4T5dK8Ouuu861G1Rbtmxxws1ELrbFsSkX2+KYysV2VExFqcCTcjm5le4ACxcudA4dOhRvmZuPdq9SpYrz8ccfJ9iopItfrVq1oj4fU2VGHMosEtuniQv51q1bO++++65+/dprrzm5cuVynnjiCadIkSLO/fff71oul3cNCOwyEPhzqN0GTMSxKReTFWAm8rGp0tDXvebycnO7e02lSpX08T9NmjS6FbVM8lrmVa5cOd4UClP5mIhjUy6m4tiUi8k4wW4gb9682bXWmXXq1HG+/fZbJ9xM5GJbHJtysS2OqVxsxzOZo1S2bNniDZ4olYj16tUL6+DKffr0UW3atFF79uzRgx3KE7jkcdGTJ09Ws2fPjvp8TJUZcSizSGyfjRo10v+3bNnSH1tiiMaNG7sSc8SIEf4nCr366qsqTZo0ehDq5s2bq169eim3LFy40LX3inQcm3IxtZ6ZysdULibiyAMWKleurKZOnZro4Mpuadq0qTLBVD4m4tiUi6k4NuViMk6xYsXUmjVrEjzha+7cuapMmTKuxHj22WfVCy+8oB/mUL58eX0eEKhChQqeycW2ODblYlscU7nYjoqpKGXqIiSQPNpaniD02muvqYwZM+qKqipVquh5d955Z0jvzQViyo5jUy4m45iOmSNHDv/rmJgY1aNHj7DEudonFMlj62+88UaVM2fOqI1jUy4m120T+di0P5BHUc+aNUuVKFEirHH69u2rTDCVj4k4NuViKo5NuZiM061bN/XMM8/oG0hS2b18+XJdGTZw4EA1btw4V2LIjShfZZuPVLS5fbPNRC62xbEpF9vimMrFepFusgUAcE/Hjh2dgwcPJvvvL1y4oLshSVc+mT7//HPn/PnzEfmKMmfO7G8O7fU4NuXixnoWTfmYyiWUOI0aNdLbokknTpwIW9dkU/mYiGNTLqbi2JSLyThCnpxdokQJf5dBGRty3Lhxrr2/dEdKavJSLjbGsSkX2+KYysVmqeSfSFeOIWnr1q1LdL7cuUiXLp0qXLiwiouLc60Yz507pw4cOKC78wWSOF7Jx1SZEYcyM719XkmWLFl0c+LixYtf89/+8ccf6r777tNN+EuVKqXnbd68WeXKlUu3nCxXrpwyKXPmzGrt2rXJyiXa4tiUS6jrWbTlYyqXUOJ88MEH6o033tCtGBLrXiPbrRu2b9+uOnfurBYtWuTv1ivcbi1hKh8TcWzKxVQcm3IxGSfQ6dOn1cmTJ1Xu3LmV15nKxaY4NuViWxybtk3TqJjyAOlSk1R/dTkAtmrVSo0dO1ZfCCfXli1b9EFVxpQJ5PYJqYl8TJUZcSgzU+uAiQv56tWr60qoSZMmqezZs+t5R48eVW3btlUHDx5MsG8IN5sqc2zKxbY4XshF9jPBuHl8rlmzpj7md+nSJdGxcq62C2a05GMijk25mIpjUy4m45jy8ccfqzFjxuiK6qVLl+pxc4YOHarH0ZFhPwAgHBhjygO+/PJL1b17d/XSSy+pqlWr6nnSd/Xdd9/V40FcuHBBjwUjgxO/8847yY4jF5+xsbF6oPN8+fKFbfBGE/mYKjPiUGam1gETpDXHihUr/JVSQl4PGDBA3XLLLRH9bEBKdnkL5nCRirOVK1f6W0x6PR8TcWzKxVQcm3IJdxwZVP1qz8dXrVoVcrzRo0frMWa7du2qj/2+SjV56ItUToVSMWUqF5vi2JSLbXFMb5spARVTHiAHhmHDhqm77rrLP0+aChcsWFD17t1bXwTLYOXyFI1QLnzlolROSEuXLq28no+pMiMOZWZqHTChZMmSav/+/Xqw6UDStTfcg7oCuDrSxS5crS+lAnr37t1hr5gylY/pODblYiqOTbmEI07gkzLlvUeNGqXKli2rWziLX3/9VXfDlwdFuOH9999XH374oY47aNAg//ybb75Zvfjii57IxaY4NuViWxzT22aKEOlBrnBl6dKlczZu3JhgvsyTZWL79u1O+vTpQyrOm2++2VmyZIkV+ZgqM+JQZqbWgauVKVOmZA8WPWfOHOfGG2/Ug5/v3r1bT/K6fPnyelk4BkEOVy7RFsemXGyL44Vc5KEE8jCC/PnzO6lTp/a/T69evVwdXHXr1q1O/fr1nYkTJzorVqxw1q5dG29yi6l8TMSxKRdTcWzKxWSc9u3b6/e8XJ8+fZx27dq5EkPOW3yDnAfuszZv3uw/p/FKLrbFsSkX2+KYysV2VEx5QKVKlZw2bdo4Z8+e9c87d+6cnifLxE8//eQULVr0mt878EJzwYIFTvXq1Z2FCxc6hw4dCtvTeMKZj8kYxKHMTK4DJi5+fU8SkSkmJkZPif0s/5vQoUMHI09LMxHHplxMVuaYyMcLFVP9+/d3ihcvrp/6I5Xcvvf59NNPnWrVqrn2GZcuXeoUK1Yswb7A7e3eVD4m4tiUi6k4NuViMk6WLFl0BdHlZJ4sc0OZMmWcmTNnJthnDR8+3KlcubLjpVxsi2NTLrbFMZWL7ejK5wEjR47UT/SQrkEVKlTQ89avX6/7fct4UGLbtm3JaioofcYD+8dKZWW9evXCOvh5OPMxGYM4lJnJdeBqPfLII/rpX8mxcOFCZYJ0b5QBVeXpfyJv3ry66bNvjK7AsS6iPY5NuZhaz6Itn1BzMRFn8uTJ+slfcnzu0KGDf37FihXVpk2bXPuM8gAUGTdj6tSpiQ5+7hZT+ZiIY1MupuLYlIvJOOnTp1c///yzuuGGG+LNl3ludR/s1q2beuaZZ3TXJDn/l3217A8GDhyoxo0bp7yUi21xbMrFtjimcrEdFVMeUKNGDf1kjP/973/60e2iRYsW6qGHHtJP+RGPPvpoVF+ImsrHZAziUGYm1wETF/JX+8QtqWSTcahy5sx5Te8vY1U1b95cH6gLFy6sL3yFjGv1/PPP6yeCffHFFyE/YtdEHJtyMbmemczHpkrDPXv2JDrOmwy6fP78eeWWnTt3qlmzZoV9TDlT+ZiIY1MupuLYlIvJODIgeceOHfVAyr79y7Jly9T48eP1mJZueOKJJ/RFtjywRR57L+cy+fPn12NpPvjgg8pLudgWx6ZcbItjKhfrRbrJFgAguP379zu1atXSXWmKFCniVK1aVU/yWubJMvkdkzJnzpysLknNmzfX3YU3bdqUYJnMq1GjhvPAAw+E/PlMxLEpF5PrmYl8TOVictusUqWK8/HHHyfoXiNdiCSOWxo1auR8/vnnTriZysdEHJtyMRXHplxMxhGfffaZ3k9mz55dT/Ja5oXDqVOnwnp+YSoXm+LYlIttcUxum7aixZSHbNiwQe3atUudO3cu3nzpRuSGdevWJTpfmvJLM0S5ux0XF6e8ko+pGMShzMK5DkjrJOkWuHHjxgRPyvrzzz911xtpdj99+nRlijTvT4558+apxYsXJ/rEL5k3fPhwVbt27ZA/n4k4NuVicj0zkY+pXExum/L49jZt2uiWGdIKY8aMGTqGdCHydRl2Q+PGjXXLNemOLE8XTZMmTViOnabyMRHHplxMxbEpF5NxRMuWLfVkQoYMGfQULqZysSmOTbnYFsfktmmtSNeM4crkzkuFChXiDUIaOBixWwLfM7EpLi7Oeeyxx5z//vsv6vMxVWbEoczCvQ7I3ddVq1YFXS5PzpLfMSm5gzhfd911zqJFi4IulwcvyO+EykQcm3IxuZ6ZyMdULqa3zcWLF+sn5uXKlUsPsFyzZk1n3rx5jpsCBz2/fHL7oQcm8jEVx6ZcTMWxKReTccJBHtQiA5tfzQQA4ULFlAdI0/omTZroJxPJSe6GDRucJUuW6C4DciB0izyFo1SpUvrRtuvWrdOTvJYndMiTReRpIwULFnReeOGFqM/HVJkRhzIL9zpgqmLCRMVUp06ddDenGTNmxHvSp7yWefLkws6dO4f8+UzEsSkXk+uZiXxsqzS8FlOmTHFOnjzp2MJUPibi2JSLqTg25eJGnAsXLjhvv/22c8sttzh58uTxdxnyTcnVr18//9SjRw/9FDF5muDzzz+vJ+l+LfNkmVvClYvNcWzKxbY4pnKxHRVTHiAntmvXrtWv5cDgG5tjwYIF/sfRu0E2prlz5yaYL/Nkmfjyyy/1I3GjPR9TZUYcyizc64CpigkTFVNnzpxxOnTo4KRNm1a3vkiXLp2e5LXM69ixo/6dUJmIY1MuJtczE/nYVmloYvy3xITaOjra8ol0HJtyMRXHplzciNO7d28nX758zjvvvKP3m6+//rrTvn17fR4ybNgwVz6jvF+vXr0SzO/Tp4/Trl07xy0mcrEtjk252BbHVC62o2LKA7Jly+Zs27ZNv5ZKoR9++EG/3rp1q24u7BbZkDZu3JhgvsyTZWL79u0hxzSRj6kyIw5lFu51wFTFhImKqcALdyknuXssk7wOvLB3i4k4tuRiej0LZz62VRqa3Dblru9rr73m5M+f30mdOrX/veRCVVpQmxZqPtEUx6ZcTMWxKRc34sg5xuzZs/3vJecZQi58W7du7cpnlBtsmzdvTjBf5skyt5jIxbY4NuViWxxTudiOwc89oFy5cmrt2rWqWLFi6tZbb1VvvfWWSps2rfrggw9U8eLFXYtTunRpNWjQIP2+8v5CHnMr82SZkIEdfY/3juZ8TJUZcSizcK8D8sABecz84MGD1cqVK+M9kv6mm25SWbJkUaY98sgjIcWVv61Tp46rnylScWzJxfR6Fs58TOUSjdtmqAYMGKAmTZqk92NPPvlkvP3c0KFDVfv27SP6+YCUTPYx8lACkSlTJnXs2DH9ulGjRq49kj59+vTq559/VjfccEO8+TJPHoTkpVxsi2NTLrbFMZWL7aiY8oBevXqpU6dO6devvfaaXslvu+02dd1116nPPvvMtTgjR47UT9wpWLCgqlChgp4nT+aRpw75niqybds2/SSiaM/HVJkRhzIztQ6YqABZvny5Wrp0abwL7OrVq6uqVavG+z25GA+H/fv3q7Fjx+onHIWTiThezcVURZuJfGypNDRJniImler16tVTHTp08M+vWLGi2rRpU0Q/G5DSyfn5P//8o5+Sff3116vvvvtOValSRf3222+uPTW7a9euqmPHjmrVqlX+Y/+yZcvU+PHjXb3ANpGLbXFsysW2OKZysV6km2wheQ4fPuxcunTJ9eI7fvy4M3r0aP+Ah2PGjNHzvJqP6RjEocxMrgNi3759Tv/+/UN6j/379zu1atXST96SMXNk4HaZ5LXMk2XyO+G2Zs0a15/8Fak4NuXi1noWLfmYysVUHDe7CklXxB07diR4rz/++MPJmDGjY5pXulhFSwzb4tiUixtxunfv7gwYMEC/locSxcbGOiVKlNBdh2WZWz777DOnRo0a/oGb5bXMc5OpXGyKY1MutsUxlYvtUsk/ka4cw9XbvXu3/r9QoUJWFJuJfEyVGXEos0hsn9KNUO7KSMvG5HrggQfU3r171YQJE1SpUqXiLfvzzz/V448/rvLnz6+mT58e0mddt25dksulRUbr1q1DysVUHJtyMbWeRUs+buUSLXECZc6cWcdNbjdi6YL4/PPP6+66ge8lrUHnz5+vlixZokwKNZ9oimNTLqbi2JRLOOJIC2eZpNtd48aNlZeZysWmODblYlscm7ZNk+jK5wEXLlxQ/fv3V8OHD1cnT57091999tlnVd++fVWaNGlcjbdhwwa1a9cude7cuXjzpZufV/IxVWbEoczCvQ5c6UJeKo5CNW/ePLV48eIElVJC5klutWvXDjlOpUqVVKpUqaSlboJlvvnyvxfi2JSLqfXMVD6mcjEV51oUKVIkpH2OdKFs06aNHk/y0qVLasaMGToP6eLn69LvpXyiKY5NuZiKY1Mu4YgjXe1lsoGpXGyKY1MutsWxads0KtJNtnBl8tSf3Llz62518lh6meR13rx59TK3SPPiChUq6K470pVC/ve9drNrhYl8TJUZcSizcK8Dl2+PgZNvfqjbpzzOdtGiRUGXL1y4UP9OqOQ9PvroI91VKLFpzpw5ruxrTMSxKRdT65mpfEzlYiqOOH/+vO7mOHfuXD3J63PnzjnhsHjxYqd+/fpOrly59JNFa9as6cybN8/VGKbyMRHHplxMxbEpF5NxxOTJk3XXOnk0va/b7ZAhQ5yZM2e68v7yZM63337bueWWW5w8efL4u/P5Ji/lYmMcm3KxLY6pXGxGxZQHyONZv/nmmwTz5QTezUe3NmrUyGnSpIlz8OBB3Q9+w4YNzpIlS/RYM3Ki6qV8TJUZcSizcK8DJi7kO3XqpMeTmjFjhnPs2DH/fHkt84oWLep07tw55FwaNGjgvP7660GXy8m8XMx7IY5NuZisADORj02VhhcvXnReffVVJ1u2bAkqv2Rer1699O+YNmXKFOfkyZNRm4+JODblYiqOTbmYjOMzatQoJ2fOnM4bb7yhK41941VNmDDBqV27tisxevfurS+s33nnHT3mnOyv27dvr/d3w4YNc7yUi21xbMrFtjimcrEdFVMeIHctpZLocjJPNgK3yEFHWnsIuaDetGmTfr1gwQKnUqVKnsrHVJkRhzIL9zpg4kL+zJkzunWXDNIoF9JyMiqTvJZ5HTt21L8TKqnk+vjjj4MuP3LkiDNx4kRPxLEpF5MVYCbysanS8KWXXtL7GGmFuX37duf06dN6ktdjx47VrTVffvllx7TMmTMnaxBnU/mYiGNTLqbi2JSLyTg+ZcqUcb788ssEA6mvX7/elVbNonjx4s7s2bP9MbZu3apfS6VU69atHS/lYlscm3KxLY6pXGxHxZQHyFN95GAQeGEorx9++GGnX79+rsWRuzvbtm3zH5h++OEH/VoOSlL766V8TJUZcSizcK8DpiomfC2kZLuX1hAyyevAFlSwl8n1LNxsqjSUrjTSNSgYWSYXv6Yl9+lipvIxEcemXEzFsSkXk3Gu9NTMzZs362VuyJAhg7Nz5079WoYkWLlypX4tsdzscWAiF9vi2JSLbXFM5WI7Bj/3gNWrV6sFCxaoggULqooVK+p58lQPGZy8Xr16qlmzZv7flYFKk6tcuXL6fYsVK6ZuvfVW9dZbb6m0adOqDz74wNUnlZjIx1SZEYcyC/c6cP/99ye5PHv27HqwYjdkyZJF1alTR4XbmTNnVLp06RJd9s8//6h8+fJ5Jo4tuZhcz8Kdj6lcTMQ5ceKEfiJmMFJOp06dUl5hKh8TcWzKxVQcm3IxGcdHzs/XrFmjB1EPNHfuXFWmTBlXYsi5jOyDCxcurK6//nr13Xff6aeL/vbbbyouLk55KRfb4tiUi21xTOViOyqmPCBbtmyqefPm8eaF43H0vXr18h9A5dHQjRo1Urfddpu67rrr1GeffeapfEyVGXEoM1PrgKkKkMTs379fjR07Vj+xyw1ykjtlyhT9hLZAX3zxherQoYM6ePCgZ+LYlIvJ9cxEPjZUGsrTMF988UX1v//9T+XMmTPeskOHDqnu3bu78sRMU0zlYyKOTbmYimNTLibj+HTr1k0988wzep8jvV6WL1+upk6dqgYOHKjGjRvnSgypcJebbXKDWp4u/Mgjj6iPPvpIP637+eefV17KxbY4NuViWxxTuVgv0k22EN0OHz7sXLp0KdIfA0jxpP/66tWrE5TD559/7uq4acHGynHzyZwyZlVcXJwzaNAg/bMMotymTRvdZfi9997zVBybcjG5npnIx1Qu4Yyza9cup1y5ck5sbKxTuXJl5+6779aTvJZ58iRd+R2vdOUzlY+JODblYiqOTbmYjBPok08+cUqUKOEfZL1AgQLOuHHjnHD55ZdfnHfffdeZNWuW6+9tKheb4tiUi21xTG+bNtKjcka6cgxJ+++//3Tta4YMGfTPO3fuVF9++aUqW7asatCgQViKb/fu3WFr+WEiH1NlRhzKzNQ60KlTJzV+/HjVv39/fRdWWjfK3Zlp06apAQMGhHQnc926dUku37Rpk2rdurW6ePGicsucOXPUE088oUqUKKFblWTKlEl98sknukuxm0zEsSmXcK5npvMxlUu441y6dEnNmzdP/frrr2rfvn16Xt68eVX16tX1PiYmJkaZljlzZt1lOTnd/E3lYyKOTbmYimNTLibjXO706dPq5MmTKnfu3MrrTOViUxybcrEtjk3bpnGRrhnDld15553O6NGj9eujR4/qgRQLFiyoB1OTx1O65fz58/rRtjK4obSOkEley6Nwz50756l8TJUZcSgzU+uAkCflyGCktWrVcq6//nqnYsWK+okfoZI7O7K9X/6468D5braYEvII7U6dOun3TpMmTZIDyEZ7HJtyCed6Fol8TOViKk60uPHGGyPSUgtA+E2ePNmpUaOGky9fPv+AzkOGDHFmzpxJ8QMIG8aY8oBVq1apIUOG6Neff/65vhMjAy7LWBwy5kvHjh1diSN9yWVwZhn0XO70iKVLl6p+/fqpw4cPq9GjR3smH1NlRhzKzNQ6IBo2bKgHU5dtMTY2Vn399deutC7JkSOH3u5lsPbE/PHHH6px48bKLX/99Zd66KGH9N1ludP8448/qvvuu091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RlWqVCGr3Llzh7p160YrV66kOXPmUN68ecmEBQsW6CGva9eubcnn8Xll+fLlesh7Hk45W7ZsVLFiRXr66act+Xwepvn111/3yzE4+nFg//79VLBgQUqZMiXdunWLfvrpJ7p586YewpnnW2HHjh00a9YsPfw4/w34s620d+9efd565plnYl3Ox7QzZ85Yct7k4+LQoUP1PspDrDM+v/B1QOfOnem1117zOcb48ePpr7/+oqlTp8a6fPDgwTR27Fi9/fmCj8UZMmSgypUrx7p80KBBehvp378/WcV1/j9+/Djt2rWLihQpYtk+44m33fLly7t/5xx4v/322299jsXb04QJE+j333/Xw597HgPatWtHOXLk8Onzq1atqv82cR2brTR9+nT3dUCzZs1o1apV1KtXL70/NWjQgD7//HN9nvAFX7fytQWfO13HgOHDh1PGjBn18k2bNuljT1zbYaDhv/fSpUtjvQ6oVauWz+uL8f7Bx+a4zll8PONzkFX3NnHhv03y5MmpWLFiln82X3PytQBff/q6zzwO/nvxOdV1/ekrPv7zecDzOoDPA6lSpbLk8/kcwMcUvscw4b///tPnA5cVK1boawHerj3nxxdf6/Mxk68Dq1WrZtmxPyFCwRQYxxdvlSpVcnwMiXEYl1VzoQufjPjiim96nZqLJ7445Ituq06sLocOHdIXpIUKFTJ2kgUA8Bc+Tv7777/6tT/OAZLxQx1+yFO4cGFKkiSJ3V8HhOHrGCseRCakXCTFkZSLtDiS9k07+V70DX7HN/J2ioyMpH79+ln2eVyazLUJevTooZ8G+4OJGBLj8LbGtX+yZMmin5D444bEZC6e+IRhdaGUa//gJ27+LpTKnz8/9enThw4ePEh23KieOHHCcbmYiCMpF884XODq9Hy4lpeJdeaK48919sUXX/hci+hx8XGfj//+OgeYzMdEHM8YXDuvZMmSfimUkrrOEOfxcU3vVq1a6RpF/PDQJK5huHr1asflIikOx+CaZBJyMR3H3+vNzn1TlMds8gc24j4FuE1x9+7dLe234HFZ0ali9E4UeSQ77uOF2/1y3yw8BPrJkycdFUNiHM9tLfoIgFaRlIvJ/ZOHOuf+P3hf5J/cp4Br9C+nHQNM5WIijqRcPOPwvun0fCT9bbh/F/587nya+3qKrYNif+K+7HgESqflYyKOpFxMxZGU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o2pTAQAAAAAAAADEB/qYAurSpQtFRUXFuSby589PK1euxJoCAAAAAAAAAEuhjykAAAAAAAAAALAFmvIBAAAAAAAAAIAtUDAFAAAAAAAAAP9fe/cBJkWxPX6/Fpacc85cJUdFkkiSJCjCVUSUIMoVhauACKhERcQIKoiKgKBI8KIiIEEEQUSQjIDkKFFykNzvc+r/zvxmNpCmp2a69vt5nmZnu4c5c2p7OtRUUEAkUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEVRMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigoopAAAAAAAARAQVUwAAwAq7du1SMTExiS7t27eP9FsEAABAHLFxVwAAAHjZvffeq9q2bev//cyZM6pz584RfU8AAABIGBVTAADACo7j6J+33Xabeuyxx/zr//77byqmAAAAohRd+QAAgBUuXryofyZPnvym/p908Uus+1+gwoULq9q1a8eL+a9//Us/d8CAAf71Cxcu1OvGjRsXL56sk23yHJ/9+/erHj16qAoVKqgsWbKo1KlTq1KlSqmhQ4eqK1euhJyHLPL+fcqXL68KFiyorl69Gu91pk6dqp8/fvx4/3u93nIrZQQAACBoMQUAAKyqmEqVKtUt/f8JEyb4H3/yySdq8eLF1/0/7733ntq2bZsK1bp169S0adPUgw8+qIoVK6YuXbqkZs+erXr37q127NihPv7441vKw2fw4MHqn3/+8f/+1FNPqa5du6p58+aphg0bBj33s88+U5kyZVIPPfSQOnDgQNDryXv85ptvdN7Zs2e/offjVhkBAAA7UTEFAACscOLECf0zXbp0N/X/pBJIWvMEdv/78ccfr1sxJZU2r732mnrggQfUd999p0Jxzz336AqowNZHzz//vHr88cfV6NGjdUujPHny3NBrBebhI68hg8MHPufFF1/UlVCBFVN79+7VlVX/+c9/VJo0aVTRokX14iMVTFIx1bx586AWWCbKCAAA2ImufAAAwApHjhzRP3PmzHnTLa1upZWVtGaSLnfX6p4mA6/LGFeBi6yLSyqBfJVS8n6OHTumnyuVRtLdbsWKFcpNmTNnVg8//LCuLDp69Kh//dixY3W8jh07uhLn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2X6EXbt2Vd98841auHChKlKkyHX/z5o1a/RPaTklqlWrpgYPHqwOHz6sm32JefPm6UqnUqVK+Z8za9asoNeR58h6AAjVjsEtE91W9OX/UcAAAAAAYEjszXbfmzhxovruu+9UhgwZ/GNCyUjr0pJJuuvJ9iZNmqhs2bLpMaa6deumZ+wrV66cfm6DBg10BdTjjz+u3nzzTf0ar7zyin5tX4unp59+Wn344YfqxRdfVE888YT66aef1JQpU/RMfQAAAAAAACY06/Gd0YL+/p0Hbvr/tG/fXn3++ef+36Vr3Z133qnrXHx1MQm1ApMxw33DM3366ae6HkbqdaQrozREevjhh/WwSeF2U33cPvroIz0TX+3atXULKN8yefJkvV2mCPzxxx915VOJEiVUjx49VMuWLdX333/vf43kyZPrboDyU1pAPfbYY6pt27Zq0KBB/udIAUgllLSSKl++vHrnnXfU6NGj9cx8AAAAAAAA+D+NGjVSBw4c0Mv8+fN15VLTpk0DnqHU2LFj/c+RZfr06Xr9mDFj9OR2//3vf3WvtyVLluiGQmfOnFEm3HRXvmuRwcZ//vnn676OzNoXt6teXFL5tXr16pt5ewAAAAAAAElOqlSpgsbt7t27t7r77rvVkSNH9KRwInPmzP7nBJIKKmkd1bFjR/+60qVLG3vvjAoOAAAAAABgiTNnzqgvvvhCFS9eXA+zdD1SWfXbb7+p3bt3q0hgDkQgTBhgGwAAAABgwowZM1T69On147Nnz+phl2Rd4Ox4rVu31sMq+UjlVfPmzVX//v1VixYtVOHChdVtt92mh12SscP//e9/39TsereKFlMAAAAAAAAeVqdOHT0+lCzLly/XY3Q3btw4qBXUe++953+OLPfee69eL5VYS5cuVevXr1fPPfecunz5smrXrp0et+rq1athf+9UTAEAAAAAAHhYunTpdNc9WWRGPplATlpOyWx7gV32fM+RRf5PoDJlyqhnnnlGt6SSyehkuZFxxENFxRQAAAAAAIBFYmJidDe8f/7555b+f6lSpfRPqdwKN8aYAgAAAAAA8LALFy6ogwcP6sfHjx9XH374oR4EvVmzZtf9v507d1Z58+ZVdevWVfnz51cHDhxQr732mp7NT8abCjcqpgAAAAAAADxs9uzZeqwokSFDBlWiRAk1depUVbt27ev+3/r166sxY8aojz76SB09elRlz55dV0jNnz//hmb1CxUVUwAAAAAAAAn4/p0Hor5cxo0bp5drcRwn0W0tW7bUS6QwxhQAAAAAAAAigoopAAAAAAAARAQVUwAAAAAAAIgIKqYAAAAAAAAQEQx+DiCq7Bic8KB7RV/+n/H3AgAAAAAIL1pMAQAAAAAAICKomAIAAAAAAEBEUDEFAAAAAACAiKBiCgAAAAAAABFBxRQAAAAAAAAigln5AAAAAAAAbmLW8HApeguzkbdv3159/vnn+nFsbKzKmjWrKleunGrdurXelizZ/2uTVLhwYbV79+6g/5svXz61b98+/fibb75RQ4cOVZs2bVJXr15VBQsWVPfee68aNmyYCidaTAEAAAAAAHhYo0aN1IEDB9SuXbvUDz/8oOrUqaOee+451bRpU3X58mX/8wYNGqSf51tWr16t18+fP1+1atVKtWzZUi1fvlytXLlSDR48WF26dCns750WUwAAAAAAAB6WKlUqlTt3bn8rqEqVKqmqVauqevXqqXHjxqknn3xSb8uQIYP/eYG+//57VaNGDdWzZ0//uttuu001b9487O+dFlMAAAAAAACWqVu3ripfvryaNm3adZ8rlVUbNmxQf/zxhzKNiikAAAAAAAALlShRQnfv8+nVq5dKnz69f3n//ff1+q5du6o777xTlS1bVo9F9cgjj6gxY8aoCxcuhP090pUPAAAAAADAQo7jqJiYGP/v0lVPBkT3yZ49u/6ZLl06NXPmTLV9+3a1YMEC9dtvv6kePXqo4cOHq6VLl6q0adOG7T3SYgoAAAAAAMBCmzZtUkWKFAmqiCpevLh/yZw5c9DzixUrpsejGj16tFq1apXauHGjmjx5cljfIy2mLJ668lammQQAAAAAAN73008/qfXr16tu3brd0v+XLn3SUurs2bMqnKiYAgAAAAAA8LALFy6ogwcPqitXrqhDhw6p2bNnqyFDhqimTZuqtm3bXvf/DxgwQJ07d041adJEFSpUSJ04cUKPP3Xp0iV17733hvW9UzGFJCmxlma0MgMAAAAAeM3s2bNVnjx5VGxsrMqSJYuejU8qltq1a6eSJbv+KE733HOPGjFihK7EkooteY2KFSuquXPnqttvvz2s752KKQAAAAAAAI82Xhg3bpxeridwdr646tSpo5dIYPBzAAAAAAAARAQtpgCPYwB8AAAAAIBX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ee1rDdR68eJF3YLaK/mYjGNTLqbi2JSLyTgysPJDDz3kfPvtt/oz6abBgwfrFtKJ2bNnj9O+fXtP5GJrHJtysS2OqVxsR8WURWQGkP379zu2MJGPqTIjDmUW6j6wZcuWG3qetNg6c+aME81M5WIijk252BbHplxulHQj3r59u2MLU/mYiGNTLqbi2JSLG3FkYOUbMWTIEOf48eNONDOVi01xbMrFtjg2fTYjiYopi6RPnz6kE97ixYsdm/KJlhjEocxM7gM23Zh65WYhWmIQhzIL9Tgj3fplrJxoYdM52qZcTMWxKReTcUI538gYOdGEcydlZtM+YNM1ejgkU8D/r27duqpIkSLqpZdeUhs3bqRcAA+SLxxuVfHixdWAAQPUli1blNdzibY4NuViWxybcgnV1KlT9XGgevXqauTIkervv/+O9FsCYPBYkzNnTtW+fXs1b948dfXq1YiXvW3HZ85pSTuOF64DIomKKfjt379f9ejRQ/3888+qTJkyqkKFCuqtt95S+/bto5SAJODZZ59VM2fOVCVLllR33nmnGj58uDp48GCk3xYAQ9auXavWrVunateurd5++22VN29edd9996mJEyeqc+fO8XcALPf555+rs2fPqgceeEDly5dPPf/882rFihWRflsAkgAqpuCXPXt21aVLF7VkyRK1fft29dBDD+kTVOHChXVrKgB269atm/r999/Vpk2bVJMmTdSIESNUgQIFVIMGDdT48eMj/fYAGFC6dGn1+uuvqx07dqgFCxboawC5Oc2dOzflD1juwQcf1C0nDx06pI8D0oOiatWq6rbbblODBg2K9NsDYDEqppAg6dLXu3dv9cYbb6iyZcvqVlQAkga5AB04cKDu0rd48WJ15MgR1aFDh0i/LQDXERMT42oZpUuXTqVJk0alTJlSXbp0yfP5RDKOTbmYimNTLibjuCFDhgz6vD937lzdilKOBXJdAADhQsUU4pEWU88884zKkyePevTRR3W3PuneAyDpWL58uW4lId+eSgWVtKAEEN3cGL9i586davDgwbrl1B133KFWr16tb0gj0a2XsUUoM/azyDh//ryaMmWKat68uapUqZI6duyY6tmzZ4TeDYCkgIopi8ig5VmzZr3l/9+nTx/dUkq67e3Zs8c/vsyECRNUo0aNlNfyiZYYxKHMTO4DhQoVUilSpLil/ysV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/Ry3vin1dUsI9PXXX7syzpypXEzEsSkX2+LYlMt//vMf59NPP010+5AhQ5wmTZo4oZKWHWXKlHFiY2N1qwjphiyLPJZ15cqV088Jlal8TMSxKRdTcWzKxWScQJMnT3aqV6/uZMmSRS/yWNa56YsvvtDHL+maKEu+fPmc0aNHO24zkYttcWzKxbY4pnKxGRVTFgm18kMGOj179qz/d+lj/t577zlz5sxxIoGKqaQdx6ZcQolTv359PZjqyZMn422TdbJNBi93Q+fOnXW3gzfeeMM/Zka7du10d4V333035Nc3lYuJODblYlscm3K5nh07djj79+935bWkq/GsWbP0YK2dOnXSizz+4YcfjHVDdjOfSMexKRdTcWzKxWScuCZOnKjP36GSe4JDhw45keRWLkkpjk252BbHVC5eRcWURUK9wZaBmz/66CP9WGYXkj6x+fPn19/8jhw50jGNyo+kHcemXEKJI5VCMhtOYmRcGHmOW2bMmOHkzp3bqVmzpp5VrHz58teMH425mIhjUy62xbEplxsR+IWSDUzlYyKOTbmYimNTLibjBJLxYm3pQWEqF5vi2JSLbXFs+myGQ7JIdyVE9JB+sXfffbd+/PXXX+txMnbv3q3Gjx+v3n///Ui/PSBJkrHfdu3aleh22SbPcUvjxo1VixYt1JIlS/TYGUOHDlVlypTxVC4m4tiUi21xbMrFp169euqvv/6Kt17Gf6lQoYIrMeTLyp07d6rLly/7x8+aPHmyvgaQcabcZCIfU3FsysVUHJtyMRnnZj7LN6NixYqqUqVKN7REey7EocyieR8wtT97FRVTFomJiQnp/587d05lyJBBP547d66+OU2WLJmqWrWqrqDyWj7REoM4lFko+8CTTz6p2rZtq9577z21bt06dejQIb3IY1nXvn171alTJ1f20+3bt6tq1aqpGTNmqDlz5qgXX3xR3X///frnpUuXQn59U7mYiGNTLrbFsSkXH5mUoFy5crqiSFy9elUNGDBA1axZUzVp0iTk19+8ebMqUqSIKl68uCpZsqSuoJIB3Dt27KgHdJV1W7duVW4Jdz4m49iUi6k4NuViMk64NG/eXE/WIEvDhg31tUCqVKn0hAeySH6yTrYBQNiEpR0WIiLULklly5Z1hg8frgc4zZgxo/Prr7/q9StWrNDTYJpGd7GkHcemXEKNI2M+5cmTRw9CmixZMr3IY1k3dOhQV99jq1atdFdenyVLlugufRUqVHAlhqlcTMSxKRfb4tiUi8+HH37opE2b1mndurVTrVo1J2/evK6NASnjYd1///26++Hzzz+vJ0KQdRcvXnTOnz/vNGvWzHnsscccr+RjOo5NuZiKY1MuJuOE+3qjY8eOziuvvBJvvYw316FDB8c0L1yjRVscm3KxLY5tE5W5jYopD5GLQ1kSIxVKly9fvuXXnzp1qpMiRQp9YS3jTfm8/vrrenYer+VjKgZxKDNT+4AMpCoVxrLIY7eNHz8+wfWnTp1ynnjiCVdjhTsXk3FsysW2ODblInr37q0rvuRcLZXGbsmRI4d/Rk4ZmFViLF682L9dYhUsWNDxSj6RiGNTLqbi2JSLyTjhvPmVL6a3bNkSb72sk22m2VQpYSqOTbnYFoeKqWujYirKzZ0712ncuLGTOXNm/7ex8ljWzZs3z/V4Bw4ccFatWhU0A8+yZcucTZs2+X/fu3fvLc/QYyIfU2VGHMrM9OczsQqvSHyL6eVcTMSxKRfb4ngxl2PHjjktWrRwMmXK5HzyySdOmzZtnHTp0jkjRoxw5fVlkPbdu3cHXTxv27YtKBeZsdMt4c7HZBybcjEVx6ZcTMYxcfMrPSTGjh0bb72sk0mRTLOpUsJUHJtysS0OFVPXRsVUFBs3bpwTGxvrPPLII/qEINM4yyKPpamwfCOTWAuHaJxRwEQ+psqMOJRZtHw+16xZoyvEQnXhwgVn8uTJuhuP5CSLPJ4yZYreZoJbuURDHJtysS2OF3ORbkE1atQIao01adIkJ2vWrE6TJk1Cfn3prhvYQkpm4pWWkj4rV67Us3W6Jdz5mIxjUy6m4tiUi8k4N6p06dK6MvlWDBkyRM/G3bVrV2fChAl66dKli+6mKNu8lEtSjWNTLrbFMZWLV8XIP+EbwQqhuO2229Rzzz2nnn322QS3jxw5Ug+w6uaApDdCBkhfu3atKlq0aNTlY6rMiEOZmdoHpk+ffs3tO3bsUD169FBXrly55Rjbtm3Tg5ru379f3XXXXSpXrlx6vQzkvGzZMpU/f371ww8/6IGRoz0XU3FsysW2ODbl4vPqq6+ql19+WU9IEmjfvn2qQ4cOat68eSG9/tNPP63uuOMOPaB7Qt544w21ePFiNXPmTOWGcOdjMo5NuZiKY1MuJuPIjJkbNmxQBw8e1L/L7NmlSpVSKVKkUG6aMmWKGj58uNq0aZP+XSY/kOudhx9+2LUYpnKxKY5NudgWx1Qu1ot0zRgSJ83m//zzz0S3yzb5VsO0W22GaCIfU2VGHMrM1D7gG1RZfia2hNoqo379+nqg45MnT8bbJutkW4MGDRwv5GIqjk252BbHplyihbQE2b9/f6TfBpAkyfAZL7/8sh4qIO4xRtbJYOW3OsTGrZo4caIejy5ac7Epjk252BYnGj+bXkbFVBSrVKmS07Nnz0S3v/jii/o5XqmYMpGPqTIjDmVmah+QLgLffvttottlwOJQb35lfJn169cnul1m6pLneCEXU3FsysW2ODblEjjW47Bhw/TgyrLIY1ln0tmzZ117LVP5mIhjUy6m4tiUi4k4cq0hExSMGjXK2blzp3Pu3Dm9yOOPP/5Yj/0k1xxeGNbDVC42xbEpF9viRONn08uomIpiCxYs0IMnli1b1unWrZuelloWeVyuXDldQfTzzz97pmLKRD6myow4lJmpfUCmae/bt+81x7GRb2ZCIVPbf//994lunz59un6OF3IxFcemXGyLY1Muhw4dcmrWrKlfp1ChQk6VKlX0Io9lnWyT57ilbt26zr59++Ktl5vsf/3rXyG/vql8TMSxKRdTcWzKxWQcGZB89uzZiW6XbaYHJr/VewFTudgUx6ZcbIsTjZ9NL6NiKspJjavUtNaqVcu57bbb9CKPe/XqpbdFwq1+S2IqH1NlRhzKzMQ+sGjRIueHH35IdLs0pV+4cGFIMeTmOkuWLM67777rrF271jl48KBe5LGskwFc+/fv73ghF1NxbMrFtjg25dKyZUunWrVqCXYblnXVq1d3/v3vfztukYGa5fMuAzcL6YIgn32ZzOG5554L+fVN5WMijk25mIpjUy4m48jA49JyOTFyrpYvyrxQMWUqF5vi2JSLbXGi8bPpZVQ0S/ebAAAZkElEQVRM4aYx1SVgH2ntJa2ifOPi+MbOkXVDhw6N9NsDkuz5dtWqVYluX7FihX6Omz788EN9sS2zi8pNt3RZnDNnjqfyMRHHplxMxbEpF5NxpMJYxnk8cuRIvG2yrlGjRs59993neOFewFQuNsWxKRfb4kTjZ9PLYiM9+DpufqT/PHny6BkywjXS/4ULF/TPVKlSJbh948aNKm/evFGdj6kyIw5lZvrzGS69evXSy86dO4NmFSlSpEik3xqQZMl5+NSpU4luP336dKLn6lslM43KbGJDhw5VsbGxauHChap69eqeysdEHJtyMRXHplxMxhk1apRq0qSJvr4oW7Zs0My569ev17N/zZgxQ3mBqVxsimNTLrbFsemzGRUiXTOG6Bjpf+7cuU7jxo316/paS8hjWTdv3jxXYtg0OwJxKLOkNBPHnj17nA4dOkT6bQBJzjPPPKPHq5k2bVrQrJnyWNYVLlzY6dKli2vxjh075rRo0cLJlCmT88knnzht2rTR3RBGjBjhqXxMxLEpF1NxbMrFZBwh1xOzZs1y+vXr53Tq1Ekv8li6E0fiWiOU3hOmcrEpjk252BYn2j6bXkbFVBQzNdL/uHHjnNjYWOeRRx5xxo4dqz9csshjacovY0uMHz/eE/nYNNODbXFsysVknGgggzjbMu094CXnz593nn76aSdlypT6M5g6dWq9yGNZ17lzZ/0ct0i3vRo1ajg7duzwr5PxpmTcKemy4JV8TMSxKRdTcWzKxWScaFS6dGn9pRUAuEVPFxPpVltImHSj+fzzz1XDhg0T3D5nzhzVtm1b3VwwFLfddpt67rnndPP9hIwcOVK99957auvWrVGfj6kyIw5lZmofMGH69OnX3L5jxw7Vo0cPdeXKFWPvCcD/ke5CK1euDOpmW7lyZZUxY0ZXi+nVV19VL7/8skqWLFnQeuna16FDBzVv3jxP5WMijk25mIpjUy4m4wSSbvfbtm3TXYjKlCkTtuEJJBfpjhTO4QnClYvNcWzKxbY4pnKxkmtVXHCdqZH+U6VKleCMIj6yTb4BCpVNsyMQhzKzaSYO34DncbskBi60mAIiY+PGjc6YMWOcTZs26d/lp7TSkO618+fP99yfxVQ+JuLYlIupODblYiqOtLw6ffq0fiwts2U2wMBzc506dfzbo314AhO52BbHplxsi2Mql6SCiqkoZmqk/0qVKuluSYmR7kjynFDZNDsCcSgzm2bikO473377baLbV69eTcUUEAEyRoV0CZKudPIFkfwuXYjr16/v1K1b10mePLmrN9nLli1zhg0b5vTu3Vsv8ljWeS0fE3FsysVUHJtyMRlHbnAPHTqkH/fp08fJnz+/89NPPzlnz551fvnlF6dYsWL68+qF4QlM5GJbHJtysS2OqVySCiqmopj03S5Tpowe/6lixYr6RlcWeSzrypUr50r/7gULFuiWHWXLlnW6deump42XRR5LDBng8Oeff/ZEPqbKjDiUmal9wIRmzZo5ffv2veYYU/LtDwCzqlWrplsxiK+++srJkiWL89JLL/m3ywXvvffeG3IcubCuWbOm/pzLYM5VqlTRizyWdbLNd/HthXxMxLEpF1NxbMrFZBz5DPo+f3LdMXHixKDt3333nXPbbbeFFCNXrlzO7NmzE90u26Ryygu52BbHplxsi2Mql6SCMaai3NWrV/VYNb/99ltQf+9q1aqpBg0axBsH4lbt2rVLffTRRwnGefrpp1XhwoU9k4+pMiMOZWZqHwi3xYsXq7Nnz6pGjRoluF22rVixQt1zzz3G3xuQlGXKlEmPXVO8eHF9vJGp55cvX64qVqyot//xxx+qfv36/uPPrfr3v/+t9u/fr8aOHatuv/32oG2bN29WTzzxhMqbN6+aOnWqJ/IxEcemXEzFsSkXk3HkWkLGq8yRI4deFi5cqEqXLu3fvnv3blWyZEl17ty5W46RLl06fS0jU94nZN26dap69erqzJkzKtpzsS2OTbnYFsdULklFbKTfAK6/wzdu3Fgv4SQVT0OHDrUiH1NlRhzKzNQ+EG533333dS9YqZQCIiMmJsZ/vEmdOrW+GfbJkCGDOnnyZMgxpIJ90aJF8SqlhKx7//33Ve3atZVX8jEVx6ZcTMWxKReTcfr27avSpk2r40glcuDN79GjR/V5OhTy+X7hhRfUl19+qbJnzx607e+//1a9evVy7RgQ7lxsjGNTLrbFMZVLUuCNr/ORaCsGuZB0cyaOtWvX6gtUWeTbkUuXLnk2n0jFIA5lZnIfAGAv+dIocEbcpUuXqoIFC/p/37Nnj575J1TS0kNmFkvM6dOn9XO8ko+JODblYiqOTbmYjFOrVi3dcnH16tV6hjxphRFo1qxZQTfDt2LUqFH6plreb6VKlfxfusljWSfbpGeFF3KxLY5NudgWx1QuSQVd+TxMKpHkhBHqFO7S/Lhfv35qxIgR8b7ZkW9+unTpogYOHBj2bklu5RPpGMShzEzuAwDsJTeLBQoUUPfdd1+C21966SV1+PBhNXr06JDiPPvss2rmzJnqvffeU/Xq1fNPcy+VVfPnz1fdu3dXTZs2VR988IEn8jERx6ZcTMWxKReTca5nx44dKmXKlCp//vyeH57ArVySUhybcrEtjqlcbEHFlIe5deP74osvqnHjxqlXX31VNWzYUOXKlUuvlz6zc+fO1U0U27dvH/auflRMJe04NuViMg4AhOrChQvq+eefV2PGjNGtp+VCWly8eFHFxsaqjh076korN1pNAXCfjGEj3YlsYCoXm+LYlIttcWz6bIYbFVNRLGvWrNfcLje8MghhqDe+8m3I559/riulEiLfnrRt21ZXVEV7PqbKjDiUmal9AABMkRZSMphzYGuJypUr+1tQAYgcac04fvx4lS9fvqD1MuD6Y489prZs2eJqvJ07d6pt27bprnxlypTxZC42xbEpF9vimP5s2orBz6P8G8zOnTsnOkOG9GOVLnahkrEjZLadxMgJScbL8UI+psqMOJSZqX0AAEzYtGmT7sIj3Xbq1Kmj/vzzTzV8+HA1YcIEfWFdt25d/hBABMng6uXKlVMjR45UrVq10l3vBg0apF5//XX1zDPPhPTa8v/ffPNNlT59evXPP/+oxx9/XE2bNs0/wLtMgDJ9+nS9PdpzsTWOTbnYFsdULtZzELWqV6/uDBs2LNHta9ascZIlSxZynCZNmjgNGjRwjhw5Em+brGvUqJFz3333eSIfU2VGHMrM1D4AAOH2ww8/OClTpnSyZs3qpE6dWv+eI0cOp379+k7dunWd5MmTO/Pnz+cPAUTYhx9+6KRNm9Zp3bq1U61aNSdv3rzOnDlzQn5duV45dOiQftynTx8nf/78zk8//eScPXvW+eWXX5xixYo5vXv3dryQi81xbMrFtjimcrEZLaaimAymeOLEiWt2JZIudqGSwRubNGmiW0ZJ64/AMabWr1+vZxmYMWOGJ/IxVWbEocxM7QMAEG7yzW7Pnj3Va6+9piZNmqQeffRR3SJ08ODBenufPn3UG2+8QaspIMJkooJ9+/bpcV9l/LeFCxeq6tWrh/y6juP4H3///fe69ZS0nBQ1atRQ7777rj5GDBkyREV7LjbHsSkX2+KYysVqka4ZQ3S4cuWKM2vWLKdfv35Op06d9CKP5VtT2QYAAOyUMWNGZ+vWrfqxnPNjY2OdVatW+bevX7/eyZUrVwTfIYBjx445LVq0cDJlyuR88sknTps2bZx06dI5I0aMCLlwYmJinMOHD+vH2bNnd/7444+g7bt27XLSpEnjiVxsjWNTLrbFMZWL7aiY8rirV686NjGRj6kyIw5lZtvnE4C9FVPbtm3z/54+fXpn+/btQTel0sUPQORI16AaNWo4O3bs8K+bNGmS7oIrw3KEWjH1n//8x+nWrZuTM2dOZ+7cuUHbV65cqSusvJCLrXFsysW2OKZysV2ySLfYwvW1b98+wcHHd+3apWrVqhX2IpTYixYt8lQ+psqMOJRZpD+fABCqwoULq61bt/p/X7p0qSpYsKD/9z179uju/gAi5+mnn9bX40WKFPGvk4GW165dqy5evBjSa8v1yubNm9Xq1av1EB4ygUugWbNmqdKlSysv5GJrHJtysS2OqVysF+maMVxfhQoVnKJFizq//vqrf924ceP0N5zNmzcPexG6PYiziXxMlRlxKLNIfz4BIFQfffSRM2PGjES3y2DIHTt2pKCBJEpaUO7duzfSbwOAxWLkn0hXjuHaLl26pF566SX1/vvvqx49eqht27apH374QQ9E+NRTT4W9+KS2t1KlSurKlSueycdUmRGHMov05xMAANhv+fLlujXjwYMH9e+5c+dW1apVU1WqVDES/9y5cypt2rSeysWmODblYlucSH82rRHpmjHcOBmMXPqAp0iRIqh1RqiyZMlyzUVafoRj2vtw5WM6BnEoM5P7AAAASDoOHTrk1KxZU19jFCpUyKlSpYpe5LGsk23yHDfUrVvX2bdvX7z1y5Ytc/71r395Jheb4tiUi21xTH42kwIqpjzg4sWLTvfu3Z1UqVI5L730klOrVi0nd+7czsyZM115/bRp0zo9evTQ3Y8SWgYOHOhqxVS48zEVgziUmcl9AAAAJD0tW7Z0qlWr5vz555/xtsm66tWrO//+979diSUDNcuAzTJws2+Wzv79++sv3Z577jnP5GJTHJtysS2Oyc9mUkDFlAeUK1fOKV68uLN06VL/TF9vvPGGvhHu3LlzyK8vH5phw4YZG2Mq3PmYikEcyszkPgAAAJIemSVz1apViW5fsWKFfo5bPvzwQ/2ldevWrfVNt8w4NmfOHE/lYlMcm3KxLY7pz6btmJXPA+644w61Zs0aVbVqVf17TEyM6tWrl+7L6sZseffdd586ceJEotuzZs2q2rZtq7ySj6kYxKHMTO4DAAAg6UmVKpU6depUottPnz6tn+OWZ599Vv33v/9VkyZNUitWrFBTp05VDRo08FQuNsWxKRfb4pj+bFov0jVjCM358+etKkIT+ZgqM+JQZrZ9PgEAgFnPPPOMHrNm2rRpzsmTJ/3r5bGsK1y4sNOlSxdXYh07dsxp0aKFkylTJueTTz5x2rRp46RLl84ZMWKEp3KxKY5NudgWx+RnMylgVj4PkBH+ly1bFjTS/1133aV/miLdPqUliFfyMVVmxKHMouHzCQAA7HThwgX1/PPPqzFjxqjLly+rlClT6vUXL15UsbGxqmPHjuq9995zpWVGvnz5VJEiRdSECRP0TzF58mT1zDPP6JbhM2fO9EQuNsWxKRfb4pj8bCYFVExFsbNnz6r//Oc/6quvvlLJkiXTXerEsWPHdEVR69at1ccff+za1K3t27dXI0aMUOnSpQtav2vXLvX444+rxYsXR30+psqMOJSZ6c8nAABIuqTL0MqVK4O+CKtcubLKmDGjazFeffVV9fLLL+vrmkD79u1THTp0UPPmzfNMLrbFsSkX2+KYysV2jDEVxZ577jm1fPlyNWvWLHX+/Hl16NAhvchjWSfb5DluWbt2rSpXrpweG8fn888/V+XLl1fZs2f3RD6myow4lJnpzycAAEi65Ca3Tp066v7779fXGj/++KNu2XT06FHXYvTt2zdepZTInz+/a5VSpnKxLY5NudgWx1Qu1ot0X0IkLnPmzM6SJUsS3f7LL7/o57g57f0LL7zgpEyZ0unTp4/z0EMP6ZkEpI+5V/IxVWbEocxMfz4BAEDSU7JkSefo0aP68Z49e/S4NTIG1J133ulkzZrVyZkzp7Njxw5XYi1btkzP1N27d2+9yGNZ57VcbIpjUy62xTH52UwKaDEVxa5evervq5oQ2SbPcUuKFCnUW2+9pXr37q3eeOMN9e2336q5c+eqp556yjP5mCoz4lBmpj+fAAAg6fnzzz/1+DWiT58+Km/evGr37t26Zbb8lN4O0v0uFIcPH1Z33323HkdKxsT56aef9CKPZZ1sk+d4IRfb4tiUi21xTOWSZES6ZgyJe/TRR52KFSs6q1atirdN1lWuXFnPluFmi6nu3bs7qVKlcl566SWnVq1aTu7cuZ2ZM2d6Jh9TZUYcysz05xMAACQ9MTExzqFDh/TjokWLOnPnzg3aLq23CxQoEFKMli1bOtWqVXP+/PPPeNtkXfXq1Z1///vfjhdysS2OTbnYFsdULkkFFVNRTKZsbdSokd7ppTlgiRIl9CKPkyVL5jRu3Ng5fvy4a/HKlSvnFC9e3Fm6dKn+/erVq84bb7yhK6o6d+7siXxMlRlxKDPTn08AAJD0yHXG4cOH9eO8efM669evD9q+a9cuJ3Xq1CHFkKE7EvqizWfFihX6OV7IxbY4NuViWxxTuSQVsZFusYXEZcmSRf3www9q06ZN6rfffgsa6b9atWqqRIkSrhbfHXfcod5//33/rHwxMTGqV69eqkGDBnpWPi/kY6rMiEOZmf58AgCApKlevXp6+nmZ/Wvz5s2qTJky/m3SZShbtmwhvb5MZy+vnZjTp0+7NuV9uHOxMY5NudgWx1QuSQEVUx5QsmRJvYTbZ599luD6ihUr6ikwvZSPqTIjDmVmah8AAABJT//+/YN+T58+fdDv33//vR4DKhStWrVS7dq102NKyY22b5p7udmeP3++6t69u2rdurXyQi62xbEpF9vimMolqYiRZlORfhNI3MWLF/Ug5EuXLg1qkVG9enX1wAMPXHPw5Zslr79s2bKgOHfddZf+6aV8TJUZcSgzk59PAACAcLhw4YJ6/vnn1ZgxY/Rgzr7rF7nOkdYgHTt21JVWbrWaAoC4qJiKYtu2bVMNGzZU+/fv1xVEuXLl0usPHTqkK5Dy58+vuxIVL148pDhnz55V//nPf9RXX32lkiVLprJmzarXHzt2TMYg09+QfPzxxypt2rRRn4+pMiMOZWZqHwAAADBBWkhJL4nAL9sqV67sb0EFAOFCxVQUu/fee/V4T+PHj493QpATR9u2bdU///yj5syZE1KcJ598Ui1atEh98MEHqn79+ip58uR6/ZUrV3Tz3a5du6patWqpTz/9NOrzMVVmxKHMTO0DAAAAJsmX1lOmTNFfwuXNm1c98sgjjJUDIKyomIpi0kJp+fLlQYOoBVq/fr1uqXHu3LmQB3GeOXOm7n6UkCVLlqimTZuq48ePR30+psqMOJSZqX0AAAAgnEqVKqV++eUX3Wti7969+gtpue6/7bbb1Pbt23V3PpnopUiRIvwhAIRFsvC8LNyQOXNmtWvXrkS3yzZ5TqiuXr16zbFwZJs8xwv5mCoz4lBmpvYBAACAcPrzzz/12FKiT58+upWUzCgmX8DJz3LlyqmXX36ZPwKAsGFWvigmXeykO1Dfvn31DBmBY9hIF7vXXntNd7MLlbSG6tSpk56VT2bgC7R69WrVuXNn1axZM0/kY6rMiEOZmdoHAAAATJEJXUaNGqUyZcrkn2ls4MCBujsfAISNzMqH6PXGG284efLkcWJiYpxkyZLpRR7LuqFDh7oS49ixY06jRo3062bNmtUpUaKEXuSxxGvcuLFz/Phxz+RjIgZxKDOT+wAAAEC4yLXL4cOH9eO8efM669evD9q+a9cuJ3Xq1PwBAIQNY0x5xM6dO4NmyAhHH+9Nmzbp/uOBcapVq6ZKlCjhyXxMxCAOZWZyHwAAAHCbzMotY2bKWFJbt25V48aNUy1btvRvl0mSHn30UbVv3z4KH0BYUDHlYTI4Yf/+/dWYMWOUDUzkY6rMiEOZ2fb5BAAAdpKueoGqVq2qGjZs6P+9Z8+eulLqq6++isC7A5AUUDHlYWvXrlWVKlVSV65cCfm1Ll68qL799lvdrzyw5YfM1PfAAw9cc3D0aMwnkjGIQ5mZ3AcAAAAAwMsY/DyKTZ8+/Zrbd+zY4Uqcbdu26W9F9u/fr6e39w3iLAOfy+CH+fPnVz/88IMqXrx41OdjqsyIQ5mZ2gcAAAAAwGa0mIry/t4xMTEyQH2iz5HtobbIuPfee1W6dOnU+PHjVcaMGYO2nTp1Ss889s8//6g5c+ZEfT6myow4lJmpfQAAAAAAbJYs0m8AicuTJ4+aNm2aunr1aoLLqlWrXCm+JUuW6Knt41ZKCVn36quvqsWLF3siH1NlRhzKzNQ+AAAAAAA2o2IqilWuXFmtXLky0e3Xa61xozJnzqx27dqV6HbZJs/xQj6myow4lJmpfQAAAAAAbMYYU1FMZsA4e/ZsottlzKcFCxaEHOfJJ5/U3fX69u2r6tWr5x9j6tChQ2r+/Pm6NVXXrl09kY+pMiMOZWZqHwAAAAAAmzHGFLShQ4eq4cOH6xn5pKWHkNYeMjPf888/r1588UVKCgAAAAAAuIqKKQTZuXOnrpwSUilVpEgRSggAAAAAAIQFFVO4rr1796r+/furMWPGUFoAAAAAAMA1VEzhutauXasqVarEtPcAAAAAAMBVDH4ONX369GuWwo4dOyglAAAAAADgOlpMQSVLluy6U9vL9itXrlBaAAAAAADANcnceyl4VZ48edS0adPU1atXE1xWrVoV6bcIAAAAAAAsRMUUVOXKldXKlSsTLYnrtaYCAAAAAAC4FYwxBdWzZ0919uzZREuiePHiasGCBZQUAAAAAABwFWNMAQAAAAAAICLoygcAAAAAAICIoGIKAAAAAAAAEUHFFAAAAAAAACKCiikAAAAAAABEBBVTAAAAAAAAiAgqpgAAAAAAABARVEwBAAAAAAAgIqiYAgAAAAAAgIqE/w+R78c7vzxy2gAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "\n", + "df = pd.DataFrame(results)\n", + "\n", + "metrics = [\"время_мс\", \"посещено_клеток\", \"длина_пути\"]\n", + "titles = [\"Время (мс)\", \"Посещено клеток\", \"Длина пути\"]\n", + "colors = {\"BFS\": \"#4C72B0\", \"DFS\": \"#DD8452\", \"A*\": \"#55A868\"}\n", + "\n", + "fig, axes = plt.subplots(len(metrics), 1, figsize=(12, 14))\n", + "\n", + "for ax, metric, title in zip(axes, metrics, titles):\n", + " for strategy_name in df[\"стратегия\"].unique():\n", + " subset = df[df[\"стратегия\"] == strategy_name].reset_index(drop=True)\n", + " ax.bar(\n", + " [\n", + " i + list(df[\"стратегия\"].unique()).index(strategy_name) * 0.25\n", + " for i in range(len(subset))\n", + " ],\n", + " subset[metric],\n", + " width=0.25,\n", + " label=strategy_name,\n", + " color=colors[strategy_name],\n", + " )\n", + " ax.set_title(title, fontsize=13)\n", + " ax.set_xticks([i + 0.25 for i in range(len(df[\"лабиринт\"].unique()))])\n", + " ax.set_xticklabels(df[\"лабиринт\"].unique(), rotation=90, ha=\"right\")\n", + " ax.legend()\n", + " ax.grid(axis=\"y\", alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(\"results.png\", dpi=150)\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/skorohodovsa/task_2/practice/results.csv b/skorohodovsa/task_2/practice/results.csv new file mode 100644 index 0000000..8e373ed --- /dev/null +++ b/skorohodovsa/task_2/practice/results.csv @@ -0,0 +1,121 @@ +лабиринт,стратегия,время_мс,посещено_клеток,длина_пути +100x100_spaghetti_v1.txt,BFS,9.2513,205.0,205.0 +100x100_spaghetti_v1.txt,DFS,8.2451,2129.0,2129.0 +100x100_spaghetti_v1.txt,A*,7.1113,205.0,205.0 +100x100_spaghetti_v10.txt,BFS,9.2555,207.0,207.0 +100x100_spaghetti_v10.txt,DFS,8.1821,2489.0,2489.0 +100x100_spaghetti_v10.txt,A*,8.4803,207.0,207.0 +100x100_spaghetti_v2.txt,BFS,9.3921,217.0,217.0 +100x100_spaghetti_v2.txt,DFS,6.7196,2063.0,2063.0 +100x100_spaghetti_v2.txt,A*,10.5764,217.0,217.0 +100x100_spaghetti_v3.txt,BFS,8.4084,217.0,217.0 +100x100_spaghetti_v3.txt,DFS,5.7855,2107.0,2107.0 +100x100_spaghetti_v3.txt,A*,6.3385,217.0,217.0 +100x100_spaghetti_v4.txt,BFS,8.8661,205.0,205.0 +100x100_spaghetti_v4.txt,DFS,6.8166,2409.0,2409.0 +100x100_spaghetti_v4.txt,A*,6.1874,205.0,205.0 +100x100_spaghetti_v5.txt,BFS,8.3117,217.0,217.0 +100x100_spaghetti_v5.txt,DFS,6.3364,2071.0,2071.0 +100x100_spaghetti_v5.txt,A*,8.495,217.0,217.0 +100x100_spaghetti_v6.txt,BFS,8.212,243.0,243.0 +100x100_spaghetti_v6.txt,DFS,7.0348,1869.0,1869.0 +100x100_spaghetti_v6.txt,A*,12.8413,243.0,243.0 +100x100_spaghetti_v7.txt,BFS,8.3471,211.0,211.0 +100x100_spaghetti_v7.txt,DFS,6.1699,2283.0,2283.0 +100x100_spaghetti_v7.txt,A*,6.9637,211.0,211.0 +100x100_spaghetti_v8.txt,BFS,8.3499,221.0,221.0 +100x100_spaghetti_v8.txt,DFS,7.1166,2473.0,2473.0 +100x100_spaghetti_v8.txt,A*,9.5093,221.0,221.0 +100x100_spaghetti_v9.txt,BFS,8.5536,209.0,209.0 +100x100_spaghetti_v9.txt,DFS,5.4126,1939.0,1939.0 +100x100_spaghetti_v9.txt,A*,6.7365,209.0,209.0 +10x10_path_v1.txt,BFS,0.032,13.0,13.0 +10x10_path_v1.txt,DFS,0.0341,13.0,13.0 +10x10_path_v1.txt,A*,0.0399,13.0,13.0 +10x10_path_v10.txt,BFS,0.0323,13.0,13.0 +10x10_path_v10.txt,DFS,0.036,13.0,13.0 +10x10_path_v10.txt,A*,0.037,13.0,13.0 +10x10_path_v2.txt,BFS,0.0354,17.0,17.0 +10x10_path_v2.txt,DFS,0.0433,17.0,17.0 +10x10_path_v2.txt,A*,0.044,17.0,17.0 +10x10_path_v3.txt,BFS,0.0348,17.0,17.0 +10x10_path_v3.txt,DFS,0.0492,17.0,17.0 +10x10_path_v3.txt,A*,0.0439,17.0,17.0 +10x10_path_v4.txt,BFS,0.0476,29.0,29.0 +10x10_path_v4.txt,DFS,0.0475,29.0,29.0 +10x10_path_v4.txt,A*,0.0652,29.0,29.0 +10x10_path_v5.txt,BFS,0.0302,13.0,13.0 +10x10_path_v5.txt,DFS,0.0334,13.0,13.0 +10x10_path_v5.txt,A*,0.0371,13.0,13.0 +10x10_path_v6.txt,BFS,0.0307,13.0,13.0 +10x10_path_v6.txt,DFS,0.0339,13.0,13.0 +10x10_path_v6.txt,A*,0.0375,13.0,13.0 +10x10_path_v7.txt,BFS,0.0401,17.0,17.0 +10x10_path_v7.txt,DFS,0.0499,17.0,17.0 +10x10_path_v7.txt,A*,0.0489,17.0,17.0 +10x10_path_v8.txt,BFS,0.0615,29.0,29.0 +10x10_path_v8.txt,DFS,0.0536,29.0,29.0 +10x10_path_v8.txt,A*,0.0801,29.0,29.0 +10x10_path_v9.txt,BFS,0.0579,17.0,17.0 +10x10_path_v9.txt,DFS,0.046,17.0,17.0 +10x10_path_v9.txt,A*,0.0468,17.0,17.0 +30x30_empty_v1.txt,BFS,1.1046,55.0,55.0 +30x30_empty_v1.txt,DFS,0.7781,379.0,379.0 +30x30_empty_v1.txt,A*,1.9965,55.0,55.0 +30x30_empty_v10.txt,BFS,1.1246,55.0,55.0 +30x30_empty_v10.txt,DFS,0.7002,379.0,379.0 +30x30_empty_v10.txt,A*,2.0086,55.0,55.0 +30x30_empty_v2.txt,BFS,1.1401,55.0,55.0 +30x30_empty_v2.txt,DFS,0.7263,379.0,379.0 +30x30_empty_v2.txt,A*,2.0245,55.0,55.0 +30x30_empty_v3.txt,BFS,1.1038,55.0,55.0 +30x30_empty_v3.txt,DFS,0.7249,379.0,379.0 +30x30_empty_v3.txt,A*,2.007,55.0,55.0 +30x30_empty_v4.txt,BFS,1.1224,55.0,55.0 +30x30_empty_v4.txt,DFS,0.7053,379.0,379.0 +30x30_empty_v4.txt,A*,1.989,55.0,55.0 +30x30_empty_v5.txt,BFS,1.1294,55.0,55.0 +30x30_empty_v5.txt,DFS,0.7202,379.0,379.0 +30x30_empty_v5.txt,A*,2.1138,55.0,55.0 +30x30_empty_v6.txt,BFS,1.0843,55.0,55.0 +30x30_empty_v6.txt,DFS,0.7746,379.0,379.0 +30x30_empty_v6.txt,A*,2.009,55.0,55.0 +30x30_empty_v7.txt,BFS,1.1449,55.0,55.0 +30x30_empty_v7.txt,DFS,0.7076,379.0,379.0 +30x30_empty_v7.txt,A*,2.033,55.0,55.0 +30x30_empty_v8.txt,BFS,1.3196,55.0,55.0 +30x30_empty_v8.txt,DFS,0.7794,379.0,379.0 +30x30_empty_v8.txt,A*,1.9972,55.0,55.0 +30x30_empty_v9.txt,BFS,1.1088,55.0,55.0 +30x30_empty_v9.txt,DFS,0.7131,379.0,379.0 +30x30_empty_v9.txt,A*,2.0128,55.0,55.0 +50x50_deadends_v1.txt,BFS,1.7809,729.0,729.0 +50x50_deadends_v1.txt,DFS,1.7167,729.0,729.0 +50x50_deadends_v1.txt,A*,2.5217,729.0,729.0 +50x50_deadends_v10.txt,BFS,0.7362,261.0,261.0 +50x50_deadends_v10.txt,DFS,1.7627,261.0,261.0 +50x50_deadends_v10.txt,A*,0.9753,261.0,261.0 +50x50_deadends_v2.txt,BFS,0.9246,249.0,249.0 +50x50_deadends_v2.txt,DFS,1.7347,249.0,249.0 +50x50_deadends_v2.txt,A*,1.0804,249.0,249.0 +50x50_deadends_v3.txt,BFS,0.945,297.0,297.0 +50x50_deadends_v3.txt,DFS,1.7483,297.0,297.0 +50x50_deadends_v3.txt,A*,1.0832,297.0,297.0 +50x50_deadends_v4.txt,BFS,1.5487,413.0,413.0 +50x50_deadends_v4.txt,DFS,1.6526,413.0,413.0 +50x50_deadends_v4.txt,A*,1.9521,413.0,413.0 +50x50_deadends_v5.txt,BFS,0.9255,309.0,309.0 +50x50_deadends_v5.txt,DFS,1.7299,309.0,309.0 +50x50_deadends_v5.txt,A*,1.1469,309.0,309.0 +50x50_deadends_v6.txt,BFS,1.0637,337.0,337.0 +50x50_deadends_v6.txt,DFS,1.7728,337.0,337.0 +50x50_deadends_v6.txt,A*,1.3449,337.0,337.0 +50x50_deadends_v7.txt,BFS,0.7827,261.0,261.0 +50x50_deadends_v7.txt,DFS,1.6948,261.0,261.0 +50x50_deadends_v7.txt,A*,0.9527,261.0,261.0 +50x50_deadends_v8.txt,BFS,1.5551,565.0,565.0 +50x50_deadends_v8.txt,DFS,1.7707,565.0,565.0 +50x50_deadends_v8.txt,A*,2.3158,565.0,565.0 +50x50_deadends_v9.txt,BFS,0.6693,209.0,209.0 +50x50_deadends_v9.txt,DFS,1.052,209.0,209.0 +50x50_deadends_v9.txt,A*,0.7957,209.0,209.0 diff --git a/skorohodovsa/task_2/practice/results.png b/skorohodovsa/task_2/practice/results.png new file mode 100644 index 0000000..e94ceb8 Binary files /dev/null and b/skorohodovsa/task_2/practice/results.png differ diff --git a/skorohodovsa/task_2/requirements.txt b/skorohodovsa/task_2/requirements.txt index 791d693..20ae8cb 100644 --- a/skorohodovsa/task_2/requirements.txt +++ b/skorohodovsa/task_2/requirements.txt @@ -5,4 +5,7 @@ nbsphinx myst-nb tabulate bibtexparser -pytest \ No newline at end of file +pytest +sphinxcontrib-mermaid +matplotlib +pandas \ No newline at end of file diff --git a/skorohodovsa/task_2/source/models/base.py b/skorohodovsa/task_2/source/models/base.py index 52c874a..1496091 100644 --- a/skorohodovsa/task_2/source/models/base.py +++ b/skorohodovsa/task_2/source/models/base.py @@ -183,7 +183,7 @@ class Maze: neighbors.append(cell) return neighbors - + @property def start(self) -> Optional[Cell]: for y in range(self._height): @@ -191,7 +191,7 @@ class Maze: if self[y, x].is_start: return self[y, x] return None - + @property def exit(self) -> Optional[Cell]: for y in range(self._height): diff --git a/skorohodovsa/task_2/source/strategy/__init__.py b/skorohodovsa/task_2/source/strategy/__init__.py index 0da8de2..72c56bf 100644 --- a/skorohodovsa/task_2/source/strategy/__init__.py +++ b/skorohodovsa/task_2/source/strategy/__init__.py @@ -8,4 +8,4 @@ __all__ = [ "BFSStrategy", "DFSStrategy", "AStarStrategy", -] \ No newline at end of file +] diff --git a/skorohodovsa/task_2/source/strategy/algorithms.py b/skorohodovsa/task_2/source/strategy/algorithms.py index c6fd162..c8b1537 100644 --- a/skorohodovsa/task_2/source/strategy/algorithms.py +++ b/skorohodovsa/task_2/source/strategy/algorithms.py @@ -23,6 +23,26 @@ class PathFindingStrategy(ABC): Пустой список, если путь не найден. """ + def _validate( + self, maze: Maze, start: Optional[Cell], exit: Optional[Cell] + ) -> tuple[Optional[Cell], Optional[Cell]]: + """Подставляет start/exit из лабиринта если не переданы явно. + + Raises: + ValueError: Если старт или выход не найдены. + """ + if start is None: + start = maze.start + if exit is None: + exit = maze.exit + + if start is None: + raise ValueError("Стартовая клетка не найдена в лабиринте") + if exit is None: + raise ValueError("Выходная клетка не найдена в лабиринте") + + return start, exit + def _reconstruct_path( self, came_from: dict[Cell, Optional[Cell]], end: Cell ) -> list[Cell]: @@ -46,11 +66,3 @@ class PathFindingStrategy(ABC): current = came_from[current] path.reverse() return path - - - - - - - - diff --git a/skorohodovsa/task_2/source/strategy/astar.py b/skorohodovsa/task_2/source/strategy/astar.py index 37f76b0..ead84f6 100644 --- a/skorohodovsa/task_2/source/strategy/astar.py +++ b/skorohodovsa/task_2/source/strategy/astar.py @@ -4,6 +4,11 @@ from typing import Optional from source.models.base import Cell, Maze from source.strategy.algorithms import PathFindingStrategy +# ---------------------------------------------------------------------------- # +# Моя азиатская жена называет меня расистом. Но как я могу # +# быть расистом, если я женился на женщине низшей расы?! # +# ---------------------------------------------------------------------------- # + def _manhattan(a: Cell, b: Cell) -> int: """Манхэттенское расстояние между двумя клетками.""" @@ -13,11 +18,10 @@ def _manhattan(a: Cell, b: Cell) -> int: class AStarStrategy(PathFindingStrategy): """Алгоритм A* с манхэттенской эвристикой.""" - def find_path(self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None) -> list[Cell]: - if start is None: - start = maze.start - if exit is None: - exit = self.exit + def find_path( + self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None + ) -> list[Cell]: + start, exit = self._validate(maze, start, exit) g_score: dict[Cell, int] = {start: 0} came_from: dict[Cell, Optional[Cell]] = {start: None} @@ -43,4 +47,4 @@ class AStarStrategy(PathFindingStrategy): counter += 1 heapq.heappush(open_heap, (f, counter, neighbor)) - return [] \ No newline at end of file + return [] diff --git a/skorohodovsa/task_2/source/strategy/bfs.py b/skorohodovsa/task_2/source/strategy/bfs.py index fb6317f..b0bba06 100644 --- a/skorohodovsa/task_2/source/strategy/bfs.py +++ b/skorohodovsa/task_2/source/strategy/bfs.py @@ -15,10 +15,7 @@ class BFSStrategy(PathFindingStrategy): def find_path( self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None ) -> list[Cell]: - if start is None: - start = maze.start - if exit is None: - exit = maze.exit + start, exit = self._validate(maze, start, exit) came_from: dict[Cell, Optional[Cell]] = {start: None} queue: deque[Cell] = deque([start]) @@ -34,4 +31,4 @@ class BFSStrategy(PathFindingStrategy): came_from[neighbor] = current queue.append(neighbor) - return [] \ No newline at end of file + return [] diff --git a/skorohodovsa/task_2/source/strategy/dfs.py b/skorohodovsa/task_2/source/strategy/dfs.py index 11cde79..8b41845 100644 --- a/skorohodovsa/task_2/source/strategy/dfs.py +++ b/skorohodovsa/task_2/source/strategy/dfs.py @@ -3,6 +3,12 @@ from typing import Optional from source.models.base import Maze, Cell from source.strategy.algorithms import PathFindingStrategy +# ---------------------------------------------------------------------------- # +# Как называется пресмыкающийся, который в прошлом был программистом? # +# ---------------------------------------------------------------------------- # +# крокодил # +# ---------------------------------------------------------------------------- # + class DFSStrategy(PathFindingStrategy): """Поиск в глубину (Depth-First Search). @@ -13,10 +19,7 @@ class DFSStrategy(PathFindingStrategy): def find_path( self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None ) -> list[Cell]: - if start is None: - start = maze.start - if exit is None: - exit = maze.exit + start, exit = self._validate(maze, start, exit) came_from: dict[Cell, Optional[Cell]] = {start: None} stack: list[Cell] = [start] @@ -32,4 +35,4 @@ class DFSStrategy(PathFindingStrategy): came_from[neighbor] = current stack.append(neighbor) - return [] \ No newline at end of file + return [] diff --git a/skorohodovsa/task_2/source/strategy/solver.py b/skorohodovsa/task_2/source/strategy/solver.py index eb17b64..ac1379f 100644 --- a/skorohodovsa/task_2/source/strategy/solver.py +++ b/skorohodovsa/task_2/source/strategy/solver.py @@ -15,6 +15,7 @@ class SearchStats: path_length: Длина найденного пути (0 если путь не найден). path: Найденный путь — список клеток от старта до выхода. """ + elapsed_ms: float visited_count: int path_length: int @@ -90,4 +91,4 @@ class MazeSolver: visited_count=len(path), path_length=len(path), path=path, - ) \ No newline at end of file + ) diff --git a/skorohodovsa/task_2/source/view/__init__.py b/skorohodovsa/task_2/source/view/__init__.py index 2865973..cad01a4 100644 --- a/skorohodovsa/task_2/source/view/__init__.py +++ b/skorohodovsa/task_2/source/view/__init__.py @@ -10,4 +10,4 @@ __all__ = [ "MoveCommand", "CommandHistory", "DIRECTIONS", -] \ No newline at end of file +] diff --git a/skorohodovsa/task_2/source/view/command.py b/skorohodovsa/task_2/source/view/command.py index 9ae96e5..fe4c304 100644 --- a/skorohodovsa/task_2/source/view/command.py +++ b/skorohodovsa/task_2/source/view/command.py @@ -8,6 +8,7 @@ from source.models.base import Maze, Cell # Игрок # # ---------------------------------------------------------------------------- # + class Player: """Хранит текущее положение игрока в лабиринте. @@ -31,6 +32,7 @@ class Player: # Интерфейс команды # # ---------------------------------------------------------------------------- # + class Command(ABC): """Интерфейс команды с поддержкой отмены.""" @@ -53,9 +55,9 @@ class Command(ABC): DIRECTIONS = { "w": (0, -1), - "s": (0, 1), + "s": (0, 1), "a": (-1, 0), - "d": (1, 0), + "d": (1, 0), } @@ -77,11 +79,13 @@ class MoveCommand(Command): ValueError: Если направление не распознано. """ if direction not in DIRECTIONS: - raise ValueError(f"Неизвестное направление '{direction}'. Используй: w/a/s/d") + raise ValueError( + f"Неизвестное направление '{direction}'. Используй: w/a/s/d" + ) - self._player = player + self._player = player self._direction = direction - self._maze = maze + self._maze = maze self._prev_cell: Optional[Cell] = None def execute(self) -> bool: @@ -99,7 +103,7 @@ class MoveCommand(Command): if target is None or not target.is_possible(): return False - self._prev_cell = self._player.cell + self._prev_cell = self._player.cell self._player.cell = target return True @@ -113,6 +117,7 @@ class MoveCommand(Command): # История команд # # ---------------------------------------------------------------------------- # + class CommandHistory: """Хранит историю выполненных команд и позволяет отменять их. @@ -150,4 +155,4 @@ class CommandHistory: def clear(self) -> None: """Очищает историю команд.""" - self._history.clear() \ No newline at end of file + self._history.clear() diff --git a/skorohodovsa/task_2/source/view/observer.py b/skorohodovsa/task_2/source/view/observer.py index 5c51210..c5df3f7 100644 --- a/skorohodovsa/task_2/source/view/observer.py +++ b/skorohodovsa/task_2/source/view/observer.py @@ -10,6 +10,7 @@ from source.settings import cell_mapping # События # # ---------------------------------------------------------------------------- # + @dataclass class Event: """Событие, передаваемое наблюдателям. @@ -18,6 +19,7 @@ class Event: type: Тип события ('maze_loaded', 'path_found', 'move', 'no_path'). payload: Дополнительные данные события. """ + type: str payload: dict = None @@ -26,6 +28,7 @@ class Event: # Интерфейс наблюдателя # # ---------------------------------------------------------------------------- # + class Observer(ABC): """Интерфейс наблюдателя за событиями лабиринта.""" @@ -42,12 +45,13 @@ class Observer(ABC): # Консольный наблюдатель # --------------------------------------------------------------------------- + class ConsoleView(Observer): """Отображает состояние лабиринта и события в консоли.""" # Символ игрока на карте PLAYER_SYMBOL = "P" - PATH_SYMBOL = "·" + PATH_SYMBOL = "·" def update(self, event: Event) -> None: """Реагирует на события и выводит информацию в консоль. @@ -108,4 +112,4 @@ class ConsoleView(Observer): row_str += str(cell) row_str += "|" print(row_str) - print("+" + "─" * cols + "+") \ No newline at end of file + print("+" + "─" * cols + "+") diff --git a/skorohodovsa/task_2/test/map/__init__.py b/skorohodovsa/task_2/test/map/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skorohodovsa/task_2/test/map/test_cell.py b/skorohodovsa/task_2/test/map/test_cell.py deleted file mode 100644 index 31d7714..0000000 --- a/skorohodovsa/task_2/test/map/test_cell.py +++ /dev/null @@ -1,111 +0,0 @@ -import pytest -import sys -import os - -from source.models.base import Cell - -class TestCellCreation: - """Тесты создания клетки и начальных значений.""" - - def test_coordinates_are_set(self): - cell = Cell(3, 7) - assert cell.x == 3 - assert cell.y == 7 - - def test_default_flags_are_false(self): - cell = Cell(0, 0) - assert cell.is_wall is False - assert cell.is_start is False - assert cell.is_exit is False - - def test_create_wall(self): - cell = Cell(0, 0, is_wall=True) - assert cell.is_wall is True - - def test_create_start(self): - cell = Cell(0, 0, is_start=True) - assert cell.is_start is True - - def test_create_exit(self): - cell = Cell(0, 0, is_exit=True) - assert cell.is_exit is True - - -class TestCellIsPassable: - """Тесты метода is_possible.""" - - def test_empty_cell_is_passable(self): - cell = Cell(0, 0) - assert cell.is_possible() is True - - def test_wall_is_not_passable(self): - cell = Cell(0, 0, is_wall=True) - assert cell.is_possible() is False - - def test_start_cell_is_passable(self): - cell = Cell(0, 0, is_start=True) - assert cell.is_possible() is True - - def test_exit_cell_is_passable(self): - cell = Cell(0, 0, is_exit=True) - assert cell.is_possible() is True - - -class TestCellFlagsAreMutuallyExclusive: - """Тесты взаимного исключения флагов.""" - - def test_set_wall_clears_start(self): - cell = Cell(0, 0, is_start=True) - cell.is_wall = True - assert cell.is_start is False - assert cell.is_wall is True - - def test_set_wall_clears_exit(self): - cell = Cell(0, 0, is_exit=True) - cell.is_wall = True - assert cell.is_exit is False - assert cell.is_wall is True - - def test_set_start_clears_wall(self): - cell = Cell(0, 0, is_wall=True) - cell.is_start = True - assert cell.is_wall is False - assert cell.is_start is True - - def test_set_start_clears_exit(self): - cell = Cell(0, 0, is_exit=True) - cell.is_start = True - assert cell.is_exit is False - assert cell.is_start is True - - def test_set_exit_clears_wall(self): - cell = Cell(0, 0, is_wall=True) - cell.is_exit = True - assert cell.is_wall is False - assert cell.is_exit is True - - def test_set_exit_clears_start(self): - cell = Cell(0, 0, is_start=True) - cell.is_exit = True - assert cell.is_start is False - assert cell.is_exit is True - - def test_unset_wall_does_not_clear_others(self): - # снятие флага (False) не должно трогать остальные - cell = Cell(0, 0, is_wall=True) - cell.is_wall = False - assert cell.is_start is False - assert cell.is_exit is False - - -class TestCellStr: - """Тесты строкового представления клетки.""" - - def test_str_returns_string(self): - cell = Cell(0, 0) - assert isinstance(str(cell), str) - - def test_repr_contains_coordinates(self): - cell = Cell(4, 9) - assert "4" in repr(cell) - assert "9" in repr(cell) \ No newline at end of file diff --git a/skorohodovsa/task_2/test/play.py b/skorohodovsa/task_2/test/play.py index d6cc572..d46a16c 100644 --- a/skorohodovsa/task_2/test/play.py +++ b/skorohodovsa/task_2/test/play.py @@ -19,9 +19,9 @@ if start is None: print("Стартовая клетка не найдена!") exit() -player = Player(start) +player = Player(start) history = CommandHistory() -view = ConsoleView() +view = ConsoleView() view.update(Event("maze_loaded", {"maze": maze})) print("Управление: w/a/s/d — движение, z — отмена, q — выход\n") @@ -29,24 +29,29 @@ print("Управление: w/a/s/d — движение, z — отмена, q while True: key = input(">>> ").strip().lower() - if key == 'q': + if key == "q": print("Выход.") break - elif key == 'z': + elif key == "z": if history.undo(): print("Ход отменён.") view.render(maze, player=player.cell) - elif key in ('w', 'a', 's', 'd'): + elif key in ("w", "a", "s", "d"): cmd = MoveCommand(player, key, maze) if cmd.execute(): history.push(cmd) - view.update(Event("move", { - "maze": maze, - "player_cell": player.cell, - "direction": key, - })) + view.update( + Event( + "move", + { + "maze": maze, + "player_cell": player.cell, + "direction": key, + }, + ) + ) if player.cell.is_exit: print("Выход найден! Победа!") break @@ -54,4 +59,4 @@ while True: print("Туда нельзя — стена или граница.") else: - print("Неизвестная команда. Используй: w/a/s/d, z, q") \ No newline at end of file + print("Неизвестная команда. Используй: w/a/s/d, z, q") diff --git a/skorohodovsa/task_2/test/map/test_maze.py b/skorohodovsa/task_2/test/test_base.py similarity index 54% rename from skorohodovsa/task_2/test/map/test_maze.py rename to skorohodovsa/task_2/test/test_base.py index 5b7e111..56e1e3d 100644 --- a/skorohodovsa/task_2/test/map/test_maze.py +++ b/skorohodovsa/task_2/test/test_base.py @@ -7,8 +7,114 @@ from source.models.base import Cell, Maze from source.settings import cell_mapping -class TestMaze: +class TestCellCreation: + """Тесты создания клетки и начальных значений.""" + def test_coordinates_are_set(self): + cell = Cell(3, 7) + assert cell.x == 3 + assert cell.y == 7 + + def test_default_flags_are_false(self): + cell = Cell(0, 0) + assert cell.is_wall is False + assert cell.is_start is False + assert cell.is_exit is False + + def test_create_wall(self): + cell = Cell(0, 0, is_wall=True) + assert cell.is_wall is True + + def test_create_start(self): + cell = Cell(0, 0, is_start=True) + assert cell.is_start is True + + def test_create_exit(self): + cell = Cell(0, 0, is_exit=True) + assert cell.is_exit is True + + +class TestCellIsPassable: + """Тесты метода is_possible.""" + + def test_empty_cell_is_passable(self): + cell = Cell(0, 0) + assert cell.is_possible() is True + + def test_wall_is_not_passable(self): + cell = Cell(0, 0, is_wall=True) + assert cell.is_possible() is False + + def test_start_cell_is_passable(self): + cell = Cell(0, 0, is_start=True) + assert cell.is_possible() is True + + def test_exit_cell_is_passable(self): + cell = Cell(0, 0, is_exit=True) + assert cell.is_possible() is True + + +class TestCellFlagsAreMutuallyExclusive: + """Тесты взаимного исключения флагов.""" + + def test_set_wall_clears_start(self): + cell = Cell(0, 0, is_start=True) + cell.is_wall = True + assert cell.is_start is False + assert cell.is_wall is True + + def test_set_wall_clears_exit(self): + cell = Cell(0, 0, is_exit=True) + cell.is_wall = True + assert cell.is_exit is False + assert cell.is_wall is True + + def test_set_start_clears_wall(self): + cell = Cell(0, 0, is_wall=True) + cell.is_start = True + assert cell.is_wall is False + assert cell.is_start is True + + def test_set_start_clears_exit(self): + cell = Cell(0, 0, is_exit=True) + cell.is_start = True + assert cell.is_exit is False + assert cell.is_start is True + + def test_set_exit_clears_wall(self): + cell = Cell(0, 0, is_wall=True) + cell.is_exit = True + assert cell.is_wall is False + assert cell.is_exit is True + + def test_set_exit_clears_start(self): + cell = Cell(0, 0, is_start=True) + cell.is_exit = True + assert cell.is_start is False + assert cell.is_exit is True + + def test_unset_wall_does_not_clear_others(self): + # снятие флага (False) не должно трогать остальные + cell = Cell(0, 0, is_wall=True) + cell.is_wall = False + assert cell.is_start is False + assert cell.is_exit is False + + +class TestCellStr: + """Тесты строкового представления клетки.""" + + def test_str_returns_string(self): + cell = Cell(0, 0) + assert isinstance(str(cell), str) + + def test_repr_contains_coordinates(self): + cell = Cell(4, 9) + assert "4" in repr(cell) + assert "9" in repr(cell) + + +class TestMaze: def test_default_size(self): """Проверка размеров лабиринта со значениями по умолчанию""" maze = Maze() @@ -42,8 +148,8 @@ class TestMaze: maze = Maze(size=(5, 5)) assert maze.get_cell(-1, 0) is None assert maze.get_cell(0, -1) is None - assert maze.get_cell(5, 0) is None - assert maze.get_cell(0, 5) is None + assert maze.get_cell(5, 0) is None + assert maze.get_cell(0, 5) is None def test_center_has_four_neighbors(self): """Проверка нахождения соседей""" @@ -58,32 +164,32 @@ class TestMaze: def test_wall_excluded_from_neighbors(self): """Проверка что стена не попадает в список соседей""" maze = Maze(size=(5, 5)) - maze[1, 2] = cell_mapping['wall'] + maze[1, 2] = cell_mapping["wall"] assert all(not n.is_wall for n in maze.get_neighbors(2, 2)) def test_setitem_wall(self): """Проверка установки стены через оператор []""" maze = Maze(size=(5, 5)) - maze[0, 0] = cell_mapping['wall'] + maze[0, 0] = cell_mapping["wall"] assert maze[0, 0].is_wall is True def test_setitem_start(self): """Проверка установки старта через оператор []""" maze = Maze(size=(5, 5)) - maze[0, 0] = cell_mapping['start'] + maze[0, 0] = cell_mapping["start"] assert maze[0, 0].is_start is True def test_setitem_exit(self): """Проверка установки выхода через оператор []""" maze = Maze(size=(5, 5)) - maze[0, 0] = cell_mapping['exit'] + maze[0, 0] = cell_mapping["exit"] assert maze[0, 0].is_exit is True def test_setitem_empty_clears_flags(self): """Проверка сброса флагов клетки при установке пустого типа""" maze = Maze(size=(5, 5)) - maze[0, 0] = cell_mapping['wall'] - maze[0, 0] = cell_mapping['empty'] + maze[0, 0] = cell_mapping["wall"] + maze[0, 0] = cell_mapping["empty"] assert not maze[0, 0].is_wall def test_getitem_out_of_bounds_raises(self): @@ -108,4 +214,4 @@ class TestMaze: """Проверка что длина каждой строки в строковом представлении совпадает с шириной""" maze = Maze(size=(5, 3)) for line in str(maze).strip().splitlines(): - assert len(line) == 5 \ No newline at end of file + assert len(line) == 5 diff --git a/skorohodovsa/task_2/test/test_strategies.py b/skorohodovsa/task_2/test/test_strategies.py new file mode 100644 index 0000000..81b211c --- /dev/null +++ b/skorohodovsa/task_2/test/test_strategies.py @@ -0,0 +1,162 @@ +import pytest + +from source.models.base import Maze, Cell +from source.settings import cell_mapping +from source.strategy import BFSStrategy, DFSStrategy, AStarStrategy + + +def make_open_maze(width: int = 5, height: int = 5) -> Maze: + """Открытый лабиринт без внутренних стен, S в углу, E в противоположном.""" + maze = Maze(size=(width, height)) + maze[0, 0] = cell_mapping["start"] + maze[height - 1, width - 1] = cell_mapping["exit"] + return maze + + +def make_blocked_maze() -> Maze: + """Лабиринт где S и E разделены сплошной стеной — пути нет.""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["start"] + maze[4, 4] = cell_mapping["exit"] + for col in range(5): + maze[2, col] = cell_mapping["wall"] + return maze + + +def make_corridor_maze() -> Maze: + """Узкий коридор 1×5: S → . → . → . → E.""" + maze = Maze(size=(5, 1)) + maze[0, 0] = cell_mapping["start"] + maze[0, 4] = cell_mapping["exit"] + return maze + + +STRATEGIES = [BFSStrategy, DFSStrategy, AStarStrategy] +STRATEGY_IDS = ["BFS", "DFS", "A*"] + + +# ---------------------------------------------------------------------------- # +# Общие тесты для всех стратегий # +# ---------------------------------------------------------------------------- # + + +class TestAllStrategies: + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_returns_list(self, StrategyClass): + """find_path всегда возвращает список.""" + maze = make_open_maze() + result = StrategyClass().find_path(maze) + assert isinstance(result, list) + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_starts_with_start(self, StrategyClass): + """Первая клетка пути — старт.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + assert path[0] is maze.start + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_ends_with_exit(self, StrategyClass): + """Последняя клетка пути — выход.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + assert path[-1] is maze.exit + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_cells_are_passable(self, StrategyClass): + """Все клетки пути проходимы.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + assert all(cell.is_possible() for cell in path) + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_path_cells_are_neighbors(self, StrategyClass): + """Каждая следующая клетка пути — сосед предыдущей.""" + maze = make_open_maze() + path = StrategyClass().find_path(maze) + for a, b in zip(path, path[1:]): + assert abs(a.x - b.x) + abs(a.y - b.y) == 1 + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_no_path_returns_empty(self, StrategyClass): + """Если пути нет — возвращает пустой список.""" + maze = make_blocked_maze() + path = StrategyClass().find_path(maze) + assert path == [] + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_corridor_path_length(self, StrategyClass): + """В коридоре 1×5 путь содержит ровно 5 клеток.""" + maze = make_corridor_maze() + path = StrategyClass().find_path(maze) + assert len(path) == 5 + + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_maze_not_modified(self, StrategyClass): + """Алгоритм не изменяет состояние лабиринта.""" + maze = make_open_maze() + before = str(maze) + StrategyClass().find_path(maze) + assert str(maze) == before + + +# ---------------------------------------------------------------------------- # +# Тесты специфичные для BFS и A* (оптимальность пути) # +# ---------------------------------------------------------------------------- # + + +class TestOptimalStrategies: + @pytest.mark.parametrize( + "StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"] + ) + def test_shortest_path_in_corridor(self, StrategyClass): + """BFS и A* находят кратчайший путь в коридоре.""" + maze = make_corridor_maze() + path = StrategyClass().find_path(maze) + assert len(path) == 5 + + @pytest.mark.parametrize( + "StrategyClass", [BFSStrategy, AStarStrategy], ids=["BFS", "A*"] + ) + def test_bfs_and_astar_same_length(self, StrategyClass): + """BFS и A* возвращают путь одинаковой длины на открытом лабиринте.""" + maze = make_open_maze(7, 7) + bfs_len = len(BFSStrategy().find_path(maze)) + astar_len = len(AStarStrategy().find_path(maze)) + assert bfs_len == astar_len + + +# ---------------------------------------------------------------------------- # +# Тесты с явной передачей start / exit # +# ---------------------------------------------------------------------------- # + + +class TestExplicitStartExit: + @pytest.mark.parametrize("StrategyClass", STRATEGIES, ids=STRATEGY_IDS) + def test_explicit_start_and_exit(self, StrategyClass): + """find_path работает с явно переданными start и exit.""" + maze = Maze(size=(5, 5)) + start = maze.get_cell(0, 0) + exit = maze.get_cell(4, 4) + maze[0, 0] = cell_mapping["start"] + maze[4, 4] = cell_mapping["exit"] + + path = StrategyClass().find_path(maze, start=start, exit=exit) + assert path[0] is start + assert path[-1] is exit + + +class TestEdgeCases: + def test_no_start_raises(self): + """Если нет старта — ValueError.""" + maze = Maze(size=(5, 5)) + maze[4, 4] = cell_mapping["exit"] + with pytest.raises(ValueError): + BFSStrategy().find_path(maze) + + def test_no_exit_raises(self): + """Если нет выхода — ValueError.""" + maze = Maze(size=(5, 5)) + maze[0, 0] = cell_mapping["start"] + with pytest.raises(ValueError): + BFSStrategy().find_path(maze) diff --git a/skorohodovsa/task_2/test/__init__.py b/skorohodovsa/task_2/test/test_text_builder.py similarity index 100% rename from skorohodovsa/task_2/test/__init__.py rename to skorohodovsa/task_2/test/test_text_builder.py