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:
git_admin 2026-05-30 11:16:23 +00:00
commit a481877040
19 changed files with 973 additions and 0 deletions

349
lomakinae/docs/02_report.md Normal file
View 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 мс.
### Графики
![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.

View File

@ -0,0 +1,11 @@
# Задание 2: Поиск выхода из лабиринта (объектно-ориентированная реализация с паттернами)
## Как запустить
```sh
python main.py
```
## Результаты
График сравнения алгоритмов сохраняется в `benchmark_charts.png`, данные - в `results.csv`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -0,0 +1,10 @@
from src.facade import MazeTestingFacade
def main():
facade = MazeTestingFacade()
facade.run_full_diagnostic()
if __name__ == "__main__":
main()

View 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
1 maze strategy time_ms visited_cells path_length
2 maze_100x100 BFS 3.1472706992644817 2319 202
3 maze_100x100 DFS 0.770840000041062 662 202
4 maze_100x100 A* 0.9810699004447088 380 202
5 maze_10x10 BFS 0.03959560053772293 33 14
6 maze_10x10 DFS 0.03451459851930849 32 14
7 maze_10x10 A* 0.046758499956922606 18 14
8 maze_50x50 BFS 1.3058404991170391 1046 107
9 maze_50x50 DFS 0.24829840040183626 231 107
10 maze_50x50 A* 1.0543492011493072 414 107
11 maze_empty BFS 0.11438779984018765 90 18
12 maze_empty DFS 0.07362129908869974 90 54
13 maze_empty A* 0.2248886004963424 90 18
14 maze_no_exit BFS 0.021572699915850535 16 0
15 maze_no_exit DFS 0.01997379949898459 16 0
16 maze_no_exit A* 0.03601359931053594 16 0

View File

View 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

View 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

View 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()

View 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Готово!")

View 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

View File

@ -0,0 +1,100 @@
####################################################################################################
#S # ## ## # ## ## # # ## # ## ## # ## ## ## ## # # # # # ## ## # #
# ## # # ## #### ##### ## ## ######## # # ## ### ## ### # ### ###### ###### # #
# ## # ## # # ###### ### # # # # # ## # # ### # ## ### # # #### ## # ##### # #
## # ## ## ## ###### # # ######### ### # ## ## #### ### ## # ## # # ## ##
### #### ### # # ## ## # # ## # ### # #### # # #### # # ##### #### # ### ## #
# ### ## ## ### ########### # ## ## ####### # ## # ####### # # ## ## ###### ## ##
# ### ### ### ## # # ## # #### # # # ## ## ### # # # ## # ## ## #
## # ## ## ## # ######### # ## ## # ####### ######### # ############ # # ## #####
### #### ## ## ### # # ### ######### ## ## ## # ## # # ## ## # ## # # ## ## # # # # #
## ## # #### # ########## # ## ###### ## # ## # ## # # ### # ##### # #
# # ### #### ## # ## ## # # ## ## # ## ## ######### ## ## # ## # ## ## ## ##
####### #### ### # ##### ### ##### # # # ## ## # ## ## # # ## # # ###### ##### #######
### # ## ## ##### #### # ### ## # ## #### ### # ## ## ####### ## # # ## # #
# # # #### ###### ## ### #### # ## # ## # # # ### ## ##### ### # # ## # # ## #
### # ## ## # ### ## #### # # # # ## #### ####### ## ## # # ### # # # ##### # ##### #
# # # # ## ## # # # ### ###### ## #### ### ## ## ## ### # ### ## # ## ## #
# # ## ## # ###### # ## ## ### ## ## # # # #### # ## ## # # # ###### # ###### ## ## ##
# # ## # ## # # #### # ######## # ## # # ### # ## ## ## # # #### # ## # # #### #
### ## ### ## ## #### # ## ## # #### # ## # # # # ## ## # # ## ## # ####### # # # ###
# # # # ## # # # # ## ## # # # ## # ## # # # # # # ## #### ### ### ### ## ## # #
# # ### ### ## # ## # ## #### # # # ## # ## # ## ### #### #### ## ## ####### # #
# # ### # ## # # # # #### ## ## ## ## # #### ### # ### # ## ## ## ## #### #
### # # ## # # ## ## ############## # #### ## ### ## ## ### # # ## # #### #
# # # ## ## # # ## ### # # ### ## ## ## # ### ## ###### # # ## ## ### ## ## #### #
## ######## # # # ## ## # ## ## # ## # ## ## # #### ## ## ## ## ###### ## ## #
###### # ###### # ### ## #### ### # #### ####### ## ## # ## # ### # ### # ## #### #
# ## # # # ## # ######### ## ### ### ## #### ## ## ## # ## # # ### #### ## #
########## # # ## # # ## # # ## ##### ## ## ### ## ## ## ### ### ##
### #### ##### # # # # #### ## ############# # ## ## ## # ## ###### # ## # #
# ######## # ## ### ## # ## ## # # #### # # ##### ##### ## # # ##### ## # ## ## ### # ##
# ###### ### ### ## # #### ## ##### ## ## # # ## # #### ## # # # # # ## ## # #
# #### #### ## ## ###### ###### # # ### # # ## # # # #### ## ## ## #####
###### # # # ## #### ### ### ## ## ## ##### # # ## # ## ##### ## # # # # ## ##
# # # # # ## #### ## ######### # # # ########### ###### # ### # ## # ## ## #
####### # ## # ## # ## ### # # ## ##### # # # #### ## ## # # ### ## ### ### # ## #### #
# # # ##### ## ## # ## ###### # # # #### # # # ## ##### # ## ## #
# ## ## ## ### # #### ###### # ## ### # # ### ########## ##### ## ## # # ## ## ##
##### ## ######## # # ## ######### # ## # # # ## ## ## # ###### # # ## # # ## ###
# # ## ### # ## ## ## ## ## # ## #### ###### # ## ##### #### # ## ## ## #
# # ## ## ### ##### # # ## #### ########## # ## #### ###### # ## ###### ## ## ##
##### ## # # ### # # ## # # # ## ## # ## # ### # ### ## ## # # # ##### # ## # #
# ### # # # ## ##### # # # ##### ## ### ## # # ## # ### # #### # ## ##
###### ## ## ####### # ##### # ## # # ## ## # ### ## # ####### # # # # # ## # ## ## #
# # # # ## # ## #### ## # # # #### ## ## ### # ## ## ##### ## # # ## ### ## ##
# ## # # ## ## # #### ## ## ## ### # # # # ## # # # # # ## # # ## # # ## # ## # #
# ### ## ## ## # ### ## #### ### #### ## # # # ## ###### # ## # # #### ## ## ##
## # # ### ### ###### # ## ## # # # ####### ## # # # # ## ### ## # ## ### ## ##
# ##### ### ## ### # ## # ### ## ## #### # # ## # ######### ## ## # ## #
###### ### ##### ############ ## ## #### ## #### ## # # ## #### ##### # ## ######
# ## # ## # # # # # # ## ######### # ### # ## ##### ## # ##### ## # # ## ## # ###
# ## ## ######## ## ## #### ## ## ## # ## # # # ### # ##### ## ## ## #
## ### ########### ### # # ## ####### ## # ## # # #### ### # ### # # # ### # ## # #
#### # # ## ## #### # # # # # ## # ###### ## # # # ## # ## # # # ##### ## # ##
# # # # ## ###### #### ## # # # ## # ## ## ## ### ## # ### # ## #
# ### ############# # ### ##### ######## ####### # # # # ### ## ## ####### ## ## ### # # ##
# # ## # # ### # # # ## ## ## # # ### ## # ##### ## ## ## ## # ## # ## #
# #### #### # ## ######## #### # # #### ##### # # # # ## ## ## ## ## # ## ## # ## ##
# ## # #### ###### # # # ### # # ## # # # ## ### ### ## ## ### ## ## # # #
# #### ## ## # ## # #### # ###### # ## # ####### # # # ## # ### # ## ### # ## # ## ### ###
# ## # # # ### # # # ### # ## # # # ## # ## ### ## # ## ## #
# #### ### # ## # # # ##### ###### ## # ## ## ## # # ### #### ## ## # ## # ### #
# # ### ## # ### # ## # # ## ## ###### # ### # # ### # ### # #### ########
## # ## ## # # ## ## # ### ## ##### ## ### #### # # ## ### # ### ## # ## ##
# ############## ### #### ## ###### ## #### ### # ## ## # ## # # ##### ####### ## #
#### # # # #### #### ## ##### # ## #### # # # # # ##### # # ## ### ### ##### ##
#### ## ### ## ## # # ##### # ### ### ## ## ## ## # # # # # ### ### # # # #
# # #### ## ## ## ## # # # ## ######### ###### ### ## #### ## ## ## # ## ##
#### ###### ## # ### # ## ######## # ###### ####### ## ## # # ### ## # # # #### #######
### ## # ### ### ## ### # # ## # # ## ## # ## ### ## ##### ### # ## ## # #
# ### ###### # ## # # # # ### ## # # ##### ## ### # # # ### ## ### ## ## #####
# # ##### # # # ####### # ## # # #### # # ### # ### ## # ## ## ## ###
# # # ## # ## # # # # # ###### ## # ##### ## ## #### ## ## # ### ##### ## ### #
# # ###### #### ### ### # ### ## # # #### ### ## ## ### # ## #### # # # ## #### #
# ### # # ###### ## ## ## ## ## ### # ## ## ####### #### ## ## # # ### # ## #
# ##### ### ### ### ### #### ### # ## # ## ####### #### # # # ### # ##### #### ### # #
### ### # ## # ## # # # ## ## # # # ## ### #### # ## ##### ## # ## ## # #
## # ## ## ## ########### # # ## # ## ###### ### ###### # ## ## # ##### ## # #### #
# ### ######### # # #### #### ## # # ### # # ## #### # ## ## ## # ### #
# ## ## # # ######## # ##### ## ## # # # ## ## ###### ### # ## #### #
# ## #### # # ## ### ######## # # # ## # # # # ########### # # # # # ### # #### ##
#### ## ## ## ##### #### # # ##### ### #### # # # # # ## # ## # # ## ## #### #
# ##### # ## # # ### ##### ### # # # #### # ## # # # ## ### # ## ## ### ###
### ## ### ##### ## # # ##### #### ### # ## # #### # ## ## # ### ## ## ## #
# # # # ## # ##### ###### # # # # ## # # # #### # ### # # ## #### ###
## ####### # ### ## ######### ## # # # # ######### ### ## # # # ## # ### # ## ### ## #
# # # # ######## # # ##### # ## ## ### ######### ### ## # # # # # ###
## # # # # # ##### ###### # ## # ## ########## ## # ############ # ## # # ## ## # ###
############## ##### ### ## # ## # ## # ### ############# # # # # #### ## # ## # #
# # ## # # ### # #### # ############# # # # # ### # ###### #### #### ## # ###
# # ## ## ### # # ### # # # # # # ##### # ## # ### # ## # ## # ## # #
# ####### ## # ###### ####### #### ####### # ## # # ####### # # # ## # ## # ####
## #### ## ## ### ### # # ## ## # # # ##### ####### ##### # # # # #
# ## # ## # ## ## # ## # ## ###### # ### # ## # # # # # # ## ## ## ##### # #
## ##### # # ## # # ## # # # ###### ## # # # # ###### ####### ################# # # ### #
# ## # ################# ## ## # ### # ### ## ##### ## ## # ## ### # # ## # #
# # ### ## ## # # # # ## ## # # # ##### ### ## ### #### #### #######
#### # # # # ## ## # ### # # ##### # ### ########## ############ ## # #
# # # # ## # # # # # # ## ## # # # # # # ## ## # ## # ##E##
####################################################################################################

View File

@ -0,0 +1,10 @@
##########
##S# ## ##
# ## #
## # ##
###### ###
## # #
# # # #
# #### # #
# # #E#
##########

View File

@ -0,0 +1,50 @@
##################################################
#S # # ## ## # # # # ## ## # # ## # # ## #
## ### # # ## ### ## ## # # # #
# ##### ########## ## ## ### # ## ######
# ## # # ## # # ## ## ## # ### #### #
# ###### # ## ### ## ### # # ## # ###
# # # ###### ### # ## ## # ###### # ## #
# # # # ### ##### ### # ###### ## #
### # # #### # ##### # ###### # ####### # # #
# # # # # # # ## # # # #### #
##### # #### # # ####### ###### # ### ### ## # # #
# ## # # # # # # # ## ## # #
######## ### ### ##### ## #### # ### ####
# #### #### #### # ####### #### ##
## ## ## ## ## ## ### # # ## ## #### # #
#### # # ## ##### ## ## ### # ## ###
# #### # ## # ############# ## ## #####
###### # ## # # ## ### # # ## ######### #
## ## # # # # # ## # # ### #
# ## #### # # ## ##### # ###### ### #
##### ###### # ### #### ## ##### ######### ###
# # ## ###### ## # # #
# # # # ## #### # ## # ## #### # ######## ####
########### # # ## # ## # ## # #
## # # ## #### # # ## #### ## # ## ## ####
# # ### # ## ## # ## ### #### ##
## # # ##### # # ## ## ### ##### ## #### #
###### # ####### ## # ## #### # ### ###
# ## ## ## ## ### ## # # # ## #
# ## # # # ## # # ## ## ###### ## ### #### #
# ### # ### ############## ## #### # #
# ###### # ##### # # # # # # #### ## ### # # #
#### ##### ## # ## # ## ### #
### ###### # # # # ## # # ## ### # ## ###
# ## # ############ # ### ###### ## ######
# ##### #### # #### ## # ## #
## ### # ## #### ####### ### ## ### # # #
# #### ## # #### ##### # ###### ##### #
########## ### # ### ## ## ## ## ### ## ## #
# # # #### ## ## # # # # #
###### #### ## # # ## ## ### ### # # ######
# # # # ## #### ## ## ## ####### # ##
####### #### ## ## # # # # # # # #
# ##### ## # #### # ## ## # ## #####
########## ## ### ## ## # # # # # # #
## #### ## ## # # ### ## ## # ## # # # #####
# ## ## ####### # # # ### ## # # # ####
# # # # ## ### # # # ## ### # # # ## #
# # # # # ## # # # # # # # # # # # # ## ##E#
##################################################

View File

@ -0,0 +1,11 @@
############
#S #
# #
# #
# #
# #
# #
# #
# #
# E#
############

View File

@ -0,0 +1,7 @@
#######
#S #
# ### #
# #E# #
# ### #
# #
#######

View 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()

View 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),
)

View 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 []