[2] Task 2

This commit is contained in:
Alex 2026-05-19 22:39:51 +03:00
parent e10b075b06
commit 3fa79f06c3
32 changed files with 1816 additions and 0 deletions

4
shahovaa/zadanie 2/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
__pycache__/
*.pyc
.pytest_cache/

View File

@ -0,0 +1,67 @@
# Поиск выхода из лабиринта
Объектно-ориентированная реализация поиска пути в лабиринте с паттернами GoF:
Builder, Strategy, Observer и Command.
## Что реализовано
- модель `Cell` и `Maze`;
- загрузка лабиринта из текстового файла через `TextFileMazeBuilder`;
- стратегии поиска пути: BFS, DFS, A*, Дейкстра;
- `MazeSolver`, который измеряет время, число посещенных клеток и длину пути;
- консольный `Observer` для сообщений и отрисовки;
- `MoveCommand` и `Player` для ручного режима с undo;
- генератор тестовых лабиринтов;
- экспериментальный скрипт, CSV и SVG-графики;
- отчет: `reports/report.md`.
## Формат лабиринта
```text
# - стена
- проход
S - старт
E - выход
2, 3, ~ - проходимые клетки с увеличенным весом
```
Все строки в файле лабиринта должны иметь одинаковую длину.
## Запуск
```bash
python3 scripts/generate_mazes.py
python3 main.py --maze data/mazes/small.txt --strategy astar --render
```
Доступные стратегии:
```text
bfs
dfs
astar
dijkstra
```
Ручной режим с командами `W/A/S/D`, undo через `Z`:
```bash
python3 main.py --maze data/mazes/small.txt --manual
```
## Эксперименты
```bash
python3 scripts/run_experiments.py
```
Скрипт перегенерирует лабиринты, запускает каждую стратегию 10 раз и сохраняет:
- `reports/results.csv`;
- SVG-графики в `reports/charts/`.
## Проверка
```bash
python3 -m unittest
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,94 @@
from __future__ import annotations
import argparse
from maze_solver import (
AStarStrategy,
BFSStrategy,
ConsoleView,
DFSStrategy,
DijkstraStrategy,
Direction,
MazeSolver,
MoveCommand,
Player,
TextFileMazeBuilder,
)
STRATEGIES = {
"bfs": BFSStrategy,
"dfs": DFSStrategy,
"astar": AStarStrategy,
"dijkstra": DijkstraStrategy,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Find a path through a text maze.")
parser.add_argument("--maze", default="data/mazes/small.txt")
parser.add_argument(
"--strategy",
choices=sorted(STRATEGIES),
default="astar",
help="Path-finding algorithm.",
)
parser.add_argument("--render", action="store_true", help="Print maze with path.")
parser.add_argument(
"--manual",
action="store_true",
help="Manual W/A/S/D mode with Z undo and Q quit.",
)
args = parser.parse_args()
maze = TextFileMazeBuilder().build_from_file(args.maze)
strategy = STRATEGIES[args.strategy]()
solver = MazeSolver(maze, strategy)
view = ConsoleView()
solver.add_observer(view)
stats = solver.solve()
print(
f"Summary: strategy={stats.strategy_name}, time={stats.time_ms:.3f} ms, "
f"visited={stats.visited_cells}, path_length={stats.path_length}"
)
if args.render:
print(view.render(maze, path=stats.path))
if args.manual:
run_manual_mode(maze, view)
def run_manual_mode(maze, view: ConsoleView) -> None:
player = Player.at_start(maze)
history: list[MoveCommand] = []
while True:
print(view.render(maze, player_position=player.current_cell))
if player.current_cell == maze.exit:
print("Exit reached.")
return
key = input("Move W/A/S/D, undo Z, quit Q: ").strip().lower()
if key == "q":
return
if key == "z":
if history:
history.pop().undo()
continue
try:
command = MoveCommand(player, Direction.from_key(key))
except ValueError as exc:
print(exc)
continue
if command.execute():
history.append(command)
else:
print("Move blocked.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,34 @@
from .builders import MazeBuilder, TextFileMazeBuilder
from .commands import Direction, MoveCommand, Player
from .models import Cell, Maze
from .observers import ConsoleView, Event, Observer
from .solver import MazeSolver, SearchStats
from .strategies import (
AStarStrategy,
BFSStrategy,
DFSStrategy,
DijkstraStrategy,
PathFindingStrategy,
PathResult,
)
__all__ = [
"AStarStrategy",
"BFSStrategy",
"Cell",
"ConsoleView",
"DFSStrategy",
"DijkstraStrategy",
"Direction",
"Event",
"Maze",
"MazeBuilder",
"MazeSolver",
"MoveCommand",
"Observer",
"PathFindingStrategy",
"PathResult",
"Player",
"SearchStats",
"TextFileMazeBuilder",
]

View File

@ -0,0 +1,75 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from pathlib import Path
from .models import Cell, Maze
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str | Path) -> Maze:
raise NotImplementedError
def buildFromFile(self, filename: str | Path) -> Maze:
return self.build_from_file(filename)
class TextFileMazeBuilder(MazeBuilder):
WALL = "#"
START = "S"
EXIT = "E"
PASSAGES = {" ", "."}
WEIGHTS = {"1": 1, "2": 2, "3": 3, "~": 3}
def build_from_file(self, filename: str | Path) -> Maze:
path = Path(filename)
rows = path.read_text(encoding="utf-8").splitlines()
if not rows:
raise ValueError(f"Maze file is empty: {path}")
width = len(rows[0])
if width == 0:
raise ValueError("Maze width must be greater than zero")
if any(len(row) != width for row in rows):
raise ValueError("All maze rows must have the same width")
cells: list[list[Cell]] = []
start: Cell | None = None
exit: Cell | None = None
for y, row in enumerate(rows):
cell_row: list[Cell] = []
for x, char in enumerate(row):
cell = self._create_cell(x, y, char)
if cell.is_start:
if start is not None:
raise ValueError("Maze must contain exactly one start cell")
start = cell
if cell.is_exit:
if exit is not None:
raise ValueError("Maze must contain exactly one exit cell")
exit = cell
cell_row.append(cell)
cells.append(cell_row)
if start is None:
raise ValueError("Maze must contain a start cell marked with 'S'")
if exit is None:
raise ValueError("Maze must contain an exit cell marked with 'E'")
return Maze(cells, start, exit)
def _create_cell(self, x: int, y: int, char: str) -> Cell:
if char == self.WALL:
return Cell(x=x, y=y, is_wall=True, symbol=char)
if char == self.START:
return Cell(x=x, y=y, is_start=True, symbol=char)
if char == self.EXIT:
return Cell(x=x, y=y, is_exit=True, symbol=char)
if char in self.PASSAGES:
return Cell(x=x, y=y, symbol=" ")
if char in self.WEIGHTS:
return Cell(x=x, y=y, weight=self.WEIGHTS[char], symbol=char)
raise ValueError(f"Unsupported maze symbol {char!r} at ({x}, {y})")

View File

@ -0,0 +1,79 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from .models import Cell, Maze
class Direction(Enum):
UP = (0, -1)
RIGHT = (1, 0)
DOWN = (0, 1)
LEFT = (-1, 0)
@classmethod
def from_key(cls, key: str) -> "Direction":
mapping = {
"w": cls.UP,
"d": cls.RIGHT,
"s": cls.DOWN,
"a": cls.LEFT,
}
try:
return mapping[key.lower()]
except KeyError as exc:
raise ValueError("Use W/A/S/D for movement") from exc
@dataclass
class Player:
maze: Maze
current_cell: Cell
@classmethod
def at_start(cls, maze: Maze) -> "Player":
return cls(maze=maze, current_cell=maze.start)
def move_to(self, cell: Cell) -> None:
if not cell.is_passable():
raise ValueError("Player cannot move into a wall")
self.current_cell = cell
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
raise NotImplementedError
@abstractmethod
def undo(self) -> bool:
raise NotImplementedError
class MoveCommand(Command):
def __init__(self, player: Player, direction: Direction) -> None:
self.player = player
self.direction = direction
self.previous_cell: Cell | None = None
self.executed = False
def execute(self) -> bool:
dx, dy = self.direction.value
current = self.player.current_cell
target = self.player.maze.get_cell(current.x + dx, current.y + dy)
if target is None or not target.is_passable():
return False
self.previous_cell = current
self.player.move_to(target)
self.executed = True
return True
def undo(self) -> bool:
if not self.executed or self.previous_cell is None:
return False
self.player.move_to(self.previous_cell)
self.executed = False
return True

View File

@ -0,0 +1,81 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Cell:
x: int
y: int
is_wall: bool = False
is_start: bool = False
is_exit: bool = False
weight: int = 1
symbol: str = " "
def is_passable(self) -> bool:
return not self.is_wall
def isPassable(self) -> bool:
return self.is_passable()
class Maze:
def __init__(self, cells: list[list[Cell]], start: Cell, exit: Cell) -> None:
if not cells or not cells[0]:
raise ValueError("Maze must contain at least one cell")
width = len(cells[0])
if any(len(row) != width for row in cells):
raise ValueError("Maze rows must have equal width")
self.cells = cells
self.height = len(cells)
self.width = width
self.start = start
self.exit = exit
def get_cell(self, x: int, y: int) -> Cell | None:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def getCell(self, x: int, y: int) -> Cell | None:
return self.get_cell(x, y)
def get_neighbors(self, cell: Cell) -> list[Cell]:
neighbors: list[Cell] = []
for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)):
neighbor = self.get_cell(cell.x + dx, cell.y + dy)
if neighbor is not None and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
def getNeighbors(self, cell: Cell) -> list[Cell]:
return self.get_neighbors(cell)
def to_text(self, path: list[Cell] | None = None, player: Cell | None = None) -> str:
path_cells = {(cell.x, cell.y) for cell in path or []}
lines: list[str] = []
for row in self.cells:
chars: list[str] = []
for cell in row:
position = (cell.x, cell.y)
if player is not None and position == (player.x, player.y):
chars.append("@")
elif cell.is_start:
chars.append("S")
elif cell.is_exit:
chars.append("E")
elif cell.is_wall:
chars.append("#")
elif position in path_cells:
chars.append(".")
elif cell.weight > 1:
chars.append(str(cell.weight))
else:
chars.append(" ")
lines.append("".join(chars))
return "\n".join(lines)

View File

@ -0,0 +1,40 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
from .models import Cell, Maze
@dataclass(frozen=True)
class Event:
event_type: str
payload: dict[str, Any] = field(default_factory=dict)
class Observer(ABC):
@abstractmethod
def update(self, event: Event) -> None:
raise NotImplementedError
class ConsoleView(Observer):
def update(self, event: Event) -> None:
if event.event_type == "search_started":
print(f"Search started: {event.payload['strategy']}")
elif event.event_type in {"path_found", "path_not_found"}:
stats = event.payload["stats"]
print(
f"{event.event_type}: strategy={stats.strategy_name}, "
f"time={stats.time_ms:.3f} ms, visited={stats.visited_cells}, "
f"path_length={stats.path_length}"
)
def render(
self,
maze: Maze,
player_position: Cell | None = None,
path: list[Cell] | None = None,
) -> str:
return maze.to_text(path=path, player=player_position)

View File

@ -0,0 +1,57 @@
from __future__ import annotations
import time
from dataclasses import dataclass
from .models import Cell, Maze
from .observers import Event, Observer
from .strategies import PathFindingStrategy
@dataclass(frozen=True)
class SearchStats:
strategy_name: str
time_ms: float
visited_cells: int
path_length: int
path: list[Cell]
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy) -> None:
self.maze = maze
self.strategy = strategy
self._observers: list[Observer] = []
def set_strategy(self, strategy: PathFindingStrategy) -> None:
self.strategy = strategy
def setStrategy(self, strategy: PathFindingStrategy) -> None:
self.set_strategy(strategy)
def add_observer(self, observer: Observer) -> None:
self._observers.append(observer)
def remove_observer(self, observer: Observer) -> None:
self._observers.remove(observer)
def solve(self) -> SearchStats:
self._notify(Event("search_started", {"strategy": self.strategy.name}))
started_at = time.perf_counter()
result = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
elapsed_ms = (time.perf_counter() - started_at) * 1000
stats = SearchStats(
strategy_name=self.strategy.name,
time_ms=elapsed_ms,
visited_cells=result.visited_count,
path_length=len(result.path),
path=result.path,
)
event_name = "path_found" if result.path else "path_not_found"
self._notify(Event(event_name, {"stats": stats}))
return stats
def _notify(self, event: Event) -> None:
for observer in self._observers:
observer.update(event)

View File

@ -0,0 +1,150 @@
from __future__ import annotations
import heapq
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass
from itertools import count
from .models import Cell, Maze
@dataclass(frozen=True)
class PathResult:
path: list[Cell]
visited_count: int
class PathFindingStrategy(ABC):
name = "abstract"
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
raise NotImplementedError
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
return self.find_path(maze, start, exit)
class BFSStrategy(PathFindingStrategy):
name = "BFS"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
queue: deque[Cell] = deque([start])
parents: dict[Cell, Cell | None] = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
queue.append(neighbor)
return PathResult([], len(visited))
class DFSStrategy(PathFindingStrategy):
name = "DFS"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
stack = [start]
parents: dict[Cell, Cell | None] = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in reversed(maze.get_neighbors(current)):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
stack.append(neighbor)
return PathResult([], len(visited))
class DijkstraStrategy(PathFindingStrategy):
name = "Dijkstra"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
tie_breaker = count()
heap: list[tuple[int, int, Cell]] = [(0, next(tie_breaker), start)]
distances: dict[Cell, int] = {start: 0}
parents: dict[Cell, Cell | None] = {start: None}
visited: set[Cell] = set()
while heap:
current_distance, _, current = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in maze.get_neighbors(current):
new_distance = current_distance + neighbor.weight
if new_distance < distances.get(neighbor, 10**12):
distances[neighbor] = new_distance
parents[neighbor] = current
heapq.heappush(heap, (new_distance, next(tie_breaker), neighbor))
return PathResult([], len(visited))
class AStarStrategy(PathFindingStrategy):
name = "A*"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
tie_breaker = count()
start_heuristic = _manhattan(start, exit)
heap: list[tuple[int, int, int, Cell]] = [
(start_heuristic, start_heuristic, next(tie_breaker), start)
]
g_score: dict[Cell, int] = {start: 0}
parents: dict[Cell, Cell | None] = {start: None}
visited: set[Cell] = set()
while heap:
_, _, _, current = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in maze.get_neighbors(current):
tentative_score = g_score[current] + neighbor.weight
if tentative_score < g_score.get(neighbor, 10**12):
g_score[neighbor] = tentative_score
parents[neighbor] = current
heuristic = _manhattan(neighbor, exit)
priority = tentative_score + heuristic
heapq.heappush(
heap,
(priority, heuristic, next(tie_breaker), neighbor),
)
return PathResult([], len(visited))
def _reconstruct_path(parents: dict[Cell, Cell | None], end: Cell) -> list[Cell]:
path: list[Cell] = []
current: Cell | None = end
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path
def _manhattan(first: Cell, second: Cell) -> int:
return abs(first.x - second.x) + abs(first.y - second.y)

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Пустой 50x50: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.01</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2.02</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">3.03</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">4.04</text>
<rect x="109.0" y="124.5" width="96.0" height="177.5" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="116.5" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2.89</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="293.4" width="96.0" height="8.6" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="285.4" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.14</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="287.4" width="96.0" height="14.6" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="279.4" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.24</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4.04</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Пустой 50x50: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">576.00</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1152.00</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1728.00</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2304.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2304.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="281.9" width="96.0" height="20.1" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="273.9" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">187.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="291.8" width="96.0" height="10.2" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="283.8" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">95.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2304.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Большой 100x100: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2.16</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">4.33</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">6.49</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">8.66</text>
<rect x="109.0" y="150.2" width="96.0" height="151.8" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="142.2" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">5.30</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="230.3" width="96.0" height="71.7" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="222.3" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2.50</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">8.66</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="97.1" width="96.0" height="204.9" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="89.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">7.15</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Большой 100x100: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1200.25</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2400.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">3600.75</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">4801.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4801.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="190.7" width="96.0" height="111.3" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="182.7" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2155.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.5" width="96.0" height="247.5" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="46.5" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4791.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.1" width="96.0" height="247.9" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4800.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Средний 50x50: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.50</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.00</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.51</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2.01</text>
<rect x="109.0" y="144.4" width="96.0" height="157.6" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="136.4" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.28</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="189.6" width="96.0" height="112.4" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="181.6" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.91</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2.01</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="91.6" width="96.0" height="210.4" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="83.6" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.70</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Средний 50x50: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">287.75</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">575.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">863.25</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1151.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1151.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="133.1" width="96.0" height="168.9" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="125.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">784.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="57.9" width="96.0" height="244.1" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="49.9" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1133.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1151.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Без пути 30x30: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<rect x="109.0" y="95.3" width="96.0" height="206.7" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="87.3" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="122.9" width="96.0" height="179.1" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="114.9" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="81.6" width="96.0" height="220.4" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="73.6" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Без пути 30x30: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.25</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.75</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Маленький 10x10: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.02</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.03</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.05</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.07</text>
<rect x="109.0" y="147.0" width="96.0" height="155.0" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="139.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.04</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="202.0" width="96.0" height="100.0" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="194.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.03</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.07</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="100.2" width="96.0" height="201.8" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="92.2" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.06</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Маленький 10x10: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">9.25</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">18.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">27.75</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">37.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">37.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="141.1" width="96.0" height="160.9" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="133.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">24.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="94.2" width="96.0" height="207.8" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="86.2" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">31.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">37.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,208 @@
# Отчет по заданию: поиск выхода из лабиринта
## 1. Описание задачи и выбранных паттернов
Цель работы - реализовать расширяемую программу для загрузки лабиринта из файла,
поиска пути от старта `S` до выхода `E`, визуализации результата и сравнения
алгоритмов на лабиринтах разной сложности.
В проекте реализованы четыре паттерна GoF:
| Паттерн | Где реализован | Зачем нужен |
|---|---|---|
| Builder | `MazeBuilder`, `TextFileMazeBuilder` | Изолирует парсинг и валидацию файла от остального приложения. |
| Strategy | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy`, `DijkstraStrategy` | Позволяет менять алгоритм поиска без изменения `MazeSolver`. |
| Observer | `Observer`, `ConsoleView`, события `search_started`, `path_found`, `path_not_found` | Отделяет вычисления от отображения в консоли. |
| Command | `Command`, `MoveCommand`, `Player` | Инкапсулирует ход игрока и поддерживает отмену хода. |
Диаграмма классов:
```mermaid
classDiagram
class Cell {
+int x
+int y
+bool is_wall
+bool is_start
+bool is_exit
+int weight
+is_passable() bool
}
class Maze {
+list cells
+int width
+int height
+Cell start
+Cell exit
+get_cell(x, y) Cell
+get_neighbors(cell) list
+to_text(path, player) str
}
class MazeBuilder {
<<interface>>
+build_from_file(filename) Maze
}
class TextFileMazeBuilder {
+build_from_file(filename) Maze
}
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit) PathResult
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class DijkstraStrategy
class SearchStats {
+str strategy_name
+float time_ms
+int visited_cells
+int path_length
+list path
}
class MazeSolver {
+set_strategy(strategy)
+add_observer(observer)
+solve() SearchStats
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player_position, path) str
}
class Command {
<<interface>>
+execute() bool
+undo() bool
}
class MoveCommand
class Player
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder --> Maze : creates
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
MazeSolver --> Observer : notifies
Observer <|.. ConsoleView
Command <|.. MoveCommand
MoveCommand --> Player
Player --> Cell
```
## 2. Ключевые классы
Основные файлы проекта:
| Файл | Назначение |
|---|---|
| `maze_solver/models.py` | Классы `Cell` и `Maze`, поиск соседей, текстовая отрисовка. |
| `maze_solver/builders.py` | Интерфейс Builder и загрузка лабиринта из `.txt`. |
| `maze_solver/strategies.py` | BFS, DFS, A* и Дейкстра. |
| `maze_solver/solver.py` | Оркестратор поиска и сбор статистики. |
| `maze_solver/observers.py` | Observer и консольное представление. |
| `maze_solver/commands.py` | Command, игрок и undo перемещения. |
| `main.py` | CLI для запуска поиска и ручного режима. |
| `scripts/run_experiments.py` | Замеры и построение SVG-графиков. |
Пример запуска:
```bash
python3 main.py --maze data/mazes/small.txt --strategy astar --render
```
## 3. Результаты экспериментов
Для каждого лабиринта и каждой стратегии выполнено 10 запусков. В таблице указаны
средние значения. Длина пути считается в клетках, включая старт и выход.
| Лабиринт | Стратегия | Время, мс | Посещено клеток | Длина пути | Путь найден |
|---|---:|---:|---:|---:|---|
| Маленький 10x10 | BFS | 0.0423 | 37.0 | 20.0 | да |
| Маленький 10x10 | DFS | 0.0273 | 24.0 | 22.0 | да |
| Маленький 10x10 | A* | 0.0677 | 31.0 | 20.0 | да |
| Маленький 10x10 | Dijkstra | 0.0551 | 37.0 | 20.0 | да |
| Средний 50x50 | BFS | 1.2769 | 1151.0 | 709.0 | да |
| Средний 50x50 | DFS | 0.9106 | 784.0 | 709.0 | да |
| Средний 50x50 | A* | 2.0089 | 1133.0 | 709.0 | да |
| Средний 50x50 | Dijkstra | 1.7041 | 1151.0 | 709.0 | да |
| Большой 100x100 | BFS | 5.2983 | 4801.0 | 1685.0 | да |
| Большой 100x100 | DFS | 2.5044 | 2155.0 | 1685.0 | да |
| Большой 100x100 | A* | 8.6574 | 4791.0 | 1685.0 | да |
| Большой 100x100 | Dijkstra | 7.1532 | 4800.0 | 1685.0 | да |
| Пустой 50x50 | BFS | 2.8927 | 2304.0 | 95.0 | да |
| Пустой 50x50 | DFS | 0.1404 | 187.0 | 95.0 | да |
| Пустой 50x50 | A* | 0.2374 | 95.0 | 95.0 | да |
| Пустой 50x50 | Dijkstra | 4.0408 | 2304.0 | 95.0 | да |
| Без пути 30x30 | BFS | 0.0015 | 1.0 | 0.0 | нет |
| Без пути 30x30 | DFS | 0.0013 | 1.0 | 0.0 | нет |
| Без пути 30x30 | A* | 0.0016 | 1.0 | 0.0 | нет |
| Без пути 30x30 | Dijkstra | 0.0018 | 1.0 | 0.0 | нет |
CSV с результатами сохранен в `reports/results.csv`.
Графики:
| Лабиринт | Время | Посещенные клетки |
|---|---|---|
| Маленький | ![](charts/small_time.svg) | ![](charts/small_visited.svg) |
| Средний | ![](charts/medium_time.svg) | ![](charts/medium_visited.svg) |
| Большой | ![](charts/large_time.svg) | ![](charts/large_visited.svg) |
| Пустой | ![](charts/empty_time.svg) | ![](charts/empty_visited.svg) |
| Без пути | ![](charts/no_exit_time.svg) | ![](charts/no_exit_visited.svg) |
## 4. Анализ эффективности
BFS гарантирует кратчайший путь в невзвешенном лабиринте. Это видно на маленьком
лабиринте: BFS, A* и Дейкстра нашли путь длиной 20, а DFS нашел более длинный путь
длиной 22. Недостаток BFS - широкий фронт поиска, из-за чего в пустом лабиринте он
посетил все 2304 доступные клетки.
DFS не гарантирует кратчайший путь, но часто работает быстро, потому что уходит
глубоко по одному направлению. На маленьком лабиринте это дало путь хуже оптимального.
На сгенерированных идеальных лабиринтах путь между двумя клетками единственный, поэтому
DFS, BFS, A* и Дейкстра получили одинаковую длину пути.
A* использует манхэттенскую эвристику. На пустом лабиринте он посетил только 95 клеток,
то есть фактически прошел по оптимальному маршруту. В запутанных идеальных лабиринтах
эвристика помогает слабее: прямое направление к выходу часто упирается в стены, поэтому
A* посещает почти столько же клеток, сколько BFS, а из-за приоритетной очереди тратит
больше времени.
Дейкстра в невзвешенном лабиринте по результату близок к BFS, но работает медленнее
из-за приоритетной очереди. Его преимущество проявляется при взвешенных клетках.
В проекте Builder уже поддерживает символы `2`, `3` и `~` как клетки с повышенной
стоимостью прохода, поэтому Дейкстру и A* можно использовать для дополнительного
сравнения на взвешенных картах.
Лабиринт "Без пути" проверяет корректную обработку отсутствия решения: стратегии
возвращают пустой путь, а `MazeSolver` фиксирует длину 0.
## 5. Выводы
ООП позволило разделить предметную модель, загрузку данных, алгоритмы и интерфейс.
Паттерн Builder делает формат входного файла заменяемым: можно добавить JSON-builder,
не меняя `Maze` и стратегии. Strategy позволяет добавлять новые алгоритмы без правок
в `MazeSolver`. Observer отделяет вычисления от вывода, а Command показывает, как
инкапсулировать пользовательские действия и поддержать undo.
Без этих паттернов код быстро стал бы монолитным: парсинг файла, поиск, статистика,
печать и ручное управление оказались бы в одном месте. Тогда добавление нового формата,
алгоритма или режима отображения требовало бы менять уже работающую логику.

