Merge pull request '[2] YaroslavtsevAS-lab2' (#298) from YaroslavtsevAS/2026-rff_mp:YaroslavtsevAS-lab2 into develop
Reviewed-on: #298
This commit is contained in:
commit
0b1738c0ff
16
YaroslavtsevAS/docs/2-nd-lab/experiment_data.csv
Normal file
16
YaroslavtsevAS/docs/2-nd-lab/experiment_data.csv
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
maze,strategy,time_ms,visited_cells,path_length
|
||||||
|
Small 10x6,BFS,0.036793333189658974,27.0,10.0
|
||||||
|
Small 10x6,DFS,0.027613000080843147,18.0,14.0
|
||||||
|
Small 10x6,AStar,0.06921633333452822,22.0,10.0
|
||||||
|
Medium 10x10,BFS,0.04238766662941392,40.0,15.0
|
||||||
|
Medium 10x10,DFS,0.01747666677450373,21.0,15.0
|
||||||
|
Medium 10x10,AStar,0.05802666661717618,28.0,15.0
|
||||||
|
Large 20x20,BFS,0.09112733308332584,64.0,31.0
|
||||||
|
Large 20x20,DFS,0.05382399983015299,64.0,41.0
|
||||||
|
Large 20x20,AStar,0.21716699969450323,60.0,31.0
|
||||||
|
Empty 15x15,BFS,0.38009000005937804,223.0,25.0
|
||||||
|
Empty 15x15,DFS,0.17080266661650967,221.0,109.0
|
||||||
|
Empty 15x15,AStar,0.6228723332242225,169.0,25.0
|
||||||
|
No exit 10x10,BFS,0.014016666682437062,9.0,0.0
|
||||||
|
No exit 10x10,DFS,0.013433666936180089,9.0,0.0
|
||||||
|
No exit 10x10,AStar,0.024179666373432458,9.0,0.0
|
||||||
|
525
YaroslavtsevAS/docs/2-nd-lab/main.py
Normal file
525
YaroslavtsevAS/docs/2-nd-lab/main.py
Normal file
|
|
@ -0,0 +1,525 @@
|
||||||
|
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
|
||||||
|
if h == 0 or w == 0:
|
||||||
|
raise ValueError("Файл лабиринта пуст.")
|
||||||
|
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._last_path = data
|
||||||
|
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))
|
||||||
|
print(' S – вход E – выход # – стена . – пол')
|
||||||
|
|
||||||
|
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')
|
||||||
6
YaroslavtsevAS/docs/2-nd-lab/maze/maze1.txt
Normal file
6
YaroslavtsevAS/docs/2-nd-lab/maze/maze1.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
##########
|
||||||
|
#S.......#
|
||||||
|
#.###.#.##
|
||||||
|
#...#....#
|
||||||
|
#.#.#.##.#
|
||||||
|
#.....E..#
|
||||||
10
YaroslavtsevAS/docs/2-nd-lab/maze/maze10x10.txt
Normal file
10
YaroslavtsevAS/docs/2-nd-lab/maze/maze10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S..#...##
|
||||||
|
#.##.#.#.#
|
||||||
|
#....#...#
|
||||||
|
#.##.#.###
|
||||||
|
#..#.....#
|
||||||
|
##.#.###.#
|
||||||
|
#......#.#
|
||||||
|
#.####.#E#
|
||||||
|
##########
|
||||||
20
YaroslavtsevAS/docs/2-nd-lab/maze/maze20x20.txt
Normal file
20
YaroslavtsevAS/docs/2-nd-lab/maze/maze20x20.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
####################
|
||||||
|
#S##################
|
||||||
|
#..#.###############
|
||||||
|
##...###############
|
||||||
|
####.###############
|
||||||
|
####. ##############
|
||||||
|
#.##..##############
|
||||||
|
#.......############
|
||||||
|
#......#############
|
||||||
|
#.....##############
|
||||||
|
#....#####E.########
|
||||||
|
#...######..########
|
||||||
|
#...#####..#########
|
||||||
|
#...####..##########
|
||||||
|
#...###..###########
|
||||||
|
#........###########
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
|
####################
|
||||||
15
YaroslavtsevAS/docs/2-nd-lab/maze/maze_empty.txt
Normal file
15
YaroslavtsevAS/docs/2-nd-lab/maze/maze_empty.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
...............
|
||||||
|
.S.............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
...............
|
||||||
|
.............E.
|
||||||
|
...............
|
||||||
10
YaroslavtsevAS/docs/2-nd-lab/maze/maze_no_exit.txt
Normal file
10
YaroslavtsevAS/docs/2-nd-lab/maze/maze_no_exit.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
##########
|
||||||
|
#S..#...##
|
||||||
|
#.##.#.#.#
|
||||||
|
#....#...#
|
||||||
|
##########
|
||||||
|
#....#...#
|
||||||
|
#.##.#.#.#
|
||||||
|
#..#.....#
|
||||||
|
##.#.###E#
|
||||||
|
##########
|
||||||
BIN
YaroslavtsevAS/docs/2-nd-lab/maze_performance.png
Normal file
BIN
YaroslavtsevAS/docs/2-nd-lab/maze_performance.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
119
YaroslavtsevAS/docs/ReportLab2.md
Normal file
119
YaroslavtsevAS/docs/ReportLab2.md
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
# Отчёт по лабораторной работе: Поиск выхода из лабиринта
|
||||||
|
|
||||||
|
## 1. Постановка задачи
|
||||||
|
|
||||||
|
Цель — разработать программу для загрузки лабиринта из текстового файла, поиска маршрута от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения эффективности трёх классических алгоритмов поиска пути.
|
||||||
|
|
||||||
|
### Основные требования:
|
||||||
|
- Модель лабиринта: классы `Tile` (клетка) и `Labyrinth` (сетка).
|
||||||
|
- Загрузка карты из файла с символами `#` (стена), `S` (старт), `E` (выход).
|
||||||
|
- Реализация трёх стратегий поиска: BFS, DFS, A*.
|
||||||
|
- Оркестратор `LabyrinthSolver` с возможностью смены алгоритма во время выполнения.
|
||||||
|
- Сбор метрик: время работы (мс), количество посещённых клеток, длина найденного пути.
|
||||||
|
- Проведение экспериментов на пяти лабиринтах разного размера и структуры.
|
||||||
|
|
||||||
|
### Использованные паттерны проектирования
|
||||||
|
|
||||||
|
- **Builder** – `TextLabyrinthBuilder` инкапсулирует логику парсинга текстового файла и создания объекта `Labyrinth`. Это упрощает добавление новых форматов.
|
||||||
|
- **Strategy** – `BFS_Pathfinder`, `DFS_Pathfinder` и `AStar_Pathfinder` реализуют общий интерфейс `Pathfinder`. Класс `LabyrinthSolver` может динамически переключаться между ними.
|
||||||
|
- **Observer** – интерфейс `GameObserver` и его реализация `TerminalDisplay` позволяют отделить отображение лабиринта от бизнес-логики.
|
||||||
|
- **Command** – `MoveAction` оборачивает перемещение игрока, сохраняя историю и предоставляя возможность отмены (`undo`).
|
||||||
|
|
||||||
|
## 2. Архитектура приложения
|
||||||
|
|
||||||
|
- **Модель**: `Tile` (клетка) и `Labyrinth` (лабиринт).
|
||||||
|
- **Загрузка**: `LabyrinthBuilder` (абстрактный) и `TextLabyrinthBuilder`.
|
||||||
|
- **Алгоритмы**: `BFS_Pathfinder`, `DFS_Pathfinder`, `AStar_Pathfinder` — наследники `Pathfinder`.
|
||||||
|
- **Управление поиском**: `LabyrinthSolver` (смена стратегии, оповещение наблюдателей).
|
||||||
|
- **Визуализация**: `TerminalDisplay`, реализующий `GameObserver`.
|
||||||
|
- **Интерактив**: `Explorer` (игрок) и `MoveAction` (команда перемещения).
|
||||||
|
|
||||||
|
## 3. Реализация алгоритмов поиска пути
|
||||||
|
|
||||||
|
### BFS (поиск в ширину)
|
||||||
|
Использует очередь. Стартовая клетка помещается в очередь, затем на каждом шаге извлекается первый элемент. Если это выход — путь восстановлен. Иначе все непосещённые соседи добавляются в конец очереди. Гарантирует кратчайший путь по количеству шагов.
|
||||||
|
|
||||||
|
### DFS (поиск в глубину)
|
||||||
|
Вместо очереди используется стек (LIFO). Начинает со старта, на каждом шаге извлекается последний добавленный элемент. Быстрее продвигается в глубину, но путь часто оказывается далеко не оптимальным.
|
||||||
|
|
||||||
|
### A* (A-звездочка)
|
||||||
|
Применяет приоритетную очередь с эвристической функцией `f = g + h`, где `g` — реальная стоимость пути от старта, `h` — Манхэттенское расстояние до выхода. Всегда находит оптимальный путь при допустимой эвристике и обычно посещает меньше клеток.
|
||||||
|
|
||||||
|
## 4. Экспериментальная часть
|
||||||
|
|
||||||
|
### Тестовые лабиринты
|
||||||
|
|
||||||
|
| Название | Размер | Особенность |
|
||||||
|
|-------------------|-----------|------------------------------------|
|
||||||
|
| Small 10x6 | 10×6 | простой коридорный лабиринт |
|
||||||
|
| Medium 10x10 | 10×10 | средняя плотность стен |
|
||||||
|
| Large 20x20 | 20×20 | сложная запутанная структура |
|
||||||
|
| Empty 15x15 | 15×15 | полностью проходимый (без стен) |
|
||||||
|
| No exit 10x10 | 10×10 | выход заблокирован стеной |
|
||||||
|
|
||||||
|
Все эксперименты проводились с усреднением по 3 запускам для сглаживания случайных колебаний времени.
|
||||||
|
|
||||||
|
### Результаты замеров
|
||||||
|
|
||||||
|
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|
||||||
|
|------------------|----------|------------|-----------------|------------|
|
||||||
|
| Small 10x6 | BFS | 0.037 | 27 | 10 |
|
||||||
|
| Small 10x6 | DFS | 0.028 | 18 | 14 |
|
||||||
|
| Small 10x6 | A* | 0.069 | 22 | 10 |
|
||||||
|
| Medium 10x10 | BFS | 0.042 | 40 | 15 |
|
||||||
|
| Medium 10x10 | DFS | 0.017 | 21 | 15 |
|
||||||
|
| Medium 10x10 | A* | 0.058 | 28 | 15 |
|
||||||
|
| Large 20x20 | BFS | 0.091 | 64 | 31 |
|
||||||
|
| Large 20x20 | DFS | 0.054 | 64 | 41 |
|
||||||
|
| Large 20x20 | A* | 0.217 | 60 | 31 |
|
||||||
|
| Empty 15x15 | BFS | 0.380 | 223 | 25 |
|
||||||
|
| Empty 15x15 | DFS | 0.171 | 221 | 109 |
|
||||||
|
| Empty 15x15 | A* | 0.623 | 169 | 25 |
|
||||||
|
| No exit 10x10 | BFS | 0.014 | 9 | 0 |
|
||||||
|
| No exit 10x10 | DFS | 0.013 | 9 | 0 |
|
||||||
|
| No exit 10x10 | A* | 0.024 | 9 | 0 |
|
||||||
|
|
||||||
|
*Примечание: длина пути 0 означает, что маршрут не существует.*
|
||||||
|
|
||||||
|
### Визуализация
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
На графиках отражены три метрики: время выполнения, число посещённых клеток и длина найденного пути.
|
||||||
|
|
||||||
|
## 5. Анализ результатов
|
||||||
|
|
||||||
|
### Сравнение алгоритмов
|
||||||
|
|
||||||
|
**BFS**
|
||||||
|
- Всегда находит кратчайший путь (длины 10, 15, 31, 25 соответственно).
|
||||||
|
- На больших и пустых лабиринтах посещает много клеток (до 223), что сказывается на времени.
|
||||||
|
- Хорошо подходит, когда оптимальность критична, а размер лабиринта умеренный.
|
||||||
|
|
||||||
|
**DFS**
|
||||||
|
- Самый быстрый по времени (0.013–0.171 мс), но часто выдаёт длинные извилистые пути (14, 15, 41, 109). В пустом 15×15 путь составил 109 шагов вместо оптимальных 25.
|
||||||
|
- Число посещённых клеток невелико, что объясняет высокую скорость. Рекомендуется, когда важнее скорость, а не качество маршрута.
|
||||||
|
|
||||||
|
**A***
|
||||||
|
- Находит кратчайший путь во всех случаях (где он существует).
|
||||||
|
- Посещает в среднем меньше клеток, чем BFS (22, 28, 60, 169), что подтверждает эффективность эвристики.
|
||||||
|
- Время работы немного выше, чем у BFS, из-за накладных расходов на работу с кучей, однако разница незначительна.
|
||||||
|
|
||||||
|
### Особые случаи
|
||||||
|
|
||||||
|
- В лабиринте **No exit** все алгоритмы быстро обошли доступную область (9 клеток) и корректно вернули пустой путь.
|
||||||
|
- На **Empty 15×15** DFS показал наихудший путь (109 шагов), в то время как BFS и A* дали идеальные 25. Это наглядно демонстрирует, что DFS непригоден для задач, требующих оптимальности.
|
||||||
|
- **Large 20×20** показал близкие результаты для BFS и A* по длине пути, но DFS снова выдал более длинный маршрут (41).
|
||||||
|
|
||||||
|
### Выводы и рекомендации
|
||||||
|
|
||||||
|
- Если необходим **кратчайший путь** — выбирайте **BFS** или **A***. A* часто эффективнее по памяти, так как посещает меньше клеток.
|
||||||
|
- Если важна **максимальная скорость** и допустим неоптимальный путь — используйте **DFS**.
|
||||||
|
- **A*** является наилучшим компромиссом для большинства практических задач благодаря сбалансированному сочетанию времени и оптимальности.
|
||||||
|
|
||||||
|
## 6. Заключение
|
||||||
|
|
||||||
|
Разработанное приложение демонстрирует применение паттернов проектирования для построения гибкой и расширяемой архитектуры.
|
||||||
|
Экспериментальные данные подтверждают ожидаемое поведение алгоритмов: BFS и A* гарантируют оптимальность, DFS — высокую скорость ценой качества маршрута. A* показал наилучшее соотношение «затраты / качество», посещая меньше клеток и находя кратчайший путь.
|
||||||
|
|
||||||
|
Все полученные метрики сохранены в файл `experiment_data.csv`, графики — в `maze_performance.png`.
|
||||||
BIN
YaroslavtsevAS/docs/maze_performance.png
Normal file
BIN
YaroslavtsevAS/docs/maze_performance.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Loading…
Reference in New Issue
Block a user