2026-rff_mp/zaharoves/задание 2/Maze_solver.py
2026-05-12 12:02:51 +03:00

660 lines
21 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 os
import time
import heapq
import csv
import random
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass
from typing import Optional
#1. Модель лабиринта — классы Cell и Maze
class Cell:
"""Клетка лабиринта."""
def __init__(self, x: int, y: int, is_wall: bool = False,
is_start: bool = False, is_exit: bool = False, weight: int = 1):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
self.weight = weight # для взвешенного лабиринта (Этап 6 доп.)
def is_passable(self) -> bool:
return not self.is_wall
def __repr__(self):
return f"Cell({self.x},{self.y})"
def __eq__(self, other):
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
class Maze:
"""Лабиринт — двумерный массив клеток."""
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells: list[list[Cell]] = []
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def get_cell(self, x: int, y: int) -> Optional[Cell]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def get_neighbors(self, cell: Cell) -> list[Cell]:
"""Возвращает проходимых соседей (вверх, вниз, влево, вправо)."""
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
neighbors = []
for dx, dy in directions:
neighbor = self.get_cell(cell.x + dx, cell.y + dy)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
#2. Загрузка лабиринта из файла — паттерн Builder
class MazeBuilder(ABC):
"""Интерфейс строителя лабиринта."""
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
WEIGHT_MAP = {' ': 1, 'S': 1, 'E': 1, '.': 2, '~': 3, '#': 0}
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = f.read().splitlines()
if not lines:
raise ValueError("Файл лабиринта пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
for y, line in enumerate(lines):
row = []
for x in range(width):
ch = line[x] if x < len(line) else ' '
is_wall = ch == '#'
is_start = ch == 'S'
is_exit = ch == 'E'
weight = self.WEIGHT_MAP.get(ch, 1)
cell = Cell(x, y, is_wall=is_wall, is_start=is_start,
is_exit=is_exit, weight=weight)
row.append(cell)
if is_start:
maze.start = cell
if is_exit:
maze.exit = cell
maze.cells.append(row)
if maze.start is None:
raise ValueError("В лабиринте не задана стартовая клетка (S)")
if maze.exit is None:
raise ValueError("В лабиринте не задана выходная клетка (E)")
return maze
#3. Стратегии поиска пути — паттерн Strategy
@dataclass
class SearchStats:
"""Статистика одного запуска поиска."""
time_ms: float = 0.0
visited_cells: int = 0
path_length: int = 0
class PathFindingStrategy(ABC):
"""Интерфейс стратегии поиска пути."""
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
pass
def _reconstruct_path(self, parent: dict, start: Cell, exit_cell: Cell) -> list[Cell]:
path = []
current = exit_cell
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
if path and path[0] == start:
return path
return []
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину"""
def __init__(self):
self.visited_count = 0
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
queue = deque([start])
parent: dict[Cell, Optional[Cell]] = {start: None}
self.visited_count = 0
while queue:
current = queue.popleft()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parent, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in parent:
parent[neighbor] = current
queue.append(neighbor)
return []
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину"""
def __init__(self):
self.visited_count = 0
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
stack = [start]
parent: dict[Cell, Optional[Cell]] = {start: None}
self.visited_count = 0
while stack:
current = stack.pop()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parent, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in parent:
parent[neighbor] = current
stack.append(neighbor)
return []
class AStarStrategy(PathFindingStrategy):
"""A* с манхэттенской эвристикой"""
def __init__(self):
self.visited_count = 0
def _heuristic(self, a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
g_score = {start: 0}
parent: dict[Cell, Optional[Cell]] = {start: None}
open_heap = [(self._heuristic(start, exit_cell), 0, start)]
closed_set: set[Cell] = set() # уже обработанные клетки
self.visited_count = 0
counter = 0 # счётчик для устранения неоднозначности
while open_heap:
_, _, current = heapq.heappop(open_heap)
if current in closed_set:
continue
closed_set.add(current)
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parent, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor in closed_set:
continue
tentative_g = g_score[current] + neighbor.weight
if tentative_g < g_score.get(neighbor, float('inf')):
g_score[neighbor] = tentative_g
parent[neighbor] = current
f = tentative_g + self._heuristic(neighbor, exit_cell)
counter += 1
heapq.heappush(open_heap, (f, counter, neighbor))
return []
class DijkstraStrategy(PathFindingStrategy):
"""Дейкстра"""
def __init__(self):
self.visited_count = 0
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
dist = {start: 0}
parent: dict[Cell, Optional[Cell]] = {start: None}
open_heap = [(0, 0, start)]
self.visited_count = 0
counter = 0
while open_heap:
cost, _, current = heapq.heappop(open_heap)
if cost > dist.get(current, float('inf')):
continue
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parent, start, exit_cell)
for neighbor in maze.get_neighbors(current):
new_cost = dist[current] + neighbor.weight
if new_cost < dist.get(neighbor, float('inf')):
dist[neighbor] = new_cost
parent[neighbor] = current
counter += 1
heapq.heappush(open_heap, (new_cost, counter, neighbor))
return []
#4. Оркестратор — MazeSolver
class MazeSolver:
"""Оркестратор"""
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
self._observers: list['Observer'] = []
self._last_path: list[Cell] = []
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def add_observer(self, observer: 'Observer'):
self._observers.append(observer)
def _notify(self, event: str, **kwargs):
for obs in self._observers:
obs.update(event, **kwargs)
def solve(self) -> SearchStats:
start = self.maze.start
exit_cell = self.maze.exit
self._notify("search_start", strategy=type(self.strategy).__name__)
t0 = time.perf_counter()
path = self.strategy.find_path(self.maze, start, exit_cell)
t1 = time.perf_counter()
self._last_path = path
visited = getattr(self.strategy, 'visited_count', 0)
stats = SearchStats(
time_ms=(t1 - t0) * 1000,
visited_cells=visited,
path_length=len(path)
)
self._notify("path_found", path=path, stats=stats)
return stats
def get_last_path(self) -> list[Cell]:
return self._last_path
#5.1. Observer — ConsoleView
class Observer(ABC):
"""Интерфейс наблюдателя."""
@abstractmethod
def update(self, event: str, **kwargs):
pass
class ConsoleView(Observer):
"""Консольный вид"""
SYMBOLS = {
'wall': '',
'path': '·',
'start': 'S',
'exit': 'E',
'player': '@',
'visited': '°',
'empty': ' ',
}
def update(self, event: str, **kwargs):
if event == "search_start":
print(f"\n[Поиск] Алгоритм: {kwargs.get('strategy', '?')}")
elif event == "path_found":
path = kwargs.get('path', [])
stats = kwargs.get('stats')
if path:
print(f"[Готово] Путь найден! Длина: {stats.path_length}, "
f"Посещено: {stats.visited_cells}, Время: {stats.time_ms:.2f} мс")
else:
print("[Готово] Путь не найден!")
elif event == "move":
cell = kwargs.get('cell')
print(f"[Ход] Игрок перемещается в {cell}")
def render(self, maze: Maze, player_pos: Optional[Cell] = None,
path: Optional[list[Cell]] = None):
"""Рисует лабиринт в консоли."""
path_set = set(path) if path else set()
print()
for y in range(maze.height):
row = ''
for x in range(maze.width):
cell = maze.cells[y][x]
if cell.is_wall:
row += self.SYMBOLS['wall']
elif player_pos and cell == player_pos:
row += self.SYMBOLS['player']
elif cell.is_start:
row += self.SYMBOLS['start']
elif cell.is_exit:
row += self.SYMBOLS['exit']
elif cell in path_set:
row += self.SYMBOLS['path']
else:
row += self.SYMBOLS['empty']
print(row)
print()
#5.2. Command — Player и MoveCommand
class Player:
"""Игрок с текущей позицией в лабиринте."""
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
def move_to(self, cell: Cell):
self.current_cell = cell
class Command(ABC):
"""Интерфейс команды."""
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
"""Команда перемещения игрока в указанную клетку."""
def __init__(self, player: Player, target_cell: Cell, solver: MazeSolver):
self.player = player
self.target_cell = target_cell
self.previous_cell = player.current_cell
self.solver = solver
def execute(self):
self.previous_cell = self.player.current_cell
self.player.move_to(self.target_cell)
self.solver._notify("move", cell=self.target_cell)
def undo(self):
self.player.move_to(self.previous_cell)
self.solver._notify("move", cell=self.previous_cell)
#6. Экспериментальная часть
def generate_maze_file(filename: str, width: int, height: int,
wall_density: float = 0.3, no_exit: bool = False):
"""Генерирует случайный лабиринт и сохраняет в файл."""
random.seed(42)
grid = []
for y in range(height):
row = []
for x in range(width):
if x == 0 or y == 0 or x == width - 1 or y == height - 1:
row.append('#')
else:
row.append('#' if random.random() < wall_density else ' ')
grid.append(row)
# Старт и выход
grid[1][1] = 'S'
grid[height - 2][width - 2] = 'E'
if no_exit:
ex, ey = width - 2, height - 2
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = ex + dx, ey + dy
if 0 < nx < width - 1 and 0 < ny < height - 1:
grid[ny][nx] = '#'
with open(filename, 'w', encoding='utf-8') as f:
for row in grid:
f.write(''.join(row) + '\n')
def run_experiments(mazes_config: list[dict], strategies: dict,
runs: int = 5) -> list[dict]:
builder = TextFileMazeBuilder()
results = []
for maze_cfg in mazes_config:
name = maze_cfg['name']
filepath = maze_cfg['file']
# Генерируем файл лабиринта
if 'generate' in maze_cfg:
gen = maze_cfg['generate']
generate_maze_file(filepath, gen['width'], gen['height'],
gen.get('density', 0.3),
gen.get('no_exit', False))
try:
maze = builder.build_from_file(filepath)
except Exception as e:
print(f"[Пропуск] {name}: {e}")
continue
for strat_name, strategy_cls in strategies.items():
times, visited, lengths = [], [], []
for _ in range(runs):
strategy = strategy_cls()
solver = MazeSolver(maze, strategy)
stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_cells)
lengths.append(stats.path_length)
results.append({
'лабиринт': name,
'стратегия': strat_name,
'время_мс': round(sum(times) / runs, 4),
'посещено_клеток': int(sum(visited) / runs),
'длина_пути': int(sum(lengths) / runs),
})
print(f" {name} / {strat_name}: "
f"time={results[-1]['время_мс']:.3f}ms, "
f"visited={results[-1]['посещено_клеток']}, "
f"path={results[-1]['длина_пути']}")
return results
def save_csv(results: list[dict], filename: str):
if not results:
return
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=results[0].keys())
writer.writeheader()
writer.writerows(results)
print(f"\nРезультаты сохранены в {filename}")
def print_table(results: list[dict]):
if not results:
print("Нет результатов.")
return
header = f"{'Лабиринт':<20} {'Стратегия':<12} {'Время,мс':>10} {'Посещено':>10} {'Путь':>8}"
print("\n" + "=" * len(header))
print(header)
print("=" * len(header))
for r in results:
print(f"{r['лабиринт']:<20} {r['стратегия']:<12} "
f"{r['время_мс']:>10.4f} {r['посещено_клеток']:>10} {r['длина_пути']:>8}")
print("=" * len(header))
#Демонстрация
def demo_interactive(maze: Maze, solver: MazeSolver, view: ConsoleView):
"""Пошаговый режим с Command."""
path = solver.get_last_path()
if not path:
print("Путь не найден — пошаговый режим недоступен.")
return
player = Player(maze.start)
history: list[MoveCommand] = []
view.render(maze, player_pos=player.current_cell, path=path)
print("Пошаговый режим: [N] — следующий шаг, [U] — отмена, [Q] — выход")
step_index = 1 # 0-й шаг — старт, уже там
while True:
cmd = input("Команда: ").strip().upper()
if cmd == 'Q':
break
elif cmd == 'N':
if step_index < len(path):
move = MoveCommand(player, path[step_index], solver)
move.execute()
history.append(move)
step_index += 1
os.system('cls' if os.name == 'nt' else 'clear')
view.render(maze, player_pos=player.current_cell, path=path)
if player.current_cell == maze.exit:
print("🎉 Вы достигли выхода!")
break
else:
print("Вы уже в конце пути.")
elif cmd == 'U':
if history:
move = history.pop()
move.undo()
step_index -= 1
os.system('cls' if os.name == 'nt' else 'clear')
view.render(maze, player_pos=player.current_cell, path=path)
else:
print("Нечего отменять.")
else:
print("Неизвестная команда.")
def main():
print("=" * 60)
print(" Поиск выхода из лабиринта — ООП + паттерны GoF")
print("=" * 60)
small_maze_file = "maze_small.txt"
builder = TextFileMazeBuilder()
maze = builder.build_from_file(small_maze_file)
#2. Создаём представление (Observer)
view = ConsoleView()
#3. Демонстрация стратегий
strategies = {
'BFS': BFSStrategy,
'DFS': DFSStrategy,
'A*': AStarStrategy,
'Дейкстра': DijkstraStrategy,
}
print(f"\nЛабиринт ({maze.width}×{maze.height}):")
view.render(maze)
for name, cls in strategies.items():
strategy = cls()
solver = MazeSolver(maze, strategy)
solver.add_observer(view)
stats = solver.solve()
#Визуализация пути A*
print("\n--- Путь, найденный A* ---")
a_star = AStarStrategy()
solver = MazeSolver(maze, a_star)
solver.add_observer(view)
solver.solve()
view.render(maze, path=solver.get_last_path())
#4. Пошаговый режим Command
ans = input("Запустить пошаговый режим? (y/n): ").strip().lower()
if ans == 'y':
demo_interactive(maze, solver, view)
#5. Экспериментальная часть
print("\n" + "=" * 60)
print(" Экспериментальная часть")
print("=" * 60)
mazes_config = [
{
'name': 'Маленький 10×10',
'file': 'maze_small.txt',
},
{
'name': 'Средний 50×50',
'file': 'maze_medium.txt',
},
{
'name': 'Большой 100×100',
'file': 'maze_large.txt',
},
{
'name': 'Пустой 50×50',
'file': 'maze_empty.txt',
},
{
'name': 'Без выхода 20×20',
'file': 'maze_no_exit.txt',
},
]
all_strategies = {
'BFS': BFSStrategy,
'DFS': DFSStrategy,
'A*': AStarStrategy,
'Дейкстра': DijkstraStrategy,
}
print("\nЗапуск экспериментов (5 прогонов каждого)...")
results = run_experiments(mazes_config, all_strategies, runs=5)
print_table(results)
save_csv(results, "results.csv")
print("\nГотово!")
if __name__ == '__main__':
main()