View File

@ -0,0 +1,21 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути,путь_найден,запусков
Маленький 10x10,BFS,0.0423,37.0,20.0,да,10
Маленький 10x10,DFS,0.0273,24.0,22.0,да,10
Маленький 10x10,A*,0.0677,31.0,20.0,да,10
Маленький 10x10,Dijkstra,0.0551,37.0,20.0,да,10
Средний 50x50,BFS,1.2769,1151.0,709.0,да,10
Средний 50x50,DFS,0.9106,784.0,709.0,да,10
Средний 50x50,A*,2.0089,1133.0,709.0,да,10
Средний 50x50,Dijkstra,1.7041,1151.0,709.0,да,10
Большой 100x100,BFS,5.2983,4801.0,1685.0,да,10
Большой 100x100,DFS,2.5044,2155.0,1685.0,да,10
Большой 100x100,A*,8.6574,4791.0,1685.0,да,10
Большой 100x100,Dijkstra,7.1532,4800.0,1685.0,да,10
Пустой 50x50,BFS,2.8927,2304.0,95.0,да,10
Пустой 50x50,DFS,0.1404,187.0,95.0,да,10
Пустой 50x50,A*,0.2374,95.0,95.0,да,10
Пустой 50x50,Dijkstra,4.0408,2304.0,95.0,да,10
Без пути 30x30,BFS,0.0015,1.0,0.0,нет,10
Без пути 30x30,DFS,0.0013,1.0,0.0,нет,10
Без пути 30x30,A*,0.0016,1.0,0.0,нет,10
Без пути 30x30,Dijkstra,0.0018,1.0,0.0,нет,10
1 лабиринт стратегия время_мс посещено_клеток длина_пути путь_найден запусков
2 Маленький 10x10 BFS 0.0423 37.0 20.0 да 10
3 Маленький 10x10 DFS 0.0273 24.0 22.0 да 10
4 Маленький 10x10 A* 0.0677 31.0 20.0 да 10
5 Маленький 10x10 Dijkstra 0.0551 37.0 20.0 да 10
6 Средний 50x50 BFS 1.2769 1151.0 709.0 да 10
7 Средний 50x50 DFS 0.9106 784.0 709.0 да 10
8 Средний 50x50 A* 2.0089 1133.0 709.0 да 10
9 Средний 50x50 Dijkstra 1.7041 1151.0 709.0 да 10
10 Большой 100x100 BFS 5.2983 4801.0 1685.0 да 10
11 Большой 100x100 DFS 2.5044 2155.0 1685.0 да 10
12 Большой 100x100 A* 8.6574 4791.0 1685.0 да 10
13 Большой 100x100 Dijkstra 7.1532 4800.0 1685.0 да 10
14 Пустой 50x50 BFS 2.8927 2304.0 95.0 да 10
15 Пустой 50x50 DFS 0.1404 187.0 95.0 да 10
16 Пустой 50x50 A* 0.2374 95.0 95.0 да 10
17 Пустой 50x50 Dijkstra 4.0408 2304.0 95.0 да 10
18 Без пути 30x30 BFS 0.0015 1.0 0.0 нет 10
19 Без пути 30x30 DFS 0.0013 1.0 0.0 нет 10
20 Без пути 30x30 A* 0.0016 1.0 0.0 нет 10
21 Без пути 30x30 Dijkstra 0.0018 1.0 0.0 нет 10

