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

Reviewed-on: #217
This commit is contained in:
kit8nino 2026-05-30 11:48:46 +00:00
commit 979a03c36f
16 changed files with 1174 additions and 0 deletions

View File

@ -0,0 +1,345 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "9a863658",
"metadata": {},
"source": [
"# Отчёт: Поиск выхода из лабиринта (ООП + паттерны проектирования)\n",
"\n",
"## Цель работы\n",
"\n",
"Разработать гибкую расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма и экспериментального сравнения алгоритмов. Применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.\n",
"\n",
"---\n",
"\n",
"## Описание задачи и выбранных паттернов\n",
"\n",
"Программа решает задачу поиска пути в лабиринте, загружаемом из текстового файла. Лабиринт представляет собой сетку клеток, где `#` — стена, пробел — проход, `S` — старт, `E` — выход. Алгоритм поиска выбирается динамически, результаты выводятся через систему событий.\n",
"\n",
"В проекте применены паттерны Builder, Strategy, Observer и Command.\n",
"\n",
"### 1. Builder (Строитель) — `MazeBuilder.py`\n",
"\n",
"**Проблема:** построение объекта `Maze` из текстового файла включает несколько этапов: чтение файла, анализ символов, создание объектов клеток, определение стартовой и конечной позиции, формирование структуры лабиринта.\n",
"\n",
"**Решение:** создан абстрактный класс `MazeBuilder` с методом `build_from_file()`. Класс `TextFileMazeBuilder` реализует построение лабиринта из текстового файла.\n",
"\n",
"```python\n",
"class MazeBuilder(ABC):\n",
" @abstractmethod\n",
" def build_from_file(self, filename) -> Maze:\n",
" pass\n",
"```\n",
"\n",
"```python\n",
"class TextFileMazeBuilder(MazeBuilder):\n",
" def build_from_file(self, filename) -> Maze:\n",
"```\n",
"\n",
"Паттерн позволяет изолировать логику построения лабиринта от основной программы. При необходимости можно добавить другой формат загрузки без изменения клиентского кода.\n",
"\n",
"### 2. Strategy (Стратегия) — `FindingStrategy.py`\n",
"\n",
"**Проблема:** алгоритмы BFS, DFS и A* имеют разную реализацию, но используются одинаковым образом.\n",
"\n",
"**Решение:** создан интерфейс `PathFindingStrategy` с методом `find_path()`. Алгоритмы `BFSStrategy`, `DFSStrategy` и `AStarStrategy` реализуют общий интерфейс.\n",
"\n",
"```python\n",
"class PathFindingStrategy(ABC):\n",
" @abstractmethod\n",
" def find_path(self, maze, start, exit_cell):\n",
" pass\n",
"```\n",
"\n",
"Смена алгоритма выполняется динамически:\n",
"\n",
"```python\n",
"solver.set_strategy(BFSStrategy())\n",
"solver.solve()\n",
"\n",
"solver.set_strategy(AStarStrategy())\n",
"solver.solve()\n",
"```\n",
"\n",
"Паттерн Strategy позволяет добавлять новые алгоритмы поиска без изменения класса `MazeSolver`.\n",
"\n",
"### 3. Observer (Наблюдатель) — `MazeSolver.py`\n",
"\n",
"**Проблема:** необходимо уведомлять интерфейс о событиях поиска пути без жёсткой связи между логикой и выводом.\n",
"\n",
"**Решение:** реализован интерфейс `Observer` с методом `update()`. Класс `ConsoleView` подписывается на события `MazeSolver`.\n",
"\n",
"```python\n",
"class Observer(ABC):\n",
" @abstractmethod\n",
" def update(self, event, data=None):\n",
" pass\n",
"```\n",
"\n",
"```python\n",
"solver.add_observer(view)\n",
"```\n",
"\n",
"При нахождении пути вызывается уведомление:\n",
"\n",
"```python\n",
"self._notify('path_found', {\n",
" 'stats': stats,\n",
" 'strategy': type(self.strategy).__name__\n",
"})\n",
"```\n",
"\n",
"Паттерн позволяет отделить вывод информации от алгоритмов поиска.\n",
"\n",
"### 4. Command (Команда) — `MazeSolver.py`\n",
"\n",
"**Проблема:** необходимо реализовать возможность перемещения игрока с поддержкой отмены действий.\n",
"\n",
"**Решение:** создан интерфейс `Command` с методами `execute()` и `undo()`. Класс `MoveCommand` хранит предыдущее состояние игрока.\n",
"\n",
"```python\n",
"class Command(ABC):\n",
" @abstractmethod\n",
" def execute(self):\n",
" pass\n",
"\n",
" @abstractmethod\n",
" def undo(self):\n",
" pass\n",
"```\n",
"\n",
"```python\n",
"cmd.execute()\n",
"cmd.undo()\n",
"```\n",
"\n",
"Паттерн инкапсулирует действия в отдельные объекты и позволяет реализовать undo/redo.\n",
"\n",
"---\n",
"\n",
"## Диаграмма классов (Mermaid)\n",
"\n",
"```mermaid\n",
"classDiagram\n",
" class MazeBuilder {\n",
" <<interface>>\n",
" +build_from_file(filename) Maze\n",
" }\n",
"\n",
" class TextFileMazeBuilder {\n",
" +build_from_file(filename) Maze\n",
" }\n",
"\n",
" class Cell {\n",
" +x\n",
" +y\n",
" +is_wall\n",
" +is_start\n",
" +is_exit\n",
" +is_passable()\n",
" }\n",
"\n",
" class Maze {\n",
" +width\n",
" +height\n",
" +get_cell(x, y)\n",
" +get_neighbors(cell)\n",
" +render(path, player_pos)\n",
" }\n",
"\n",
" class PathFindingStrategy {\n",
" <<interface>>\n",
" +find_path(maze, start, exit)\n",
" }\n",
"\n",
" class BFSStrategy\n",
" class DFSStrategy\n",
" class AStarStrategy\n",
"\n",
" class MazeSolver {\n",
" +set_strategy(strategy)\n",
" +solve()\n",
" +add_observer(observer)\n",
" }\n",
"\n",
" class SearchStats\n",
"\n",
" class Observer {\n",
" <<interface>>\n",
" +update(event, data)\n",
" }\n",
"\n",
" class ConsoleView\n",
"\n",
" class Command {\n",
" <<interface>>\n",
" +execute()\n",
" +undo()\n",
" }\n",
"\n",
" class MoveCommand\n",
" class Player\n",
"\n",
" MazeBuilder <|.. TextFileMazeBuilder\n",
" Maze o-- Cell\n",
"\n",
" PathFindingStrategy <|.. BFSStrategy\n",
" PathFindingStrategy <|.. DFSStrategy\n",
" PathFindingStrategy <|.. AStarStrategy\n",
"\n",
" MazeSolver --> Maze\n",
" MazeSolver --> PathFindingStrategy\n",
" MazeSolver --> SearchStats\n",
"\n",
" Observer <|.. ConsoleView\n",
" MazeSolver --> Observer\n",
"\n",
" Command <|.. MoveCommand\n",
" MoveCommand --> Player\n",
"```\n",
"\n",
"---\n",
"\n",
"## Ключевые фрагменты реализации\n",
"\n",
"### Реализация BFS\n",
"\n",
"```python\n",
"class BFSStrategy(PathFindingStrategy):\n",
" def find_path(self, maze, start, exit_cell):\n",
" queue = deque([start])\n",
" came_from = {(start.x, start.y): None}\n",
"```\n",
"\n",
"Алгоритм BFS выполняет поиск в ширину и гарантирует нахождение кратчайшего пути.\n",
"\n",
"### Реализация DFS\n",
"\n",
"```python\n",
"class DFSStrategy(PathFindingStrategy):\n",
" def find_path(self, maze, start, exit_cell):\n",
" stack = [start]\n",
"```\n",
"\n",
"DFS использует стек и выполняет поиск в глубину.\n",
"\n",
"### Реализация A*\n",
"\n",
"```python\n",
"class AStarStrategy(PathFindingStrategy):\n",
"\n",
" def _heuristic(self, cell, goal):\n",
" return abs(cell.x - goal.x) + abs(cell.y - goal.y)\n",
"```\n",
"\n",
"A* использует манхэттенскую эвристику для направления поиска к цели.\n",
"\n",
"---\n",
"\n",
"## Экспериментальная часть\n",
"\n",
"### Параметры эксперимента\n",
"\n",
"| Параметр | Значение |\n",
"| ---------- | ------------------------------------ |\n",
"| Повторений | 7 |\n",
"| Алгоритмы | BFS, DFS, A* |\n",
"| Метрики | время, посещённые клетки, длина пути |\n",
"\n",
"### Тестовые лабиринты\n",
"\n",
"| Название | Размер |\n",
"| ------------- | ------- |\n",
"| small_10x10 | 10×10 |\n",
"| medium_50x50 | 50×50 |\n",
"| large_100x100 | 100×100 |\n",
"| open_50x50 | 50×50 |\n",
"| no_exit_20x20 | 20×20 |\n",
"\n",
"---\n",
"\n",
"## Результаты\n",
"\n",
"### Таблица результатов\n",
"\n",
"| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |\n",
"| ------------- | -------- | ---------- | --------------- | ---------- |\n",
"| small_10x10 | BFS | 0.094 | 54 | 15 |\n",
"| small_10x10 | DFS | 0.059 | 33 | 33 |\n",
"| small_10x10 | A* | 0.078 | 36 | 15 |\n",
"| medium_50x50 | BFS | 2.446 | 1639 | 95 |\n",
"| medium_50x50 | DFS | 1.480 | 1063 | 185 |\n",
"| medium_50x50 | A* | 1.528 | 588 | 95 |\n",
"| large_100x100 | BFS | 9.891 | 6564 | — |\n",
"| large_100x100 | DFS | 9.057 | 6564 | — |\n",
"| large_100x100 | A* | 17.578 | 6564 | — |\n",
"| open_50x50 | BFS | 3.296 | 2304 | 95 |\n",
"| open_50x50 | DFS | 1.830 | 1223 | 1129 |\n",
"| open_50x50 | A* | 5.566 | 2304 | 95 |\n",
"| no_exit_20x20 | BFS | 0.368 | 260 | — |\n",
"| no_exit_20x20 | DFS | 0.343 | 260 | — |\n",
"| no_exit_20x20 | A* | 0.607 | 260 | — |\n",
"\n",
"### Визуализация\n",
"\n",
"![Время выполнения](data/chart_время-мс.png)\n",
"\n",
"![Посещено клеток](data/chart_посещено-клеток.png)\n",
"\n",
"![Длина пути](data/chart_длина-пути.png)\n",
"\n",
"---\n",
"\n",
"## Анализ результатов\n",
"\n",
"### BFS\n",
"\n",
"Алгоритм BFS во всех случаях находит кратчайший путь. На лабиринте medium_50x50 длина найденного пути составила 95 шагов. Недостатком алгоритма является большое количество посещённых клеток.\n",
"\n",
"### DFS\n",
"\n",
"DFS выполняет поиск быстрее, однако найденный путь значительно длиннее. На open_50x50 длина пути составила 1129 шагов против 95 у BFS.\n",
"\n",
"### A*\n",
"\n",
"Алгоритм A* использует эвристику и уменьшает количество посещённых клеток. На medium_50x50 было посещено 588 клеток против 1639 у BFS.\n",
"\n",
"На открытом лабиринте преимущества эвристики снижаются, из-за чего время работы увеличивается.\n",
"\n",
"### Лабиринты без пути\n",
"\n",
"На large_100x100 и no_exit_20x20 путь найден не был. Все алгоритмы корректно завершили работу после обхода доступных клеток.\n",
"\n",
"---\n",
"\n",
"## Выводы\n",
"\n",
"В ходе работы была реализована объектно-ориентированная система поиска пути в лабиринте с применением паттернов проектирования GoF.\n",
"\n",
"Паттерн Strategy обеспечил возможность динамической смены алгоритма поиска. Builder отделил процесс создания лабиринта от логики приложения. Observer позволил реализовать систему уведомлений без жёстких зависимостей. Command обеспечил поддержку отмены действий.\n",
"\n",
"Экспериментальные результаты показали:\n",
"\n",
"* BFS гарантирует кратчайший путь;\n",
"* DFS работает быстрее, но может находить неоптимальные маршруты;\n",
"* A* наиболее эффективен на сложных лабиринтах благодаря эвристике.\n",
"\n",
"Архитектура программы получилась модульной и расширяемой. Добавление новых алгоритмов или способов загрузки лабиринтов возможно без изменения существующего кода.\n"
]
},
{
"cell_type": "markdown",
"id": "a9cbb6c6",
"metadata": {},
"source": []
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,100 @@
from abc import ABC, abstractmethod
from collections import deque
import heapq
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze, start, exit_cell):
pass
def _reconstruct_path(came_from, start, exit_cell):
path = []
current = exit_cell
while current is not None:
path.append(current)
current = came_from.get((current.x, current.y))
path.reverse()
if path and path[0].x == start.x and path[0].y == start.y:
return path
return []
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
queue = deque([start])
came_from = {(start.x, start.y): None}
self.visited_count = 0
while queue:
current = queue.popleft()
self.visited_count += 1
if current.x == exit_cell.x and current.y == exit_cell.y:
return _reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
key = (neighbor.x, neighbor.y)
if key not in came_from:
came_from[key] = current
queue.append(neighbor)
self.visited_count = len(came_from)
return [] # путь не найден
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
stack = [start]
came_from = {(start.x, start.y): None}
self.visited_count = 0
while stack:
current = stack.pop()
self.visited_count += 1
if current.x == exit_cell.x and current.y == exit_cell.y:
return _reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
key = (neighbor.x, neighbor.y)
if key not in came_from:
came_from[key] = current
stack.append(neighbor)
self.visited_count = len(came_from)
return []
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, cell, goal):
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find_path(self, maze, start, exit_cell):
# (f_score, счётчик для разрыва связей, клетка)
counter = 0
open_set = [(0, counter, start)]
came_from = {(start.x, start.y): None}
g_score = {(start.x, start.y): 0}
self.visited_count = 0
while open_set:
_, _, current = heapq.heappop(open_set)
self.visited_count += 1
if current.x == exit_cell.x and current.y == exit_cell.y:
return _reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
key = (neighbor.x, neighbor.y)
tentative_g = g_score[(current.x, current.y)] + 1
if key not in g_score or tentative_g < g_score[key]:
g_score[key] = tentative_g
f = tentative_g + self._heuristic(neighbor, exit_cell)
counter += 1
heapq.heappush(open_set, (f, counter, neighbor))
came_from[key] = current
self.visited_count = len(came_from)
return []

