2026-rff_mp/AgapovaDS/docs/data/2-nd/maze.py

441 lines
14 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 time
import random
import csv
import os
from collections import deque
import heapq
import matplotlib.pyplot as plt
import numpy as np
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 is_passable(self):
return not self.is_wall
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start_cell = None
self.exit_cell = None
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[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
neighbor = self.get_cell(nx, ny)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder:
def build_from_file(self, filename):
raise NotImplementedError
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = f.readlines()
lines = [line.rstrip('\n') for line in lines if line.strip() != '']
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):
for x, ch in enumerate(line):
if x >= width:
break
cell = maze.get_cell(x, y)
if ch == '#':
cell.is_wall = True
elif ch == 'S':
cell.is_start = True
maze.start_cell = cell
elif ch == 'E':
cell.is_exit = True
maze.exit_cell = cell
if maze.start_cell is None or maze.exit_cell is None:
raise ValueError("В лабиринте должны быть S и E")
return maze
class PathFindingStrategy:
def find_path(self, maze, start, exit):
raise NotImplementedError
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit):
if start == exit:
return [start]
queue = deque([start])
visited = {start}
parent = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
if exit not in parent:
return []
path = []
step = exit
while step is not None:
path.append(step)
step = parent[step]
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit):
if start == exit:
return [start]
stack = [start]
visited = {start}
parent = {start: None}
while stack:
current = stack.pop()
if current == exit:
break
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
if exit not in parent:
return []
path = []
step = exit
while step is not None:
path.append(step)
step = parent[step]
path.reverse()
return path
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, exit):
if start == exit:
return [start]
open_set = []
heapq.heappush(open_set, (0, id(start), start))
came_from = {}
g_score = {start: 0}
f_score = {start: self.heuristic(start, exit)}
while open_set:
_, _, current = heapq.heappop(open_set)
if current == exit:
path = []
step = current
while step is not None:
path.append(step)
step = came_from.get(step)
path.reverse()
return path
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]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self.heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor))
return []
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
self.observers = [] # для Observer
def set_strategy(self, strategy):
self.strategy = strategy
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self, event):
for obs in self.observers:
obs.update(event)
def solve(self):
if self.strategy is None:
raise ValueError("Стратегия не установлена")
start = self.maze.start_cell
exit_cell = self.maze.exit_cell
if start is None or exit_cell is None:
raise ValueError("Лабиринт не содержит старта или выхода")
self.notify("Поиск начат")
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, start, exit_cell)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
self.notify("Поиск завершён")
return path, elapsed_ms
class Observer:
def update(self, event):
raise NotImplementedError
class ConsoleView(Observer):
def __init__(self, maze):
self.maze = maze
def update(self, event):
if event == "Поиск начат":
print("=== Поиск начат ===")
elif event == "Поиск завершён":
print("=== Поиск завершён ===")
def render(self, path=None):
path_set = set(path) if path else set()
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 += '#'
elif cell.is_start:
row += 'S'
elif cell.is_exit:
row += 'E'
elif cell in path_set:
row += '*'
else:
row += ' '
print(row)
print()
class Command:
def execute(self):
raise NotImplementedError
def undo(self):
raise NotImplementedError
class MoveCommand(Command):
def __init__(self, player, dx, dy):
self.player = player
self.dx = dx
self.dy = dy
self.prev_x = player.x
self.prev_y = player.y
def execute(self):
new_x = self.player.x + self.dx
new_y = self.player.y + self.dy
maze = self.player.maze
cell = maze.get_cell(new_x, new_y)
if cell and cell.is_passable():
self.player.x = new_x
self.player.y = new_y
self.player.current_cell = cell
return True
return False
def undo(self):
self.player.x = self.prev_x
self.player.y = self.prev_y
self.player.current_cell = self.player.maze.get_cell(self.prev_x, self.prev_y)
class Player:
def __init__(self, maze, start_cell):
self.maze = maze
self.x = start_cell.x
self.y = start_cell.y
self.current_cell = start_cell
# EEEEEEEEEKSPERIMENTY
def generate_empty_maze(width, height):
maze = Maze(width, height)
start = maze.get_cell(0, 0)
exit_cell = maze.get_cell(width-1, height-1)
start.is_start = True
exit_cell.is_exit = True
maze.start_cell = start
maze.exit_cell = exit_cell
return maze
def generate_random_maze(width, height, wall_prob=0.3):
maze = Maze(width, height)
for x in range(width):
for y in range(height):
cell = maze.get_cell(x, y)
if random.random() < wall_prob:
cell.is_wall = True
start = maze.get_cell(0, 0)
exit_cell = maze.get_cell(width-1, height-1)
start.is_wall = False
start.is_start = True
exit_cell.is_wall = False
exit_cell.is_exit = True
maze.start_cell = start
maze.exit_cell = exit_cell
return maze
def generate_maze_with_dead_ends(width, height):
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.get_cell(x, y).is_wall = True
x, y = 0, 0
while x < width and y < height:
cell = maze.get_cell(x, y)
cell.is_wall = False
if x == width-1 and y == height-1:
break
if y+1 < height and (x == width-1 or random.choice([True, False])):
y += 1
else:
x += 1
start = maze.get_cell(0, 0)
exit_cell = maze.get_cell(width-1, height-1)
start.is_start = True
exit_cell.is_exit = True
maze.start_cell = start
maze.exit_cell = exit_cell
return maze
def generate_maze_no_exit(width, height):
maze = generate_random_maze(width, height, 0.2)
exit_cell = maze.get_cell(width-1, height-1)
for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]:
nx, ny = exit_cell.x + dx, exit_cell.y + dy
neighbor = maze.get_cell(nx, ny)
if neighbor:
neighbor.is_wall = True
start = maze.get_cell(0, 0)
start.is_wall = False
start.is_start = True
maze.start_cell = start
maze.exit_cell = exit_cell
return maze
def run_experiment():
os.makedirs("results", exist_ok=True)
maze_generators = [
("empty_10x10", lambda: generate_empty_maze(10, 10)),
("empty_50x50", lambda: generate_empty_maze(50, 50)),
("empty_100x100", lambda: generate_empty_maze(100, 100)),
("random_10x10", lambda: generate_random_maze(10, 10, 0.3)),
("random_50x50", lambda: generate_random_maze(50, 50, 0.3)),
("random_100x100", lambda: generate_random_maze(100, 100, 0.3)),
("dead_ends_10x10", lambda: generate_maze_with_dead_ends(10, 10)),
("dead_ends_50x50", lambda: generate_maze_with_dead_ends(50, 50)),
("dead_ends_100x100", lambda: generate_maze_with_dead_ends(100, 100)),
("no_exit_10x10", lambda: generate_maze_no_exit(10, 10)),
("no_exit_50x50", lambda: generate_maze_no_exit(50, 50)),
]
strategies = [
("BFS", BFSStrategy()),
("DFS", DFSStrategy()),
("AStar", AStarStrategy())
]
repeats = 5
all_results = []
for maze_name, gen_func in maze_generators:
print(f"Тестирование лабиринта: {maze_name}")
maze = gen_func()
solver = MazeSolver(maze)
for strat_name, strat in strategies:
solver.set_strategy(strat)
total_time = 0
total_path_len = 0
path = []
for rep in range(repeats):
path, elapsed_ms = solver.solve()
total_time += elapsed_ms
total_path_len += len(path) if path else 0
avg_time = total_time / repeats
avg_len = total_path_len / repeats
all_results.append({
"Maze": maze_name,
"Strategy": strat_name,
"AvgTime_ms": avg_time,
"AvgPathLen": avg_len,
"PathFound": len(path) > 0 if path else False
})
print(f" {strat_name}: время {avg_time:.3f} мс, длина пути {avg_len:.1f}")
# Сохраняем CSV
csv_path = "results/experiment_results.csv"
with open(csv_path, 'w', newline='', encoding='utf-8') as f:
fieldnames = ["Maze", "Strategy", "AvgTime_ms", "AvgPathLen", "PathFound"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(all_results)
print(f"Результаты сохранены в {csv_path}")
# Построение графика
maze_names = sorted(set(r["Maze"] for r in all_results))
strategy_names = ["BFS", "DFS", "AStar"]
data = {maze: {s: None for s in strategy_names} for maze in maze_names}
for r in all_results:
data[r["Maze"]][r["Strategy"]] = r["AvgTime_ms"]
fig, ax = plt.subplots(figsize=(14, 6))
x = np.arange(len(maze_names))
width = 0.25
colors = ['skyblue', 'lightgreen', 'salmon']
for i, strat in enumerate(strategy_names):
times = [data[maze][strat] if data[maze][strat] is not None else 0 for maze in maze_names]
ax.bar(x + i*width, times, width, label=strat, color=colors[i])
ax.set_xlabel('Лабиринт')
ax.set_ylabel('Среднее время (мс)')
ax.set_title('Сравнение стратегий поиска пути')
ax.set_xticks(x + width)
ax.set_xticklabels(maze_names, rotation=45, ha='right')
ax.legend()
plt.tight_layout()
plt.savefig("results/performance.png", dpi=150)
plt.show()
print("График сохранён в results/performance.png")
if __name__ == '__main__':
run_experiment()