[2] labirint

This commit is contained in:
MashinDD 2026-05-17 16:50:48 +03:00
parent a40c5f579a
commit 94167bedca
16 changed files with 1064 additions and 0 deletions

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: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

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,100 @@
####################################################################################################
#S### # ## ## # # # ## ####### ## # # ## ## ## # # ### # #
# # # # # # # # # # # # # # # # # ## ### # ## ## ## # ## ## #
# # # # # # ## # # ## ## # # # # # ### # # # ### # # # # ## ##
# ## ## ### # # # # # ### # # # ## # # # ## #
# ## # ## #### # # # # # # ## ## #### ## # # # #
### # # # # # # # # ### #### # # # ## # # # # # # # # #
# # ## ## # ## ##### ## ###### # # ## # ## # # ## #### #
# ## ## ## ## ## ## # # # # # # ## # # #
## # # ## # # # # # # ## # # # # ## # # ###
## # # # # # # # # ## ## # # # # ### ## # #
## # # # # # ## # ## # ## # # #### ## # ## # # # ## ## # #
# # # # # # ## # # ## # ## # # # # ### # # # # # # ### # #
## # ## ## # # # # ### # ## ## # # ### ## # #
## ## # # ## ### # # # # # # # # ## # # # # # #
# ## # # ## # ### ## # # # ## # # # ## # # # #### # # # #
# # # # # # # ## ## ## # # # # ### # # #
# # # #### # # # # ## # ### # # #### # # # # # #
# # # # # # ## # # # # # # # ## # ### # ##
## # ### ## ## # # # # # # # # # # # # # # ### ## # #
## ## ### # ## # # ### ## # # # # ## # # # # # # # #
##### # # # #### # ## # # # # # # ### # ## # # # # #
## # # ### # # # # ## # # # # # # #### # # # ### #
# # ## ## # ### # # ## # ## ## ### # # # # # # # ###
## ## # # # # # # # # # # ## ## # # ##
# # # ### # # # # # ## # # # ### # # # # # ## ## ## # ## #
# # # # # ##### # ## # # # # # # # # # ## ## # # # ##
# # # # # # # ## # ## # # # # # # ## ### ## # # ##### #
# # # # # ## # # ## # # ## # ## # # # # ## # # # ## #
## ## # # # # # # ### # ## ### ## # ### # ## # # # ## # # ## # #
# # # # #### # ## #### # # # # # # # # # # ### # ## # #
# # ## # # # # # # # # # # ###### # ## # ## # # # #### #### # #
# # ##### # # # ### # # # # # # # # # ## ### # #
# # # # # # # ## # # ## # # ## # # # # # # # ## # # ###
## # ## # # # # #### # # ## # ## ## # ## # # ## # #
## # # # ## # # # # # # # # # # # # ###### # ## # # ## ### # #### # #
## # # # # # # # # # # # ## # # # # # # ## # # # ## # ##
## # # # ### # # # # # # # # # # # # # # ###
# # ### # # # # # ## ## ## # # ## # ### ### # # #
# # # # # ## # # ## ## # # # # # # ## ## ## #
# ### # # ### # # # # ### # # # # # # # ## # ##
# # ### ## ## ## ## # # ### # ## # # # # ## ## # # # # # #
# ## # # # ## # # # # ## # ### #### # ## ###### ### #
# # # # ### ### # # ## # # # ### ## # ## # # ## ##
# # # ### #### # # # # ### # # # ## ### ## # ## #### # #
# ### ## # # # # # # # # ### # # # # ## # ### ### ## #
# # # # # # # # # # ## ### ## ### # ## # # # ## # #### # ## # #
# # # # # # # # # # # ### # # # # # ## # # # # # # #
# ## # # # # ## # # # # ## ## ## # # ## # ## # # ## # ## #
# # # ## # # # # ### # # # # # # # ## # # # ## # ### ## # # #
## # ## # ## ### ## # # # # ## # # # # # # #
## ## # # ### # # # # # ## # # # # # ## # ## # # # #
# # # ## # ### # ## # # ## # # # # # # # #
# # # # # ## #### # # ### # ## # # ## # # ## #
# # # # ## # ### # ## ## # # # # ### # # #
# # # # # # # # # ## # ## ## ### ### # # ## # # # ## #
# # # # ## # # ### ##### # # # # ## # # # # # ## # # #
## # # # ## # # ## # ## ## # ## # ### # # # # #
# ## ## # ### # ## ### # # ## # # # # # # # # # # # ###
# ## # # # # # # # # # # # ## # # # # # # # # # # # ## #
# # # # ## # # # # # ## # # ## # # ## # # # ### ### # # # ##
# # # # ## # ## # # # # # # # ## # # ## # ### ##
### # # ## ### # ## # # #### # # # # ##### # ## #### #
# # # # # # # #### ## # ### ### # ## # ## # # ## # # # # # # ###
# #### # ## # # # # # # ## # # # # # # # #
# ## # # # # # # ## # ## ## # ### #### # # # # ## #
# # ## # ## # # # # ## ## # ## # ## #
# # # # # # # ## # # # # # # ### ## ### # ## # # ###
### # # # ##### # ## ## # # # ## # ## ## # # # # # #
# # # # # # ## ##### # ### # ## # # # ## # ### #### # #
# # ### # ## # # ### ## ## # ## # ### # ## ### # ###
# ## ## ## # # # # # # ### # ## # # ## # # # #
## ## ## # ## # ## # # # ## # ## # ## # ## # # # #
# # # # # # # # ## # # # ####### # ## ## ## ##
# # # # # # # # # ## # # # # # ## # # ### # ##
# # ## #### # # # # # ## ### # ### # ### # ### ## # # #
## # # ## # # # # # # # # ## # ##### # ## ##### #### ###
# # # # ## # ## # # ## # # ### ## ## # ######
# # ## # # # # # # # # # ## ## # ## ## ## # ## # #
### #### # # ## # # # # # ## # # ## # # # #### # # ## # #
# ## ## # # ## # ## ## # # ## # # # # # #### # #
# ## # # # ## ### ## #### # # # # # # ## ### # # # ##
## # # # # # # # ## # ## ### # ## # ## # # # #
# # # # # # # # # ### # # # ## # # ## ## # #### #
# # ## # # # # # # # # # # ## ### # # # ##
## ## # ## # # # ## # # # # # #### # # ## ### #
## # ## ## # # # # ### # # ## # # # ## ## # # # # ## #
# ## # ## # # #### # # # # # # ## # # # # # # ### #
# ## # #### # # ## # # # # ### ## # ## ### # ## ## ##
# # # # # # ## # # # ## # #### # ##### # # # # # # # #
# # ## ## ### # ### ### # # #### # # # # ## # ## # # # # #### # #
# # # # ## # # ## # # ## # # ## # ## # # # ## ## #
# # ## # # # ## ## # ### ## # ## # # # # # # # ## # # #
# # ## # ## ## ## # # ## # # # # # ## # # # # ### # #
# # # ## # # # # # # # # # # # # ## # # # ## # # #
## # ## # # # # ## # # ## # # # # # # ## # # # # # # # #
# # ## # ## # ### # # ### # ## # # # ## # ### # ## # #
# # # ## # # ## # # # ## # # #### ## # # # ### # ##
# #### ## ### ### # # ### # # ## # # # ### # ####### # ## # #E#
####################################################################################################

