2026-rff_mp/ShapovalovKA/docs/data/2Task/t2.py
2026-05-25 11:58:51 +03:00

843 lines
30 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.

"""
Лабораторная работа: Применение паттернов проектирования
Этапы 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()