View File

@ -0,0 +1,44 @@
from abc import ABC, abstractmethod
from maze_model import Cell, Maze
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = f.read().splitlines()
width = max(len(line) for line in lines) if lines else 0
height = len(lines)
cells = []
start = None
exit_cell = None
for y, line in enumerate(lines):
row = []
line = line.ljust(width)
for x, char in enumerate(line):
is_wall = (char == '#')
is_start = (char == 'S')
is_exit = (char == 'E')
cell = Cell(x, y, is_wall=is_wall,
is_start=is_start, is_exit=is_exit)
if is_start:
start = cell
if is_exit:
exit_cell = cell
row.append(cell)
cells.append(row)
if start is None:
raise ValueError("В файле лабиринта не найден старт (S)")
if exit_cell is None:
raise ValueError("В файле лабиринта не найден выход (E)")
return Maze(width, height, cells, start, exit_cell)

View File

@ -0,0 +1,62 @@
class Cell:
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
def is_passable(self):
return not self.is_wall
def __repr__(self):
if self.is_wall:
return '#'
if self.is_start:
return 'S'
if self.is_exit:
return 'E'
return ' '
class Maze:
def __init__(self, width, height, cells, start, exit_cell):
self.width = width
self.height = height
self._cells = cells
self.start = start
self.exit = exit_cell
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self._cells[y][x]
return None
def get_neighbors(self, cell):
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
neighbors = []
for dx, dy in directions:
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 render(self, path=None, player_pos=None):
path_set = set((c.x, c.y) for c in path) if path else set()
for row in self._cells:
line = ''
for cell in row:
if player_pos and cell.x == player_pos.x and cell.y == player_pos.y:
line += 'P'
elif cell.is_wall:
line += '#'
elif cell.is_start:
line += 'S'
elif cell.is_exit:
line += 'E'
elif (cell.x, cell.y) in path_set:
line += '.'
else:
line += ' '
print(line)

