Compare commits

..

No commits in common. "ezhovnd-patch-2" and "develop" have entirely different histories.

5 changed files with 0 additions and 1129 deletions

View File

@ -1,825 +0,0 @@
"""
maze_all.py Полный проект лабиринта со всеми паттернами:
Builder, Strategy, Observer, Command.
Содержит все модули: model, builder, strategy, solver, generator, experiment, main.
"""
from __future__ import annotations
import abc
import csv
import heapq
import json
import os
import random
import sys
import time
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
# ============================================================================
# model.py — Модель лабиринта (классы Cell и Maze)
# ============================================================================
class Cell:
"""Одна клетка лабиринта."""
WALL = '#'
PASS = ' '
START = 'S'
EXIT = 'E'
SWAMP = 'W' # болото — вес 3
SAND = 'N' # песок — вес 2
ROAD = 'R' # асфальт — вес 1 (то же что PASS)
WEIGHTS = {WALL: 0, PASS: 1, ROAD: 1, START: 1, EXIT: 1, SAND: 2, SWAMP: 3}
def __init__(self, x: int, y: int, symbol: str = PASS):
self.x = x
self.y = y
self.symbol = symbol
@property
def is_wall(self) -> bool:
return self.symbol == self.WALL
@property
def is_start(self) -> bool:
return self.symbol == self.START
@property
def is_exit(self) -> bool:
return self.symbol == self.EXIT
@property
def weight(self) -> int:
return self.WEIGHTS.get(self.symbol, 1)
def is_passable(self) -> bool:
return not self.is_wall
def __repr__(self):
return f"Cell({self.x},{self.y},'{self.symbol}')"
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
class Maze:
"""Двумерный массив клеток + метаданные."""
def __init__(self, width: int, height: int, cells: list[list[Cell]],
start: Optional[Cell] = None, exit_: Optional[Cell] = None):
self.width = width
self.height = height
self._cells = cells # cells[y][x]
self.start = start
self.exit = exit_
def get_cell(self, x: int, y: int) -> Cell:
return self._cells[y][x]
def get_neighbors(self, cell: Cell) -> list[Cell]:
"""Четыре соседа (если в пределах карты и не стена)."""
neighbors = []
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
nx, ny = cell.x + dx, cell.y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
nb = self._cells[ny][nx]
if nb.is_passable():
neighbors.append(nb)
return neighbors
def render(self, path: set = None, visited: set = None,
player_pos: Optional[Cell] = None) -> str:
"""Возвращает строковое представление для консоли."""
path = path or set()
visited = visited or set()
lines = []
for y in range(self.height):
row = []
for x in range(self.width):
cell = self._cells[y][x]
if player_pos and cell == player_pos:
row.append('@')
elif cell in path and not cell.is_start and not cell.is_exit:
row.append('·')
elif cell in visited and not cell.is_start and not cell.is_exit:
row.append('')
else:
row.append(cell.symbol if cell.symbol != ' ' else '.')
lines.append(''.join(row))
return '\n'.join(lines)
# ============================================================================
# builder.py — Паттерн Builder
# ============================================================================
class MazeBuilder(abc.ABC):
"""Интерфейс строителя лабиринта."""
@abc.abstractmethod
def build_from_file(self, filename: str) -> Maze:
...
@abc.abstractmethod
def build_empty(self, width: int, height: int) -> Maze:
"""Создать лабиринт без стен."""
...
class TextFileMazeBuilder(MazeBuilder):
"""
Строитель для текстовых файлов:
# — стена
' ' или '.' проход (вес 1)
S старт
E выход
W болото (вес 3)
N песок (вес 2)
R асфальт (вес 1)
"""
def build_from_file(self, filename: str) -> Maze:
with open(filename, encoding='utf-8') as f:
lines = f.read().splitlines()
return self._parse_lines(lines)
def build_from_string(self, text: str) -> Maze:
return self._parse_lines(text.splitlines())
def _parse_lines(self, lines: list[str]) -> Maze:
if not lines:
raise ValueError("Файл лабиринта пуст")
height = len(lines)
width = max(len(line) for line in lines)
cells: list[list[Cell]] = []
start = exit_ = None
for y, line in enumerate(lines):
row = []
for x in range(width):
ch = line[x] if x < len(line) else ' '
if ch == '.':
ch = ' ' # нормализация
cell = Cell(x, y, ch)
if cell.is_start:
start = cell
if cell.is_exit:
exit_ = cell
row.append(cell)
cells.append(row)
if start is None:
raise ValueError("В лабиринте не найдена клетка 'S' (старт)")
if exit_ is None:
raise ValueError("В лабиринте не найдена клетка 'E' (выход)")
return Maze(width, height, cells, start, exit_)
def build_empty(self, width: int, height: int) -> Maze:
"""Лабиринт без стен: рамка из стен, внутри проходы."""
cells = []
start = exit_ = None
for y in range(height):
row = []
for x in range(width):
if x == 0 or x == width - 1 or y == 0 or y == height - 1:
ch = Cell.WALL
else:
ch = Cell.PASS
cell = Cell(x, y, ch)
row.append(cell)
cells.append(row)
# Старт и выход
cells[1][1].symbol = Cell.START
start = cells[1][1]
cells[height - 2][width - 2].symbol = Cell.EXIT
exit_ = cells[height - 2][width - 2]
return Maze(width, height, cells, start, exit_)
class JsonMazeBuilder(MazeBuilder):
"""
Альтернативный строитель JSON-формат.
Демонстрирует расширяемость паттерна Builder без изменения клиента.
Формат:
{
"width": 5, "height": 5,
"cells": ["#####", "#S E#", "#####"]
}
"""
def build_from_file(self, filename: str) -> Maze:
with open(filename, encoding='utf-8') as f:
data = json.load(f)
lines = data['cells']
text_builder = TextFileMazeBuilder()
return text_builder._parse_lines(lines)
def build_empty(self, width: int, height: int) -> Maze:
return TextFileMazeBuilder().build_empty(width, height)
# ============================================================================
# strategy.py — Паттерн Strategy (алгоритмы поиска пути)
# ============================================================================
class PathFindingStrategy(abc.ABC):
"""Интерфейс стратегии поиска пути."""
@abc.abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_: Cell
) -> tuple[list[Cell], int]:
"""
Возвращает (path, visited_count).
path список клеток от start до exit_ включительно,
или [] если пути нет.
visited_count количество клеток, посещённых во время поиска.
"""
...
def name(self) -> str:
return self.__class__.__name__
def _reconstruct(parent: dict, end: Cell) -> list[Cell]:
"""Восстановить путь по словарю parent."""
path, node = [], end
while node is not None:
path.append(node)
node = parent.get(node)
path.reverse()
return path
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину — гарантирует кратчайший путь (по количеству шагов)."""
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
queue = deque([start])
parent = {start: None}
visited = 0
while queue:
cell = queue.popleft()
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
for nb in maze.get_neighbors(cell):
if nb not in parent:
parent[nb] = cell
queue.append(nb)
return [], visited
def name(self): return "BFS"
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину — быстрый, но путь не оптимален."""
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
stack = [start]
parent = {start: None}
visited = 0
while stack:
cell = stack.pop()
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
for nb in maze.get_neighbors(cell):
if nb not in parent:
parent[nb] = cell
stack.append(nb)
return [], visited
def name(self): return "DFS"
class AStarStrategy(PathFindingStrategy):
"""
A* с манхэттенской эвристикой оптимальный и быстрый.
Учитывает веса клеток (болото, песок и т.д.).
"""
def _h(self, cell: Cell, goal: Cell) -> int:
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
# (f, g, cell)
heap = [(self._h(start, exit_), 0, id(start), start)]
g_score = {start: 0}
parent = {start: None}
visited = 0
while heap:
f, g, _, cell = heapq.heappop(heap)
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
if g > g_score.get(cell, float('inf')):
continue # устаревшая запись
for nb in maze.get_neighbors(cell):
new_g = g + nb.weight
if new_g < g_score.get(nb, float('inf')):
g_score[nb] = new_g
parent[nb] = cell
f_val = new_g + self._h(nb, exit_)
heapq.heappush(heap, (f_val, new_g, id(nb), nb))
return [], visited
def name(self): return "A*"
class DijkstraStrategy(PathFindingStrategy):
"""
Алгоритм Дейкстры оптимален для взвешенных клеток.
На равновесных лабиринтах совпадает с BFS.
"""
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
heap = [(0, id(start), start)]
dist = {start: 0}
parent = {start: None}
visited = 0
while heap:
d, _, cell = heapq.heappop(heap)
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
if d > dist.get(cell, float('inf')):
continue
for nb in maze.get_neighbors(cell):
new_d = d + nb.weight
if new_d < dist.get(nb, float('inf')):
dist[nb] = new_d
parent[nb] = cell
heapq.heappush(heap, (new_d, id(nb), nb))
return [], visited
def name(self): return "Dijkstra"
# Реестр всех стратегий (используется в экспериментальной части)
ALL_STRATEGIES = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
# ============================================================================
# solver.py — Паттерны Observer и Command, класс MazeSolver
# ============================================================================
# ─────────────────────────────────────────────
# Observer
# ─────────────────────────────────────────────
class Observer(abc.ABC):
@abc.abstractmethod
def update(self, event: str, payload: dict) -> None: ...
class Subject:
"""Миксин: любой класс, у которого есть наблюдатели."""
def __init__(self):
self._observers: list[Observer] = []
def add_observer(self, obs: Observer):
self._observers.append(obs)
def remove_observer(self, obs: Observer):
self._observers.remove(obs)
def notify(self, event: str, payload: dict = None):
for obs in self._observers:
obs.update(event, payload or {})
class ConsoleView(Observer):
"""
Консольный наблюдатель: печатает события и рендерит лабиринт.
"""
def update(self, event: str, payload: dict) -> None:
if event == 'maze_loaded':
maze: Maze = payload['maze']
print(f"\n[ConsoleView] Лабиринт загружен ({maze.width}×{maze.height})")
print(maze.render())
elif event == 'search_started':
print(f"\n[ConsoleView] Начат поиск алгоритмом: {payload['strategy']}")
elif event == 'path_found':
maze: Maze = payload['maze']
path: list[Cell] = payload['path']
stats = payload['stats']
path_set = set(path)
print(f"\n[ConsoleView] Путь найден! Длина: {stats.path_length}, "
f"посещено: {stats.visited_cells}, "
f"время: {stats.elapsed_ms:.2f} мс")
print(maze.render(path=path_set))
elif event == 'no_path':
print("\n[ConsoleView] Путь не найден.")
elif event == 'player_moved':
maze: Maze = payload['maze']
player = payload['player']
path_set = set(payload.get('path', []))
print(f"\n[ConsoleView] Игрок переместился → ({player.position.x}, {player.position.y})")
print(maze.render(path=path_set, player_pos=player.position))
elif event == 'undo':
print(f"[ConsoleView] Отмена хода. Игрок → ({payload['position'].x}, {payload['position'].y})")
class FileLogObserver(Observer):
"""Наблюдатель-логгер: записывает события в файл."""
def __init__(self, filepath: str):
self._path = filepath
def update(self, event: str, payload: dict) -> None:
with open(self._path, 'a', encoding='utf-8') as f:
f.write(f"[{event}] {payload.get('strategy','')}"
f" visited={payload.get('stats', None) and payload['stats'].visited_cells}\n")
# ─────────────────────────────────────────────
# SearchStats
# ─────────────────────────────────────────────
@dataclass
class SearchStats:
strategy_name: str
elapsed_ms: float
visited_cells: int
path_length: int
# ─────────────────────────────────────────────
# MazeSolver — оркестратор
# ─────────────────────────────────────────────
class MazeSolver(Subject):
"""
Принимает лабиринт и стратегию, выполняет поиск, собирает статистику,
уведомляет наблюдателей.
"""
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
super().__init__()
self._maze = maze
self._strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self._strategy = strategy
def load_maze(self, maze: Maze):
self._maze = maze
self.notify('maze_loaded', {'maze': maze})
def solve(self) -> tuple[SearchStats, list[Cell]]:
self.notify('search_started', {'strategy': self._strategy.name()})
t0 = time.perf_counter()
path, visited = self._strategy.find_path(
self._maze, self._maze.start, self._maze.exit)
elapsed_ms = (time.perf_counter() - t0) * 1000
stats = SearchStats(
strategy_name=self._strategy.name(),
elapsed_ms=elapsed_ms,
visited_cells=visited,
path_length=len(path),
)
if path:
self.notify('path_found', {'maze': self._maze, 'path': path, 'stats': stats})
else:
self.notify('no_path', {'maze': self._maze, 'stats': stats})
return stats, path
# ─────────────────────────────────────────────
# Command
# ─────────────────────────────────────────────
class Command(abc.ABC):
@abc.abstractmethod
def execute(self) -> None: ...
@abc.abstractmethod
def undo(self) -> None: ...
class Player:
"""Хранит текущую позицию игрока."""
def __init__(self, position: Cell):
self.position = position
class MoveCommand(Command):
"""
Перемещение игрока в клетку target.
Undo возвращает на previous_position.
"""
def __init__(self, player: Player, target: Cell,
solver: 'MazeSolver', path: list[Cell] = None):
self._player = player
self._target = target
self._previous = player.position
self._solver = solver
self._path = path or []
def execute(self):
self._player.position = self._target
self._solver.notify('player_moved', {
'maze': self._solver._maze,
'player': self._player,
'path': self._path,
})
def undo(self):
self._player.position = self._previous
self._solver.notify('undo', {'position': self._previous})
class CommandHistory:
"""Стек выполненных команд для поддержки undo."""
def __init__(self):
self._stack: list[Command] = []
def execute(self, cmd: Command):
cmd.execute()
self._stack.append(cmd)
def undo(self):
if self._stack:
self._stack.pop().undo()
else:
print("[CommandHistory] Нечего отменять.")
# ============================================================================
# generator.py — Генерация тестовых лабиринтов
# ============================================================================
def generate_maze(width: int, height: int, seed: int = 42,
weighted: bool = False) -> Maze:
"""
Генерирует лабиринт алгоритмом DFS (Recursive Backtracker).
width/height размеры (лучше нечётные для красивого рисунка).
"""
rng = random.Random(seed)
# Начальный массив — всё стены
cells = [[Cell(x, y, Cell.WALL) for x in range(width)]
for y in range(height)]
def carve(x, y):
cells[y][x].symbol = Cell.PASS
directions = [(0, 2), (0, -2), (2, 0), (-2, 0)]
rng.shuffle(directions)
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < width and 0 <= ny < height:
if cells[ny][nx].symbol == Cell.WALL:
# Убрать стену между (x,y) и (nx,ny)
cells[y + dy // 2][x + dx // 2].symbol = Cell.PASS
carve(nx, ny)
sys.setrecursionlimit(width * height + 100)
carve(1, 1)
# Старт и выход
cells[1][1].symbol = Cell.START
cells[height - 2][width - 2].symbol = Cell.EXIT
start = cells[1][1]
exit_ = cells[height - 2][width - 2]
# Взвешенные клетки (болото, песок)
if weighted:
for y in range(height):
for x in range(width):
if cells[y][x].symbol == Cell.PASS:
r = rng.random()
if r < 0.05:
cells[y][x].symbol = Cell.SWAMP
elif r < 0.15:
cells[y][x].symbol = Cell.SAND
return Maze(width, height, cells, start, exit_)
def generate_no_exit(width: int, height: int, seed: int = 42) -> Maze:
"""Лабиринт без выхода — выход заблокирован стенами."""
maze = generate_maze(width, height, seed)
ex = maze.exit
# Окружить выход стенами
for dx, dy in ((0,-1),(0,1),(-1,0),(1,0)):
nx, ny = ex.x + dx, ex.y + dy
if 0 <= nx < maze.width and 0 <= ny < maze.height:
maze._cells[ny][nx].symbol = Cell.WALL
return maze
def save_maze(maze: Maze, filepath: str):
"""Сохранить лабиринт в текстовый файл."""
lines = []
for y in range(maze.height):
row = ''.join(maze._cells[y][x].symbol for x in range(maze.width))
lines.append(row)
with open(filepath, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
def make_empty_maze(width: int, height: int) -> Maze:
"""Лабиринт-поле без внутренних стен (только рамка)."""
return TextFileMazeBuilder().build_empty(width, height)
# ============================================================================
# experiment.py — Экспериментальная часть
# ============================================================================
REPEATS = 7
OUT_CSV = os.path.join(os.path.dirname(__file__), 'docs/data/maze_results.csv')
MAZE_DIR = os.path.join(os.path.dirname(__file__), 'docs/data/mazes')
def run_bench(maze, strategy, repeats=REPEATS):
times, visited_list, path_lens = [], [], []
for _ in range(repeats):
solver = MazeSolver(maze, strategy)
stats, path = solver.solve()
times.append(stats.elapsed_ms)
visited_list.append(stats.visited_cells)
path_lens.append(stats.path_length)
return {
'time_ms_mean': statistics.mean(times),
'time_ms_std': statistics.stdev(times) if len(times) > 1 else 0,
'visited_mean': statistics.mean(visited_list),
'path_length': path_lens[0], # детерминировано
}
def run_experiment():
os.makedirs(MAZE_DIR, exist_ok=True)
# Определяем лабиринты
configs = [
('small_10x10', generate_maze(11, 11, seed=1)),
('medium_50x50', generate_maze(51, 51, seed=2)),
('large_100x100', generate_maze(101, 101, seed=3)),
('empty_50x50', make_empty_maze(52, 52)),
('no_exit_20x20', generate_no_exit(21, 21, seed=4)),
('weighted_50x50', generate_maze(51, 51, seed=5, weighted=True)),
]
# Сохраним лабиринты в файлы
for name, maze in configs:
save_maze(maze, os.path.join(MAZE_DIR, f'{name}.txt'))
rows = [['Лабиринт', 'Алгоритм', 'Время_мс', 'Посещено_клеток', 'Длина_пути',
'Стд_время_мс']]
for maze_name, maze in configs:
print(f'\n=== {maze_name} ({maze.width}×{maze.height}) ===')
for strategy in ALL_STRATEGIES:
print(f' [{strategy.name()}]', end=' ', flush=True)
res = run_bench(maze, strategy)
print(f"time={res['time_ms_mean']:.3f}ms visited={res['visited_mean']:.0f} "
f"path={res['path_length']}")
rows.append([
maze_name,
strategy.name(),
round(res['time_ms_mean'], 4),
round(res['visited_mean'], 1),
res['path_length'],
round(res['time_ms_std'], 4),
])
with open(OUT_CSV, 'w', newline='', encoding='utf-8') as f:
csv.writer(f).writerows(rows)
print(f'\nРезультаты сохранены: {OUT_CSV}')
return rows
# ============================================================================
# main.py — Точка входа
# ============================================================================
STRATEGIES = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy(),
'4': DijkstraStrategy(),
}
STRATEGY_LABELS = {
'1': 'BFS (поиск в ширину)',
'2': 'DFS (поиск в глубину)',
'3': 'A* (эвристический)',
'4': 'Dijkstra (взвешенный)',
}
def choose_strategy() -> PathFindingStrategy:
print("\nВыберите алгоритм поиска:")
for k, label in STRATEGY_LABELS.items():
print(f" {k}. {label}")
choice = input("Ваш выбор [1-4, Enter=3]: ").strip() or '3'
return STRATEGIES.get(choice, AStarStrategy())
def interactive_walk(solver: MazeSolver, path: list, maze):
"""Пошаговое прохождение найденного пути командами."""
if not path:
print("Путь не найден, пошаговый режим недоступен.")
return
player = Player(maze.start)
history = CommandHistory()
step = 0
print("\n=== Пошаговый режим ===")
print("n — следующий шаг по пути | u — отмена | q — выход")
print(maze.render(path=set(path), player_pos=player.position))
while True:
cmd_input = input("\nКоманда: ").strip().lower()
if cmd_input == 'q':
break
elif cmd_input == 'n':
step += 1
if step < len(path):
cmd = MoveCommand(player, path[step], solver, path)
history.execute(cmd)
else:
print("Вы достигли выхода! 🎉")
elif cmd_input == 'u':
step = max(0, step - 1)
history.undo()
else:
print("Неизвестная команда.")
def demo_auto(size: int = 21):
"""Автоматическая демонстрация всех паттернов на сгенерированном лабиринте."""
print("=" * 60)
print(" Демонстрация: Поиск выхода из лабиринта")
print("=" * 60)
# Builder
print("\n[Builder] Генерация лабиринта...")
maze = generate_maze(size, size, seed=77)
# Observer + Strategy
strategy = choose_strategy()
solver = MazeSolver(maze, strategy)
view = ConsoleView()
solver.add_observer(view)
solver.notify('maze_loaded', {'maze': maze})
stats, path = solver.solve()
print(f"\n[MazeSolver] Статистика:")
print(f" Алгоритм: {stats.strategy_name}")
print(f" Время: {stats.elapsed_ms:.3f} мс")
print(f" Посещено клеток: {stats.visited_cells}")
print(f" Длина пути: {stats.path_length}")
# Command / пошаговый режим
choice = input("\nЗапустить пошаговый режим? [y/N]: ").strip().lower()
if choice == 'y':
interactive_walk(solver, path, maze)
# Strategy swap demo
print("\n[Strategy] Смена алгоритма на BFS без изменения кода...")
solver.set_strategy(BFSStrategy())
stats2, _ = solver.solve()
print(f" BFS: {stats2.elapsed_ms:.3f} мс, посещено: {stats2.visited_cells}")
# ============================================================================
# Запуск
# ============================================================================
if __name__ == '__main__':
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
run_experiment()
else:
demo_auto()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

View File

@ -1,279 +0,0 @@
# Задание 2 — Поиск выхода из лабиринта (ООП + паттерны GoF)
## 1. Описание задачи
Разработать расширяемую программу поиска пути в лабиринте с возможностью смены алгоритма, визуализации и пошагового управления. Применить минимум 3 паттерна GoF.
---
## 2. Применённые паттерны GoF
### 2.1 Builder (Строитель) — `builder.py`
**Проблема:** Создание `Maze` — многошаговый процесс: чтение файла → парсинг символов → валидация (есть ли S и E) → расстановка координат. Если делать всё в конструкторе `Maze` — нарушение Single Responsibility.
**Решение:** Интерфейс `MazeBuilder` с методом `build_from_file()`. Реализации:
- `TextFileMazeBuilder` — текстовый формат (`#`, пробел, `S`, `E`, `W`, `N`)
- `JsonMazeBuilder` — JSON-формат (для демонстрации расширяемости)
**Преимущество:** Добавление нового формата (бинарный, XML) не требует изменения `Maze`, `MazeSolver` или стратегий.
### 2.2 Strategy (Стратегия) — `strategy.py`
**Проблема:** Алгоритмы поиска (BFS, DFS, A*, Dijkstra) взаимозаменяемы по интерфейсу, но различаются реализацией. Жёсткая связь с конкретным алгоритмом нарушает Open/Closed Principle.
**Решение:** Интерфейс `PathFindingStrategy` с методом `find_path(maze, start, exit_)`. `MazeSolver` хранит ссылку на стратегию и вызывает `strategy.find_path()` — не зная, какой именно алгоритм работает. Метод `set_strategy()` меняет алгоритм в рантайме.
**Преимущество:** Новый алгоритм (IDA*, Bidirectional BFS) — это один новый класс без изменения остального кода.
### 2.3 Observer (Наблюдатель) — `solver.py`
**Проблема:** `MazeSolver` не должен знать о способе отображения. Консоль, GUI, файловый лог — разные задачи.
**Решение:** `MazeSolver` наследует `Subject` (хранит список `_observers`, метод `notify()`). При событиях (`maze_loaded`, `path_found`, `no_path`, `player_moved`) уведомляет всех наблюдателей. Реализации: `ConsoleView`, `FileLogObserver`.
**Преимущество:** GUI-интерфейс добавляется как новый `Observer` без изменения `MazeSolver`.
### 2.4 Command (Команда) — `solver.py`
**Проблема:** Пошаговое перемещение игрока должно поддерживать отмену (undo).
**Решение:** Интерфейс `Command` с методами `execute()` / `undo()`. `MoveCommand` хранит предыдущую позицию. `CommandHistory` — стек команд. Undo — `pop()` + `cmd.undo()`.
**Преимущество:** Любое новое действие (телепортация, открытие двери) реализуется как отдельная команда, не меняя `Player` или `MazeSolver`.
---
## 3. Диаграмма классов (Mermaid)
```mermaid
classDiagram
%% ── Model ──────────────────────────────────
class Cell {
+int x, y
+str symbol
+bool is_wall
+bool is_start
+bool is_exit
+int weight
+is_passable() bool
}
class Maze {
+int width, height
+Cell start, exit
+get_cell(x,y) Cell
+get_neighbors(cell) list
+render(path, visited, player) str
}
Maze "1" *-- "many" Cell
%% ── Builder ─────────────────────────────────
class MazeBuilder {
<<interface>>
+build_from_file(filename) Maze
+build_empty(w, h) Maze
}
class TextFileMazeBuilder {
+build_from_file(filename) Maze
+build_from_string(text) Maze
+build_empty(w, h) Maze
}
class JsonMazeBuilder {
+build_from_file(filename) Maze
+build_empty(w, h) Maze
}
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder <|.. JsonMazeBuilder
TextFileMazeBuilder ..> Maze : creates
JsonMazeBuilder ..> Maze : creates
%% ── Strategy ────────────────────────────────
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit) tuple
+name() str
}
class BFSStrategy { +find_path() tuple }
class DFSStrategy { +find_path() tuple }
class AStarStrategy { +find_path() tuple }
class DijkstraStrategy { +find_path() tuple }
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
%% ── Observer ────────────────────────────────
class Observer {
<<interface>>
+update(event, payload)
}
class Subject {
-observers list
+add_observer(obs)
+notify(event, payload)
}
class ConsoleView { +update(event, payload) }
class FileLogObserver { +update(event, payload) }
Observer <|.. ConsoleView
Observer <|.. FileLogObserver
Subject "1" o-- "many" Observer
%% ── MazeSolver ──────────────────────────────
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+set_strategy(strategy)
+load_maze(maze)
+solve() tuple
}
MazeSolver --|> Subject
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
%% ── Command ─────────────────────────────────
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Cell target, previous
+execute()
+undo()
}
class CommandHistory {
-stack list
+execute(cmd)
+undo()
}
class Player { +Cell position }
Command <|.. MoveCommand
CommandHistory o-- Command
MoveCommand --> Player
```
---
## 4. Структура файлов
```
maze/
├── model.py — Cell, Maze
├── builder.py — MazeBuilder, TextFileMazeBuilder, JsonMazeBuilder
├── strategy.py — PathFindingStrategy, BFS, DFS, A*, Dijkstra
├── solver.py — Observer, Subject, ConsoleView, MazeSolver, Command, Player
├── generator.py — Генератор лабиринтов (DFS-backtracker)
├── experiment.py — Экспериментальная часть, запись CSV
├── main.py — Интерактивная демонстрация
└── docs/
└── data/
├── maze_results.csv
├── maze_benchmark.png
├── path_lengths.png
└── mazes/ — текстовые файлы лабиринтов
```
---
## 5. Результаты экспериментов
### 5.1 Таблица результатов (N = 7 повторов, среднее)
| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути |
|---|---|---:|---:|---:|
| Маленький 11×11 | BFS | 0.077 | 37 | 29 |
| Маленький 11×11 | DFS | 0.059 | 29 | 29 |
| Маленький 11×11 | A* | 0.111 | 29 | 29 |
| Маленький 11×11 | Dijkstra | 0.115 | 37 | 29 |
| Средний 51×51 | BFS | 2.228 | 755 | 405 |
| Средний 51×51 | DFS | 0.935 | 423 | 405 |
| Средний 51×51 | A* | 2.343 | 663 | 405 |
| Средний 51×51 | Dijkstra | 2.528 | 755 | 405 |
| Большой 101×101 | BFS | 3.299 | 1533 | 993 |
| Большой 101×101 | DFS | 2.289 | 1061 | 993 |
| Большой 101×101 | A* | 5.948 | 1515 | 993 |
| Большой 101×101 | Dijkstra | 5.137 | 1533 | 993 |
| Пустой 52×52 | BFS | 5.859 | 2500 | 99 |
| Пустой 52×52 | DFS | 3.518 | 1275 | 1275 |
| Пустой 52×52 | A* | 10.803 | 2500 | 99 |
| Пустой 52×52 | Dijkstra | 10.227 | 2500 | 99 |
| Без выхода 21×21 | BFS | 0.213 | 105 | 0 |
| Без выхода 21×21 | DFS | 0.220 | 105 | 0 |
| Без выхода 21×21 | A* | 0.361 | 105 | 0 |
| Без выхода 21×21 | Dijkstra | 0.336 | 105 | 0 |
| Взвешенный 51×51 | BFS | 0.722 | 335 | 221 |
| Взвешенный 51×51 | DFS | 0.518 | 221 | 221 |
| Взвешенный 51×51 | A* | 0.970 | 278 | 221 |
| Взвешенный 51×51 | Dijkstra | 1.091 | 337 | 221 |
### 5.2 Графики
![Benchmark](data/maze_benchmark.png)
![Path Lengths](data/path_lengths.png)
---
## 6. Анализ алгоритмов
### BFS (поиск в ширину)
- **Гарантирует кратчайший путь** по количеству шагов.
- Посещает все клетки на расстоянии k до нахождения выхода на расстоянии k+1.
- На пустом лабиринте (максимум свободного пространства) посещает **все 2500 клеток** — это худший сценарий по памяти (хранит всю «волну»).
- Практически эквивалентен Dijkstra для равновесных лабиринтов.
### DFS (поиск в глубину)
- **Самый быстрый по времени** на лабиринтах с длинными коридорами.
- На правильно построенном лабиринте (DFS-backtracker) коридоры длинные → DFS «угадывает» направление и находит путь, посетив меньше клеток.
- На пустом поле даёт **огромный зигзагообразный путь** (1275 клеток вместо оптимального 99) — классическая проблема DFS.
- **Не гарантирует оптимальность**, зато экономит память (O(глубина) вместо O(ширина)).
### A* (с манхэттенской эвристикой)
- На лабиринтах с коридорами посещает **меньше клеток**, чем BFS, но при этом тоже гарантирует оптимальный путь.
- На пустом поле ведёт себя хуже BFS по времени из-за накладных расходов на приоритетную очередь (heapq).
- Манхэттенская эвристика работает хуже на лабиринтах с длинными обходами препятствий: оценка расстояния «по прямой» слишком оптимистична.
- **Выигрывает** когда карта большая и путь очевиден по направлению (прямые коридоры к выходу).
### Dijkstra
- На равновесных лабиринтах идентичен BFS, но медленнее из-за приоритетной очереди.
- **Незаменим на взвешенных картах**: учитывает клетки с весом 2 (песок) и 3 (болото) и находит **минимальный суммарный вес** пути, а не минимальное число шагов.
- На взвешенном лабиринте Dijkstra и A* могут выдать более короткий (по весу) путь, чем BFS, при одинаковой длине в клетках.
### Без выхода
- Все алгоритмы обошли все 105 достижимых клеток и вернули пустой путь — корректная обработка.
- Время идентично для всех алгоритмов (доминирует полный обход, а не структура поиска).
---
## 7. Выводы: ООП и паттерны
### Что дали паттерны
**Builder** отделил детали парсинга от модели. Без него клиент сам читал бы файл и вручную собирал `Maze` — хрупкий, нерасширяемый код. С Builder: добавление JSON-формата заняло 10 строк.
**Strategy** позволил сравнить 4 алгоритма, не меняя `MazeSolver`. Переключение алгоритма — одна строка `solver.set_strategy(new_algo)`. Без паттерна потребовался бы if/elif на 4 ветки в каждом методе.
**Observer** полностью развязал логику поиска и отображение. `MazeSolver` не знает, куда идут уведомления. Добавление `FileLogObserver` — новый класс из 5 строк, нулевые изменения в `MazeSolver`.
**Command** дал возможность реализовать undo за счёт хранения предыдущего состояния в объекте команды. Без паттерна — ручное сохранение/восстановление переменных.
### Что было бы сложно без ООП и паттернов
| Задача | Без паттернов | С паттернами |
|---|---|---|
| Добавить новый алгоритм | Правка MazeSolver + if/elif | Новый класс, наследующий интерфейс |
| Добавить формат файла | Правка загрузчика | Новый Builder |
| Добавить GUI | Вставка GUI-кода в логику поиска | Новый Observer |
| Отмена шага игрока | Ручное сохранение переменной | Command.undo() |
### Рекомендации по выбору алгоритма
| Задача | Рекомендация |
|---|---|
| Гарантированно кратчайший путь, равные веса | **BFS** |
| Быстро найти *какой-нибудь* путь (поиск с препятствиями) | **DFS** |
| Большая карта, путь по направлению к цели | **A\*** |
| Взвешенная карта (болото, песок, дорога) | **Dijkstra** или **A\*** с весовой эвристикой |
| Нужна гарантия оптимальности на взвешенном графе | **Dijkstra** |

View File

@ -1,25 +0,0 @@
Лабиринт,Алгоритм,Время_мсосещено_клеток,Длина_пути,Стд_время_мс
small_10x10,BFS,0.0772,37,29,0.0122
small_10x10,DFS,0.0588,29,29,0.0044
small_10x10,A*,0.1112,29,29,0.029
small_10x10,Dijkstra,0.1155,37,29,0.0135
medium_50x50,BFS,2.228,755,405,1.6518
medium_50x50,DFS,0.9346,423,405,0.0899
medium_50x50,A*,2.3432,663,405,0.125
medium_50x50,Dijkstra,2.5281,755,405,0.1245
large_100x100,BFS,3.2987,1533,993,0.148
large_100x100,DFS,2.2887,1061,993,0.0737
large_100x100,A*,5.9477,1515,993,1.2811
large_100x100,Dijkstra,5.1375,1533,993,0.1435
empty_50x50,BFS,5.8593,2500,99,0.1605
empty_50x50,DFS,3.5185,1275,1275,0.1171
empty_50x50,A*,10.8025,2500,99,0.0902
empty_50x50,Dijkstra,10.227,2500,99,0.1552
no_exit_20x20,BFS,0.213,105,0,0.0434
no_exit_20x20,DFS,0.2204,105,0,0.049
no_exit_20x20,A*,0.3615,105,0,0.0489
no_exit_20x20,Dijkstra,0.3361,105,0,0.033
weighted_50x50,BFS,0.7224,335,221,0.069
weighted_50x50,DFS,0.5183,221,221,0.0912
weighted_50x50,A*,0.97,278,221,0.0444
weighted_50x50,Dijkstra,1.0906,337,221,0.0414
1 Лабиринт Алгоритм Время_мс Посещено_клеток Длина_пути Стд_время_мс
2 small_10x10 BFS 0.0772 37 29 0.0122
3 small_10x10 DFS 0.0588 29 29 0.0044
4 small_10x10 A* 0.1112 29 29 0.029
5 small_10x10 Dijkstra 0.1155 37 29 0.0135
6 medium_50x50 BFS 2.228 755 405 1.6518
7 medium_50x50 DFS 0.9346 423 405 0.0899
8 medium_50x50 A* 2.3432 663 405 0.125
9 medium_50x50 Dijkstra 2.5281 755 405 0.1245
10 large_100x100 BFS 3.2987 1533 993 0.148
11 large_100x100 DFS 2.2887 1061 993 0.0737
12 large_100x100 A* 5.9477 1515 993 1.2811
13 large_100x100 Dijkstra 5.1375 1533 993 0.1435
14 empty_50x50 BFS 5.8593 2500 99 0.1605
15 empty_50x50 DFS 3.5185 1275 1275 0.1171
16 empty_50x50 A* 10.8025 2500 99 0.0902
17 empty_50x50 Dijkstra 10.227 2500 99 0.1552
18 no_exit_20x20 BFS 0.213 105 0 0.0434
19 no_exit_20x20 DFS 0.2204 105 0 0.049
20 no_exit_20x20 A* 0.3615 105 0 0.0489
21 no_exit_20x20 Dijkstra 0.3361 105 0 0.033
22 weighted_50x50 BFS 0.7224 335 221 0.069
23 weighted_50x50 DFS 0.5183 221 221 0.0912
24 weighted_50x50 A* 0.97 278 221 0.0444
25 weighted_50x50 Dijkstra 1.0906 337 221 0.0414

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB