843 lines
30 KiB
Python
843 lines
30 KiB
Python
|
|
"""
|
|||
|
|
Лабораторная работа: Применение паттернов проектирования
|
|||
|
|
Этапы 1-6: Модель лабиринта, Builder, Strategy, MazeSolver, Observer/Command, эксперименты
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import time
|
|||
|
|
import csv
|
|||
|
|
import random
|
|||
|
|
from collections import deque
|
|||
|
|
from typing import List, Tuple, Dict, Set, Optional
|
|||
|
|
import heapq
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
from abc import ABC, abstractmethod
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Этап 1. Модель лабиринта
|
|||
|
|
# ============================================================
|
|||
|
|
class Cell:
|
|||
|
|
"""Клетка лабиринта."""
|
|||
|
|
def __init__(self, x: int, y: int, is_wall: bool = False, weight: int = 1):
|
|||
|
|
self.x = x
|
|||
|
|
self.y = y
|
|||
|
|
self.is_wall = is_wall
|
|||
|
|
self.is_start = False
|
|||
|
|
self.is_exit = False
|
|||
|
|
self.weight = weight # для взвешенных лабиринтов
|
|||
|
|
|
|||
|
|
def is_passable(self) -> bool:
|
|||
|
|
return not self.is_wall
|
|||
|
|
|
|||
|
|
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 __repr__(self):
|
|||
|
|
return f"Cell({self.x},{self.y})"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Maze:
|
|||
|
|
"""Лабиринт, содержащий сетку клеток."""
|
|||
|
|
def __init__(self, width: int, height: int):
|
|||
|
|
self.width = width
|
|||
|
|
self.height = height
|
|||
|
|
self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
|
|||
|
|
self.start_cell = None
|
|||
|
|
self.exit_cell = None
|
|||
|
|
|
|||
|
|
def get_cell(self, x: int, y: int) -> Cell:
|
|||
|
|
if 0 <= x < self.width and 0 <= y < self.height:
|
|||
|
|
return self.cells[y][x]
|
|||
|
|
raise IndexError("Координаты вне границ лабиринта")
|
|||
|
|
|
|||
|
|
def get_neighbors(self, cell: Cell) -> List[Cell]:
|
|||
|
|
"""Возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо)."""
|
|||
|
|
neighbors = []
|
|||
|
|
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
|||
|
|
nx, ny = cell.x + dx, cell.y + dy
|
|||
|
|
if 0 <= nx < self.width and 0 <= ny < self.height:
|
|||
|
|
n = self.cells[ny][nx]
|
|||
|
|
if n.is_passable():
|
|||
|
|
neighbors.append(n)
|
|||
|
|
return neighbors
|
|||
|
|
|
|||
|
|
def set_start(self, x: int, y: int):
|
|||
|
|
cell = self.get_cell(x, y)
|
|||
|
|
cell.is_start = True
|
|||
|
|
self.start_cell = cell
|
|||
|
|
|
|||
|
|
def set_exit(self, x: int, y: int):
|
|||
|
|
cell = self.get_cell(x, y)
|
|||
|
|
cell.is_exit = True
|
|||
|
|
self.exit_cell = cell
|
|||
|
|
|
|||
|
|
def copy(self):
|
|||
|
|
"""Создаёт глубокую копию лабиринта (для взвешенных вариантов)."""
|
|||
|
|
new_maze = Maze(self.width, self.height)
|
|||
|
|
for y in range(self.height):
|
|||
|
|
for x in range(self.width):
|
|||
|
|
orig = self.cells[y][x]
|
|||
|
|
new_maze.cells[y][x] = Cell(x, y, orig.is_wall, orig.weight)
|
|||
|
|
if orig.is_start:
|
|||
|
|
new_maze.set_start(x, y)
|
|||
|
|
if orig.is_exit:
|
|||
|
|
new_maze.set_exit(x, y)
|
|||
|
|
return new_maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Этап 2. Builder для загрузки из текстового файла
|
|||
|
|
# ============================================================
|
|||
|
|
class MazeBuilder(ABC):
|
|||
|
|
@abstractmethod
|
|||
|
|
def build_from_file(self, filename: str) -> Maze:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TextFileMazeBuilder(MazeBuilder):
|
|||
|
|
"""Строитель лабиринта из текстового файла."""
|
|||
|
|
def build_from_file(self, filename: str) -> Maze:
|
|||
|
|
with open(filename, 'r', encoding='utf-8') as f:
|
|||
|
|
lines = [line.rstrip('\n') for line in f]
|
|||
|
|
|
|||
|
|
if not lines:
|
|||
|
|
raise ValueError("Файл пуст")
|
|||
|
|
|
|||
|
|
height = len(lines)
|
|||
|
|
width = max(len(line) for line in lines)
|
|||
|
|
|
|||
|
|
grid = []
|
|||
|
|
for y, line in enumerate(lines):
|
|||
|
|
row = []
|
|||
|
|
for x in range(width):
|
|||
|
|
ch = line[x] if x < len(line) else ' '
|
|||
|
|
row.append(ch)
|
|||
|
|
grid.append(row)
|
|||
|
|
|
|||
|
|
maze = Maze(width, height)
|
|||
|
|
start_found = exit_found = False
|
|||
|
|
|
|||
|
|
for y in range(height):
|
|||
|
|
for x in range(width):
|
|||
|
|
ch = grid[y][x]
|
|||
|
|
cell = maze.get_cell(x, y)
|
|||
|
|
|
|||
|
|
if ch == '#':
|
|||
|
|
cell.is_wall = True
|
|||
|
|
elif ch == 'S':
|
|||
|
|
if start_found:
|
|||
|
|
raise ValueError("Обнаружено несколько стартовых клеток 'S'")
|
|||
|
|
cell.is_start = True
|
|||
|
|
maze.start_cell = cell
|
|||
|
|
start_found = True
|
|||
|
|
elif ch == 'E':
|
|||
|
|
if exit_found:
|
|||
|
|
raise ValueError("Обнаружено несколько выходных клеток 'E'")
|
|||
|
|
cell.is_exit = True
|
|||
|
|
maze.exit_cell = cell
|
|||
|
|
exit_found = True
|
|||
|
|
elif ch != ' ':
|
|||
|
|
raise ValueError(f"Недопустимый символ '{ch}' в позиции ({x},{y})")
|
|||
|
|
|
|||
|
|
if not start_found:
|
|||
|
|
raise ValueError("Отсутствует стартовая клетка 'S'")
|
|||
|
|
if not exit_found:
|
|||
|
|
raise ValueError("Отсутствует выходная клетка 'E'")
|
|||
|
|
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Этап 3. Стратегии поиска пути (возвращают путь и число посещённых)
|
|||
|
|
# ============================================================
|
|||
|
|
class PathFindingStrategy(ABC):
|
|||
|
|
@abstractmethod
|
|||
|
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
|||
|
|
"""Возвращает (путь, количество посещённых клеток)."""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class BFSStrategy(PathFindingStrategy):
|
|||
|
|
"""Поиск в ширину – гарантирует кратчайший путь."""
|
|||
|
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
|||
|
|
if start == exit_cell:
|
|||
|
|
return [start], 1
|
|||
|
|
|
|||
|
|
queue = deque([start])
|
|||
|
|
visited = {start}
|
|||
|
|
parent = {start: None}
|
|||
|
|
visited_count = 1
|
|||
|
|
|
|||
|
|
while queue:
|
|||
|
|
cur = queue.popleft()
|
|||
|
|
if cur == exit_cell:
|
|||
|
|
path = []
|
|||
|
|
while cur is not None:
|
|||
|
|
path.append(cur)
|
|||
|
|
cur = parent[cur]
|
|||
|
|
path.reverse()
|
|||
|
|
return path, visited_count
|
|||
|
|
|
|||
|
|
for nb in maze.get_neighbors(cur):
|
|||
|
|
if nb not in visited:
|
|||
|
|
visited.add(nb)
|
|||
|
|
visited_count += 1
|
|||
|
|
parent[nb] = cur
|
|||
|
|
queue.append(nb)
|
|||
|
|
|
|||
|
|
return [], visited_count
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DFSStrategy(PathFindingStrategy):
|
|||
|
|
"""Поиск в глубину – быстрый, но не гарантирует кратчайший путь."""
|
|||
|
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
|||
|
|
if start == exit_cell:
|
|||
|
|
return [start], 1
|
|||
|
|
|
|||
|
|
stack = [(start, [start])]
|
|||
|
|
visited = set()
|
|||
|
|
visited_count = 0
|
|||
|
|
|
|||
|
|
while stack:
|
|||
|
|
cur, path = stack.pop()
|
|||
|
|
if cur in visited:
|
|||
|
|
continue
|
|||
|
|
visited.add(cur)
|
|||
|
|
visited_count += 1
|
|||
|
|
|
|||
|
|
if cur == exit_cell:
|
|||
|
|
return path, visited_count
|
|||
|
|
|
|||
|
|
for nb in maze.get_neighbors(cur):
|
|||
|
|
if nb not in visited:
|
|||
|
|
stack.append((nb, path + [nb]))
|
|||
|
|
|
|||
|
|
return [], visited_count
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AStarStrategy(PathFindingStrategy):
|
|||
|
|
"""А* с эвристикой Манхэттенского расстояния."""
|
|||
|
|
def _heuristic(self, a: Cell, b: Cell) -> float:
|
|||
|
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
|||
|
|
|
|||
|
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
|||
|
|
if start == exit_cell:
|
|||
|
|
return [start], 1
|
|||
|
|
|
|||
|
|
open_set = []
|
|||
|
|
counter = 0
|
|||
|
|
heapq.heappush(open_set, (0, counter, start))
|
|||
|
|
g_score = {start: 0}
|
|||
|
|
f_score = {start: self._heuristic(start, exit_cell)}
|
|||
|
|
parent = {start: None}
|
|||
|
|
visited_count = 1
|
|||
|
|
|
|||
|
|
while open_set:
|
|||
|
|
_, _, cur = heapq.heappop(open_set)
|
|||
|
|
if cur == exit_cell:
|
|||
|
|
path = []
|
|||
|
|
while cur is not None:
|
|||
|
|
path.append(cur)
|
|||
|
|
cur = parent[cur]
|
|||
|
|
path.reverse()
|
|||
|
|
return path, visited_count
|
|||
|
|
|
|||
|
|
for nb in maze.get_neighbors(cur):
|
|||
|
|
move_cost = nb.weight
|
|||
|
|
tentative = g_score[cur] + move_cost
|
|||
|
|
if nb not in g_score or tentative < g_score[nb]:
|
|||
|
|
parent[nb] = cur
|
|||
|
|
g_score[nb] = tentative
|
|||
|
|
f_score[nb] = tentative + self._heuristic(nb, exit_cell)
|
|||
|
|
counter += 1
|
|||
|
|
heapq.heappush(open_set, (f_score[nb], counter, nb))
|
|||
|
|
visited_count += 1
|
|||
|
|
|
|||
|
|
return [], visited_count
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DijkstraStrategy(PathFindingStrategy):
|
|||
|
|
"""Алгоритм Дейкстры для взвешенных графов."""
|
|||
|
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
|||
|
|
if start == exit_cell:
|
|||
|
|
return [start], 1
|
|||
|
|
|
|||
|
|
pq = []
|
|||
|
|
counter = 0
|
|||
|
|
heapq.heappush(pq, (0, counter, start))
|
|||
|
|
dist = {start: 0}
|
|||
|
|
parent = {start: None}
|
|||
|
|
visited_count = 1
|
|||
|
|
|
|||
|
|
while pq:
|
|||
|
|
cur_dist, _, cur = heapq.heappop(pq)
|
|||
|
|
if cur_dist > dist.get(cur, float('inf')):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if cur == exit_cell:
|
|||
|
|
path = []
|
|||
|
|
while cur is not None:
|
|||
|
|
path.append(cur)
|
|||
|
|
cur = parent[cur]
|
|||
|
|
path.reverse()
|
|||
|
|
return path, visited_count
|
|||
|
|
|
|||
|
|
for nb in maze.get_neighbors(cur):
|
|||
|
|
new_dist = cur_dist + nb.weight
|
|||
|
|
if new_dist < dist.get(nb, float('inf')):
|
|||
|
|
dist[nb] = new_dist
|
|||
|
|
parent[nb] = cur
|
|||
|
|
counter += 1
|
|||
|
|
heapq.heappush(pq, (new_dist, counter, nb))
|
|||
|
|
visited_count += 1
|
|||
|
|
|
|||
|
|
return [], visited_count
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Этап 4. MazeSolver (оркестратор)
|
|||
|
|
# ============================================================
|
|||
|
|
@dataclass
|
|||
|
|
class SearchStats:
|
|||
|
|
path_length: int
|
|||
|
|
visited_cells: int
|
|||
|
|
time_ms: float
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MazeSolver:
|
|||
|
|
"""Оркестратор: управляет лабиринтом и стратегией поиска."""
|
|||
|
|
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
|
|||
|
|
self.maze = maze
|
|||
|
|
self.strategy = strategy
|
|||
|
|
|
|||
|
|
def set_strategy(self, strategy: PathFindingStrategy):
|
|||
|
|
self.strategy = strategy
|
|||
|
|
|
|||
|
|
def solve(self) -> SearchStats:
|
|||
|
|
start_time = time.perf_counter()
|
|||
|
|
path, visited = self.strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
|||
|
|
end_time = time.perf_counter()
|
|||
|
|
return SearchStats(len(path), visited, (end_time - start_time) * 1000.0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Этап 5. Observer и Command (визуализация и пошаговое управление)
|
|||
|
|
# ============================================================
|
|||
|
|
class Observer(ABC):
|
|||
|
|
@abstractmethod
|
|||
|
|
def update(self, event: str, data: dict = None):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ConsoleView(Observer):
|
|||
|
|
"""Отображает лабиринт, позицию игрока и найденный путь."""
|
|||
|
|
def __init__(self):
|
|||
|
|
self.last_maze = None
|
|||
|
|
self.last_player_pos = None
|
|||
|
|
self.last_path = None
|
|||
|
|
|
|||
|
|
def update(self, event: str, data: dict = None):
|
|||
|
|
if event == "maze_loaded":
|
|||
|
|
self.last_maze = data["maze"]
|
|||
|
|
self.render()
|
|||
|
|
elif event == "player_moved":
|
|||
|
|
self.last_maze = data["maze"]
|
|||
|
|
self.last_player_pos = data["player_pos"]
|
|||
|
|
self.render()
|
|||
|
|
elif event == "path_found":
|
|||
|
|
self.last_path = data["path"]
|
|||
|
|
self.render()
|
|||
|
|
elif event == "clear_path":
|
|||
|
|
self.last_path = None
|
|||
|
|
self.render()
|
|||
|
|
|
|||
|
|
def render(self):
|
|||
|
|
if self.last_maze is None:
|
|||
|
|
print("Нет лабиринта для отображения")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
maze = self.last_maze
|
|||
|
|
player = self.last_player_pos
|
|||
|
|
path_set = set(self.last_path) if self.last_path else set()
|
|||
|
|
|
|||
|
|
for y in range(maze.height):
|
|||
|
|
row = []
|
|||
|
|
for x in range(maze.width):
|
|||
|
|
cell = maze.get_cell(x, y)
|
|||
|
|
if player and cell == player:
|
|||
|
|
row.append('@')
|
|||
|
|
elif cell == maze.start_cell:
|
|||
|
|
row.append('S')
|
|||
|
|
elif cell == maze.exit_cell:
|
|||
|
|
row.append('E')
|
|||
|
|
elif cell in path_set and cell.is_passable():
|
|||
|
|
row.append('*')
|
|||
|
|
elif cell.is_wall:
|
|||
|
|
row.append('#')
|
|||
|
|
else:
|
|||
|
|
row.append(' ')
|
|||
|
|
print(''.join(row))
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Player:
|
|||
|
|
"""Игрок, перемещающийся по лабиринту."""
|
|||
|
|
def __init__(self, start_cell: Cell):
|
|||
|
|
self.position = start_cell
|
|||
|
|
|
|||
|
|
def move_to(self, new_cell: Cell):
|
|||
|
|
self.position = new_cell
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Command(ABC):
|
|||
|
|
@abstractmethod
|
|||
|
|
def execute(self) -> bool:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def undo(self):
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MoveCommand(Command):
|
|||
|
|
"""Команда перемещения игрока."""
|
|||
|
|
def __init__(self, player: Player, maze: Maze, direction: str):
|
|||
|
|
self.player = player
|
|||
|
|
self.maze = maze
|
|||
|
|
self.direction = direction
|
|||
|
|
self.prev_position = player.position
|
|||
|
|
self.new_position = None
|
|||
|
|
|
|||
|
|
def execute(self) -> bool:
|
|||
|
|
dx, dy = 0, 0
|
|||
|
|
if self.direction == 'W':
|
|||
|
|
dy = -1
|
|||
|
|
elif self.direction == 'S':
|
|||
|
|
dy = 1
|
|||
|
|
elif self.direction == 'A':
|
|||
|
|
dx = -1
|
|||
|
|
elif self.direction == 'D':
|
|||
|
|
dx = 1
|
|||
|
|
else:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
nx = self.player.position.x + dx
|
|||
|
|
ny = self.player.position.y + dy
|
|||
|
|
try:
|
|||
|
|
target = self.maze.get_cell(nx, ny)
|
|||
|
|
if target.is_passable():
|
|||
|
|
self.new_position = target
|
|||
|
|
self.player.move_to(target)
|
|||
|
|
return True
|
|||
|
|
except IndexError:
|
|||
|
|
pass
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def undo(self):
|
|||
|
|
if self.prev_position:
|
|||
|
|
self.player.move_to(self.prev_position)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class GameController:
|
|||
|
|
"""Управляет игрой, наблюдателями и командами."""
|
|||
|
|
def __init__(self, maze: Maze):
|
|||
|
|
self.maze = maze
|
|||
|
|
self.player = Player(maze.start_cell)
|
|||
|
|
self.observers = []
|
|||
|
|
self.command_stack = []
|
|||
|
|
|
|||
|
|
def attach(self, observer: Observer):
|
|||
|
|
self.observers.append(observer)
|
|||
|
|
|
|||
|
|
def detach(self, observer: Observer):
|
|||
|
|
self.observers.remove(observer)
|
|||
|
|
|
|||
|
|
def notify(self, event: str, data: dict = None):
|
|||
|
|
for obs in self.observers:
|
|||
|
|
obs.update(event, data or {})
|
|||
|
|
|
|||
|
|
def load_maze(self, maze: Maze):
|
|||
|
|
self.maze = maze
|
|||
|
|
self.player = Player(maze.start_cell)
|
|||
|
|
self.notify("maze_loaded", {"maze": maze})
|
|||
|
|
|
|||
|
|
def find_path(self, strategy: PathFindingStrategy) -> List[Cell]:
|
|||
|
|
solver = MazeSolver(self.maze, strategy)
|
|||
|
|
stats = solver.solve()
|
|||
|
|
print(f"Длина пути: {stats.path_length}, посещено: {stats.visited_cells}, время: {stats.time_ms:.3f} мс")
|
|||
|
|
path, _ = strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
|||
|
|
self.notify("path_found", {"path": path})
|
|||
|
|
return path
|
|||
|
|
|
|||
|
|
def clear_path(self):
|
|||
|
|
self.notify("clear_path", {})
|
|||
|
|
|
|||
|
|
def execute_command(self, cmd: Command):
|
|||
|
|
if cmd.execute():
|
|||
|
|
self.command_stack.append(cmd)
|
|||
|
|
self.notify("player_moved", {"maze": self.maze, "player_pos": self.player.position})
|
|||
|
|
|
|||
|
|
def undo(self):
|
|||
|
|
if self.command_stack:
|
|||
|
|
cmd = self.command_stack.pop()
|
|||
|
|
cmd.undo()
|
|||
|
|
self.notify("player_moved", {"maze": self.maze, "player_pos": self.player.position})
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Этап 6. Генераторы тестовых лабиринтов
|
|||
|
|
# ============================================================
|
|||
|
|
def generate_simple_maze(width: int, height: int) -> Maze:
|
|||
|
|
"""Маленький лабиринт с простым путём."""
|
|||
|
|
maze = Maze(width, height)
|
|||
|
|
for y in range(height):
|
|||
|
|
for x in range(width):
|
|||
|
|
maze.cells[y][x].is_wall = True
|
|||
|
|
|
|||
|
|
x, y = 0, 0
|
|||
|
|
path = [(x, y)]
|
|||
|
|
while x < width - 1 or y < height - 1:
|
|||
|
|
if x < width - 1 and (y == height - 1 or random.random() < 0.5):
|
|||
|
|
x += 1
|
|||
|
|
else:
|
|||
|
|
if y < height - 1:
|
|||
|
|
y += 1
|
|||
|
|
path.append((x, y))
|
|||
|
|
|
|||
|
|
for px, py in path:
|
|||
|
|
maze.cells[py][px].is_wall = False
|
|||
|
|
|
|||
|
|
maze.set_start(0, 0)
|
|||
|
|
maze.set_exit(width - 1, height - 1)
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
def generate_with_dead_ends(width: int, height: int) -> Maze:
|
|||
|
|
"""Средний лабиринт с гарантированным путём и множеством тупиков."""
|
|||
|
|
maze = Maze(width, height)
|
|||
|
|
for y in range(height):
|
|||
|
|
for x in range(width):
|
|||
|
|
maze.cells[y][x].is_wall = True
|
|||
|
|
|
|||
|
|
x, y = 0, 0
|
|||
|
|
main_path = []
|
|||
|
|
while x < width - 1 or y < height - 1:
|
|||
|
|
main_path.append((x, y))
|
|||
|
|
if x < width - 1 and (y == height - 1 or random.random() < 0.6):
|
|||
|
|
x += 1
|
|||
|
|
else:
|
|||
|
|
if y < height - 1:
|
|||
|
|
y += 1
|
|||
|
|
else:
|
|||
|
|
x += 1
|
|||
|
|
main_path.append((width - 1, height - 1))
|
|||
|
|
|
|||
|
|
for px, py in main_path:
|
|||
|
|
maze.cells[py][px].is_wall = False
|
|||
|
|
|
|||
|
|
num_dead_ends = int(width * height * 0.08)
|
|||
|
|
for _ in range(num_dead_ends):
|
|||
|
|
base_x, base_y = random.choice(main_path)
|
|||
|
|
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
|
|||
|
|
random.shuffle(directions)
|
|||
|
|
for dx, dy in directions:
|
|||
|
|
nx, ny = base_x + dx, base_y + dy
|
|||
|
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
|||
|
|
length = random.randint(2, 4)
|
|||
|
|
for step in range(length):
|
|||
|
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
|||
|
|
maze.cells[ny][nx].is_wall = False
|
|||
|
|
nx += dx
|
|||
|
|
ny += dy
|
|||
|
|
else:
|
|||
|
|
break
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
maze.set_start(0, 0)
|
|||
|
|
maze.set_exit(width - 1, height - 1)
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
def generate_complex_maze(width: int, height: int) -> Maze:
|
|||
|
|
"""Большой лабиринт с гарантированным путём и высокой запутанностью."""
|
|||
|
|
maze = Maze(width, height)
|
|||
|
|
for y in range(height):
|
|||
|
|
for x in range(width):
|
|||
|
|
maze.cells[y][x].is_wall = True
|
|||
|
|
|
|||
|
|
x, y = 0, 0
|
|||
|
|
main_path = []
|
|||
|
|
while x < width - 1 or y < height - 1:
|
|||
|
|
main_path.append((x, y))
|
|||
|
|
if x < width - 1 and (y == height - 1 or random.random() < 0.7):
|
|||
|
|
x += 1
|
|||
|
|
else:
|
|||
|
|
if y < height - 1:
|
|||
|
|
y += 1
|
|||
|
|
else:
|
|||
|
|
x += 1
|
|||
|
|
main_path.append((width - 1, height - 1))
|
|||
|
|
|
|||
|
|
for px, py in main_path:
|
|||
|
|
maze.cells[py][px].is_wall = False
|
|||
|
|
|
|||
|
|
num_branches = int(width * height * 0.12)
|
|||
|
|
for _ in range(num_branches):
|
|||
|
|
base_x, base_y = random.choice(main_path)
|
|||
|
|
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
|
|||
|
|
random.shuffle(directions)
|
|||
|
|
for dx, dy in directions:
|
|||
|
|
nx, ny = base_x + dx, base_y + dy
|
|||
|
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
|||
|
|
length = random.randint(1, 5)
|
|||
|
|
branch = []
|
|||
|
|
for step in range(length):
|
|||
|
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
|||
|
|
maze.cells[ny][nx].is_wall = False
|
|||
|
|
branch.append((nx, ny))
|
|||
|
|
nx += dx
|
|||
|
|
ny += dy
|
|||
|
|
else:
|
|||
|
|
break
|
|||
|
|
if random.random() < 0.3 and len(branch) >= 2:
|
|||
|
|
bx, by = branch[-1]
|
|||
|
|
for ddx, ddy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
|
|||
|
|
nnx, nny = bx + ddx, by + ddy
|
|||
|
|
if (0 <= nnx < width and 0 <= nny < height and
|
|||
|
|
maze.cells[nny][nnx].is_wall and random.random() < 0.5):
|
|||
|
|
maze.cells[nny][nnx].is_wall = False
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
maze.set_start(0, 0)
|
|||
|
|
maze.set_exit(width - 1, height - 1)
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
def generate_empty_maze(width: int, height: int) -> Maze:
|
|||
|
|
"""Пустой лабиринт без стен."""
|
|||
|
|
maze = Maze(width, height)
|
|||
|
|
for y in range(height):
|
|||
|
|
for x in range(width):
|
|||
|
|
maze.cells[y][x].is_wall = False
|
|||
|
|
maze.set_start(0, 0)
|
|||
|
|
maze.set_exit(width - 1, height - 1)
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
def generate_no_exit_maze(width: int, height: int) -> Maze:
|
|||
|
|
"""Лабиринт без выхода (выход окружён стенами)."""
|
|||
|
|
maze = generate_empty_maze(width, height)
|
|||
|
|
ex, ey = width - 1, height - 1
|
|||
|
|
for dx, dy in [(0, 0), (0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (-1, 1), (1, -1), (1, 1)]:
|
|||
|
|
nx, ny = ex + dx, ey + dy
|
|||
|
|
if 0 <= nx < width and 0 <= ny < height:
|
|||
|
|
if not (nx == 0 and ny == 0):
|
|||
|
|
maze.cells[ny][nx].is_wall = True
|
|||
|
|
maze.cells[ey][ex].is_wall = False
|
|||
|
|
maze.set_exit(ex, ey)
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Экспериментальная часть
|
|||
|
|
# ============================================================
|
|||
|
|
def run_experiment(maze: Maze, strategies: List[Tuple[str, PathFindingStrategy]], runs: int = 5) -> List[dict]:
|
|||
|
|
"""Запускает эксперимент на одном лабиринте и возвращает усреднённые результаты."""
|
|||
|
|
results = []
|
|||
|
|
for name, strategy in strategies:
|
|||
|
|
times = []
|
|||
|
|
visited_counts = []
|
|||
|
|
path_lengths = []
|
|||
|
|
for _ in range(runs):
|
|||
|
|
solver = MazeSolver(maze, strategy)
|
|||
|
|
stats = solver.solve()
|
|||
|
|
times.append(stats.time_ms)
|
|||
|
|
visited_counts.append(stats.visited_cells)
|
|||
|
|
path_lengths.append(stats.path_length)
|
|||
|
|
|
|||
|
|
avg_time = sum(times) / runs
|
|||
|
|
variance = sum((t - avg_time) ** 2 for t in times) / runs
|
|||
|
|
std_time = variance ** 0.5
|
|||
|
|
|
|||
|
|
results.append({
|
|||
|
|
'maze_type': '',
|
|||
|
|
'strategy': name,
|
|||
|
|
'avg_time_ms': avg_time,
|
|||
|
|
'std_time_ms': std_time,
|
|||
|
|
'avg_visited': sum(visited_counts) / runs,
|
|||
|
|
'avg_path_len': sum(path_lengths) / runs,
|
|||
|
|
'path_found': all(l > 0 for l in path_lengths)
|
|||
|
|
})
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
|
|||
|
|
def save_results_to_csv(results: List[dict], filename: str):
|
|||
|
|
"""Сохраняет результаты в CSV с разделителем ';' для совместимости с Excel."""
|
|||
|
|
if not results:
|
|||
|
|
return
|
|||
|
|
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
|||
|
|
writer = csv.DictWriter(f, fieldnames=results[0].keys(), delimiter=';')
|
|||
|
|
writer.writeheader()
|
|||
|
|
for row in results:
|
|||
|
|
row_copy = {}
|
|||
|
|
for k, v in row.items():
|
|||
|
|
if isinstance(v, float):
|
|||
|
|
row_copy[k] = f"{v:.6f}".replace(',', '.')
|
|||
|
|
else:
|
|||
|
|
row_copy[k] = v
|
|||
|
|
writer.writerow(row_copy)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Взвешенные лабиринты (опциональное задание)
|
|||
|
|
# ============================================================
|
|||
|
|
def assign_weights_random(maze: Maze, weights: List[Tuple[float, int]]) -> Maze:
|
|||
|
|
"""Присваивает веса клеткам согласно вероятностям."""
|
|||
|
|
for y in range(maze.height):
|
|||
|
|
for x in range(maze.width):
|
|||
|
|
if not maze.cells[y][x].is_wall:
|
|||
|
|
r = random.random()
|
|||
|
|
cum = 0
|
|||
|
|
for prob, w in weights:
|
|||
|
|
cum += prob
|
|||
|
|
if r < cum:
|
|||
|
|
maze.cells[y][x].weight = w
|
|||
|
|
break
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
def weighted_experiment():
|
|||
|
|
"""Дополнительный эксперимент со взвешенными клетками."""
|
|||
|
|
print("\n=== ВЗВЕШЕННЫЕ ЛАБИРИНТЫ (опциональное задание) ===")
|
|||
|
|
maze = generate_with_dead_ends(30, 30)
|
|||
|
|
assign_weights_random(maze, [(0.8, 1), (0.15, 3), (0.05, 2)])
|
|||
|
|
|
|||
|
|
strategies = [
|
|||
|
|
("A* (манхэттен)", AStarStrategy()),
|
|||
|
|
("Dijkstra", DijkstraStrategy())
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
print("Лабиринт 30x30 со взвешенными клетками (болото 3, песок 2, асфальт 1)")
|
|||
|
|
results = run_experiment(maze, strategies, runs=10)
|
|||
|
|
|
|||
|
|
for r in results:
|
|||
|
|
print(f"{r['strategy']:15} | Время: {r['avg_time_ms']:.2f} мс | "
|
|||
|
|
f"Посещено: {r['avg_visited']:.0f} | Длина пути: {r['avg_path_len']:.0f}")
|
|||
|
|
|
|||
|
|
# Сравнение с BFS
|
|||
|
|
bfs = BFSStrategy()
|
|||
|
|
path_bfs, _ = bfs.find_path(maze, maze.start_cell, maze.exit_cell)
|
|||
|
|
if path_bfs:
|
|||
|
|
cost_bfs = sum(cell.weight for cell in path_bfs)
|
|||
|
|
print(f"BFS нашёл путь длиной {len(path_bfs)} клеток, стоимость = {cost_bfs}")
|
|||
|
|
|
|||
|
|
path_dijkstra, _ = DijkstraStrategy().find_path(maze, maze.start_cell, maze.exit_cell)
|
|||
|
|
if path_dijkstra:
|
|||
|
|
cost_dijkstra = sum(cell.weight for cell in path_dijkstra)
|
|||
|
|
print(f"Dijkstra нашёл путь длиной {len(path_dijkstra)} клеток, стоимость = {cost_dijkstra}")
|
|||
|
|
|
|||
|
|
path_astar, _ = AStarStrategy().find_path(maze, maze.start_cell, maze.exit_cell)
|
|||
|
|
if path_astar:
|
|||
|
|
cost_astar = sum(cell.weight for cell in path_astar)
|
|||
|
|
print(f"A* нашёл путь длиной {len(path_astar)} клеток, стоимость = {cost_astar}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Демонстрация работы Observer и Command (по желанию)
|
|||
|
|
# ============================================================
|
|||
|
|
def demo_observer_command():
|
|||
|
|
"""Демонстрирует паттерны Observer и Command."""
|
|||
|
|
print("\n=== ДЕМОНСТРАЦИЯ OBSERVER И COMMAND ===")
|
|||
|
|
maze = generate_simple_maze(10, 10)
|
|||
|
|
|
|||
|
|
controller = GameController(maze)
|
|||
|
|
view = ConsoleView()
|
|||
|
|
controller.attach(view)
|
|||
|
|
|
|||
|
|
print("Лабиринт загружен:")
|
|||
|
|
controller.load_maze(maze)
|
|||
|
|
|
|||
|
|
print("Поиск пути с помощью BFS:")
|
|||
|
|
controller.find_path(BFSStrategy())
|
|||
|
|
|
|||
|
|
input("Нажмите Enter для пошагового управления...")
|
|||
|
|
|
|||
|
|
controller.clear_path()
|
|||
|
|
print("\nУправление: W/A/S/D - движение, Z - отмена, Q - выход")
|
|||
|
|
while True:
|
|||
|
|
cmd = input("> ").upper().strip()
|
|||
|
|
if cmd == 'Q':
|
|||
|
|
break
|
|||
|
|
elif cmd == 'Z':
|
|||
|
|
controller.undo()
|
|||
|
|
elif cmd in ('W', 'A', 'S', 'D'):
|
|||
|
|
move_cmd = MoveCommand(controller.player, controller.maze, cmd)
|
|||
|
|
controller.execute_command(move_cmd)
|
|||
|
|
else:
|
|||
|
|
print("Неизвестная команда")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Основной эксперимент
|
|||
|
|
# ============================================================
|
|||
|
|
def main():
|
|||
|
|
"""Основной эксперимент: сравнение стратегий на различных лабиринтах."""
|
|||
|
|
print("=== ЗАПУСК ЭКСПЕРИМЕНТОВ ===")
|
|||
|
|
|
|||
|
|
strategies = [
|
|||
|
|
("BFS", BFSStrategy()),
|
|||
|
|
("DFS", DFSStrategy()),
|
|||
|
|
("A*", AStarStrategy()),
|
|||
|
|
("Dijkstra", DijkstraStrategy())
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# Генерация тестовых лабиринтов
|
|||
|
|
maze_definitions = {
|
|||
|
|
"small_10x10_simple": generate_simple_maze(10, 10),
|
|||
|
|
"medium_50x50_deadends": generate_with_dead_ends(50, 50),
|
|||
|
|
"large_100x100_complex": generate_complex_maze(100, 100),
|
|||
|
|
"empty_50x50": generate_empty_maze(50, 50),
|
|||
|
|
"no_exit_50x50": generate_no_exit_maze(50, 50)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
all_results = []
|
|||
|
|
|
|||
|
|
for maze_name, maze in maze_definitions.items():
|
|||
|
|
print(f"\nЗапуск на лабиринте: {maze_name} ({maze.width}x{maze.height})")
|
|||
|
|
results = run_experiment(maze, strategies, runs=5)
|
|||
|
|
|
|||
|
|
for r in results:
|
|||
|
|
r['maze_type'] = maze_name
|
|||
|
|
all_results.append(r)
|
|||
|
|
|
|||
|
|
# Вывод промежуточных результатов
|
|||
|
|
for r in results:
|
|||
|
|
print(f" {r['strategy']:8} | Время: {r['avg_time_ms']:7.2f}±{r['std_time_ms']:.2f} мс | "
|
|||
|
|
f"Посещено: {r['avg_visited']:7.0f} | Длина пути: {r['avg_path_len']:5.0f}")
|
|||
|
|
|
|||
|
|
# Сохранение результатов
|
|||
|
|
save_results_to_csv(all_results, "experiment_results.csv")
|
|||
|
|
print("\nРезультаты сохранены в experiment_results.csv")
|
|||
|
|
|
|||
|
|
# Вывод сводной таблицы
|
|||
|
|
print("\n" + "=" * 100)
|
|||
|
|
print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ")
|
|||
|
|
print("=" * 100)
|
|||
|
|
print(f"{'Лабиринт':<25} | {'Стратегия':<10} | {'Время (мс)':<15} | {'Посещено':<10} | {'Длина пути':<10}")
|
|||
|
|
print("-" * 100)
|
|||
|
|
for r in all_results:
|
|||
|
|
print(f"{r['maze_type']:<25} | {r['strategy']:<10} | {r['avg_time_ms']:>8.2f} ± {r['std_time_ms']:<5.2f} | "
|
|||
|
|
f"{r['avg_visited']:>8.0f} | {r['avg_path_len']:>8.0f}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# Запуск
|
|||
|
|
# ============================================================
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|
|||
|
|
|
|||
|
|
# Раскомментируйте для демонстрации:
|
|||
|
|
# demo_observer_command()
|
|||
|
|
# weighted_experiment()
|