View File

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

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,20 @@
####################
#S# # # ## #
# # # ## #
# # # # #
# # # # #
# ###
## # # # #
# # # # #
## # # # # #
# # # #
# # # ## #
# # # ##
# # # #
# # # # ## #
# # #
## # # ##
# ### ##
# # ## ###
# # # #E#
####################

View File

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

View File

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

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,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,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,16 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути,замер_1,замер_2,замер_3,замер_4,замер_5,замер_6,замер_7
small_10x10,BFS,0.0594,54,15,0.0748,0.0593,0.0566,0.0550,0.0572,0.0570,0.0558
small_10x10,DFS,0.0376,33,33,0.0434,0.0371,0.0356,0.0354,0.0351,0.0398,0.0368
small_10x10,A*,0.0567,36,15,0.0700,0.0572,0.0526,0.0532,0.0543,0.0557,0.0538
medium_50x50,BFS,1.7956,1639,95,1.9363,2.0031,1.8182,1.6895,1.7023,1.7157,1.7044
medium_50x50,DFS,1.1344,1063,185,1.1353,1.1197,1.0973,1.1086,1.1111,1.1919,1.1772
medium_50x50,A*,0.9810,588,95,0.9948,1.0057,0.9409,1.0389,0.9598,0.9937,0.9334
large_100x100,BFS,7.6139,6564,0,8.5640,7.3117,7.5035,7.3270,7.2386,7.8444,7.5083
large_100x100,DFS,7.0206,6564,0,7.2992,6.9348,7.0939,7.2919,6.9533,6.7842,6.7870
large_100x100,A*,10.8821,6564,0,11.4343,11.5845,10.8324,10.3998,10.9124,10.6376,10.3738
open_50x50,BFS,2.3515,2304,95,2.5185,2.3020,2.3359,2.2938,2.4015,2.3200,2.2889
open_50x50,DFS,1.4357,1223,1129,1.6775,1.6397,1.3331,1.3384,1.3042,1.4293,1.3276
open_50x50,A*,3.6998,2304,95,3.9999,3.5379,3.4987,3.9374,3.7333,3.7175,3.4739
no_exit_20x20,BFS,0.2706,260,0,0.3313,0.3122,0.2750,0.2455,0.2482,0.2424,0.2397
no_exit_20x20,DFS,0.2807,260,0,0.3192,0.3031,0.3427,0.2430,0.2509,0.2406,0.2654
no_exit_20x20,A*,0.4247,260,0,0.4879,0.4647,0.4534,0.4483,0.3620,0.3585,0.3979
1 лабиринт стратегия время_мс посещено_клеток длина_пути замер_1 замер_2 замер_3 замер_4 замер_5 замер_6 замер_7
2 small_10x10 BFS 0.0594 54 15 0.0748 0.0593 0.0566 0.0550 0.0572 0.0570 0.0558
3 small_10x10 DFS 0.0376 33 33 0.0434 0.0371 0.0356 0.0354 0.0351 0.0398 0.0368
4 small_10x10 A* 0.0567 36 15 0.0700 0.0572 0.0526 0.0532 0.0543 0.0557 0.0538
5 medium_50x50 BFS 1.7956 1639 95 1.9363 2.0031 1.8182 1.6895 1.7023 1.7157 1.7044
6 medium_50x50 DFS 1.1344 1063 185 1.1353 1.1197 1.0973 1.1086 1.1111 1.1919 1.1772
7 medium_50x50 A* 0.9810 588 95 0.9948 1.0057 0.9409 1.0389 0.9598 0.9937 0.9334
8 large_100x100 BFS 7.6139 6564 0 8.5640 7.3117 7.5035 7.3270 7.2386 7.8444 7.5083
9 large_100x100 DFS 7.0206 6564 0 7.2992 6.9348 7.0939 7.2919 6.9533 6.7842 6.7870
10 large_100x100 A* 10.8821 6564 0 11.4343 11.5845 10.8324 10.3998 10.9124 10.6376 10.3738
11 open_50x50 BFS 2.3515 2304 95 2.5185 2.3020 2.3359 2.2938 2.4015 2.3200 2.2889
12 open_50x50 DFS 1.4357 1223 1129 1.6775 1.6397 1.3331 1.3384 1.3042 1.4293 1.3276
13 open_50x50 A* 3.6998 2304 95 3.9999 3.5379 3.4987 3.9374 3.7333 3.7175 3.4739
14 no_exit_20x20 BFS 0.2706 260 0 0.3313 0.3122 0.2750 0.2455 0.2482 0.2424 0.2397
15 no_exit_20x20 DFS 0.2807 260 0 0.3192 0.3031 0.3427 0.2430 0.2509 0.2406 0.2654
16 no_exit_20x20 A* 0.4247 260 0 0.4879 0.4647 0.4534 0.4483 0.3620 0.3585 0.3979

