2026-rff_mp/maze_main.py

578 lines
18 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.

class Cell:
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
def is_passable(self):
return not self.is_wall
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start = None
self.exit = None
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[x][y]
return None
def get_neighbors(self, cell):
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.is_passable():
neighbors.append(nb)
return neighbors
def __repr__(self):
rows = []
for y in range(self.height):
row = []
for x in range(self.width):
c = self.get_cell(x, y)
if c.is_wall:
row.append('#')
elif c.is_start:
row.append('S')
elif c.is_exit:
row.append('E')
else:
row.append(' ')
rows.append(''.join(row))
return '\n'.join(rows)
def set_start(self, x, y):
cell = self.get_cell(x, y)
if cell and cell.is_passable():
cell.is_start = True
self.start = cell
def set_exit(self, x, y):
cell = self.get_cell(x, y)
if cell and cell.is_passable():
cell.is_exit = True
self.exit = cell
from abc import ABC, abstractmethod
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
h = len(lines)
w = len(lines[0]) if h > 0 else 0
maze = Maze(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
cell = maze.get_cell(x, y)
if ch == '#':
cell.is_wall = True
elif ch == 'S':
cell.is_start = True
maze.start = cell
elif ch == 'E':
cell.is_exit = True
maze.exit = cell
else:
cell.is_wall = False
if not maze.start:
raise ValueError("Нет старта (S)")
if not maze.exit:
raise ValueError("Нет выхода (E)")
return maze
from collections import deque
import heapq
import time
# ========== Strategy ==========
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze):
"""Возвращает список клеток от старта до выхода (включительно) или []"""
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze):
start = maze.start
exit_cell = maze.exit
if not start or not exit_cell:
return []
queue = deque([start])
visited = {start}
parent = {start: None}
while queue:
current = queue.popleft()
if current == exit_cell:
break
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
if exit_cell not in parent:
return []
# Восстановление пути
path = []
step = exit_cell
while step:
path.append(step)
step = parent[step]
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze):
start = maze.start
exit_cell = maze.exit
if not start or not exit_cell:
return []
stack = [(start, [start])]
visited = {start}
while stack:
current, path = stack.pop()
if current == exit_cell:
return path
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
stack.append((neighbor, path + [neighbor]))
return []
class AStarStrategy(PathFindingStrategy):
def heuristic(self, a, b):
# Манхэттенское расстояние
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze):
start = maze.start
exit_cell = maze.exit
if not start or not exit_cell:
return []
open_set = [(self.heuristic(start, exit_cell), 0, start)]
g_score = {start: 0}
parent = {start: None}
visited = {start}
while open_set:
_, cost, current = heapq.heappop(open_set)
if current == exit_cell:
break
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
parent[neighbor] = current
g_score[neighbor] = tentative_g
f = tentative_g + self.heuristic(neighbor, exit_cell)
heapq.heappush(open_set, (f, tentative_g, neighbor))
visited.add(neighbor)
if exit_cell not in parent:
return []
path = []
step = exit_cell
while step:
path.append(step)
step = parent[step]
path.reverse()
return path
# ========== SearchStats ==========
class SearchStats:
def __init__(self, time_ms=0.0, visited_cells=0, path_length=0):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __repr__(self):
return f"time={self.time_ms:.3f} ms, visited={self.visited_cells}, path_len={self.path_length}"
# ========== MazeSolver ==========
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
self.observers = []
def attach(self, observer):
self.observers.append(observer)
def notify(self, event_type, data=None):
for obs in self.observers:
obs.update(event_type, data)
def set_strategy(self, strategy):
self.strategy = strategy
def solve(self):
if not self.strategy:
raise ValueError("Стратегия не установлена")
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze)
end_time = time.perf_counter()
stats = SearchStats()
stats.time_ms = (end_time - start_time) * 1000
stats.path_length = len(path) if path else 0
if path:
self.notify("path_found", path)
return path, stats
# ========== Observer ==========
class Observer(ABC):
@abstractmethod
def update(self, event_type, data):
pass
class ConsoleView(Observer):
def __init__(self, maze):
self.maze = maze
def update(self, event_type, data):
if event_type == "path_found":
path = data
self.render(path)
elif event_type == "move":
player_pos = data
self.render(player_pos=player_pos)
else:
self.render()
def render(self, path=None, player_pos=None):
"""Отрисовка лабиринта с путём и/или позицией игрока"""
# Копия лабиринта для отображения
display = []
for y in range(self.maze.height):
row = []
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
if cell.is_wall:
row.append('')
elif cell.is_start:
row.append('S')
elif cell.is_exit:
row.append('E')
else:
row.append(' ')
display.append(row)
# Отметить путь (кроме старта и выхода)
if path:
for cell in path:
if cell != self.maze.start and cell != self.maze.exit:
display[cell.y][cell.x] = ''
# Отметить игрока (если есть)
if player_pos:
x, y = player_pos.x, player_pos.y
if display[y][x] not in ('S', 'E'):
display[y][x] = 'P'
# Очистка консоли (для красоты, можно закомментировать)
import os
os.system('cls' if os.name == 'nt' else 'clear')
for row in display:
print(''.join(row))
print()
# ========== Command ==========
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player, direction, maze):
self.player = player
self.direction = direction # (dx, dy)
self.maze = maze
self.previous_cell = player.current_cell
def execute(self):
dx, dy = self.direction
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
return True
return False
def undo(self):
self.player.move_to(self.previous_cell)
class Player:
def __init__(self, start_cell):
self.current_cell = start_cell
def move_to(self, cell):
self.current_cell = cell
# ========== Observer ==========
class Observer(ABC):
@abstractmethod
def update(self, event_type, data):
pass
class ConsoleView(Observer):
def __init__(self, maze):
self.maze = maze
def update(self, event_type, data):
if event_type == "path_found":
path = data
self.render(path=path)
elif event_type == "move":
player_pos = data
self.render(player_pos=player_pos)
else:
self.render()
def render(self, path=None, player_pos=None):
"""Отрисовка лабиринта с путём и/или позицией игрока"""
display = []
for y in range(self.maze.height):
row = []
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
if cell.is_wall:
row.append('#')
elif cell.is_start:
row.append('S')
elif cell.is_exit:
row.append('E')
else:
row.append(' ')
display.append(row)
if path:
for cell in path:
if cell != self.maze.start and cell != self.maze.exit:
display[cell.y][cell.x] = ''
if player_pos:
x, y = player_pos.x, player_pos.y
if display[y][x] not in ('S', 'E'):
display[y][x] = 'P'
# Очистка консоли для красоты (можно закомментировать)
import os
os.system('cls' if os.name == 'nt' else 'clear')
for row in display:
print(''.join(row))
print()
# ========== Command ==========
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player, direction, maze):
self.player = player
self.direction = direction
self.maze = maze
self.previous_cell = player.current_cell
def execute(self):
dx, dy = self.direction
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
return True
return False
def undo(self):
self.player.move_to(self.previous_cell)
class Player:
def __init__(self, start_cell):
self.current_cell = start_cell
def move_to(self, cell):
self.current_cell = cell
# ========== ЭКСПЕРИМЕНТЫ ==========
import csv
import random
def generate_test_mazes():
"""Создаёт несколько лабиринтов для тестирования"""
mazes = {}
# 1. Маленький лабиринт 5x5
small = Maze(5, 5)
for x in range(5):
small.get_cell(x, 0).is_wall = True
small.get_cell(x, 4).is_wall = True
for y in range(5):
small.get_cell(0, y).is_wall = True
small.get_cell(4, y).is_wall = True
small.get_cell(1, 1).is_wall = False
small.get_cell(2, 1).is_wall = False
small.get_cell(3, 1).is_wall = False
small.get_cell(3, 2).is_wall = False
small.get_cell(3, 3).is_wall = False
small.set_start(1, 1)
small.set_exit(3, 3)
mazes["small"] = small
# 2. Средний лабиринт 15x15 (стены по краям и простой коридор)
medium = Maze(15, 15)
for x in range(15):
medium.get_cell(x, 0).is_wall = True
medium.get_cell(x, 14).is_wall = True
for y in range(15):
medium.get_cell(0, y).is_wall = True
medium.get_cell(14, y).is_wall = True
# Простой зигзаг
for i in range(1, 14):
medium.get_cell(i, i).is_wall = False
medium.set_start(1, 1)
medium.set_exit(13, 13)
mazes["medium"] = medium
# 3. Пустой лабиринт (нет стен)
empty = Maze(20, 20)
for x in range(20):
for y in range(20):
empty.get_cell(x, y).is_wall = False
empty.set_start(0, 0)
empty.set_exit(19, 19)
mazes["empty"] = empty
# 4. Лабиринт без выхода (путь заблокирован)
no_exit = Maze(10, 10)
for x in range(10):
for y in range(10):
no_exit.get_cell(x, y).is_wall = False
for x in range(5, 10):
no_exit.get_cell(x, 5).is_wall = True # стена блокирует
no_exit.set_start(0, 0)
no_exit.set_exit(9, 9)
mazes["no_exit"] = no_exit
return mazes
def run_experiments(mazes, strategies, repeats=5):
"""Прогоняет все стратегии на всех лабиринтах repeats раз, возвращает список результатов"""
results = []
for maze_name, maze in mazes.items():
for strategy_name, strategy in strategies.items():
solver = MazeSolver(maze)
solver.set_strategy(strategy)
for _ in range(repeats):
path, stats = solver.solve()
results.append({
"maze": maze_name,
"strategy": strategy_name,
"time_ms": stats.time_ms,
"path_length": stats.path_length
})
return results
def save_results_to_csv(results, filename="maze_results.csv"):
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "time_ms", "path_length"])
writer.writeheader()
writer.writerows(results)
print(f"Результаты сохранены в {filename}")
def plot_maze_results(csv_file="maze_results.csv", output_png="maze_graphs.png"):
try:
import matplotlib.pyplot as plt
import pandas as pd
except ImportError:
print("matplotlib или pandas не установлены. Установи: pip install matplotlib pandas")
return
df = pd.read_csv(csv_file)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# График времени
for strategy in df["strategy"].unique():
subset = df[df["strategy"] == strategy]
axes[0].plot(subset["maze"], subset["time_ms"], marker='o', label=strategy)
axes[0].set_title("Время поиска пути")
axes[0].set_ylabel("Время (мс)")
axes[0].legend()
# График длины пути
for strategy in df["strategy"].unique():
subset = df[df["strategy"] == strategy]
axes[1].plot(subset["maze"], subset["path_length"], marker='s', label=strategy)
axes[1].set_title("Длина найденного пути")
axes[1].set_ylabel("Клеток")
axes[1].legend()
plt.tight_layout()
plt.savefig(output_png)
print(f"График сохранён как {output_png}")
# plt.show() # раскомментируй, если хочешь увидеть окно с графиком
if __name__ == "__main__":
# Генерируем тестовые лабиринты
mazes = generate_test_mazes()
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy(),
}
print("Запуск экспериментов (может занять 1020 секунд)...")
results = run_experiments(mazes, strategies, repeats=5)
save_results_to_csv(results)
plot_maze_results()
print("Готово! Файлы maze_results.csv и maze_graphs.png созданы.")