View File

@ -0,0 +1,121 @@
import time
from abc import ABC, abstractmethod
class SearchStats:
def __init__(self, time_ms, visited_cells, path_length, path):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
self.path = path
def __repr__(self):
return (f"SearchStats(time={self.time_ms:.3f}ms, "
f"visited={self.visited_cells}, "
f"path_len={self.path_length})")
class Observer(ABC):
@abstractmethod
def update(self, event, data=None):
pass
class ConsoleView(Observer):
def update(self, event, data=None):
if event == 'maze_loaded':
print(f"\n[ConsoleView] Лабиринт загружен: "
f"{data['width']}×{data['height']}")
elif event == 'path_found':
stats = data['stats']
strategy_name = data['strategy']
if stats.path_length > 0:
print(f"\n[ConsoleView] [{strategy_name}] Путь найден! "
f"Длина: {stats.path_length}, "
f"Посещено клеток: {stats.visited_cells}, "
f"Время: {stats.time_ms:.3f} мс")
else:
print(f"\n[ConsoleView] [{strategy_name}] Путь не найден. "
f"Посещено клеток: {stats.visited_cells}")
elif event == 'move':
print(f"[ConsoleView] Игрок переместился в "
f"({data['x']}, {data['y']})")
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
self._observers = []
def set_strategy(self, strategy):
self.strategy = strategy
def add_observer(self, observer):
self._observers.append(observer)
def _notify(self, event, data=None):
for obs in self._observers:
obs.update(event, data)
def solve(self):
if self.strategy is None:
raise RuntimeError("Стратегия не задана. Используйте set_strategy().")
start = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end = time.perf_counter()
stats = SearchStats(
time_ms=(end - start) * 1000,
visited_cells=getattr(self.strategy, 'visited_count', 0),
path_length=len(path),
path=path
)
self._notify('path_found', {
'stats': stats,
'strategy': type(self.strategy).__name__
})
return stats
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class Player:
def __init__(self, start_cell):
self.current_cell = start_cell
def move_to(self, cell):
self.current_cell = cell
class MoveCommand(Command):
def __init__(self, player, target_cell, observers=None):
self.player = player
self.target_cell = target_cell
self.previous_cell = None
self._observers = observers or []
def execute(self):
self.previous_cell = self.player.current_cell
self.player.move_to(self.target_cell)
for obs in self._observers:
obs.update('move', {'x': self.target_cell.x,
'y': self.target_cell.y})
def undo(self):
if self.previous_cell is not None:
self.player.move_to(self.previous_cell)
for obs in self._observers:
obs.update('move', {'x': self.previous_cell.x,
'y': self.previous_cell.y})

