forked from UNN/2026-rff_mp
Merge pull request '[2] maze' (#332) from lomakinae/2026-rff_mp:maze into develop
Reviewed-on: UNN/2026-rff_mp#332
This commit is contained in:
commit
a481877040
349
lomakinae/docs/02_report.md
Normal file
349
lomakinae/docs/02_report.md
Normal file
|
|
@ -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 {
|
||||||
|
<<interface>>
|
||||||
|
+build_from_file(filename): Maze
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextFileMazeBuilder {
|
||||||
|
+build_from_file(filename): Maze
|
||||||
|
}
|
||||||
|
|
||||||
|
class PathFindingStrategy {
|
||||||
|
<<interface>>
|
||||||
|
+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 мс.
|
||||||
|
|
||||||
|
### Графики
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
11
lomakinae/docs/data/02/README.md
Normal file
11
lomakinae/docs/data/02/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Задание 2: Поиск выхода из лабиринта (объектно-ориентированная реализация с паттернами)
|
||||||
|
|
||||||
|
## Как запустить
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Результаты
|
||||||
|
|
||||||
|
График сравнения алгоритмов сохраняется в `benchmark_charts.png`, данные - в `results.csv`.
|
||||||
BIN
lomakinae/docs/data/02/benchmark_charts.png
Normal file
BIN
lomakinae/docs/data/02/benchmark_charts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
10
lomakinae/docs/data/02/main.py
Normal file
10
lomakinae/docs/data/02/main.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from src.facade import MazeTestingFacade
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
facade = MazeTestingFacade()
|
||||||
|
facade.run_full_diagnostic()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
16
lomakinae/docs/data/02/results.csv
Normal file
16
lomakinae/docs/data/02/results.csv
Normal file
|
|
@ -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
|
||||||
|
0
lomakinae/docs/data/02/src/__init__.py
Normal file
0
lomakinae/docs/data/02/src/__init__.py
Normal file
38
lomakinae/docs/data/02/src/builder.py
Normal file
38
lomakinae/docs/data/02/src/builder.py
Normal file
|
|
@ -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
|
||||||
10
lomakinae/docs/data/02/src/cell.py
Normal file
10
lomakinae/docs/data/02/src/cell.py
Normal file
|
|
@ -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
|
||||||
69
lomakinae/docs/data/02/src/experiment.py
Normal file
69
lomakinae/docs/data/02/src/experiment.py
Normal file
|
|
@ -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()
|
||||||
13
lomakinae/docs/data/02/src/facade.py
Normal file
13
lomakinae/docs/data/02/src/facade.py
Normal file
|
|
@ -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Готово!")
|
||||||
49
lomakinae/docs/data/02/src/maze.py
Normal file
49
lomakinae/docs/data/02/src/maze.py
Normal file
|
|
@ -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
|
||||||
100
lomakinae/docs/data/02/src/mazes/maze_100x100.txt
Normal file
100
lomakinae/docs/data/02/src/mazes/maze_100x100.txt
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
####################################################################################################
|
||||||
|
#S # ## ## # ## ## # # ## # ## ## # ## ## ## ## # # # # # ## ## # #
|
||||||
|
# ## # # ## #### ##### ## ## ######## # # ## ### ## ### # ### ###### ###### # #
|
||||||
|
# ## # ## # # ###### ### # # # # # ## # # ### # ## ### # # #### ## # ##### # #
|
||||||
|
## # ## ## ## ###### # # ######### ### # ## ## #### ### ## # ## # # ## ##
|
||||||
|
### #### ### # # ## ## # # ## # ### # #### # # #### # # ##### #### # ### ## #
|
||||||
|
# ### ## ## ### ########### # ## ## ####### # ## # ####### # # ## ## ###### ## ##
|
||||||
|
# ### ### ### ## # # ## # #### # # # ## ## ### # # # ## # ## ## #
|
||||||
|
## # ## ## ## # ######### # ## ## # ####### ######### # ############ # # ## #####
|
||||||
|
### #### ## ## ### # # ### ######### ## ## ## # ## # # ## ## # ## # # ## ## # # # # #
|
||||||
|
## ## # #### # ########## # ## ###### ## # ## # ## # # ### # ##### # #
|
||||||
|
# # ### #### ## # ## ## # # ## ## # ## ## ######### ## ## # ## # ## ## ## ##
|
||||||
|
####### #### ### # ##### ### ##### # # # ## ## # ## ## # # ## # # ###### ##### #######
|
||||||
|
### # ## ## ##### #### # ### ## # ## #### ### # ## ## ####### ## # # ## # #
|
||||||
|
# # # #### ###### ## ### #### # ## # ## # # # ### ## ##### ### # # ## # # ## #
|
||||||
|
### # ## ## # ### ## #### # # # # ## #### ####### ## ## # # ### # # # ##### # ##### #
|
||||||
|
# # # # ## ## # # # ### ###### ## #### ### ## ## ## ### # ### ## # ## ## #
|
||||||
|
# # ## ## # ###### # ## ## ### ## ## # # # #### # ## ## # # # ###### # ###### ## ## ##
|
||||||
|
# # ## # ## # # #### # ######## # ## # # ### # ## ## ## # # #### # ## # # #### #
|
||||||
|
### ## ### ## ## #### # ## ## # #### # ## # # # # ## ## # # ## ## # ####### # # # ###
|
||||||
|
# # # # ## # # # # ## ## # # # ## # ## # # # # # # ## #### ### ### ### ## ## # #
|
||||||
|
# # ### ### ## # ## # ## #### # # # ## # ## # ## ### #### #### ## ## ####### # #
|
||||||
|
# # ### # ## # # # # #### ## ## ## ## # #### ### # ### # ## ## ## ## #### #
|
||||||
|
### # # ## # # ## ## ############## # #### ## ### ## ## ### # # ## # #### #
|
||||||
|
# # # ## ## # # ## ### # # ### ## ## ## # ### ## ###### # # ## ## ### ## ## #### #
|
||||||
|
## ######## # # # ## ## # ## ## # ## # ## ## # #### ## ## ## ## ###### ## ## #
|
||||||
|
###### # ###### # ### ## #### ### # #### ####### ## ## # ## # ### # ### # ## #### #
|
||||||
|
# ## # # # ## # ######### ## ### ### ## #### ## ## ## # ## # # ### #### ## #
|
||||||
|
########## # # ## # # ## # # ## ##### ## ## ### ## ## ## ### ### ##
|
||||||
|
### #### ##### # # # # #### ## ############# # ## ## ## # ## ###### # ## # #
|
||||||
|
# ######## # ## ### ## # ## ## # # #### # # ##### ##### ## # # ##### ## # ## ## ### # ##
|
||||||
|
# ###### ### ### ## # #### ## ##### ## ## # # ## # #### ## # # # # # ## ## # #
|
||||||
|
# #### #### ## ## ###### ###### # # ### # # ## # # # #### ## ## ## #####
|
||||||
|
###### # # # ## #### ### ### ## ## ## ##### # # ## # ## ##### ## # # # # ## ##
|
||||||
|
# # # # # ## #### ## ######### # # # ########### ###### # ### # ## # ## ## #
|
||||||
|
####### # ## # ## # ## ### # # ## ##### # # # #### ## ## # # ### ## ### ### # ## #### #
|
||||||
|
# # # ##### ## ## # ## ###### # # # #### # # # ## ##### # ## ## #
|
||||||
|
# ## ## ## ### # #### ###### # ## ### # # ### ########## ##### ## ## # # ## ## ##
|
||||||
|
##### ## ######## # # ## ######### # ## # # # ## ## ## # ###### # # ## # # ## ###
|
||||||
|
# # ## ### # ## ## ## ## ## # ## #### ###### # ## ##### #### # ## ## ## #
|
||||||
|
# # ## ## ### ##### # # ## #### ########## # ## #### ###### # ## ###### ## ## ##
|
||||||
|
##### ## # # ### # # ## # # # ## ## # ## # ### # ### ## ## # # # ##### # ## # #
|
||||||
|
# ### # # # ## ##### # # # ##### ## ### ## # # ## # ### # #### # ## ##
|
||||||
|
###### ## ## ####### # ##### # ## # # ## ## # ### ## # ####### # # # # # ## # ## ## #
|
||||||
|
# # # # ## # ## #### ## # # # #### ## ## ### # ## ## ##### ## # # ## ### ## ##
|
||||||
|
# ## # # ## ## # #### ## ## ## ### # # # # ## # # # # # ## # # ## # # ## # ## # #
|
||||||
|
# ### ## ## ## # ### ## #### ### #### ## # # # ## ###### # ## # # #### ## ## ##
|
||||||
|
## # # ### ### ###### # ## ## # # # ####### ## # # # # ## ### ## # ## ### ## ##
|
||||||
|
# ##### ### ## ### # ## # ### ## ## #### # # ## # ######### ## ## # ## #
|
||||||
|
###### ### ##### ############ ## ## #### ## #### ## # # ## #### ##### # ## ######
|
||||||
|
# ## # ## # # # # # # ## ######### # ### # ## ##### ## # ##### ## # # ## ## # ###
|
||||||
|
# ## ## ######## ## ## #### ## ## ## # ## # # # ### # ##### ## ## ## #
|
||||||
|
## ### ########### ### # # ## ####### ## # ## # # #### ### # ### # # # ### # ## # #
|
||||||
|
#### # # ## ## #### # # # # # ## # ###### ## # # # ## # ## # # # ##### ## # ##
|
||||||
|
# # # # ## ###### #### ## # # # ## # ## ## ## ### ## # ### # ## #
|
||||||
|
# ### ############# # ### ##### ######## ####### # # # # ### ## ## ####### ## ## ### # # ##
|
||||||
|
# # ## # # ### # # # ## ## ## # # ### ## # ##### ## ## ## ## # ## # ## #
|
||||||
|
# #### #### # ## ######## #### # # #### ##### # # # # ## ## ## ## ## # ## ## # ## ##
|
||||||
|
# ## # #### ###### # # # ### # # ## # # # ## ### ### ## ## ### ## ## # # #
|
||||||
|
# #### ## ## # ## # #### # ###### # ## # ####### # # # ## # ### # ## ### # ## # ## ### ###
|
||||||
|
# ## # # # ### # # # ### # ## # # # ## # ## ### ## # ## ## #
|
||||||
|
# #### ### # ## # # # ##### ###### ## # ## ## ## # # ### #### ## ## # ## # ### #
|
||||||
|
# # ### ## # ### # ## # # ## ## ###### # ### # # ### # ### # #### ########
|
||||||
|
## # ## ## # # ## ## # ### ## ##### ## ### #### # # ## ### # ### ## # ## ##
|
||||||
|
# ############## ### #### ## ###### ## #### ### # ## ## # ## # # ##### ####### ## #
|
||||||
|
#### # # # #### #### ## ##### # ## #### # # # # # ##### # # ## ### ### ##### ##
|
||||||
|
#### ## ### ## ## # # ##### # ### ### ## ## ## ## # # # # # ### ### # # # #
|
||||||
|
# # #### ## ## ## ## # # # ## ######### ###### ### ## #### ## ## ## # ## ##
|
||||||
|
#### ###### ## # ### # ## ######## # ###### ####### ## ## # # ### ## # # # #### #######
|
||||||
|
### ## # ### ### ## ### # # ## # # ## ## # ## ### ## ##### ### # ## ## # #
|
||||||
|
# ### ###### # ## # # # # ### ## # # ##### ## ### # # # ### ## ### ## ## #####
|
||||||
|
# # ##### # # # ####### # ## # # #### # # ### # ### ## # ## ## ## ###
|
||||||
|
# # # ## # ## # # # # # ###### ## # ##### ## ## #### ## ## # ### ##### ## ### #
|
||||||
|
# # ###### #### ### ### # ### ## # # #### ### ## ## ### # ## #### # # # ## #### #
|
||||||
|
# ### # # ###### ## ## ## ## ## ### # ## ## ####### #### ## ## # # ### # ## #
|
||||||
|
# ##### ### ### ### ### #### ### # ## # ## ####### #### # # # ### # ##### #### ### # #
|
||||||
|
### ### # ## # ## # # # ## ## # # # ## ### #### # ## ##### ## # ## ## # #
|
||||||
|
## # ## ## ## ########### # # ## # ## ###### ### ###### # ## ## # ##### ## # #### #
|
||||||
|
# ### ######### # # #### #### ## # # ### # # ## #### # ## ## ## # ### #
|
||||||
|
# ## ## # # ######## # ##### ## ## # # # ## ## ###### ### # ## #### #
|
||||||
|
# ## #### # # ## ### ######## # # # ## # # # # ########### # # # # # ### # #### ##
|
||||||
|
#### ## ## ## ##### #### # # ##### ### #### # # # # # ## # ## # # ## ## #### #
|
||||||
|
# ##### # ## # # ### ##### ### # # # #### # ## # # # ## ### # ## ## ### ###
|
||||||
|
### ## ### ##### ## # # ##### #### ### # ## # #### # ## ## # ### ## ## ## #
|
||||||
|
# # # # ## # ##### ###### # # # # ## # # # #### # ### # # ## #### ###
|
||||||
|
## ####### # ### ## ######### ## # # # # ######### ### ## # # # ## # ### # ## ### ## #
|
||||||
|
# # # # ######## # # ##### # ## ## ### ######### ### ## # # # # # ###
|
||||||
|
## # # # # # ##### ###### # ## # ## ########## ## # ############ # ## # # ## ## # ###
|
||||||
|
############## ##### ### ## # ## # ## # ### ############# # # # # #### ## # ## # #
|
||||||
|
# # ## # # ### # #### # ############# # # # # ### # ###### #### #### ## # ###
|
||||||
|
# # ## ## ### # # ### # # # # # # ##### # ## # ### # ## # ## # ## # #
|
||||||
|
# ####### ## # ###### ####### #### ####### # ## # # ####### # # # ## # ## # ####
|
||||||
|
## #### ## ## ### ### # # ## ## # # # ##### ####### ##### # # # # #
|
||||||
|
# ## # ## # ## ## # ## # ## ###### # ### # ## # # # # # # ## ## ## ##### # #
|
||||||
|
## ##### # # ## # # ## # # # ###### ## # # # # ###### ####### ################# # # ### #
|
||||||
|
# ## # ################# ## ## # ### # ### ## ##### ## ## # ## ### # # ## # #
|
||||||
|
# # ### ## ## # # # # ## ## # # # ##### ### ## ### #### #### #######
|
||||||
|
#### # # # # ## ## # ### # # ##### # ### ########## ############ ## # #
|
||||||
|
# # # # ## # # # # # # ## ## # # # # # # ## ## # ## # ##E##
|
||||||
|
####################################################################################################
|
||||||
10
lomakinae/docs/data/02/src/mazes/maze_10x10.txt
Normal file
10
lomakinae/docs/data/02/src/mazes/maze_10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
##S# ## ##
|
||||||
|
# ## #
|
||||||
|
## # ##
|
||||||
|
###### ###
|
||||||
|
## # #
|
||||||
|
# # # #
|
||||||
|
# #### # #
|
||||||
|
# # #E#
|
||||||
|
##########
|
||||||
50
lomakinae/docs/data/02/src/mazes/maze_50x50.txt
Normal file
50
lomakinae/docs/data/02/src/mazes/maze_50x50.txt
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
##################################################
|
||||||
|
#S # # ## ## # # # # ## ## # # ## # # ## #
|
||||||
|
## ### # # ## ### ## ## # # # #
|
||||||
|
# ##### ########## ## ## ### # ## ######
|
||||||
|
# ## # # ## # # ## ## ## # ### #### #
|
||||||
|
# ###### # ## ### ## ### # # ## # ###
|
||||||
|
# # # ###### ### # ## ## # ###### # ## #
|
||||||
|
# # # # ### ##### ### # ###### ## #
|
||||||
|
### # # #### # ##### # ###### # ####### # # #
|
||||||
|
# # # # # # # ## # # # #### #
|
||||||
|
##### # #### # # ####### ###### # ### ### ## # # #
|
||||||
|
# ## # # # # # # # ## ## # #
|
||||||
|
######## ### ### ##### ## #### # ### ####
|
||||||
|
# #### #### #### # ####### #### ##
|
||||||
|
## ## ## ## ## ## ### # # ## ## #### # #
|
||||||
|
#### # # ## ##### ## ## ### # ## ###
|
||||||
|
# #### # ## # ############# ## ## #####
|
||||||
|
###### # ## # # ## ### # # ## ######### #
|
||||||
|
## ## # # # # # ## # # ### #
|
||||||
|
# ## #### # # ## ##### # ###### ### #
|
||||||
|
##### ###### # ### #### ## ##### ######### ###
|
||||||
|
# # ## ###### ## # # #
|
||||||
|
# # # # ## #### # ## # ## #### # ######## ####
|
||||||
|
########### # # ## # ## # ## # #
|
||||||
|
## # # ## #### # # ## #### ## # ## ## ####
|
||||||
|
# # ### # ## ## # ## ### #### ##
|
||||||
|
## # # ##### # # ## ## ### ##### ## #### #
|
||||||
|
###### # ####### ## # ## #### # ### ###
|
||||||
|
# ## ## ## ## ### ## # # # ## #
|
||||||
|
# ## # # # ## # # ## ## ###### ## ### #### #
|
||||||
|
# ### # ### ############## ## #### # #
|
||||||
|
# ###### # ##### # # # # # # #### ## ### # # #
|
||||||
|
#### ##### ## # ## # ## ### #
|
||||||
|
### ###### # # # # ## # # ## ### # ## ###
|
||||||
|
# ## # ############ # ### ###### ## ######
|
||||||
|
# ##### #### # #### ## # ## #
|
||||||
|
## ### # ## #### ####### ### ## ### # # #
|
||||||
|
# #### ## # #### ##### # ###### ##### #
|
||||||
|
########## ### # ### ## ## ## ## ### ## ## #
|
||||||
|
# # # #### ## ## # # # # #
|
||||||
|
###### #### ## # # ## ## ### ### # # ######
|
||||||
|
# # # # ## #### ## ## ## ####### # ##
|
||||||
|
####### #### ## ## # # # # # # # #
|
||||||
|
# ##### ## # #### # ## ## # ## #####
|
||||||
|
########## ## ### ## ## # # # # # # #
|
||||||
|
## #### ## ## # # ### ## ## # ## # # # #####
|
||||||
|
# ## ## ####### # # # ### ## # # # ####
|
||||||
|
# # # # ## ### # # # ## ### # # # ## #
|
||||||
|
# # # # # ## # # # # # # # # # # # # ## ##E#
|
||||||
|
##################################################
|
||||||
11
lomakinae/docs/data/02/src/mazes/maze_empty.txt
Normal file
11
lomakinae/docs/data/02/src/mazes/maze_empty.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
############
|
||||||
|
#S #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# E#
|
||||||
|
############
|
||||||
7
lomakinae/docs/data/02/src/mazes/maze_no_exit.txt
Normal file
7
lomakinae/docs/data/02/src/mazes/maze_no_exit.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#######
|
||||||
|
#S #
|
||||||
|
# ### #
|
||||||
|
# #E# #
|
||||||
|
# ### #
|
||||||
|
# #
|
||||||
|
#######
|
||||||
92
lomakinae/docs/data/02/src/plots.py
Normal file
92
lomakinae/docs/data/02/src/plots.py
Normal file
|
|
@ -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()
|
||||||
35
lomakinae/docs/data/02/src/solver.py
Normal file
35
lomakinae/docs/data/02/src/solver.py
Normal file
|
|
@ -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),
|
||||||
|
)
|
||||||
103
lomakinae/docs/data/02/src/strategies.py
Normal file
103
lomakinae/docs/data/02/src/strategies.py
Normal file
|
|
@ -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 []
|
||||||
Loading…
Reference in New Issue
Block a user