2026-rff_mp/lukovnikovde/docs/data/2-nd-exercise/main.py

527 lines
18 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
from collections import deque
import heapq
import time
import csv
import matplotlib.pyplot as plt
import numpy as np
# ---------- 1. Модель клетки и лабиринта ----------
class Tile:
def __init__(self, x, y):
self.x = x
self.y = y
self.wall = False
self.start = False
self.exit = False
def passable(self):
return not self.wall
class Labyrinth:
def __init__(self, width, height):
self.width = width
self.height = height
self._tiles = [[Tile(x, y) for x in range(width)] for y in range(height)]
self.start_tile = None
self.exit_tile = None
def get_tile(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self._tiles[y][x]
return None
def set_tile(self, x, y, kind):
tile = self.get_tile(x, y)
if tile is None:
return
if kind == 'wall':
tile.wall = True
elif kind == 'start':
if self.start_tile:
self.start_tile.start = False
tile.start = True
tile.wall = False
self.start_tile = tile
elif kind == 'exit':
if self.exit_tile:
self.exit_tile.exit = False
tile.exit = True
tile.wall = False
self.exit_tile = tile
elif kind == 'path':
tile.wall = False
def neighbours(self, tile):
dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
result = []
for dx, dy in dirs:
nx, ny = tile.x + dx, tile.y + dy
nb = self.get_tile(nx, ny)
if nb and nb.passable():
result.append(nb)
return result
# ---------- 2. Загрузка лабиринта из файла ----------
class LabyrinthLoader:
def load(self, filename):
raise NotImplementedError
class TextLabyrinthLoader(LabyrinthLoader):
def load(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(line) for line in lines) if h > 0 else 0
start_cnt = 0
exit_cnt = 0
lab = Labyrinth(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
lab.set_tile(x, y, 'wall')
elif ch == 'S':
lab.set_tile(x, y, 'start')
start_cnt += 1
elif ch == 'E':
lab.set_tile(x, y, 'exit')
exit_cnt += 1
else:
lab.set_tile(x, y, 'path')
if start_cnt != 1 or exit_cnt != 1:
raise ValueError(f"Нужны ровно S и E, найдено S={start_cnt}, E={exit_cnt}")
return lab
# ---------- 3. Алгоритмы поиска пути (стратегии) ----------
class Pathfinder:
def find_path(self, lab, start, goal):
raise NotImplementedError
def _build_path(self, came_from, start, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = came_from.get(cur)
path.reverse()
return path
def visited_count(self):
return getattr(self, '_visited', 0)
class BFS(Pathfinder):
def find_path(self, lab, start, goal):
q = deque([start])
parent = {start: None}
visited = {start}
while q:
cur = q.popleft()
if cur == goal:
self._visited = len(visited)
return self._build_path(parent, start, goal)
for nb in lab.neighbours(cur):
if nb not in visited:
visited.add(nb)
parent[nb] = cur
q.append(nb)
self._visited = len(visited)
return []
class DFS(Pathfinder):
def find_path(self, lab, start, goal):
stack = [start]
parent = {start: None}
visited = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._visited = len(visited)
return self._build_path(parent, start, goal)
for nb in lab.neighbours(cur):
if nb not in visited:
visited.add(nb)
parent[nb] = cur
stack.append(nb)
self._visited = len(visited)
return []
class AStar(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 = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g = {start: 0}
f = {start: start_f}
visited = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
visited.add(cur)
if cur == goal:
self._visited = len(visited)
return self._build_path(parent, start, goal)
if cur_f > f.get(cur, float('inf')):
continue
for nb in lab.neighbours(cur):
new_g = g[cur] + 1
if new_g < g.get(nb, float('inf')):
parent[nb] = cur
g[nb] = new_g
new_f = new_g + self._heuristic(nb, goal)
f[nb] = new_f
heapq.heappush(heap, (new_f, counter, nb))
counter += 1
self._visited = len(visited)
return []
# ---------- 4. Оркестратор с поддержкой наблюдателей ----------
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.notify(event, data)
def set_strategy(self, strategy):
self.strategy = strategy
def solve(self):
if not self.strategy:
return None
t0 = time.perf_counter()
path = self.strategy.find_path(self.lab, self.lab.start_tile, self.lab.exit_tile)
t1 = time.perf_counter()
self.notify("path_found", path)
return {
'time_ms': (t1 - t0) * 1000,
'visited': self.strategy.visited_count(),
'length': len(path)
}
# ---------- 5. Визуализация (наблюдатель) ----------
class EventListener:
def notify(self, event, data):
raise NotImplementedError
class ConsoleRenderer(EventListener):
def __init__(self, walker=None):
self.last_path = None
self.walker = walker
def notify(self, event, data):
if event == "maze_loaded":
self._draw_maze(data)
elif event == "path_found":
self.last_path = data
self._show_path_info(data)
elif event == "player_moved":
self._draw_maze_with_player(data)
def _draw_maze(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.get_tile(x, y)
if t == lab.start_tile:
print('S', end=' ')
elif t == lab.exit_tile:
print('E', end=' ')
elif t.wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (lab.width * 2 + 4))
print(" S - старт E - выход # - стена . - проход")
def _draw_maze_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.get_tile(x, y)
if self.walker and t == self.walker.current:
print('P', end=' ')
elif t == lab.start_tile:
print('S', end=' ')
elif t == lab.exit_tile:
print('E', end=' ')
elif t.wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (lab.width * 2 + 4))
if self.walker:
print(f" Позиция игрока: ({self.walker.current.x}, {self.walker.current.y})")
def _show_path_info(self, path):
if not path:
print("\n Путь не найден!")
else:
print(f"\n Путь найден! Длина = {len(path)}")
# ---------- 6. Игрок и команды с отменой ----------
class Walker:
def __init__(self, start_tile, lab):
self.current = start_tile
self.prev = None
self.lab = lab
def move(self, target_tile):
if target_tile and target_tile.passable():
self.prev = self.current
self.current = target_tile
return True
return False
def undo(self):
if self.prev:
self.current, self.prev = self.prev, None
return True
return False
class Action:
def do(self):
raise NotImplementedError
def undo(self):
raise NotImplementedError
class MoveAction(Action):
def __init__(self, walker, dx, dy, lab):
self.walker = walker
self.dx = dx
self.dy = dy
self.lab = lab
self.done = False
def do(self):
new_x = self.walker.current.x + self.dx
new_y = self.walker.current.y + self.dy
target = self.lab.get_tile(new_x, new_y)
if target and target.passable():
self.walker.move(target)
self.done = True
return True
return False
def undo(self):
if self.done:
self.walker.undo()
self.done = False
return True
return False
# ---------- 7. Экспериментальные функции ----------
def run_benchmark(maze_file, strategy, runs=5):
loader = TextLabyrinthLoader()
lab = loader.load(maze_file)
total_time = 0.0
total_visited = 0
total_len = 0
for _ in range(runs):
solver = LabyrinthSolver(lab)
solver.set_strategy(strategy)
stats = solver.solve()
if stats:
total_time += stats['time_ms']
total_visited += stats['visited']
total_len += stats['length']
return {
'time_ms': total_time / runs,
'visited': total_visited / runs,
'length': total_len / runs
}
def make_plots(results):
mazes = list({r['maze'] for r in results})
algos = ['BFS', 'DFS', 'AStar']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
x = np.arange(len(mazes))
width = 0.25
for i, algo in enumerate(algos):
times = []
for m in mazes:
val = next((r['time_ms'] for r in results if r['maze'] == m and r['algo'] == algo), 0)
times.append(val)
axes[0].bar(x + i*width, times, width, label=algo)
axes[0].set_xlabel('Лабиринт')
axes[0].set_ylabel('Время (мс)')
axes[0].set_title('Сравнение времени выполнения')
axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=45, ha='right')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
for i, algo in enumerate(algos):
visited_vals = []
for m in mazes:
val = next((r['visited'] for r in results if r['maze'] == m and r['algo'] == algo), 0)
visited_vals.append(val)
axes[1].bar(x + i*width, visited_vals, width, label=algo)
axes[1].set_xlabel('Лабиринт')
axes[1].set_ylabel('Посещено клеток')
axes[1].set_title('Сравнение посещённых клеток')
axes[1].set_xticks(x + width)
axes[1].set_xticklabels(mazes, rotation=45, ha='right')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
for i, algo in enumerate(algos):
lengths = []
for m in mazes:
val = next((r['length'] for r in results if r['maze'] == m and r['algo'] == algo), 0)
lengths.append(val)
axes[2].bar(x + i*width, lengths, width, label=algo)
axes[2].set_xlabel('Лабиринт')
axes[2].set_ylabel('Длина пути')
axes[2].set_title('Сравнение длины пути')
axes[2].set_xticks(x + width)
axes[2].set_xticklabels(mazes, rotation=45, ha='right')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# ---------- 8. Главный блок ----------
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
# Режим экспериментов
test_mazes = [
("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()),
("DFS", DFS()),
("AStar", AStar())
]
results = []
for maze_path, maze_name in test_mazes:
print(f"Тестируем {maze_name}...")
for algo_name, algo in strategies:
try:
stats = run_benchmark(maze_path, algo, runs=3)
results.append({
'maze': maze_name,
'algo': algo_name,
'time_ms': stats['time_ms'],
'visited': stats['visited'],
'length': stats['length']
})
print(f" {algo_name}: время={stats['time_ms']:.3f}мс, посещено={stats['visited']:.0f}, длина={stats['length']:.0f}")
except Exception as e:
print(f" {algo_name}: ошибка - {e}")
# Сохраняем CSV
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'algo', 'time_ms', 'visited', 'length'])
writer.writeheader()
writer.writerows(results)
# Строим графики
if results:
make_plots(results)
print("\nРезультаты сохранены в experiment_results.csv и performance_comparison.png")
else:
# Интерактивный режим
loader = TextLabyrinthLoader()
lab = loader.load("maze/maze1.txt")
player = Walker(lab.start_tile, lab)
view = ConsoleRenderer(player)
view.notify("maze_loaded", lab)
solver = LabyrinthSolver(lab)
solver.attach(view)
print("\n УПРАВЛЕНИЕ:")
print(" H (влево) J (вниз) K (вверх) L (вправо)")
print(" U - отменить ход Q - выход")
print("\n АВТОПОИСК:")
print(" B - BFS D - DFS A - A*")
print("\n" + "=" * 50)
command_stack = []
while True:
key = input("\n Команда > ").lower()
if key == 'q':
print("\n До свидания!")
break
elif key == 'b':
solver.set_strategy(BFS())
stats = solver.solve()
if stats:
print(f"\n BFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
elif key == 'd':
solver.set_strategy(DFS())
stats = solver.solve()
print(f"\n DFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
elif key == 'a':
solver.set_strategy(AStar())
stats = solver.solve()
print(f"\n A*: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
elif key in ['h', 'j', 'k', 'l']:
dirs = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)}
act = MoveAction(player, dirs[key][0], dirs[key][1], lab)
if act.do():
command_stack.append(act)
view.notify("player_moved", lab)
if player.current == lab.exit_tile:
print("\n ПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД!")
print(f" Сделано ходов: {len(command_stack)}")
break
else:
print("\n Туда нельзя стена!")
elif key == 'u':
if command_stack:
cmd = command_stack.pop()
cmd.undo()
view.notify("player_moved", lab)
print("\n Отмена последнего хода")
else:
print("\n Нечего отменять")
else:
print("\n Неизвестная команда. Используйте h,j,k,l, u, b, d, a, q")
print("\n Игра окончена. Спасибо!")