forked from UNN/2026-rff_mp
Compare commits
No commits in common. "ezhovnd-patch-2" and "develop" have entirely different histories.
ezhovnd-pa
...
develop
|
|
@ -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 |
|
|
@ -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 Графики
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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** |
|
|
||||||
|
|
@ -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
|
|
||||||
|
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
Loading…
Reference in New Issue
Block a user