Merge pull request '[2] zadanie2' (#231) from shahovaa/2026-rff_mp:zadanie2 into develop

Reviewed-on: UNN/2026-rff_mp#231
This commit is contained in:
AlexanderVah 2026-05-30 11:42:42 +00:00
commit d9a501b086
41 changed files with 5111 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
Internal Server Error - DRE.lab repo

Internal Server Error

Gitea Version: 1.22.0