[2] add experimenal and plots
This commit is contained in:
parent
fc6b64b7cd
commit
11d65436e1
|
|
@ -1,9 +1,13 @@
|
|||
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
|
||||
|
|
@ -30,7 +34,6 @@ class Tile:
|
|||
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
|
||||
|
|
@ -79,7 +82,7 @@ class Labyrinth:
|
|||
res.append(nb)
|
||||
return res
|
||||
|
||||
|
||||
# ---------------- Загрузка лабиринта ----------------
|
||||
class LabyrinthBuilder:
|
||||
def build(self, filename): raise NotImplementedError
|
||||
|
||||
|
|
@ -93,24 +96,21 @@ class TextLabyrinthBuilder(LabyrinthBuilder):
|
|||
lab = Labyrinth(w, h)
|
||||
for y, row in enumerate(lines):
|
||||
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':
|
||||
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')
|
||||
else: lab.configure_tile(x, y, 'floor')
|
||||
if entries != 1 or exits != 1:
|
||||
raise ValueError(f"Некорректный лабиринт: найдено S={entries}, E={exits}")
|
||||
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
|
||||
|
|
@ -119,11 +119,8 @@ class Pathfinder:
|
|||
cur = preds.get(cur)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
@property
|
||||
def visited_count(self):
|
||||
return getattr(self, '_visited', 0)
|
||||
|
||||
def visited_count(self): return getattr(self, '_visited', 0)
|
||||
|
||||
class BFS_Pathfinder(Pathfinder):
|
||||
def find_path(self, lab, start, goal):
|
||||
|
|
@ -143,7 +140,6 @@ class BFS_Pathfinder(Pathfinder):
|
|||
self._visited = len(seen)
|
||||
return []
|
||||
|
||||
|
||||
class DFS_Pathfinder(Pathfinder):
|
||||
def find_path(self, lab, start, goal):
|
||||
stack = [start]
|
||||
|
|
@ -162,11 +158,9 @@ class DFS_Pathfinder(Pathfinder):
|
|||
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
|
||||
|
|
@ -197,43 +191,36 @@ class AStar_Pathfinder(Pathfinder):
|
|||
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 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
|
||||
|
|
@ -243,7 +230,6 @@ class MoveAction(Action):
|
|||
self._done = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def undo(self):
|
||||
if self._done:
|
||||
self._explorer.undo()
|
||||
|
|
@ -251,25 +237,17 @@ class MoveAction(Action):
|
|||
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._last_path = data
|
||||
self._show_path_info(data)
|
||||
elif event == 'player_moved':
|
||||
self._draw_with_player(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))
|
||||
|
|
@ -285,8 +263,6 @@ class TerminalDisplay(GameObserver):
|
|||
else: print('.', end=' ')
|
||||
print()
|
||||
print('=' * (lab.width * 2 + 4))
|
||||
print(' S – вход E – выход # – стена . – пол')
|
||||
|
||||
def _draw_with_player(self, lab):
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
print('=' * (lab.width * 2 + 4))
|
||||
|
|
@ -296,8 +272,7 @@ class TerminalDisplay(GameObserver):
|
|||
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=' ')
|
||||
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=' ')
|
||||
|
|
@ -305,48 +280,130 @@ class TerminalDisplay(GameObserver):
|
|||
print()
|
||||
print('=' * (lab.width * 2 + 4))
|
||||
if self._explorer:
|
||||
print(f' Позиция игрока: ({self._explorer.current.x}, {self._explorer.current.y})')
|
||||
print(' S – вход E – выход # – стена . – пол P – игрок')
|
||||
|
||||
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)} клеток.')
|
||||
|
||||
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 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
|
||||
|
||||
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
|
||||
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_interactive(lab):
|
||||
player = Explorer(lab.start, lab)
|
||||
display = TerminalDisplay(player)
|
||||
display.update('labyrinth_loaded', lab)
|
||||
|
||||
# ---------------- Экспериментальный режим ----------------
|
||||
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 УПРАВЛЕНИЕ:')
|
||||
|
|
@ -356,7 +413,6 @@ def run_interactive(lab):
|
|||
print('=' * 50)
|
||||
|
||||
history = []
|
||||
|
||||
while True:
|
||||
cmd = input('\n Команда > ').lower().strip()
|
||||
if cmd == 'q':
|
||||
|
|
@ -376,11 +432,11 @@ def run_interactive(lab):
|
|||
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], lab)
|
||||
action = MoveAction(player, dirs[cmd], labyrinth)
|
||||
if action.execute():
|
||||
history.append(action)
|
||||
display.update('player_moved', lab)
|
||||
if player.current == lab.exit:
|
||||
display.update('player_moved', labyrinth)
|
||||
if player.current == labyrinth.exit:
|
||||
print('\n ПОЗДРАВЛЯЕМ! ВЫ ВЫБРАЛИСЬ ИЗ ЛАБИРИНТА!')
|
||||
print(f' Всего ходов: {len(history)}')
|
||||
break
|
||||
|
|
@ -390,18 +446,9 @@ def run_interactive(lab):
|
|||
if history:
|
||||
act = history.pop()
|
||||
act.undo()
|
||||
display.update('player_moved', lab)
|
||||
display.update('player_moved', labyrinth)
|
||||
print('\n Ход отменён')
|
||||
else:
|
||||
print('\n Нечего отменять')
|
||||
else:
|
||||
print('\n Неизвестная команда. Используйте H,J,K,L,U,Q,B,D,A')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
maze_file = 'maze1.txt'
|
||||
if len(sys.argv) > 1:
|
||||
maze_file = sys.argv[1]
|
||||
builder = TextLabyrinthBuilder()
|
||||
labyrinth = builder.build(maze_file)
|
||||
run_interactive(labyrinth)
|
||||
Loading…
Reference in New Issue
Block a user