View File

@ -0,0 +1,103 @@
import csv
import os
try:
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
HAS_MPL = True
except ImportError:
HAS_MPL = False
print(" matplotlib не установлен: pip install matplotlib\n")
CSV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'results.csv')
OUT_DIR = os.path.dirname(os.path.abspath(__file__))
COLORS = {'BFS': '#4E9AF1', 'DFS': '#F4845F', 'A*': '#6BCB77'}
STRATEGIES = ['BFS', 'DFS', 'A*']
METRICS = [
('время_мс', 'Среднее время (мс)'),
('посещено_клеток', 'Посещено клеток'),
('длина_пути', 'Длина пути (шагов)'),
]
def load_csv(path):
data = {}
with open(path, newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
key = (row['лабиринт'], row['стратегия'])
data[key] = {
'время_мс': float(row['время_мс']),
'посещено_клеток': float(row['посещено_клеток']),
'длина_пути': float(row['длина_пути']),
}
return data
def get_mazes(data):
seen = []
for (maze, _) in data:
if maze not in seen:
seen.append(maze)
return seen
def plot_by_metric(data):
mazes = get_mazes(data)
x = range(len(mazes))
w = 0.25
for metric_key, metric_label in METRICS:
fig, ax = plt.subplots(figsize=(12, 5))
fig.suptitle(f'{metric_label} по лабиринтам', fontweight='bold')
for i, strat in enumerate(STRATEGIES):
vals = [data.get((m, strat), {}).get(metric_key, 0) for m in mazes]
offset = [xi + (i - 1) * w for xi in x]
bars = ax.bar(offset, vals, width=w,
label=strat, color=COLORS[strat], edgecolor='white')
for bar, val in zip(bars, vals):
if val > 0:
ax.text(bar.get_x() + bar.get_width() / 2,
bar.get_height() + max(vals) * 0.01,
f'{val:.1f}', ha='center', va='bottom', fontsize=7)
ax.set_xticks(list(x))
ax.set_xticklabels(mazes, rotation=15, ha='right', fontsize=9)
ax.set_ylabel(metric_label)
ax.legend()
ax.grid(axis='y', alpha=0.3)
safe = metric_key.replace('_', '-')
out = os.path.join(OUT_DIR, f'chart_{safe}.png')
plt.tight_layout()
plt.savefig(out, dpi=150, bbox_inches='tight')
print(f" График сохранён: {out}")
plt.show()
def print_table(data):
print(f"\n{'Лабиринт':<20} {'Алгоритм':<6} "
f"{'Время мс':>10} {'Посещено':>10} {'Путь':>6}")
print('-' * 56)
for (maze, strat), vals in sorted(data.items()):
print(f"{maze:<20} {strat:<6} "
f"{vals['время_мс']:>10.3f} "
f"{vals['посещено_клеток']:>10.0f} "
f"{vals['длина_пути']:>6.0f}")
if __name__ == '__main__':
if not os.path.exists(CSV_PATH):
print(f" Файл не найден: {CSV_PATH}")
print(" Сначала запустите: python benchmark.py")
exit(1)
data = load_csv(CSV_PATH)
print_table(data)
if HAS_MPL:
plot_by_metric(data)
else:
print("\n Установите matplotlib: pip install matplotlib")

View File

@ -0,0 +1,153 @@
import time
import csv
import os
import random
from maze_builder import TextFileMazeBuilder
from maze_solver import MazeSolver
from maze_strategies import BFSStrategy, DFSStrategy, AStarStrategy
REPEATS = 7
OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__))
CSV_PATH = os.path.join(OUTPUT_DIR, 'results.csv')
STRATEGIES = {
'BFS': BFSStrategy,
'DFS': DFSStrategy,
'A*': AStarStrategy,
}
MAZES = [
('small_10x10', 'maze_small.txt'),
('medium_50x50', 'maze_medium.txt'),
('large_100x100', 'maze_large.txt'),
('open_50x50', 'maze_open.txt'),
('no_exit_20x20', 'maze_no_exit.txt'),
]
def _make_grid(width, height, density=0.0, has_exit=True, seed=42):
rng = random.Random(seed)
grid = []
for y in range(height):
row = []
for x in range(width):
on_border = (x == 0 or x == width - 1 or y == 0 or y == height - 1)
row.append('#' if on_border else ' ')
grid.append(row)
for y in range(1, height - 1):
for x in range(1, width - 1):
if rng.random() < density:
grid[y][x] = '#'
grid[1][1] = 'S'
if has_exit:
grid[height - 2][width - 2] = 'E'
return '\n'.join(''.join(row) for row in grid)
def generate_maze_files():
mazes_data = {
'maze_small.txt': _make_grid(10, 10, density=0.15),
'maze_medium.txt': _make_grid(50, 50, density=0.28),
'maze_large.txt': _make_grid(100, 100, density=0.30),
'maze_open.txt': _make_grid(50, 50, density=0.0),
'maze_no_exit.txt': _make_grid(20, 20, density=0.20, has_exit=False),
}
no_exit = list(mazes_data['maze_no_exit.txt'].splitlines())
no_exit[18] = no_exit[18][:18] + 'E' + no_exit[18][19:]
no_exit[17] = no_exit[17][:18] + '#' + no_exit[17][19:]
no_exit[18] = no_exit[18][:17] + '#' + no_exit[18][18:]
mazes_data['maze_no_exit.txt'] = '\n'.join(no_exit)
maze_dir = os.path.dirname(os.path.abspath(__file__))
for fname, content in mazes_data.items():
path = os.path.join(maze_dir, fname)
with open(path, 'w', encoding='utf-8') as f:
f.write(content)
print("Файлы лабиринтов созданы")
def avg(lst):
return sum(lst) / len(lst) if lst else 0
def run_benchmark():
builder = TextFileMazeBuilder()
maze_dir = os.path.dirname(os.path.abspath(__file__))
all_results = [
['лабиринт', 'стратегия', 'время_мс', 'посещено_клеток', 'длина_пути']
+ [f'замер_{i+1}' for i in range(REPEATS)]
]
print(f"\nЗапуск бенчмарков (повторений: {REPEATS})\n")
print(f" {'Лабиринт':<18} {'Алгоритм':<6} {'Время мс':>10} "
f"{'Посещено':>10} {'Путь':>6}")
print(' ' + '-' * 56)
for maze_label, maze_file in MAZES:
maze_path = os.path.join(maze_dir, maze_file)
try:
maze = builder.build_from_file(maze_path)
except Exception as e:
print(f" {maze_file}: {e}")
continue
solver = MazeSolver(maze)
for strat_name, StratClass in STRATEGIES.items():
times_ms, visited_list, path_len = [], [], 0
for _ in range(REPEATS):
strat = StratClass()
solver.set_strategy(strat)
stats = solver.solve()
times_ms.append(stats.time_ms)
visited_list.append(stats.visited_cells)
path_len = stats.path_length
mean_t = avg(times_ms)
mean_v = avg(visited_list)
print(f" {maze_label:<18} {strat_name:<6} "
f"{mean_t:>10.3f} {mean_v:>10.0f} {path_len:>6}")
all_results.append([
maze_label, strat_name,
f"{mean_t:.4f}", f"{mean_v:.0f}", str(path_len)
] + [f"{t:.4f}" for t in times_ms])
with open(CSV_PATH, 'w', newline='', encoding='utf-8') as f:
csv.writer(f).writerows(all_results)
print(f"\n Результаты сохранены: {CSV_PATH}")
def smoke_test():
print(" Smoke Test\n")
maze_dir = os.path.dirname(os.path.abspath(__file__))
test_path = os.path.join(maze_dir, '_test_maze.txt')
with open(test_path, 'w', encoding='utf-8') as f:
f.write("#######\n#S #\n# #\n# E#\n#######")
builder = TextFileMazeBuilder()
maze = builder.build_from_file(test_path)
for name, StratClass in STRATEGIES.items():
strat = StratClass()
path = strat.find_path(maze, maze.start, maze.exit)
assert len(path) > 0, f"{name}: путь не найден!"
assert path[0].is_start
assert path[-1].is_exit
print(f" {name}: путь длиной {len(path)} — OK")
os.remove(test_path)
print("\nВсе тесты пройдены!\n")
if __name__ == '__main__':
smoke_test()
generate_maze_files()
run_benchmark()

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,16 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути,замер_1,замер_2,замер_3,замер_4,замер_5,замер_6,замер_7
small_10x10,BFS,0.2443,54,15,0.2512,0.2377,0.2104,0.2098,0.3295,0.2628,0.2088
small_10x10,DFS,0.1729,33,33,0.1438,0.3230,0.1467,0.1388,0.1570,0.1518,0.1490
small_10x10,A*,0.2503,36,15,0.2849,0.4248,0.2063,0.2027,0.2010,0.2022,0.2305
medium_50x50,BFS,7.8016,1639,95,9.7252,7.6549,7.1375,6.5061,7.8024,7.9052,7.8801
medium_50x50,DFS,5.9674,1063,185,5.5163,4.6949,7.7879,7.1488,5.6215,5.1059,5.8962
medium_50x50,A*,4.0049,588,95,4.1553,4.5408,4.1015,3.6816,3.9481,3.7989,3.8081
large_100x100,BFS,30.9012,6564,0,33.9413,31.6451,30.0917,31.2012,30.8385,29.2073,29.3836
large_100x100,DFS,29.7523,6564,0,31.0923,29.3284,29.7215,29.0498,29.9187,29.9990,29.1565
large_100x100,A*,51.4814,6564,0,49.1005,51.0370,50.5359,54.6521,53.4745,49.4512,52.1183
open_50x50,BFS,12.2218,2304,95,9.8705,12.1266,9.9576,10.8552,12.0067,18.9221,11.8140
open_50x50,DFS,6.8283,1223,1129,7.7999,7.7336,7.2627,5.7576,6.6328,7.1781,5.4337
open_50x50,A*,16.5740,2304,95,16.4528,16.5879,17.9772,16.4465,16.1444,16.7420,15.6672
no_exit_20x20,BFS,1.0499,260,0,1.1731,1.0768,1.0067,1.0100,1.0201,1.0508,1.0120
no_exit_20x20,DFS,1.0160,260,0,1.0409,1.0098,1.0178,1.0072,1.0130,1.0069,1.0166
no_exit_20x20,A*,1.5796,260,0,1.5919,1.5782,1.5758,1.6349,1.5690,1.5579,1.5496
1 лабиринт стратегия время_мс посещено_клеток длина_пути замер_1 замер_2 замер_3 замер_4 замер_5 замер_6 замер_7
2 small_10x10 BFS 0.2443 54 15 0.2512 0.2377 0.2104 0.2098 0.3295 0.2628 0.2088
3 small_10x10 DFS 0.1729 33 33 0.1438 0.3230 0.1467 0.1388 0.1570 0.1518 0.1490
4 small_10x10 A* 0.2503 36 15 0.2849 0.4248 0.2063 0.2027 0.2010 0.2022 0.2305
5 medium_50x50 BFS 7.8016 1639 95 9.7252 7.6549 7.1375 6.5061 7.8024 7.9052 7.8801
6 medium_50x50 DFS 5.9674 1063 185 5.5163 4.6949 7.7879 7.1488 5.6215 5.1059 5.8962
7 medium_50x50 A* 4.0049 588 95 4.1553 4.5408 4.1015 3.6816 3.9481 3.7989 3.8081
8 large_100x100 BFS 30.9012 6564 0 33.9413 31.6451 30.0917 31.2012 30.8385 29.2073 29.3836
9 large_100x100 DFS 29.7523 6564 0 31.0923 29.3284 29.7215 29.0498 29.9187 29.9990 29.1565
10 large_100x100 A* 51.4814 6564 0 49.1005 51.0370 50.5359 54.6521 53.4745 49.4512 52.1183
11 open_50x50 BFS 12.2218 2304 95 9.8705 12.1266 9.9576 10.8552 12.0067 18.9221 11.8140
12 open_50x50 DFS 6.8283 1223 1129 7.7999 7.7336 7.2627 5.7576 6.6328 7.1781 5.4337
13 open_50x50 A* 16.5740 2304 95 16.4528 16.5879 17.9772 16.4465 16.1444 16.7420 15.6672
14 no_exit_20x20 BFS 1.0499 260 0 1.1731 1.0768 1.0067 1.0100 1.0201 1.0508 1.0120
15 no_exit_20x20 DFS 1.0160 260 0 1.0409 1.0098 1.0178 1.0072 1.0130 1.0069 1.0166
16 no_exit_20x20 A* 1.5796 260 0 1.5919 1.5782 1.5758 1.6349 1.5690 1.5579 1.5496