View File

@ -0,0 +1 @@
"""Helper scripts for maze generation and experiments."""

View File

@ -0,0 +1,126 @@
from __future__ import annotations
import random
from collections import deque
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MAZE_DIR = ROOT / "data" / "mazes"
def main() -> None:
generate_all()
def generate_all() -> None:
MAZE_DIR.mkdir(parents=True, exist_ok=True)
_write("small.txt", _small_maze())
_write("medium.txt", _perfect_maze(50, 50, seed=2026))
_write("large.txt", _perfect_maze(100, 100, seed=2027))
_write("empty.txt", _empty_maze(50, 50))
_write("no_exit.txt", _no_path_maze(30, 30))
def _write(filename: str, rows: list[str]) -> None:
(MAZE_DIR / filename).write_text("\n".join(rows) + "\n", encoding="utf-8")
def _small_maze() -> list[str]:
return [
"##########",
"#S #E#",
"# #### # #",
"# # # #",
"# # #### #",
"# # #",
"# ###### #",
"# #",
"######## #",
"##########",
]
def _empty_maze(width: int, height: int) -> list[str]:
grid = _bordered_grid(width, height, fill=" ")
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return _to_rows(grid)
def _no_path_maze(width: int, height: int) -> list[str]:
grid = [["#" for _ in range(width)] for _ in range(height)]
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return _to_rows(grid)
def _perfect_maze(width: int, height: int, seed: int) -> list[str]:
if width < 5 or height < 5:
raise ValueError("Maze must be at least 5x5")
randomizer = random.Random(seed)
grid = [["#" for _ in range(width)] for _ in range(height)]
start = (1, 1)
stack = [start]
grid[start[1]][start[0]] = " "
while stack:
x, y = stack[-1]
candidates = []
for dx, dy in ((0, -2), (2, 0), (0, 2), (-2, 0)):
nx, ny = x + dx, y + dy
if 1 <= nx < width - 1 and 1 <= ny < height - 1 and grid[ny][nx] == "#":
candidates.append((nx, ny, dx, dy))
if not candidates:
stack.pop()
continue
nx, ny, dx, dy = randomizer.choice(candidates)
grid[y + dy // 2][x + dx // 2] = " "
grid[ny][nx] = " "
stack.append((nx, ny))
exit_x, exit_y = _farthest_open_cell(grid, start)
grid[start[1]][start[0]] = "S"
grid[exit_y][exit_x] = "E"
return _to_rows(grid)
def _farthest_open_cell(grid: list[list[str]], start: tuple[int, int]) -> tuple[int, int]:
queue = deque([start])
distances = {start: 0}
farthest = start
while queue:
x, y = queue.popleft()
if distances[(x, y)] > distances[farthest]:
farthest = (x, y)
for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)):
nx, ny = x + dx, y + dy
if (nx, ny) not in distances and grid[ny][nx] != "#":
distances[(nx, ny)] = distances[(x, y)] + 1
queue.append((nx, ny))
return farthest
def _bordered_grid(width: int, height: int, fill: str) -> list[list[str]]:
grid = [[fill for _ in range(width)] for _ in range(height)]
for x in range(width):
grid[0][x] = "#"
grid[height - 1][x] = "#"
for y in range(height):
grid[y][0] = "#"
grid[y][width - 1] = "#"
return grid
def _to_rows(grid: list[list[str]]) -> list[str]:
return ["".join(row) for row in grid]
if __name__ == "__main__":
main()