View File

@ -0,0 +1,235 @@
# Отчёт: Поиск выхода из лабиринта (ООП + паттерны проектирования)
## Цель работы
Разработать гибкую расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. Применить минимум 3 паттерна проектирования из списка GoF и обосновать их выбор.
---
## Применённые паттерны проектирования
### 1. Builder (Строитель) — `maze_builder.py`
**Задача:** загрузка лабиринта из файла — сложный процесс (парсинг, валидация, расстановка старта/выхода).
**Решение:** интерфейс `MazeBuilder` с методом `build_from_file()` и реализация `TextFileMazeBuilder`. Клиентский код работает только с интерфейсом и не знает деталей парсинга.
**Преимущество:** чтобы добавить поддержку JSON или бинарного формата — достаточно создать новый класс, не трогая ничего остального.
### 2. Strategy (Стратегия) — `maze_strategies.py`
**Задача:** несколько алгоритмов поиска пути (BFS, DFS, A*) нужно переключать без изменения кода оркестратора.
**Решение:** интерфейс `PathFindingStrategy` с методом `find_path()`. Каждый алгоритм — отдельный класс. `MazeSolver.set_strategy()` меняет алгоритм в одну строку.
**Преимущество:** новый алгоритм (например, Dijkstra) добавляется реализацией интерфейса, без правок в `MazeSolver`.
### 3. Observer (Наблюдатель) — `maze_solver.py`
**Задача:** отображать события (путь найден, игрок переместился) без жёсткой связи между логикой и интерфейсом.
**Решение:** интерфейс `Observer` с методом `update(event, data)`. `ConsoleView` подписывается на `MazeSolver` и реагирует на события `path_found`, `maze_loaded`, `move`.
**Преимущество:** можно добавить графический интерфейс или логгер, не меняя логику решателя.
### 4. Command (Команда) — `maze_solver.py`
**Задача:** пошаговое перемещение игрока с возможностью отмены хода.
**Решение:** интерфейс `Command` с `execute()` и `undo()`. `MoveCommand` хранит предыдущую клетку и умеет откатить ход. `Player` хранит текущую позицию.
**Преимущество:** история команд позволяет реализовать `Ctrl+Z` для любого количества шагов.
---
## Диаграмма классов (Mermaid)
```mermaid
classDiagram
class MazeBuilder {
<<interface>>
+build_from_file(filename) Maze
}
class TextFileMazeBuilder {
+build_from_file(filename) Maze
}
class Maze {
-int width, height
-Cell[][] cells
-Cell start
-Cell exit
+get_cell(x, y) Cell
+get_neighbors(cell) list
+render(path, player_pos)
}
class Cell {
-int x, y
-bool is_wall
-bool is_start
-bool is_exit
+is_passable() bool
}
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit) list
}
class BFSStrategy { +find_path() }
class DFSStrategy { +find_path() }
class AStarStrategy { +find_path() }
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
-list observers
+set_strategy(strategy)
+add_observer(observer)
+solve() SearchStats
}
class SearchStats {
+float time_ms
+int visited_cells
+int path_length
+list path
}
class Observer {
<<interface>>
+update(event, data)
}
class ConsoleView { +update(event, data) }
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Cell target_cell
-Cell previous_cell
+execute()
+undo()
}
class Player {
-Cell current_cell
+move_to(cell)
}
MazeBuilder <|.. TextFileMazeBuilder
TextFileMazeBuilder ..> Maze : creates
Maze o-- Cell
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
MazeSolver --> Maze
MazeSolver --> PathFindingStrategy
MazeSolver --> SearchStats
MazeSolver --> Observer
Observer <|.. ConsoleView
Command <|.. MoveCommand
MoveCommand --> Player
Player --> Cell
```
---
## Экспериментальная часть
### Параметры эксперимента
| Параметр | Значение |
|---|---|
| Повторений на замер | 7 |
| Алгоритмы | BFS, DFS, A* |
### Тестовые лабиринты
| Название | Размер | Особенность |
|---|---|---|
| small_10x10 | 10×10 | Маленький, простой путь |
| medium_50x50 | 50×50 | Средний, тупики (28% стен) |
| large_100x100 | 100×100 | Большой (30% стен) |
| open_50x50 | 50×50 | Без внутренних стен |
| no_exit_20x20 | 20×20 | Выход недостижим |
---
## Результаты
### Таблица средних значений
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|---|---|---|---|---|
| small_10x10 | BFS | 0.094 | 54 | 15 |
| small_10x10 | DFS | 0.059 | 33 | 33 |
| small_10x10 | A* | 0.078 | 36 | 15 |
| medium_50x50 | BFS | 2.446 | 1639 | 95 |
| medium_50x50 | DFS | 1.480 | 1063 | 185 |
| medium_50x50 | A* | 1.528 | 588 | 95 |
| large_100x100 | BFS | 9.891 | 6564 | — |
| large_100x100 | DFS | 9.057 | 6564 | — |
| large_100x100 | A* | 17.578 | 6564 | — |
| open_50x50 | BFS | 3.296 | 2304 | 95 |
| open_50x50 | DFS | 1.830 | 1223 | 1129 |
| open_50x50 | A* | 5.566 | 2304 | 95 |
| no_exit_20x20 | BFS | 0.368 | 260 | — |
| no_exit_20x20 | DFS | 0.343 | 260 | — |
| no_exit_20x20 | A* | 0.607 | 260 | — |
*«—» означает путь не найден (все доступные клетки исчерпаны)*
### Визуализация
![Время выполнения](data/chart_время-мс.png)
![Посещено клеток](data/chart_посещено-клеток.png)
![Длина пути](data/chart_длина-пути.png)
---
## Анализ результатов
### 1. BFS — оптимальный путь, высокое покрытие
BFS всегда находит **кратчайший путь** (15 шагов на small, 95 на medium). Но для этого он обходит больше клеток, чем DFS: на medium_50x50 посетил 1639 против 1063 у DFS. Это нормально — BFS расширяется волнами во все стороны.
### 2. DFS — быстрый по времени, длинный путь
DFS посещает меньше клеток в среднем, но путь получается значительно длиннее: 185 шагов против 95 у BFS на том же лабиринте. На открытом лабиринте без стен DFS нашёл путь в **1129 шагов** вместо 95 у BFS — наглядная демонстрация того, что DFS не гарантирует оптимальности.
### 3. A* — меньше всего посещённых клеток
На medium_50x50 A* посетил всего **588 клеток** против 1639 у BFS — в 2.8 раза меньше. При этом путь тот же оптимальный (95 шагов). Манхэттенская эвристика направляет поиск к выходу и отсекает лишние направления.
На открытом лабиринте без стен A* тратит больше времени (5.566 мс против 3.296 мс у BFS) — эвристика считается для каждого узла, а без препятствий нет выигрыша в отсечении.
### 4. Большой лабиринт (100×100) — путь не найден
Все три алгоритма исчерпали все 6564 доступные клетки и не нашли пути. При плотности стен 30% на данном лабиринте выход оказался недостижим. Все алгоритмы корректно вернули пустой результат.
### 5. Лабиринт без выхода
Все алгоритмы корректно обработали случай недостижимого выхода, посетив все 260 доступных клеток.
---
## Выводы
### Когда какой алгоритм выбирать
| Задача | Рекомендация |
|---|---|
| Нужен кратчайший путь | BFS или A* |
| Нужно быстро найти хоть какой-то путь | DFS |
| Большой лабиринт, нужна оптимальность | A* (посещает меньше клеток) |
| Лабиринт без препятствий | BFS (A* теряет преимущество) |
| Обнаружить недостижимый выход | Любой — все обходят все клетки |
### Как ООП и паттерны помогли
**Без паттернов** весь код был бы в одной функции: парсинг, алгоритм и вывод перемешаны. Добавление нового алгоритма требовало бы правки основного кода.
**С паттернами:**
- `Builder` — смена формата файла (txt → JSON) не затрагивает логику поиска
- `Strategy` — новый алгоритм добавляется одним классом без правок `MazeSolver`
- `Observer``ConsoleView` отключается или заменяется GUI без правок логики
- `Command` — история ходов и отмена реализуются без изменения `Player`
Каждый класс отвечает за одну вещь, код можно тестировать по частям независимо.