diff --git a/lomakinae/docs/02_report.md b/lomakinae/docs/02_report.md new file mode 100644 index 0000000..1a252a1 --- /dev/null +++ b/lomakinae/docs/02_report.md @@ -0,0 +1,349 @@ +# Отчёт. Задание 2 - Поиск выхода из лабиринта + +## Цель + +Разработать расширяемую программу для загрузки лабиринта из файла и поиска пути от старта до выхода с +возможностью выбора алгоритма. Выполнить экспериментальное сравнение алгоритмов BFS, DFS и A\* на картах +различной сложности. Применить минимум 3 паттерна проектирования GoF. + +## 1. Описание задачи и выбранные паттерны + +Программный комплекс построен на принципах объектно-ориентированного программирования. + +Применены три паттерна проектирования GoF: + +1. **Builder (Строитель)** - изолирует логику парсинга текстовых файлов и валидации структуры лабиринта + (ровно один старт `S` и один выход `E`). Добавление нового формата (JSON, бинарный) требует только нового наследника `MazeBuilder`. +2. **Strategy (Стратегия)** - позволяет динамически менять алгоритм поиска пути (BFS, DFS, A\*) без модификации кода. Новый + алгоритм добавляется наследованием от `PathFindingStrategy`. +3. **Facade (Фасад)** - предоставляет единую точку входа `MazeTestingFacade.run_full_diagnostic()` для последовательного запуска подсистемы + бенчмарков и генерации графиков. + +### Диаграмма классов (Mermaid) + +```mermaid +classDiagram + class Maze { + +int width + +int height + +List~List~Cell~~ cells + +Cell start + +Cell exit + +get_cell(x, y): Cell + +set_cell(x, y, cell_type) + +get_neighbors(cell): List~Cell~ + } + + class Cell { + +int x + +int y + +bool is_wall + +bool is_start + +bool is_exit + +is_passable(): bool + } + + class MazeBuilder { + <> + +build_from_file(filename): Maze + } + + class TextFileMazeBuilder { + +build_from_file(filename): Maze + } + + class PathFindingStrategy { + <> + +int visited_count + +find_path(maze, start, exit_cell): List~Cell~ + +reconstruct_path(came_from, exit_cell): List~Cell~ + } + + class BFSStrategy + class DFSStrategy + class AStarStrategy + + class SearchStats { + +float time_ms + +int visited_cells + +int path_length + } + + class MazeSolver { + +Maze maze + +PathFindingStrategy strategy + +set_strategy(strategy) + +solve(): SearchStats + } + + class MazeTestingFacade { + +run_full_diagnostic() + } + + MazeBuilder <|.. TextFileMazeBuilder + MazeBuilder --> Maze : создает + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + MazeSolver --> PathFindingStrategy : использует + MazeSolver --> Maze : содержит + Maze *-- Cell : содержит + MazeTestingFacade --> MazeSolver : оркестрирует +``` + +## 2. Листинги ключевых классов + +### Builder - загрузка лабиринта из файла (`src/builder.py`) + +```python +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename: str) -> Maze: + with open(filename, "r", encoding="utf-8") as f: + lines = [line.rstrip("\n") for line in f.readlines()] + height = len(lines) + width = max(len(line) for line in lines) if height > 0 else 0 + + start_count = 0 + exit_count = 0 + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + maze.set_cell(x, y, "wall") + elif ch == "S": + maze.set_cell(x, y, "start") + start_count += 1 + elif ch == "E": + maze.set_cell(x, y, "exit") + exit_count += 1 + else: + maze.set_cell(x, y, "path") + + if start_count != 1 or exit_count != 1: + raise ValueError(f"S={start_count}, E={exit_count}") + return maze +``` + +### Strategy - алгоритмы поиска пути (`src/strategies.py`) + +```python +class PathFindingStrategy(ABC): + def __init__(self): + self.visited_count = 0 + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + + def reconstruct_path(self, came_from: dict, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path + + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + queue = deque([start]) + came_from = {start: None} + visited = {start} + + while queue: + current = queue.popleft() + if current == exit_cell: + self.visited_count = len(visited) + return self.reconstruct_path(came_from, exit_cell) + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + came_from[neighbor] = current + queue.append(neighbor) + self.visited_count = len(visited) + return [] + + +class AStarStrategy(PathFindingStrategy): + def heuristic(self, cell: Cell, exit_cell: Cell) -> int: + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + heap = [] + counter = 0 + start_f = self.heuristic(start, exit_cell) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + came_from = {} + g_score = {start: 0} + f_score = {start: start_f} + visited = set() + + while heap: + current_f, _, current = heapq.heappop(heap) + visited.add(current) + + if current == exit_cell: + self.visited_count = len(visited) + return self.reconstruct_path(came_from, exit_cell) + if current_f > f_score.get(current, float("inf")): + continue + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + 1 + if tentative_g < g_score.get(neighbor, float("inf")): + came_from[neighbor] = current + g_score[neighbor] = tentative_g + new_f = tentative_g + self.heuristic(neighbor, exit_cell) + f_score[neighbor] = new_f + heapq.heappush(heap, (new_f, counter, neighbor)) + counter += 1 + self.visited_count = len(visited) + return [] +``` + +### Оркестратор (`src/solver.py`) + +```python +class SearchStats(NamedTuple): + time_ms: float + visited_cells: int + path_length: int + + +class MazeSolver: + def __init__(self, maze: Maze): + self.maze = maze + self.strategy = None + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def solve(self) -> SearchStats: + if self.strategy is None: + raise ValueError("Strategy not set") + start_time = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + time_ms = (end_time - start_time) * 1000 + return SearchStats( + time_ms=time_ms, + visited_cells=self.strategy.visited_count, + path_length=len(path), + ) +``` + +### Facade - точка входа (`src/facade.py` и `main.py`) + +```python +# src/facade.py +class MazeTestingFacade: + def run_full_diagnostic(self): + run_benchmarks() + generate_plots() + +# main.py +from src.facade import MazeTestingFacade + +def main(): + facade = MazeTestingFacade() + facade.run_full_diagnostic() +``` + +## 3. Результаты экспериментов + +### Параметры эксперимента + +| Параметр | Значение | +| --------- | -------------------------------------------------------------- | +| Итераций | 10 запусков на каждый лабиринт | +| Лабиринты | maze_no_exit, maze_empty, maze_10x10, maze_50x50, maze_100x100 | +| Алгоритмы | BFS (ширина), DFS (глубина), A\* (манхэттенская эвристика) | +| Метрики | Время выполнения (мс), посещенные клетки, длина пути | + +Тесты проводились на 5 разных лабиринтах. Так как основные лабиринты (10x10, 50x50, 100x100) имеют только +один верный путь, итоговая длина маршрута у всех алгоритмов совпадает. Сравниваем время работы и количество +проверенных клеток. + +**1. Маленький лабиринт (10x10)** +Все алгоритмы отработали быстрее 0.05 мс. Алгоритм A\* оказался наиболее точным, так как посетил всего 18 клеток. +BFS проверил 33 клетки, а DFS проверил 32 клетки. + +**2. Средний лабиринт (50x50)** +Здесь BFS обошел больше всего пространства (1046 клеток) и работал дольше всех (1.31 мс). В данном тесте DFS удачно +выбрал направление и проверил меньше клеток (231 клетка за 0.25 мс). Алгоритм A\* показал стабильный результат, +посетив 414 клеток за 1.05 мс. + +**3. Большой лабиринт (100x100)** +На большой карте заметно преимущество A\*. Он нашел выход за 0.98 мс, посетив всего 380 клеток. BFS пришлось проверить +почти весь лабиринт (2319 клеток), на что ушло 3.15 мс. DFS справился за 0.77 мс и проверил 662 клетки. + +**4. Пустой лабиринт (без стен)** +В пустом пространстве все алгоритмы посетили одинаковое количество клеток (90 шт.). Однако DFS повел себя неоптимально +и построил длинный зигзагообразный маршрут в 54 шага. BFS и A\* нашли прямой и кратчайший путь всего за 18 шагов. + +**5. Лабиринт без выхода** +Все алгоритмы корректно обработали тупик и завершили работу без ошибок. Так как выхода нет, им пришлось проверить +абсолютно все доступные клетки лабиринта (по 16 клеток у каждого). Длина пути у всех составила 0. Дольше всех из-за +расчета эвристики работал A\* (0.036 мс), а BFS и DFS справились за 0.02 мс. + +### Графики + +![plot](data/02/benchmark_charts.png) + +--- + +## 4. Анализ эффективности алгоритмов и применимость паттернов + +### Масштабирование по размеру лабиринта + +1. **A\*** - самый эффективный на больших картах. Благодаря Манхэттенской эвристике он учитывает направление + к выходу и минимизирует лишние шаги. На лабиринте 100х100 он проверил всего 380 клеток (против 2319 у BFS). + Минус - тратит чуть больше времени на мелких картах из-за математических расчетов. + +2. **BFS (Поиск в ширину)** - выполняет избыточное исследование графа во все стороны. Из-за этого на карте 100х100 + он посетил 2319 клеток, что увеличило время выполнения до 3.15 мс (худший результат по скорости). + +3. **DFS (Поиск в глубину)** - работает на простом стеке, поэтому у него минимальные накладные + расходы по времени (всего 0.77 мс на большой карте). Однако он не ищет оптимальный путь: на пустом лабиринте + `maze_empty` вместо прямой линии в 18 шагов он построил ломаный зигзаг в 54 шага. + +4. **Обработка тупиков (`maze_no_exit`)** - все алгоритмы успешно прошли стресс-тест. При отсутствии выхода они просто + обходят 100% доступных клеток и корректно возвращают пустой путь. + +### Применимость паттернов + +Паттерн **Strategy** позволил реализовать систему бенчмарков итерацией по массиву стратегий: `solver.set_strategy(strat)`. +Без него пришлось бы использовать `if-elif` внутри решателя, что нарушило бы принцип открытости-закрытости (OCP). Добавление +нового алгоритма (например, Дейкстры) не требует изменений в существующем коде. + +Паттерн **Builder** полностью инкапсулировал работу с файловой системой. Переход на другой формат хранения (JSON, бинарный) +требует только создания нового наследника `MazeBuilder` без изменения остальной системы. + +Паттерн **Facade** скрыл последовательность вызовов `run_benchmarks()` и `generate_plots()` за единственным методом +`run_full_diagnostic()`. Код `main.py` сведен к двум строкам и не зависит от деталей оркестрации подсистем. + +--- + +## 5. Выводы + +Для реальных задач: + +- Экспериментально подтверждено, что поиск ($A^*$) с использованием Манхэттенской + эвристики превосходит слепые методы (BFS, DFS) на больших лабиринтах. + +- Поиск в глубину непригоден для пустых пространств (строит крайне неоптимальные зигзагообразные пути) + и сильно зависит от порядка обхода соседей, но не требует хранения большого объёма данных в памяти. + +- Тестирование на изолированном лабиринте ("без выхода") доказало отказоустойчивость реализованных стратегий - алгоритмы + корректно завершают работу после полного исчерпания пространства состояний. + +Применение паттернов GoF (Builder, Strategy, Facade) обеспечило модульность и расширяемость системы. Добавление нового алгоритма, +формата файлов или этапа диагностики не затрагивает существующий код, что соответствует принципам SOLID. diff --git a/lomakinae/docs/data/02/README.md b/lomakinae/docs/data/02/README.md new file mode 100644 index 0000000..af90cff --- /dev/null +++ b/lomakinae/docs/data/02/README.md @@ -0,0 +1,11 @@ +# Задание 2: Поиск выхода из лабиринта (объектно-ориентированная реализация с паттернами) + +## Как запустить + +```sh +python main.py +``` + +## Результаты + +График сравнения алгоритмов сохраняется в `benchmark_charts.png`, данные - в `results.csv`. diff --git a/lomakinae/docs/data/02/benchmark_charts.png b/lomakinae/docs/data/02/benchmark_charts.png new file mode 100644 index 0000000..9915c9e Binary files /dev/null and b/lomakinae/docs/data/02/benchmark_charts.png differ diff --git a/lomakinae/docs/data/02/main.py b/lomakinae/docs/data/02/main.py new file mode 100644 index 0000000..d5af5ba --- /dev/null +++ b/lomakinae/docs/data/02/main.py @@ -0,0 +1,10 @@ +from src.facade import MazeTestingFacade + + +def main(): + facade = MazeTestingFacade() + facade.run_full_diagnostic() + + +if __name__ == "__main__": + main() diff --git a/lomakinae/docs/data/02/results.csv b/lomakinae/docs/data/02/results.csv new file mode 100644 index 0000000..079e506 --- /dev/null +++ b/lomakinae/docs/data/02/results.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +maze_100x100,BFS,3.1472706992644817,2319,202 +maze_100x100,DFS,0.770840000041062,662,202 +maze_100x100,A*,0.9810699004447088,380,202 +maze_10x10,BFS,0.03959560053772293,33,14 +maze_10x10,DFS,0.03451459851930849,32,14 +maze_10x10,A*,0.046758499956922606,18,14 +maze_50x50,BFS,1.3058404991170391,1046,107 +maze_50x50,DFS,0.24829840040183626,231,107 +maze_50x50,A*,1.0543492011493072,414,107 +maze_empty,BFS,0.11438779984018765,90,18 +maze_empty,DFS,0.07362129908869974,90,54 +maze_empty,A*,0.2248886004963424,90,18 +maze_no_exit,BFS,0.021572699915850535,16,0 +maze_no_exit,DFS,0.01997379949898459,16,0 +maze_no_exit,A*,0.03601359931053594,16,0 diff --git a/lomakinae/docs/data/02/src/__init__.py b/lomakinae/docs/data/02/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lomakinae/docs/data/02/src/builder.py b/lomakinae/docs/data/02/src/builder.py new file mode 100644 index 0000000..1148ca0 --- /dev/null +++ b/lomakinae/docs/data/02/src/builder.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod + +from .maze import Maze + + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename: str) -> Maze: + with open(filename, "r", encoding="utf-8") as f: + lines = [line.rstrip("\n") for line in f.readlines()] + height = len(lines) + width = max(len(line) for line in lines) if height > 0 else 0 + + start_count = 0 + exit_count = 0 + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + maze.set_cell(x, y, "wall") + elif ch == "S": + maze.set_cell(x, y, "start") + start_count += 1 + elif ch == "E": + maze.set_cell(x, y, "exit") + exit_count += 1 + else: + maze.set_cell(x, y, "path") + + if start_count != 1 or exit_count != 1: + raise ValueError(f"S={start_count}, E={exit_count}") + return maze diff --git a/lomakinae/docs/data/02/src/cell.py b/lomakinae/docs/data/02/src/cell.py new file mode 100644 index 0000000..9660e19 --- /dev/null +++ b/lomakinae/docs/data/02/src/cell.py @@ -0,0 +1,10 @@ +class Cell: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + self.is_wall = False + self.is_start = False + self.is_exit = False + + def is_passable(self) -> bool: + return not self.is_wall diff --git a/lomakinae/docs/data/02/src/experiment.py b/lomakinae/docs/data/02/src/experiment.py new file mode 100644 index 0000000..f59c406 --- /dev/null +++ b/lomakinae/docs/data/02/src/experiment.py @@ -0,0 +1,69 @@ +import csv +import sys +from pathlib import Path + +# Adjust sys.path to allow imports from src when run directly +BASE_DIR = Path(__file__).resolve().parent +sys.path.append(str(BASE_DIR.parent)) + +from src.builder import TextFileMazeBuilder +from src.solver import MazeSolver +from src.strategies import AStarStrategy, BFSStrategy, DFSStrategy + +MAZE_DIR = BASE_DIR / "mazes" + + +def run_benchmarks(): + builder = TextFileMazeBuilder() + strategies = [ + ("BFS", BFSStrategy()), + ("DFS", DFSStrategy()), + ("A*", AStarStrategy()), + ] + + results = [] + + for maze_path in sorted(MAZE_DIR.glob("*.txt")): + maze_name = maze_path.stem + try: + maze = builder.build_from_file(maze_path) + except Exception as e: + continue + + solver = MazeSolver(maze) + + for strat_name, strat in strategies: + solver.set_strategy(strat) + + times = [] + stats = None + for _ in range(10): + stats = solver.solve() + times.append(stats.time_ms) + + avg_time = sum(times) / len(times) + + results.append( + { + "maze": maze_name, + "strategy": strat_name, + "time_ms": avg_time, + "visited_cells": stats.visited_cells, + "path_length": stats.path_length, + } + ) + + csv_path = BASE_DIR.parent / "results.csv" + with open(csv_path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter( + f, + fieldnames=["maze", "strategy", "time_ms", "visited_cells", "path_length"], + ) + writer.writeheader() + writer.writerows(results) + + print(f"Benchmark results stored in {csv_path}") + + +if __name__ == "__main__": + run_benchmarks() diff --git a/lomakinae/docs/data/02/src/facade.py b/lomakinae/docs/data/02/src/facade.py new file mode 100644 index 0000000..826f586 --- /dev/null +++ b/lomakinae/docs/data/02/src/facade.py @@ -0,0 +1,13 @@ +from .experiment import run_benchmarks +from .plots import generate_plots + + +class MazeTestingFacade: + def run_full_diagnostic(self): + print("Запуск экспериментов...") + run_benchmarks() + + print("\nГенерация графиков...") + generate_plots() + + print("\nГотово!") diff --git a/lomakinae/docs/data/02/src/maze.py b/lomakinae/docs/data/02/src/maze.py new file mode 100644 index 0000000..65e1753 --- /dev/null +++ b/lomakinae/docs/data/02/src/maze.py @@ -0,0 +1,49 @@ +from typing import List, Optional + +from .cell import Cell + + +class Maze: + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)] + self.start = None + self.exit = None + + def get_cell(self, x: int, y: int) -> Optional[Cell]: + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def set_cell(self, x: int, y: int, cell_type: str): + cell = self.get_cell(x, y) + if cell is None: + return + + if cell_type == "wall": + cell.is_wall = True + elif cell_type == "start": + if self.start: + self.start.is_start = False + cell.is_start = True + cell.is_wall = False + self.start = cell + elif cell_type == "exit": + if self.exit: + self.exit.is_exit = False + cell.is_exit = True + cell.is_wall = False + self.exit = cell + elif cell_type == "path": + cell.is_wall = False + + def get_neighbors(self, cell: Cell) -> List[Cell]: + neighbors = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] + for dx, dy in directions: + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.get_cell(nx, ny) + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + return neighbors diff --git a/lomakinae/docs/data/02/src/mazes/maze_100x100.txt b/lomakinae/docs/data/02/src/mazes/maze_100x100.txt new file mode 100644 index 0000000..c8ea5f9 --- /dev/null +++ b/lomakinae/docs/data/02/src/mazes/maze_100x100.txt @@ -0,0 +1,100 @@ +#################################################################################################### +#S # ## ## # ## ## # # ## # ## ## # ## ## ## ## # # # # # ## ## # # +# ## # # ## #### ##### ## ## ######## # # ## ### ## ### # ### ###### ###### # # +# ## # ## # # ###### ### # # # # # ## # # ### # ## ### # # #### ## # ##### # # +## # ## ## ## ###### # # ######### ### # ## ## #### ### ## # ## # # ## ## +### #### ### # # ## ## # # ## # ### # #### # # #### # # ##### #### # ### ## # +# ### ## ## ### ########### # ## ## ####### # ## # ####### # # ## ## ###### ## ## +# ### ### ### ## # # ## # #### # # # ## ## ### # # # ## # ## ## # +## # ## ## ## # ######### # ## ## # ####### ######### # ############ # # ## ##### +### #### ## ## ### # # ### ######### ## ## ## # ## # # ## ## # ## # # ## ## # # # # # +## ## # #### # ########## # ## ###### ## # ## # ## # # ### # ##### # # +# # ### #### ## # ## ## # # ## ## # ## ## ######### ## ## # ## # ## ## ## ## +####### #### ### # ##### ### ##### # # # ## ## # ## ## # # ## # # ###### ##### ####### +### # ## ## ##### #### # ### ## # ## #### ### # ## ## ####### ## # # ## # # +# # # #### ###### ## ### #### # ## # ## # # # ### ## ##### ### # # ## # # ## # +### # ## ## # ### ## #### # # # # ## #### ####### ## ## # # ### # # # ##### # ##### # +# # # # ## ## # # # ### ###### ## #### ### ## ## ## ### # ### ## # ## ## # +# # ## ## # ###### # ## ## ### ## ## # # # #### # ## ## # # # ###### # ###### ## ## ## +# # ## # ## # # #### # ######## # ## # # ### # ## ## ## # # #### # ## # # #### # +### ## ### ## ## #### # ## ## # #### # ## # # # # ## ## # # ## ## # ####### # # # ### +# # # # ## # # # # ## ## # # # ## # ## # # # # # # ## #### ### ### ### ## ## # # +# # ### ### ## # ## # ## #### # # # ## # ## # ## ### #### #### ## ## ####### # # +# # ### # ## # # # # #### ## ## ## ## # #### ### # ### # ## ## ## ## #### # +### # # ## # # ## ## ############## # #### ## ### ## ## ### # # ## # #### # +# # # ## ## # # ## ### # # ### ## ## ## # ### ## ###### # # ## ## ### ## ## #### # +## ######## # # # ## ## # ## ## # ## # ## ## # #### ## ## ## ## ###### ## ## # +###### # ###### # ### ## #### ### # #### ####### ## ## # ## # ### # ### # ## #### # +# ## # # # ## # ######### ## ### ### ## #### ## ## ## # ## # # ### #### ## # +########## # # ## # # ## # # ## ##### ## ## ### ## ## ## ### ### ## +### #### ##### # # # # #### ## ############# # ## ## ## # ## ###### # ## # # +# ######## # ## ### ## # ## ## # # #### # # ##### ##### ## # # ##### ## # ## ## ### # ## +# ###### ### ### ## # #### ## ##### ## ## # # ## # #### ## # # # # # ## ## # # +# #### #### ## ## ###### ###### # # ### # # ## # # # #### ## ## ## ##### +###### # # # ## #### ### ### ## ## ## ##### # # ## # ## ##### ## # # # # ## ## +# # # # # ## #### ## ######### # # # ########### ###### # ### # ## # ## ## # +####### # ## # ## # ## ### # # ## ##### # # # #### ## ## # # ### ## ### ### # ## #### # +# # # ##### ## ## # ## ###### # # # #### # # # ## ##### # ## ## # +# ## ## ## ### # #### ###### # ## ### # # ### ########## ##### ## ## # # ## ## ## +##### ## ######## # # ## ######### # ## # # # ## ## ## # ###### # # ## # # ## ### +# # ## ### # ## ## ## ## ## # ## #### ###### # ## ##### #### # ## ## ## # +# # ## ## ### ##### # # ## #### ########## # ## #### ###### # ## ###### ## ## ## +##### ## # # ### # # ## # # # ## ## # ## # ### # ### ## ## # # # ##### # ## # # +# ### # # # ## ##### # # # ##### ## ### ## # # ## # ### # #### # ## ## +###### ## ## ####### # ##### # ## # # ## ## # ### ## # ####### # # # # # ## # ## ## # +# # # # ## # ## #### ## # # # #### ## ## ### # ## ## ##### ## # # ## ### ## ## +# ## # # ## ## # #### ## ## ## ### # # # # ## # # # # # ## # # ## # # ## # ## # # +# ### ## ## ## # ### ## #### ### #### ## # # # ## ###### # ## # # #### ## ## ## +## # # ### ### ###### # ## ## # # # ####### ## # # # # ## ### ## # ## ### ## ## +# ##### ### ## ### # ## # ### ## ## #### # # ## # ######### ## ## # ## # +###### ### ##### ############ ## ## #### ## #### ## # # ## #### ##### # ## ###### +# ## # ## # # # # # # ## ######### # ### # ## ##### ## # ##### ## # # ## ## # ### +# ## ## ######## ## ## #### ## ## ## # ## # # # ### # ##### ## ## ## # +## ### ########### ### # # ## ####### ## # ## # # #### ### # ### # # # ### # ## # # +#### # # ## ## #### # # # # # ## # ###### ## # # # ## # ## # # # ##### ## # ## +# # # # ## ###### #### ## # # # ## # ## ## ## ### ## # ### # ## # +# ### ############# # ### ##### ######## ####### # # # # ### ## ## ####### ## ## ### # # ## +# # ## # # ### # # # ## ## ## # # ### ## # ##### ## ## ## ## # ## # ## # +# #### #### # ## ######## #### # # #### ##### # # # # ## ## ## ## ## # ## ## # ## ## +# ## # #### ###### # # # ### # # ## # # # ## ### ### ## ## ### ## ## # # # +# #### ## ## # ## # #### # ###### # ## # ####### # # # ## # ### # ## ### # ## # ## ### ### +# ## # # # ### # # # ### # ## # # # ## # ## ### ## # ## ## # +# #### ### # ## # # # ##### ###### ## # ## ## ## # # ### #### ## ## # ## # ### # +# # ### ## # ### # ## # # ## ## ###### # ### # # ### # ### # #### ######## +## # ## ## # # ## ## # ### ## ##### ## ### #### # # ## ### # ### ## # ## ## +# ############## ### #### ## ###### ## #### ### # ## ## # ## # # ##### ####### ## # +#### # # # #### #### ## ##### # ## #### # # # # # ##### # # ## ### ### ##### ## +#### ## ### ## ## # # ##### # ### ### ## ## ## ## # # # # # ### ### # # # # +# # #### ## ## ## ## # # # ## ######### ###### ### ## #### ## ## ## # ## ## +#### ###### ## # ### # ## ######## # ###### ####### ## ## # # ### ## # # # #### ####### +### ## # ### ### ## ### # # ## # # ## ## # ## ### ## ##### ### # ## ## # # +# ### ###### # ## # # # # ### ## # # ##### ## ### # # # ### ## ### ## ## ##### +# # ##### # # # ####### # ## # # #### # # ### # ### ## # ## ## ## ### +# # # ## # ## # # # # # ###### ## # ##### ## ## #### ## ## # ### ##### ## ### # +# # ###### #### ### ### # ### ## # # #### ### ## ## ### # ## #### # # # ## #### # +# ### # # ###### ## ## ## ## ## ### # ## ## ####### #### ## ## # # ### # ## # +# ##### ### ### ### ### #### ### # ## # ## ####### #### # # # ### # ##### #### ### # # +### ### # ## # ## # # # ## ## # # # ## ### #### # ## ##### ## # ## ## # # +## # ## ## ## ########### # # ## # ## ###### ### ###### # ## ## # ##### ## # #### # +# ### ######### # # #### #### ## # # ### # # ## #### # ## ## ## # ### # +# ## ## # # ######## # ##### ## ## # # # ## ## ###### ### # ## #### # +# ## #### # # ## ### ######## # # # ## # # # # ########### # # # # # ### # #### ## +#### ## ## ## ##### #### # # ##### ### #### # # # # # ## # ## # # ## ## #### # +# ##### # ## # # ### ##### ### # # # #### # ## # # # ## ### # ## ## ### ### +### ## ### ##### ## # # ##### #### ### # ## # #### # ## ## # ### ## ## ## # +# # # # ## # ##### ###### # # # # ## # # # #### # ### # # ## #### ### +## ####### # ### ## ######### ## # # # # ######### ### ## # # # ## # ### # ## ### ## # +# # # # ######## # # ##### # ## ## ### ######### ### ## # # # # # ### +## # # # # # ##### ###### # ## # ## ########## ## # ############ # ## # # ## ## # ### +############## ##### ### ## # ## # ## # ### ############# # # # # #### ## # ## # # +# # ## # # ### # #### # ############# # # # # ### # ###### #### #### ## # ### +# # ## ## ### # # ### # # # # # # ##### # ## # ### # ## # ## # ## # # +# ####### ## # ###### ####### #### ####### # ## # # ####### # # # ## # ## # #### +## #### ## ## ### ### # # ## ## # # # ##### ####### ##### # # # # # +# ## # ## # ## ## # ## # ## ###### # ### # ## # # # # # # ## ## ## ##### # # +## ##### # # ## # # ## # # # ###### ## # # # # ###### ####### ################# # # ### # +# ## # ################# ## ## # ### # ### ## ##### ## ## # ## ### # # ## # # +# # ### ## ## # # # # ## ## # # # ##### ### ## ### #### #### ####### +#### # # # # ## ## # ### # # ##### # ### ########## ############ ## # # +# # # # ## # # # # # # ## ## # # # # # # ## ## # ## # ##E## +#################################################################################################### diff --git a/lomakinae/docs/data/02/src/mazes/maze_10x10.txt b/lomakinae/docs/data/02/src/mazes/maze_10x10.txt new file mode 100644 index 0000000..0a07791 --- /dev/null +++ b/lomakinae/docs/data/02/src/mazes/maze_10x10.txt @@ -0,0 +1,10 @@ +########## +##S# ## ## +# ## # +## # ## +###### ### +## # # +# # # # +# #### # # +# # #E# +########## diff --git a/lomakinae/docs/data/02/src/mazes/maze_50x50.txt b/lomakinae/docs/data/02/src/mazes/maze_50x50.txt new file mode 100644 index 0000000..47fc9e4 --- /dev/null +++ b/lomakinae/docs/data/02/src/mazes/maze_50x50.txt @@ -0,0 +1,50 @@ +################################################## +#S # # ## ## # # # # ## ## # # ## # # ## # +## ### # # ## ### ## ## # # # # +# ##### ########## ## ## ### # ## ###### +# ## # # ## # # ## ## ## # ### #### # +# ###### # ## ### ## ### # # ## # ### +# # # ###### ### # ## ## # ###### # ## # +# # # # ### ##### ### # ###### ## # +### # # #### # ##### # ###### # ####### # # # +# # # # # # # ## # # # #### # +##### # #### # # ####### ###### # ### ### ## # # # +# ## # # # # # # # ## ## # # +######## ### ### ##### ## #### # ### #### +# #### #### #### # ####### #### ## +## ## ## ## ## ## ### # # ## ## #### # # +#### # # ## ##### ## ## ### # ## ### +# #### # ## # ############# ## ## ##### +###### # ## # # ## ### # # ## ######### # +## ## # # # # # ## # # ### # +# ## #### # # ## ##### # ###### ### # +##### ###### # ### #### ## ##### ######### ### +# # ## ###### ## # # # +# # # # ## #### # ## # ## #### # ######## #### +########### # # ## # ## # ## # # +## # # ## #### # # ## #### ## # ## ## #### +# # ### # ## ## # ## ### #### ## +## # # ##### # # ## ## ### ##### ## #### # +###### # ####### ## # ## #### # ### ### +# ## ## ## ## ### ## # # # ## # +# ## # # # ## # # ## ## ###### ## ### #### # +# ### # ### ############## ## #### # # +# ###### # ##### # # # # # # #### ## ### # # # +#### ##### ## # ## # ## ### # +### ###### # # # # ## # # ## ### # ## ### +# ## # ############ # ### ###### ## ###### +# ##### #### # #### ## # ## # +## ### # ## #### ####### ### ## ### # # # +# #### ## # #### ##### # ###### ##### # +########## ### # ### ## ## ## ## ### ## ## # +# # # #### ## ## # # # # # +###### #### ## # # ## ## ### ### # # ###### +# # # # ## #### ## ## ## ####### # ## +####### #### ## ## # # # # # # # # +# ##### ## # #### # ## ## # ## ##### +########## ## ### ## ## # # # # # # # +## #### ## ## # # ### ## ## # ## # # # ##### +# ## ## ####### # # # ### ## # # # #### +# # # # ## ### # # # ## ### # # # ## # +# # # # # ## # # # # # # # # # # # # ## ##E# +################################################## diff --git a/lomakinae/docs/data/02/src/mazes/maze_empty.txt b/lomakinae/docs/data/02/src/mazes/maze_empty.txt new file mode 100644 index 0000000..520e7be --- /dev/null +++ b/lomakinae/docs/data/02/src/mazes/maze_empty.txt @@ -0,0 +1,11 @@ +############ +#S # +# # +# # +# # +# # +# # +# # +# # +# E# +############ diff --git a/lomakinae/docs/data/02/src/mazes/maze_no_exit.txt b/lomakinae/docs/data/02/src/mazes/maze_no_exit.txt new file mode 100644 index 0000000..7858fee --- /dev/null +++ b/lomakinae/docs/data/02/src/mazes/maze_no_exit.txt @@ -0,0 +1,7 @@ +####### +#S # +# ### # +# #E# # +# ### # +# # +####### diff --git a/lomakinae/docs/data/02/src/plots.py b/lomakinae/docs/data/02/src/plots.py new file mode 100644 index 0000000..3e26114 --- /dev/null +++ b/lomakinae/docs/data/02/src/plots.py @@ -0,0 +1,92 @@ +import csv +import re +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +BASE_DIR = Path(__file__).resolve().parent + + +def generate_plots(): + csv_path = BASE_DIR.parent / "results.csv" + if not csv_path.exists(): + print(f"Error: {csv_path} not found. Run experiment.py first.") + return + + results = [] + with open(csv_path, "r", encoding="utf-8") as f: + reader = csv.DictReader(f) + for row in reader: + results.append( + { + "maze": row["maze"], + "strategy": row["strategy"], + "time_ms": float(row["time_ms"]), + "visited_cells": int(row["visited_cells"]), + "path_length": int(row["path_length"]), + } + ) + + # Sort mazes by requested logical order: no_exit, empty, then by size (NxN) + unique_mazes = list(dict.fromkeys(r["maze"] for r in results)) + + def get_sort_key(m_name): + name = m_name.lower() + if "no_exit" in name or "noexit" in name: + return 0 + if "empty" in name: + return 1 + + match = re.search(r"(\d+)x\d+", name) + if match: + return 100 + int(match.group(1)) + + return 999 + + maze_files_keys = sorted(unique_mazes, key=get_sort_key) + + fig, axes = plt.subplots( + len(maze_files_keys), 3, figsize=(18, 3 * len(maze_files_keys)) + ) + + for idx, maze_name in enumerate(maze_files_keys): + maze_res = [r for r in results if r["maze"] == maze_name] + if not maze_res: + continue + + strats = [r["strategy"] for r in maze_res] + times = [r["time_ms"] for r in maze_res] + visited = [r["visited_cells"] for r in maze_res] + path_lens = [r["path_length"] for r in maze_res] + + x = np.arange(len(strats)) + + # Check if axes is 1D or 2D depending on number of mazes + ax_time = axes[0] if len(maze_files_keys) == 1 else axes[idx, 0] + ax_visited = axes[1] if len(maze_files_keys) == 1 else axes[idx, 1] + ax_path = axes[2] if len(maze_files_keys) == 1 else axes[idx, 2] + + ax_time.bar(x, times, color=["red", "green", "blue"]) + ax_time.set_xticks(x) + ax_time.set_xticklabels(strats) + ax_time.set_title(f"{maze_name}: Execution Time (ms)") + + ax_visited.bar(x, visited, color=["red", "green", "blue"]) + ax_visited.set_xticks(x) + ax_visited.set_xticklabels(strats) + ax_visited.set_title(f"{maze_name}: Visited Cells") + + ax_path.bar(x, path_lens, color=["red", "green", "blue"]) + ax_path.set_xticks(x) + ax_path.set_xticklabels(strats) + ax_path.set_title(f"{maze_name}: Path Length") + + plt.tight_layout() + chart_path = BASE_DIR.parent / "benchmark_charts.png" + plt.savefig(chart_path) + print(f"Charts exported to {chart_path}") + + +if __name__ == "__main__": + generate_plots() diff --git a/lomakinae/docs/data/02/src/solver.py b/lomakinae/docs/data/02/src/solver.py new file mode 100644 index 0000000..f794471 --- /dev/null +++ b/lomakinae/docs/data/02/src/solver.py @@ -0,0 +1,35 @@ +import time +from typing import NamedTuple + +from .maze import Maze +from .strategies import PathFindingStrategy + + +class SearchStats(NamedTuple): + time_ms: float + visited_cells: int + path_length: int + + +class MazeSolver: + def __init__(self, maze: Maze): + self.maze = maze + self.strategy = None + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def solve(self) -> SearchStats: + if self.strategy is None: + raise ValueError("Strategy not set") + + start_time = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + time_ms = (end_time - start_time) * 1000 + + return SearchStats( + time_ms=time_ms, + visited_cells=self.strategy.visited_count, + path_length=len(path), + ) diff --git a/lomakinae/docs/data/02/src/strategies.py b/lomakinae/docs/data/02/src/strategies.py new file mode 100644 index 0000000..4ab2b6c --- /dev/null +++ b/lomakinae/docs/data/02/src/strategies.py @@ -0,0 +1,103 @@ +import heapq +from abc import ABC, abstractmethod +from collections import deque +from typing import List + +from .cell import Cell +from .maze import Maze + + +class PathFindingStrategy(ABC): + def __init__(self): + self.visited_count = 0 + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + + def reconstruct_path(self, came_from: dict, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path + + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + queue = deque([start]) + came_from = {start: None} + visited = {start} + + while queue: + current = queue.popleft() + if current == exit_cell: + self.visited_count = len(visited) + return self.reconstruct_path(came_from, exit_cell) + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + came_from[neighbor] = current + queue.append(neighbor) + self.visited_count = len(visited) + return [] + + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + stack = [start] + came_from = {start: None} + visited = {start} + + while stack: + current = stack.pop() + if current == exit_cell: + self.visited_count = len(visited) + return self.reconstruct_path(came_from, exit_cell) + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + came_from[neighbor] = current + stack.append(neighbor) + self.visited_count = len(visited) + return [] + + +class AStarStrategy(PathFindingStrategy): + def heuristic(self, cell: Cell, exit_cell: Cell) -> int: + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + heap = [] + counter = 0 + start_f = self.heuristic(start, exit_cell) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + + came_from = {} + g_score = {start: 0} + f_score = {start: start_f} + visited = set() + + while heap: + current_f, _, current = heapq.heappop(heap) + visited.add(current) + + if current == exit_cell: + self.visited_count = len(visited) + return self.reconstruct_path(came_from, exit_cell) + if current_f > f_score.get(current, float("inf")): + continue + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + 1 + if tentative_g < g_score.get(neighbor, float("inf")): + came_from[neighbor] = current + g_score[neighbor] = tentative_g + new_f = tentative_g + self.heuristic(neighbor, exit_cell) + f_score[neighbor] = new_f + heapq.heappush(heap, (new_f, counter, neighbor)) + counter += 1 + self.visited_count = len(visited) + return []