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

525 lines
18 KiB
Python
Raw Normal View History

2026-05-24 15:32:34 +00:00
import sys
import os
2026-05-24 15:34:20 +00:00
import time
2026-05-24 15:34:48 +00:00
import csv
2026-05-24 15:33:58 +00:00
from collections import deque
2026-05-24 15:34:20 +00:00
import heapq
2026-05-24 15:34:48 +00:00
import matplotlib.pyplot as plt
import numpy as np
2026-05-24 15:32:34 +00:00
# ========== Модель данных ==========
2026-05-24 15:32:34 +00:00
class Tile:
"""Одна клетка лабиринта."""
2026-05-24 15:32:34 +00:00
def __init__(self, x, y):
self._x = x
self._y = y
self._wall = False
self._entry = False
self._goal = False
@property
2026-05-24 15:33:58 +00:00
def x(self): return self._x
2026-05-24 15:32:34 +00:00
@property
2026-05-24 15:33:58 +00:00
def y(self): return self._y
2026-05-24 15:32:34 +00:00
@property
2026-05-24 15:33:58 +00:00
def is_wall(self): return self._wall
2026-05-24 15:32:34 +00:00
@is_wall.setter
2026-05-24 15:33:58 +00:00
def is_wall(self, value): self._wall = value
2026-05-24 15:32:34 +00:00
@property
2026-05-24 15:33:58 +00:00
def is_entry(self): return self._entry
2026-05-24 15:32:34 +00:00
@is_entry.setter
2026-05-24 15:33:58 +00:00
def is_entry(self, value): self._entry = value
2026-05-24 15:32:34 +00:00
@property
2026-05-24 15:33:58 +00:00
def is_goal(self): return self._goal
2026-05-24 15:32:34 +00:00
@is_goal.setter
2026-05-24 15:33:58 +00:00
def is_goal(self, value): self._goal = value
def can_walk(self):
"""Можно ли встать на эту клетку."""
return not self._wall
2026-05-24 15:32:34 +00:00
class Labyrinth:
"""Прямоугольный лабиринт."""
2026-05-24 15:32:34 +00:00
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
2026-05-24 15:33:58 +00:00
def width(self): return self._width
2026-05-24 15:32:34 +00:00
@property
2026-05-24 15:33:58 +00:00
def height(self): return self._height
2026-05-24 15:32:34 +00:00
@property
2026-05-24 15:33:58 +00:00
def start(self): return self._start
2026-05-24 15:32:34 +00:00
@property
2026-05-24 15:33:58 +00:00
def exit(self): return self._exit
2026-05-24 15:32:34 +00:00
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
2026-05-24 15:32:34 +00:00
if kind == 'wall':
tile.is_wall = True
elif kind == 'entry':
if self._start:
self._start.is_entry = False
2026-05-24 15:32:34 +00:00
tile.is_entry = True
tile.is_wall = False
self._start = tile
elif kind == 'goal':
if self._exit:
self._exit.is_goal = False
2026-05-24 15:32:34 +00:00
tile.is_goal = True
tile.is_wall = False
self._exit = tile
elif kind == 'floor':
tile.is_wall = False
def neighbours(self, tile):
"""Соседние проходимые клетки."""
2026-05-24 15:33:58 +00:00
res = []
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
nb = self.tile_at(tile.x + dx, tile.y + dy)
2026-05-24 15:33:58 +00:00
if nb and nb.can_walk():
res.append(nb)
return res
2026-05-24 15:32:34 +00:00
# ========== Загрузка из файла ==========
2026-05-24 15:32:34 +00:00
class LabyrinthBuilder:
def build(self, filename):
raise NotImplementedError
2026-05-24 15:32:34 +00:00
class TextLabyrinthBuilder(LabyrinthBuilder):
def build(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
2026-05-24 15:33:58 +00:00
lines = [line.rstrip('\n') for line in f]
h = len(lines)
w = max(len(l) for l in lines) if h else 0
if h == 0 or w == 0:
raise ValueError("Файл лабиринта пуст.")
2026-05-24 15:33:58 +00:00
entries = exits = 0
2026-05-24 15:32:34 +00:00
lab = Labyrinth(w, h)
2026-05-24 15:33:58 +00:00
for y, row in enumerate(lines):
2026-05-24 15:32:34 +00:00
for x, ch in enumerate(row):
if ch == '#':
lab.configure_tile(x, y, 'wall')
2026-05-24 15:32:34 +00:00
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')
2026-05-24 15:32:34 +00:00
if entries != 1 or exits != 1:
raise ValueError(f"Некорректный лабиринт: найдено S={entries}, E={exits}")
2026-05-24 15:32:34 +00:00
return lab
# ========== Алгоритмы поиска ==========
2026-05-24 15:33:58 +00:00
class Pathfinder:
def find_path(self, lab, start, goal):
raise NotImplementedError
2026-05-24 15:34:20 +00:00
def _build_path(self, preds, start, goal):
2026-05-24 15:33:58 +00:00
path = []
cur = goal
while cur is not None:
path.append(cur)
2026-05-24 15:34:20 +00:00
cur = preds.get(cur)
2026-05-24 15:33:58 +00:00
path.reverse()
return path
2026-05-24 15:33:58 +00:00
@property
def visited_count(self):
return getattr(self, '_visited', 0)
2026-05-24 15:33:58 +00:00
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 []
2026-05-24 15:33:58 +00:00
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 []
2026-05-24 15:34:20 +00:00
class AStar_Pathfinder(Pathfinder):
def _heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
2026-05-24 15:34:20 +00:00
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 []
# ========== Интерактивный игрок ==========
2026-05-24 15:34:20 +00:00
class Explorer:
def __init__(self, start_tile, labyrinth):
self._current = start_tile
self._previous = None
self._lab = labyrinth
2026-05-24 15:34:20 +00:00
@property
def current(self):
return self._current
2026-05-24 15:34:20 +00:00
def move(self, tile):
if tile and tile.can_walk():
self._previous = self._current
self._current = tile
return True
return False
2026-05-24 15:34:20 +00:00
def undo(self):
if self._previous:
self._current, self._previous = self._previous, None
return True
return False
2026-05-24 15:34:20 +00:00
class Action:
def execute(self): raise NotImplementedError
def undo(self): raise NotImplementedError
2026-05-24 15:34:20 +00:00
class MoveAction(Action):
def __init__(self, explorer, direction, lab):
self._explorer = explorer
self._dx, self._dy = direction
self._lab = lab
self._done = False
2026-05-24 15:34:20 +00:00
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
2026-05-24 15:34:20 +00:00
def undo(self):
if self._done:
self._explorer.undo()
self._done = False
return True
return False
2026-05-24 15:34:20 +00:00
class GameObserver:
def update(self, event, data): raise NotImplementedError
2026-05-24 15:34:20 +00:00
class TerminalDisplay(GameObserver):
def __init__(self, explorer=None):
self._explorer = explorer
self._last_path = None
2026-05-24 15:34:20 +00:00
def update(self, event, data):
if event == 'labyrinth_loaded':
self._draw_lab(data)
elif event == 'path_found':
self._last_path = data
self._show_path_info(data)
elif event == 'player_moved':
self._draw_with_player(data)
2026-05-24 15:34:20 +00:00
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))
print(' S вход E выход # стена . пол')
2026-05-24 15:34:20 +00:00
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=' ')
2026-05-24 15:34:20 +00:00
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:
2026-05-24 15:34:48 +00:00
print(f' Позиция: ({self._explorer.current.x}, {self._explorer.current.y})')
2026-05-24 15:34:20 +00:00
def _show_path_info(self, path):
if not path:
print('\n Путь не найден!')
else:
print(f'\n Длина найденного пути: {len(path)} клеток.')
2026-05-24 15:34:20 +00:00
2026-05-24 15:33:58 +00:00
class LabyrinthSolver:
def __init__(self, lab):
self._lab = lab
self._strategy = None
2026-05-24 15:34:20 +00:00
self._observers = []
def attach(self, obs):
self._observers.append(obs)
2026-05-24 15:34:20 +00:00
def _notify(self, event, data):
for obs in self._observers:
obs.update(event, data)
def set_strategy(self, strategy):
self._strategy = strategy
2026-05-24 15:33:58 +00:00
def solve(self):
if self._strategy is None:
return None
2026-05-24 15:34:20 +00:00
start_t = time.perf_counter()
2026-05-24 15:33:58 +00:00
path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
2026-05-24 15:34:20 +00:00
elapsed = (time.perf_counter() - start_t) * 1000
self._notify('path_found', path)
return {
'time_ms': elapsed,
'visited': self._strategy.visited_count,
'length': len(path)
}
2026-05-24 15:34:20 +00:00
# ========== Эксперименты и визуализация ==========
2026-05-24 15:34:48 +00:00
def run_benchmark(maze_file, strategy, runs=5):
2026-05-24 15:32:34 +00:00
builder = TextLabyrinthBuilder()
2026-05-24 15:34:48 +00:00
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
}
2026-05-24 15:34:48 +00:00
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
2026-05-24 15:34:48 +00:00
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]
2026-05-24 15:34:48 +00:00
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)
2026-05-24 15:34:48 +00:00
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]
2026-05-24 15:34:48 +00:00
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)
2026-05-24 15:34:48 +00:00
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]
2026-05-24 15:34:48 +00:00
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)
2026-05-24 15:34:48 +00:00
plt.tight_layout()
plt.savefig('maze_performance.png', dpi=150, bbox_inches='tight')
plt.show()
# ========== Главный вход ==========
2026-05-24 15:34:48 +00:00
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')