2026-rff_mp/YaroslavtsevAS/docs/2-nd-lab/main.py

454 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import os
import time
import csv
from collections import deque
import heapq
import matplotlib.pyplot as plt
import numpy as np
# ---------------- Модель лабиринта ----------------
class Tile:
def __init__(self, x, y):
self._x = x
self._y = y
self._wall = False
self._entry = False
self._goal = False
@property
def x(self): return self._x
@property
def y(self): return self._y
@property
def is_wall(self): return self._wall
@is_wall.setter
def is_wall(self, value): self._wall = value
@property
def is_entry(self): return self._entry
@is_entry.setter
def is_entry(self, value): self._entry = value
@property
def is_goal(self): return self._goal
@is_goal.setter
def is_goal(self, value): self._goal = value
def can_walk(self): return not self._wall
class Labyrinth:
def __init__(self, width, height):
self._width = width
self._height = height
self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)]
self._start = None
self._exit = None
@property
def width(self): return self._width
@property
def height(self): return self._height
@property
def start(self): return self._start
@property
def exit(self): return self._exit
def tile_at(self, x, y):
if 0 <= x < self._width and 0 <= y < self._height:
return self._grid[y][x]
return None
def configure_tile(self, x, y, kind):
tile = self.tile_at(x, y)
if tile is None: return
if kind == 'wall':
tile.is_wall = True
elif kind == 'entry':
if self._start: self._start.is_entry = False
tile.is_entry = True
tile.is_wall = False
self._start = tile
elif kind == 'goal':
if self._exit: self._exit.is_goal = False
tile.is_goal = True
tile.is_wall = False
self._exit = tile
elif kind == 'floor':
tile.is_wall = False
def neighbours(self, tile):
res = []
for dx, dy in ((0,-1),(0,1),(-1,0),(1,0)):
nb = self.tile_at(tile.x+dx, tile.y+dy)
if nb and nb.can_walk():
res.append(nb)
return res
# ---------------- Загрузка лабиринта ----------------
class LabyrinthBuilder:
def build(self, filename): raise NotImplementedError
class TextLabyrinthBuilder(LabyrinthBuilder):
def build(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
h = len(lines)
w = max(len(l) for l in lines) if h else 0
entries = exits = 0
lab = Labyrinth(w, h)
for y, row in enumerate(lines):
for x, ch in enumerate(row):
if ch == '#': lab.configure_tile(x, y, 'wall')
elif ch == 'S':
lab.configure_tile(x, y, 'entry')
entries += 1
elif ch == 'E':
lab.configure_tile(x, y, 'goal')
exits += 1
else: lab.configure_tile(x, y, 'floor')
if entries != 1 or exits != 1:
raise ValueError(f"Некорректный лабиринт: S={entries}, E={exits}")
return lab
# ---------------- Алгоритмы поиска пути ----------------
class Pathfinder:
def find_path(self, lab, start, goal): raise NotImplementedError
def _build_path(self, preds, start, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = preds.get(cur)
path.reverse()
return path
@property
def visited_count(self): return getattr(self, '_visited', 0)
class BFS_Pathfinder(Pathfinder):
def find_path(self, lab, start, goal):
q = deque([start])
preds = {start: None}
seen = {start}
while q:
cur = q.popleft()
if cur == goal:
self._visited = len(seen)
return self._build_path(preds, start, goal)
for nb in lab.neighbours(cur):
if nb not in seen:
seen.add(nb)
preds[nb] = cur
q.append(nb)
self._visited = len(seen)
return []
class DFS_Pathfinder(Pathfinder):
def find_path(self, lab, start, goal):
stack = [start]
preds = {start: None}
seen = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._visited = len(seen)
return self._build_path(preds, start, goal)
for nb in lab.neighbours(cur):
if nb not in seen:
seen.add(nb)
preds[nb] = cur
stack.append(nb)
self._visited = len(seen)
return []
class AStar_Pathfinder(Pathfinder):
def _heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, lab, start, goal):
heap = []
cnt = 0
f_start = self._heuristic(start, goal)
heapq.heappush(heap, (f_start, cnt, start))
cnt += 1
preds = {}
g = {start: 0}
f = {start: f_start}
seen = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
seen.add(cur)
if cur == goal:
self._visited = len(seen)
return self._build_path(preds, start, goal)
if cur_f > f.get(cur, float('inf')):
continue
for nb in lab.neighbours(cur):
tent_g = g[cur] + 1
if tent_g < g.get(nb, float('inf')):
preds[nb] = cur
g[nb] = tent_g
new_f = tent_g + self._heuristic(nb, goal)
f[nb] = new_f
heapq.heappush(heap, (new_f, cnt, nb))
cnt += 1
self._visited = len(seen)
return []
# ---------------- Компоненты интерактивной игры ----------------
class Explorer:
def __init__(self, start_tile, labyrinth):
self._current = start_tile
self._previous = None
self._lab = labyrinth
@property
def current(self): return self._current
def move(self, tile):
if tile and tile.can_walk():
self._previous = self._current
self._current = tile
return True
return False
def undo(self):
if self._previous:
self._current, self._previous = self._previous, None
return True
return False
class Action:
def execute(self): raise NotImplementedError
def undo(self): raise NotImplementedError
class MoveAction(Action):
def __init__(self, explorer, direction, lab):
self._explorer = explorer
self._dx, self._dy = direction
self._lab = lab
self._done = False
def execute(self):
nx = self._explorer.current.x + self._dx
ny = self._explorer.current.y + self._dy
target = self._lab.tile_at(nx, ny)
if target and target.can_walk():
self._explorer.move(target)
self._done = True
return True
return False
def undo(self):
if self._done:
self._explorer.undo()
self._done = False
return True
return False
class GameObserver:
def update(self, event, data): raise NotImplementedError
class TerminalDisplay(GameObserver):
def __init__(self, explorer=None):
self._explorer = explorer
self._last_path = None
def update(self, event, data):
if event == 'labyrinth_loaded': self._draw_lab(data)
elif event == 'path_found': self._show_path_info(data)
elif event == 'player_moved': self._draw_with_player(data)
def _draw_lab(self, lab):
os.system('cls' if os.name == 'nt' else 'clear')
print('=' * (lab.width * 2 + 4))
print(' ЛАБИРИНТ')
print('=' * (lab.width * 2 + 4))
for y in range(lab.height):
print(' ', end='')
for x in range(lab.width):
t = lab.tile_at(x, y)
if t == lab.start: print('S', end=' ')
elif t == lab.exit: print('E', end=' ')
elif t.is_wall: print('#', end=' ')
else: print('.', end=' ')
print()
print('=' * (lab.width * 2 + 4))
def _draw_with_player(self, lab):
os.system('cls' if os.name == 'nt' else 'clear')
print('=' * (lab.width * 2 + 4))
print(' ЛАБИРИНТ (P игрок)')
print('=' * (lab.width * 2 + 4))
for y in range(lab.height):
print(' ', end='')
for x in range(lab.width):
t = lab.tile_at(x, y)
if self._explorer and t == self._explorer.current: print('P', end=' ')
elif t == lab.start: print('S', end=' ')
elif t == lab.exit: print('E', end=' ')
elif t.is_wall: print('#', end=' ')
else: print('.', end=' ')
print()
print('=' * (lab.width * 2 + 4))
if self._explorer:
print(f' Позиция: ({self._explorer.current.x}, {self._explorer.current.y})')
def _show_path_info(self, path):
if not path: print('\n Путь не найден!')
else: print(f'\n Длина пути: {len(path)} клеток.')
class LabyrinthSolver:
def __init__(self, lab):
self._lab = lab
self._strategy = None
self._observers = []
def attach(self, obs): self._observers.append(obs)
def _notify(self, event, data):
for obs in self._observers: obs.update(event, data)
def set_strategy(self, strategy): self._strategy = strategy
def solve(self):
if self._strategy is None: return None
start_t = time.perf_counter()
path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
elapsed = (time.perf_counter() - start_t) * 1000
self._notify('path_found', path)
return {'time_ms': elapsed, 'visited': self._strategy.visited_count, 'length': len(path)}
# ---------------- Экспериментальный режим ----------------
def run_benchmark(maze_file, strategy, runs=5):
builder = TextLabyrinthBuilder()
lab = builder.build(maze_file)
total_t = total_v = total_l = 0
for _ in range(runs):
solver = LabyrinthSolver(lab)
solver.set_strategy(strategy)
stats = solver.solve()
if stats:
total_t += stats['time_ms']
total_v += stats['visited']
total_l += stats['length']
return {
'time_ms': total_t / runs,
'visited_cells': total_v / runs,
'path_length': total_l / runs
}
def create_charts(results):
mazes = sorted({r['maze'] for r in results})
strategies = ['BFS', 'DFS', 'AStar']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
x = np.arange(len(mazes))
width = 0.25
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]
axes[0].bar(x + i*width, times, width, label=strat)
axes[0].set_title('Время выполнения (мс)')
axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=30, ha='right')
axes[0].legend(); axes[0].grid(alpha=0.3)
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]
axes[1].bar(x + i*width, visited, width, label=strat)
axes[1].set_title('Посещено клеток')
axes[1].set_xticks(x + width)
axes[1].set_xticklabels(mazes, rotation=30, ha='right')
axes[1].legend(); axes[1].grid(alpha=0.3)
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]
axes[2].bar(x + i*width, lengths, width, label=strat)
axes[2].set_title('Длина пути')
axes[2].set_xticks(x + width)
axes[2].set_xticklabels(mazes, rotation=30, ha='right')
axes[2].legend(); axes[2].grid(alpha=0.3)
plt.tight_layout()
plt.savefig('maze_performance.png', dpi=150, bbox_inches='tight')
plt.show()
# ---------------- Главная точка входа ----------------
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
print('Запуск экспериментов...')
maze_list = [
('maze/maze1.txt', 'Small 10x6'),
('maze/maze10x10.txt', 'Medium 10x10'),
('maze/maze20x20.txt', 'Large 20x20'),
('maze/maze_empty.txt', 'Empty 15x15'),
('maze/maze_no_exit.txt', 'No exit 10x10')
]
strategies = [
('BFS', BFS_Pathfinder()),
('DFS', DFS_Pathfinder()),
('AStar', AStar_Pathfinder())
]
all_results = []
for file, name in maze_list:
print(f'\nТестируем {name}...')
for sname, strat in strategies:
try:
stats = run_benchmark(file, strat, runs=3)
all_results.append({
'maze': name,
'strategy': sname,
'time_ms': stats['time_ms'],
'visited_cells': stats['visited_cells'],
'path_length': stats['path_length']
})
print(f' {sname}: время={stats["time_ms"]:.3f}мс, клеток={stats["visited_cells"]:.0f}, длина={stats["path_length"]:.0f}')
except Exception as e:
print(f' {sname}: ошибка {e}')
with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze','strategy','time_ms','visited_cells','path_length'])
writer.writeheader()
writer.writerows(all_results)
if all_results:
create_charts(all_results)
print('\nРезультаты сохранены в experiment_data.csv и maze_performance.png')
else:
# Интерактивная игра
maze_file = 'maze/maze1.txt'
if len(sys.argv) > 1:
maze_file = sys.argv[1]
builder = TextLabyrinthBuilder()
labyrinth = builder.build(maze_file)
player = Explorer(labyrinth.start, labyrinth)
display = TerminalDisplay(player)
display.update('labyrinth_loaded', labyrinth)
solver = LabyrinthSolver(labyrinth)
solver.attach(display)
print('\n УПРАВЛЕНИЕ:')
print(' H влево J вниз K вверх L вправо')
print(' U отменить ход Q выход')
print(' Автопоиск: B BFS D DFS A A*')
print('=' * 50)
history = []
while True:
cmd = input('\n Команда > ').lower().strip()
if cmd == 'q':
print('\n До свидания!')
break
elif cmd == 'b':
solver.set_strategy(BFS_Pathfinder())
stats = solver.solve()
print(f"\n BFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
elif cmd == 'd':
solver.set_strategy(DFS_Pathfinder())
stats = solver.solve()
print(f"\n DFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
elif cmd == 'a':
solver.set_strategy(AStar_Pathfinder())
stats = solver.solve()
print(f"\n A*: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
elif cmd in ('h','j','k','l'):
dirs = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)}
action = MoveAction(player, dirs[cmd], labyrinth)
if action.execute():
history.append(action)
display.update('player_moved', labyrinth)
if player.current == labyrinth.exit:
print('\n ПОЗДРАВЛЯЕМ! ВЫ ВЫБРАЛИСЬ ИЗ ЛАБИРИНТА!')
print(f' Всего ходов: {len(history)}')
break
else:
print('\n Там стена!')
elif cmd == 'u':
if history:
act = history.pop()
act.undo()
display.update('player_moved', labyrinth)
print('\n Ход отменён')
else:
print('\n Нечего отменять')
else:
print('\n Неизвестная команда. Используйте H,J,K,L,U,Q,B,D,A')