[2] Final add some coments and rewrite some defs

This commit is contained in:
YaroslavtsevAS 2026-05-24 15:37:16 +00:00
parent 11d65436e1
commit aae08b7336

View File

@ -7,8 +7,9 @@ import heapq
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
# ---------------- Модель лабиринта ---------------- # ========== Модель данных ==========
class Tile: class Tile:
"""Одна клетка лабиринта."""
def __init__(self, x, y): def __init__(self, x, y):
self._x = x self._x = x
self._y = y self._y = y
@ -32,9 +33,14 @@ class Tile:
def is_goal(self): return self._goal def is_goal(self): return self._goal
@is_goal.setter @is_goal.setter
def is_goal(self, value): self._goal = value def is_goal(self, value): self._goal = value
def can_walk(self): return not self._wall
def can_walk(self):
"""Можно ли встать на эту клетку."""
return not self._wall
class Labyrinth: class Labyrinth:
"""Прямоугольный лабиринт."""
def __init__(self, width, height): def __init__(self, width, height):
self._width = width self._width = width
self._height = height self._height = height
@ -58,16 +64,19 @@ class Labyrinth:
def configure_tile(self, x, y, kind): def configure_tile(self, x, y, kind):
tile = self.tile_at(x, y) tile = self.tile_at(x, y)
if tile is None: return if tile is None:
return
if kind == 'wall': if kind == 'wall':
tile.is_wall = True tile.is_wall = True
elif kind == 'entry': elif kind == 'entry':
if self._start: self._start.is_entry = False if self._start:
self._start.is_entry = False
tile.is_entry = True tile.is_entry = True
tile.is_wall = False tile.is_wall = False
self._start = tile self._start = tile
elif kind == 'goal': elif kind == 'goal':
if self._exit: self._exit.is_goal = False if self._exit:
self._exit.is_goal = False
tile.is_goal = True tile.is_goal = True
tile.is_wall = False tile.is_wall = False
self._exit = tile self._exit = tile
@ -75,16 +84,19 @@ class Labyrinth:
tile.is_wall = False tile.is_wall = False
def neighbours(self, tile): def neighbours(self, tile):
"""Соседние проходимые клетки."""
res = [] res = []
for dx, dy in ((0,-1),(0,1),(-1,0),(1,0)): for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
nb = self.tile_at(tile.x+dx, tile.y+dy) nb = self.tile_at(tile.x + dx, tile.y + dy)
if nb and nb.can_walk(): if nb and nb.can_walk():
res.append(nb) res.append(nb)
return res return res
# ---------------- Загрузка лабиринта ----------------
# ========== Загрузка из файла ==========
class LabyrinthBuilder: class LabyrinthBuilder:
def build(self, filename): raise NotImplementedError def build(self, filename):
raise NotImplementedError
class TextLabyrinthBuilder(LabyrinthBuilder): class TextLabyrinthBuilder(LabyrinthBuilder):
def build(self, filename): def build(self, filename):
@ -92,25 +104,32 @@ class TextLabyrinthBuilder(LabyrinthBuilder):
lines = [line.rstrip('\n') for line in f] lines = [line.rstrip('\n') for line in f]
h = len(lines) h = len(lines)
w = max(len(l) for l in lines) if h else 0 w = max(len(l) for l in lines) if h else 0
if h == 0 or w == 0:
raise ValueError("Файл лабиринта пуст.")
entries = exits = 0 entries = exits = 0
lab = Labyrinth(w, h) lab = Labyrinth(w, h)
for y, row in enumerate(lines): for y, row in enumerate(lines):
for x, ch in enumerate(row): for x, ch in enumerate(row):
if ch == '#': lab.configure_tile(x, y, 'wall') if ch == '#':
lab.configure_tile(x, y, 'wall')
elif ch == 'S': elif ch == 'S':
lab.configure_tile(x, y, 'entry') lab.configure_tile(x, y, 'entry')
entries += 1 entries += 1
elif ch == 'E': elif ch == 'E':
lab.configure_tile(x, y, 'goal') lab.configure_tile(x, y, 'goal')
exits += 1 exits += 1
else: lab.configure_tile(x, y, 'floor') else:
lab.configure_tile(x, y, 'floor')
if entries != 1 or exits != 1: if entries != 1 or exits != 1:
raise ValueError(f"Некорректный лабиринт: S={entries}, E={exits}") raise ValueError(f"Некорректный лабиринт: найдено S={entries}, E={exits}")
return lab return lab
# ---------------- Алгоритмы поиска пути ----------------
# ========== Алгоритмы поиска ==========
class Pathfinder: class Pathfinder:
def find_path(self, lab, start, goal): raise NotImplementedError def find_path(self, lab, start, goal):
raise NotImplementedError
def _build_path(self, preds, start, goal): def _build_path(self, preds, start, goal):
path = [] path = []
cur = goal cur = goal
@ -119,8 +138,11 @@ class Pathfinder:
cur = preds.get(cur) cur = preds.get(cur)
path.reverse() path.reverse()
return path return path
@property @property
def visited_count(self): return getattr(self, '_visited', 0) def visited_count(self):
return getattr(self, '_visited', 0)
class BFS_Pathfinder(Pathfinder): class BFS_Pathfinder(Pathfinder):
def find_path(self, lab, start, goal): def find_path(self, lab, start, goal):
@ -140,6 +162,7 @@ class BFS_Pathfinder(Pathfinder):
self._visited = len(seen) self._visited = len(seen)
return [] return []
class DFS_Pathfinder(Pathfinder): class DFS_Pathfinder(Pathfinder):
def find_path(self, lab, start, goal): def find_path(self, lab, start, goal):
stack = [start] stack = [start]
@ -158,9 +181,11 @@ class DFS_Pathfinder(Pathfinder):
self._visited = len(seen) self._visited = len(seen)
return [] return []
class AStar_Pathfinder(Pathfinder): class AStar_Pathfinder(Pathfinder):
def _heuristic(self, a, b): def _heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y) return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, lab, start, goal): def find_path(self, lab, start, goal):
heap = [] heap = []
cnt = 0 cnt = 0
@ -191,36 +216,44 @@ class AStar_Pathfinder(Pathfinder):
self._visited = len(seen) self._visited = len(seen)
return [] return []
# ---------------- Компоненты интерактивной игры ----------------
# ========== Интерактивный игрок ==========
class Explorer: class Explorer:
def __init__(self, start_tile, labyrinth): def __init__(self, start_tile, labyrinth):
self._current = start_tile self._current = start_tile
self._previous = None self._previous = None
self._lab = labyrinth self._lab = labyrinth
@property @property
def current(self): return self._current def current(self):
return self._current
def move(self, tile): def move(self, tile):
if tile and tile.can_walk(): if tile and tile.can_walk():
self._previous = self._current self._previous = self._current
self._current = tile self._current = tile
return True return True
return False return False
def undo(self): def undo(self):
if self._previous: if self._previous:
self._current, self._previous = self._previous, None self._current, self._previous = self._previous, None
return True return True
return False return False
class Action: class Action:
def execute(self): raise NotImplementedError def execute(self): raise NotImplementedError
def undo(self): raise NotImplementedError def undo(self): raise NotImplementedError
class MoveAction(Action): class MoveAction(Action):
def __init__(self, explorer, direction, lab): def __init__(self, explorer, direction, lab):
self._explorer = explorer self._explorer = explorer
self._dx, self._dy = direction self._dx, self._dy = direction
self._lab = lab self._lab = lab
self._done = False self._done = False
def execute(self): def execute(self):
nx = self._explorer.current.x + self._dx nx = self._explorer.current.x + self._dx
ny = self._explorer.current.y + self._dy ny = self._explorer.current.y + self._dy
@ -230,6 +263,7 @@ class MoveAction(Action):
self._done = True self._done = True
return True return True
return False return False
def undo(self): def undo(self):
if self._done: if self._done:
self._explorer.undo() self._explorer.undo()
@ -237,17 +271,25 @@ class MoveAction(Action):
return True return True
return False return False
class GameObserver: class GameObserver:
def update(self, event, data): raise NotImplementedError def update(self, event, data): raise NotImplementedError
class TerminalDisplay(GameObserver): class TerminalDisplay(GameObserver):
def __init__(self, explorer=None): def __init__(self, explorer=None):
self._explorer = explorer self._explorer = explorer
self._last_path = None self._last_path = None
def update(self, event, data): def update(self, event, data):
if event == 'labyrinth_loaded': self._draw_lab(data) if event == 'labyrinth_loaded':
elif event == 'path_found': self._show_path_info(data) self._draw_lab(data)
elif event == 'player_moved': self._draw_with_player(data) elif event == 'path_found':
self._last_path = data
self._show_path_info(data)
elif event == 'player_moved':
self._draw_with_player(data)
def _draw_lab(self, lab): def _draw_lab(self, lab):
os.system('cls' if os.name == 'nt' else 'clear') os.system('cls' if os.name == 'nt' else 'clear')
print('=' * (lab.width * 2 + 4)) print('=' * (lab.width * 2 + 4))
@ -263,6 +305,8 @@ class TerminalDisplay(GameObserver):
else: print('.', end=' ') else: print('.', end=' ')
print() print()
print('=' * (lab.width * 2 + 4)) print('=' * (lab.width * 2 + 4))
print(' S вход E выход # стена . пол')
def _draw_with_player(self, lab): def _draw_with_player(self, lab):
os.system('cls' if os.name == 'nt' else 'clear') os.system('cls' if os.name == 'nt' else 'clear')
print('=' * (lab.width * 2 + 4)) print('=' * (lab.width * 2 + 4))
@ -272,7 +316,8 @@ class TerminalDisplay(GameObserver):
print(' ', end='') print(' ', end='')
for x in range(lab.width): for x in range(lab.width):
t = lab.tile_at(x, y) t = lab.tile_at(x, y)
if self._explorer and t == self._explorer.current: print('P', end=' ') if self._explorer and t == self._explorer.current:
print('P', end=' ')
elif t == lab.start: print('S', end=' ') elif t == lab.start: print('S', end=' ')
elif t == lab.exit: print('E', end=' ') elif t == lab.exit: print('E', end=' ')
elif t.is_wall: print('#', end=' ') elif t.is_wall: print('#', end=' ')
@ -281,28 +326,45 @@ class TerminalDisplay(GameObserver):
print('=' * (lab.width * 2 + 4)) print('=' * (lab.width * 2 + 4))
if self._explorer: if self._explorer:
print(f' Позиция: ({self._explorer.current.x}, {self._explorer.current.y})') print(f' Позиция: ({self._explorer.current.x}, {self._explorer.current.y})')
def _show_path_info(self, path): def _show_path_info(self, path):
if not path: print('\n Путь не найден!') if not path:
else: print(f'\n Длина пути: {len(path)} клеток.') print('\n Путь не найден!')
else:
print(f'\n Длина найденного пути: {len(path)} клеток.')
class LabyrinthSolver: class LabyrinthSolver:
def __init__(self, lab): def __init__(self, lab):
self._lab = lab self._lab = lab
self._strategy = None self._strategy = None
self._observers = [] self._observers = []
def attach(self, obs): self._observers.append(obs)
def attach(self, obs):
self._observers.append(obs)
def _notify(self, event, data): def _notify(self, event, data):
for obs in self._observers: obs.update(event, data) for obs in self._observers:
def set_strategy(self, strategy): self._strategy = strategy obs.update(event, data)
def set_strategy(self, strategy):
self._strategy = strategy
def solve(self): def solve(self):
if self._strategy is None: return None if self._strategy is None:
return None
start_t = time.perf_counter() start_t = time.perf_counter()
path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit) path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
elapsed = (time.perf_counter() - start_t) * 1000 elapsed = (time.perf_counter() - start_t) * 1000
self._notify('path_found', path) self._notify('path_found', path)
return {'time_ms': elapsed, 'visited': self._strategy.visited_count, 'length': len(path)} return {
'time_ms': elapsed,
'visited': self._strategy.visited_count,
'length': len(path)
}
# ---------------- Экспериментальный режим ----------------
# ========== Эксперименты и визуализация ==========
def run_benchmark(maze_file, strategy, runs=5): def run_benchmark(maze_file, strategy, runs=5):
builder = TextLabyrinthBuilder() builder = TextLabyrinthBuilder()
lab = builder.build(maze_file) lab = builder.build(maze_file)
@ -321,38 +383,47 @@ def run_benchmark(maze_file, strategy, runs=5):
'path_length': total_l / runs 'path_length': total_l / runs
} }
def create_charts(results): def create_charts(results):
mazes = sorted({r['maze'] for r in results}) mazes = sorted({r['maze'] for r in results})
strategies = ['BFS', 'DFS', 'AStar'] strategies = ['BFS', 'DFS', 'AStar']
fig, axes = plt.subplots(1, 3, figsize=(15, 5)) fig, axes = plt.subplots(1, 3, figsize=(15, 5))
x = np.arange(len(mazes)) x = np.arange(len(mazes))
width = 0.25 width = 0.25
for i, strat in enumerate(strategies): for i, strat in enumerate(strategies):
times = [next((r['time_ms'] for r in results if r['maze']==m and r['strategy']==strat), 0) for m in mazes] times = [next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes]
axes[0].bar(x + i*width, times, width, label=strat) axes[0].bar(x + i*width, times, width, label=strat)
axes[0].set_title('Время выполнения (мс)') axes[0].set_title('Время выполнения (мс)')
axes[0].set_xticks(x + width) axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=30, ha='right') axes[0].set_xticklabels(mazes, rotation=30, ha='right')
axes[0].legend(); axes[0].grid(alpha=0.3) axes[0].legend()
axes[0].grid(alpha=0.3)
for i, strat in enumerate(strategies): for i, strat in enumerate(strategies):
visited = [next((r['visited_cells'] for r in results if r['maze']==m and r['strategy']==strat), 0) for m in mazes] visited = [next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes]
axes[1].bar(x + i*width, visited, width, label=strat) axes[1].bar(x + i*width, visited, width, label=strat)
axes[1].set_title('Посещено клеток') axes[1].set_title('Посещено клеток')
axes[1].set_xticks(x + width) axes[1].set_xticks(x + width)
axes[1].set_xticklabels(mazes, rotation=30, ha='right') axes[1].set_xticklabels(mazes, rotation=30, ha='right')
axes[1].legend(); axes[1].grid(alpha=0.3) axes[1].legend()
axes[1].grid(alpha=0.3)
for i, strat in enumerate(strategies): for i, strat in enumerate(strategies):
lengths = [next((r['path_length'] for r in results if r['maze']==m and r['strategy']==strat), 0) for m in mazes] lengths = [next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes]
axes[2].bar(x + i*width, lengths, width, label=strat) axes[2].bar(x + i*width, lengths, width, label=strat)
axes[2].set_title('Длина пути') axes[2].set_title('Длина пути')
axes[2].set_xticks(x + width) axes[2].set_xticks(x + width)
axes[2].set_xticklabels(mazes, rotation=30, ha='right') axes[2].set_xticklabels(mazes, rotation=30, ha='right')
axes[2].legend(); axes[2].grid(alpha=0.3) axes[2].legend()
axes[2].grid(alpha=0.3)
plt.tight_layout() plt.tight_layout()
plt.savefig('maze_performance.png', dpi=150, bbox_inches='tight') plt.savefig('maze_performance.png', dpi=150, bbox_inches='tight')
plt.show() plt.show()
# ---------------- Главная точка входа ----------------
# ========== Главный вход ==========
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == 'experiment': if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
print('Запуск экспериментов...') print('Запуск экспериментов...')