View File

@ -0,0 +1,194 @@
from __future__ import annotations
import csv
import statistics
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from maze_solver import ( # noqa: E402
AStarStrategy,
BFSStrategy,
DFSStrategy,
DijkstraStrategy,
MazeSolver,
TextFileMazeBuilder,
)
from scripts.generate_mazes import generate_all # noqa: E402
MAZES = [
("small", "Маленький 10x10", ROOT / "data" / "mazes" / "small.txt"),
("medium", "Средний 50x50", ROOT / "data" / "mazes" / "medium.txt"),
("large", "Большой 100x100", ROOT / "data" / "mazes" / "large.txt"),
("empty", "Пустой 50x50", ROOT / "data" / "mazes" / "empty.txt"),
("no_exit", "Без пути 30x30", ROOT / "data" / "mazes" / "no_exit.txt"),
]
STRATEGIES = [BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy]
REPORTS_DIR = ROOT / "reports"
CHARTS_DIR = REPORTS_DIR / "charts"
def main(runs: int = 10) -> None:
generate_all()
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
CHARTS_DIR.mkdir(parents=True, exist_ok=True)
rows = _run_experiments(runs)
_write_csv(rows)
_write_charts(rows)
print(f"Wrote {REPORTS_DIR / 'results.csv'}")
print(f"Wrote SVG charts to {CHARTS_DIR}")
def _run_experiments(runs: int) -> list[dict[str, object]]:
builder = TextFileMazeBuilder()
rows: list[dict[str, object]] = []
for maze_key, maze_name, maze_path in MAZES:
maze = builder.build_from_file(maze_path)
for strategy_type in STRATEGIES:
measurements = []
for _ in range(runs):
stats = MazeSolver(maze, strategy_type()).solve()
measurements.append(stats)
avg_time = statistics.fmean(item.time_ms for item in measurements)
avg_visited = statistics.fmean(item.visited_cells for item in measurements)
avg_path = statistics.fmean(item.path_length for item in measurements)
found = measurements[-1].path_length > 0
rows.append(
{
"key": maze_key,
"лабиринт": maze_name,
"стратегия": measurements[-1].strategy_name,
"время_мс": f"{avg_time:.4f}",
"посещено_клеток": f"{avg_visited:.1f}",
"длина_пути": f"{avg_path:.1f}",
"путь_найден": "да" if found else "нет",
"запусков": runs,
}
)
return rows
def _write_csv(rows: list[dict[str, object]]) -> None:
csv_path = REPORTS_DIR / "results.csv"
headers = [
"лабиринт",
"стратегия",
"время_мс",
"посещено_клеток",
"длина_пути",
"путь_найден",
"запусков",
]
with csv_path.open("w", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=headers)
writer.writeheader()
for row in rows:
writer.writerow({header: row[header] for header in headers})
def _write_charts(rows: list[dict[str, object]]) -> None:
grouped: dict[str, list[dict[str, object]]] = defaultdict(list)
for row in rows:
grouped[str(row["key"])].append(row)
for maze_key, group in grouped.items():
title = str(group[0]["лабиринт"])
_write_bar_chart(
CHARTS_DIR / f"{maze_key}_time.svg",
title=f"{title}: среднее время, мс",
rows=group,
metric="время_мс",
color="#2f6fbb",
)
_write_bar_chart(
CHARTS_DIR / f"{maze_key}_visited.svg",
title=f"{title}: посещенные клетки",
rows=group,
metric="посещено_клеток",
color="#2f8f5b",
)
def _write_bar_chart(
path: Path,
title: str,
rows: list[dict[str, object]],
metric: str,
color: str,
) -> None:
width = 780
height = 360
left = 72
right = 28
top = 54
bottom = 58
chart_width = width - left - right
chart_height = height - top - bottom
values = [float(row[metric]) for row in rows]
max_value = max(values) if values else 1.0
max_value = max_value or 1.0
bar_area = chart_width / len(rows)
bar_width = min(96, bar_area * 0.58)
parts = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
'<rect width="100%" height="100%" fill="#ffffff"/>',
f'<text x="{left}" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">{_escape(title)}</text>',
f'<line x1="{left}" y1="{height - bottom}" x2="{width - right}" y2="{height - bottom}" stroke="#9aa5b1"/>',
f'<line x1="{left}" y1="{top}" x2="{left}" y2="{height - bottom}" stroke="#9aa5b1"/>',
]
for tick in range(5):
ratio = tick / 4
y = height - bottom - ratio * chart_height
value = max_value * ratio
parts.append(
f'<line x1="{left - 5}" y1="{y:.1f}" x2="{width - right}" y2="{y:.1f}" stroke="#edf0f2"/>'
)
parts.append(
f'<text x="{left - 10}" y="{y + 4:.1f}" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">{value:.2f}</text>'
)
for index, row in enumerate(rows):
value = float(row[metric])
ratio = value / max_value
bar_height = ratio * chart_height
x = left + index * bar_area + (bar_area - bar_width) / 2
y = height - bottom - bar_height
label = str(row["стратегия"])
parts.append(
f'<rect x="{x:.1f}" y="{y:.1f}" width="{bar_width:.1f}" height="{bar_height:.1f}" fill="{color}" rx="3"/>'
)
parts.append(
f'<text x="{x + bar_width / 2:.1f}" y="{y - 8:.1f}" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">{value:.2f}</text>'
)
parts.append(
f'<text x="{x + bar_width / 2:.1f}" y="{height - bottom + 24}" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">{_escape(label)}</text>'
)
parts.append("</svg>")
path.write_text("\n".join(parts), encoding="utf-8")
def _escape(value: str) -> str:
return (
value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1 @@
"""Unit tests for the maze solver project."""

View File

@ -0,0 +1,64 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from maze_solver import (
AStarStrategy,
BFSStrategy,
Direction,
MazeSolver,
MoveCommand,
Player,
TextFileMazeBuilder,
)
SIMPLE_MAZE = """\
#######
#S E#
# ### #
# #
#######"""
class MazeSolverTest(unittest.TestCase):
def build_maze(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "maze.txt"
path.write_text(SIMPLE_MAZE, encoding="utf-8")
return TextFileMazeBuilder().build_from_file(path)
def test_builder_reads_start_exit_and_neighbors(self) -> None:
maze = self.build_maze()
self.assertEqual((maze.start.x, maze.start.y), (1, 1))
self.assertEqual((maze.exit.x, maze.exit.y), (5, 1))
self.assertTrue(maze.get_cell(2, 1).is_passable())
self.assertFalse(maze.get_cell(0, 0).is_passable())
def test_bfs_and_astar_find_shortest_path(self) -> None:
maze = self.build_maze()
bfs_stats = MazeSolver(maze, BFSStrategy()).solve()
astar_stats = MazeSolver(maze, AStarStrategy()).solve()
self.assertEqual(bfs_stats.path_length, 5)
self.assertEqual(astar_stats.path_length, 5)
self.assertEqual(bfs_stats.path[0], maze.start)
self.assertEqual(bfs_stats.path[-1], maze.exit)
def test_move_command_can_execute_and_undo(self) -> None:
maze = self.build_maze()
player = Player.at_start(maze)
command = MoveCommand(player, Direction.RIGHT)
self.assertTrue(command.execute())
self.assertEqual((player.current_cell.x, player.current_cell.y), (2, 1))
self.assertTrue(command.undo())
self.assertEqual(player.current_cell, maze.start)
if __name__ == "__main__":
unittest.main()