2026-rff_mp/novikovsd/maze.py
novikovsd 287a6098b5 FINISH
добавлены функции для итерактивного подхода, для этого нужно запустить программу один раз и изменить желаемый лабиринт
2026-05-25 13:20:57 +03:00

568 lines
19 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 sys
import time
import csv
from collections import deque
from heapq import heappush, heappop
from abc import ABC, abstractmethod
from typing import List, Optional, Tuple, Dict, Any
import os
RESULTS_DIR = "lab2_results"
class Cell:
def __init__(self, x: int, y: int, is_wall: bool = False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = False
self.is_exit = False
def is_passable(self) -> bool:
return not self.is_wall
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: List[List[Cell]] = []
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def set_cell(self, x: int, y: int, cell: Cell):
if not self.cells:
self.cells = [[None] * self.width for _ in range(self.height)]
self.cells[y][x] = cell
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]:
neighbors = []
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
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(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.readlines()]
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
start_cell = None
exit_cell = None
for y, line in enumerate(lines):
for x, ch in enumerate(line):
is_wall = (ch == '#')
cell = Cell(x, y, is_wall)
if ch == 'S':
cell.is_start = True
start_cell = cell
elif ch == 'E':
cell.is_exit = True
exit_cell = cell
maze.set_cell(x, y, cell)
if start_cell is None or exit_cell is None:
for y in range(height):
for x in range(width):
cell = maze.get_cell(x, y)
if cell and cell.is_start:
start_cell = cell
if cell and cell.is_exit:
exit_cell = cell
if start_cell is None:
raise ValueError("Нет стартовой клетки (S)")
if exit_cell is None:
raise ValueError("Нет выходной клетки (E)")
maze.start = start_cell
maze.exit = exit_cell
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
if start == exit:
self.last_visited = 1
return [start]
queue = deque()
queue.append(start)
parent = {start: None}
visited = {start}
visited_count = 1
while queue:
current = queue.popleft()
if current == exit:
break
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
visited_count += 1
parent[neighbor] = current
queue.append(neighbor)
self.last_visited = visited_count
if exit not in parent:
return []
path = []
cur = exit
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
stack = [(start, [start])]
visited = {start}
visited_count = 1
while stack:
current, path = stack.pop()
if current == exit:
self.last_visited = visited_count
return path
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
visited_count += 1
stack.append((neighbor, path + [neighbor]))
self.last_visited = visited_count
return []
class AStarStrategy(PathFindingStrategy):
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) -> List[Cell]:
open_set = []
counter = 0
heappush(open_set, (0, counter, start))
came_from = {}
g_score = {start: 0}
f_score = {start: self.heuristic(start, exit)}
visited_count = 0
while open_set:
_, _, current = heappop(open_set)
visited_count += 1
if current == exit:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
path.reverse()
self.last_visited = visited_count
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 = tentative_g + self.heuristic(neighbor, exit)
f_score[neighbor] = f
counter += 1
heappush(open_set, (f, counter, neighbor))
self.last_visited = visited_count
return []
class SearchStats:
def __init__(self, time_ms: float, visited_cells: int, path_length: int):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __repr__(self):
return f"Stats(time={self.time_ms:.2f}ms, visited={self.visited_cells}, path_len={self.path_length})"
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self, event: str, data: Any = None):
for obs in self.observers:
obs.update(event, data)
def solve(self) -> Tuple[List[Cell], SearchStats]:
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000.0
visited_cells = getattr(self.strategy, 'last_visited', len(path) if path else 0)
stats = SearchStats(elapsed_ms, visited_cells, len(path))
self.notify("solved", {"path": path, "stats": stats})
return path, stats
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any):
pass
class ConsoleView(Observer):
def __init__(self):
self.player_pos = None
self.path = []
def update(self, event: str, data: Any):
if event == "maze_loaded":
self.maze = data["maze"]
self.render()
elif event == "player_moved":
self.player_pos = data["player_cell"]
self.render()
elif event == "path_found":
self.path = data["path"]
self.render()
elif event == "solved":
self.path = data["path"]
self.render()
def render(self, maze: Maze = None, player_cell: Cell = None, path: List[Cell] = None):
if maze:
self.maze = maze
if player_cell:
self.player_pos = player_cell
if path is not None:
self.path = path
if not hasattr(self, 'maze'):
print("Нет лабиринта для отображения")
return
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 None:
row += " "
continue
if self.player_pos and cell == self.player_pos:
row += "P"
elif cell == self.maze.start:
row += "S"
elif cell == self.maze.exit:
row += "E"
elif self.path and cell in self.path:
row += "."
elif cell.is_wall:
row += "#"
else:
row += " "
print(row)
print()
class MoveCommand(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
def move_to(self, cell: Cell):
self.current_cell = cell
class MoveCommandImpl(MoveCommand):
def __init__(self, player: Player, direction: str, maze: Maze):
self.player = player
self.direction = direction
self.maze = maze
self.previous_cell = player.current_cell
def execute(self):
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
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)
def ensure_results_dir():
if not os.path.exists(RESULTS_DIR):
os.makedirs(RESULTS_DIR)
print(f"Создана папка: {RESULTS_DIR}")
def generate_test_maze_file(filename: str, maze_type: str):
full_path = os.path.join(RESULTS_DIR, filename)
if maze_type == "small":
lines = [
"##########",
"#S #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# E#",
"##########"
]
elif maze_type == "medium":
height, width = 50, 50
lines = []
for y in range(height):
row = []
for x in range(width):
if y == 0 or y == height-1 or x == 0 or x == width-1:
row.append('#')
elif (y % 5 == 0 and x % 7 == 0) or (y % 8 == 0 and x % 3 == 0):
row.append('#')
else:
row.append(' ')
row_str = ''.join(row)
lines.append(row_str)
lines[1] = 'S' + lines[1][1:]
lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:]
elif maze_type == "large":
import random
height, width = 100, 100
random.seed(42)
lines = []
for y in range(height):
row = []
for x in range(width):
if y == 0 or y == height-1 or x == 0 or x == width-1:
row.append('#')
else:
if random.random() < 0.2:
row.append('#')
else:
row.append(' ')
lines.append(''.join(row))
lines[1] = 'S' + lines[1][1:]
lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:]
elif maze_type == "empty":
height, width = 50, 50
lines = []
for y in range(height):
if y == 0 or y == height-1:
lines.append('#' * width)
else:
lines.append('#' + ' ' * (width-2) + '#')
lines[1] = 'S' + lines[1][1:]
lines[height-2] = lines[height-2][:width-2] + 'E' + lines[height-2][width-1:]
elif maze_type == "no_exit":
lines = [
"##########",
"#S #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"##########"
]
else:
raise ValueError("Unknown maze type")
with open(full_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
def run_experiment():
ensure_results_dir()
maze_types = ["small", "medium", "large", "empty", "no_exit"]
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"AStar": AStarStrategy()
}
results = []
for maze_type in maze_types:
filename = f"maze_{maze_type}.txt"
generate_test_maze_file(filename, maze_type)
full_path = os.path.join(RESULTS_DIR, filename)
builder = TextFileMazeBuilder()
try:
maze = builder.build_from_file(full_path)
except ValueError as e:
print(f"Лабиринт {maze_type} пропущен: {e}")
continue
for strat_name, strat_obj in strategies.items():
times = []
path_lengths = []
visited_counts = []
for run in range(5):
solver = MazeSolver(maze, strat_obj)
path, stats = solver.solve()
times.append(stats.time_ms)
path_lengths.append(stats.path_length)
visited_counts.append(stats.visited_cells)
avg_time = sum(times) / len(times)
avg_path_len = sum(path_lengths) / len(path_lengths)
avg_visited = sum(visited_counts) / len(visited_counts)
results.append({
"maze": maze_type,
"strategy": strat_name,
"avg_time_ms": avg_time,
"avg_visited": avg_visited,
"avg_path_length": avg_path_len
})
print(f"{maze_type} / {strat_name}: время={avg_time:.2f}ms, посещено={avg_visited:.1f}, путь={avg_path_len:.1f}")
csv_path = os.path.join(RESULTS_DIR, "experiment_results.csv")
with open(csv_path, "w", newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "avg_time_ms", "avg_visited", "avg_path_length"])
writer.writeheader()
writer.writerows(results)
try:
import matplotlib.pyplot as plt
for maze_type in ["small", "medium", "large", "empty"]:
data = [r for r in results if r["maze"] == maze_type]
if not data:
continue
names = [d["strategy"] for d in data]
times = [d["avg_time_ms"] for d in data]
visited = [d["avg_visited"] for d in data]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.bar(names, times)
ax1.set_title(f"Время (мс) - {maze_type}")
ax2.bar(names, visited)
ax2.set_title(f"Посещено клеток - {maze_type}")
plt.tight_layout()
plot_path = os.path.join(RESULTS_DIR, f"plot_{maze_type}.png")
plt.savefig(plot_path)
plt.close()
print(f"Графики сохранены в папку {RESULTS_DIR}")
except ImportError:
print("matplotlib не установлен. Графики не построены.")
def demo_interactive():
ensure_results_dir()
builder = TextFileMazeBuilder()
filename = input("Введите имя файла с лабиринтом (например, maze_small.txt): ").strip()
if not os.path.exists(filename) and not os.path.exists(os.path.join(RESULTS_DIR, filename)):
print(f"Файл {filename} не найден. Создаю тестовый лабиринт small в папке {RESULTS_DIR}")
generate_test_maze_file("demo_maze.txt", "small")
filename = os.path.join(RESULTS_DIR, "demo_maze.txt")
elif os.path.exists(os.path.join(RESULTS_DIR, filename)):
filename = os.path.join(RESULTS_DIR, filename)
maze = builder.build_from_file(filename)
view = ConsoleView()
view.update("maze_loaded", {"maze": maze})
print("Выберите алгоритм поиска:")
print("1. BFS")
print("2. DFS")
print("3. A*")
choice = input("Ваш выбор: ")
if choice == "1":
strategy = BFSStrategy()
elif choice == "2":
strategy = DFSStrategy()
else:
strategy = AStarStrategy()
solver = MazeSolver(maze, strategy)
solver.attach(view)
path, stats = solver.solve()
print(f"Поиск завершён. Статистика: {stats}")
if path:
print("Найден путь. Хотите пройти по нему пошагово? (y/n): ", end="")
ans = input().lower()
if ans == 'y':
player = Player(maze.start)
cmd_history = []
for step_cell in path[1:]:
dx = step_cell.x - player.current_cell.x
dy = step_cell.y - player.current_cell.y
if dx == 1:
dir_char = 'd'
elif dx == -1:
dir_char = 'a'
elif dy == 1:
dir_char = 's'
else:
dir_char = 'w'
cmd = MoveCommandImpl(player, dir_char, maze)
if cmd.execute():
cmd_history.append(cmd)
view.update("player_moved", {"player_cell": player.current_cell})
input("Нажмите Enter для следующего шага...")
print("Вы достигли выхода!")
print("Отменить последний шаг? (y/n): ", end="")
if input().lower() == 'y' and cmd_history:
cmd_history[-1].undo()
view.update("player_moved", {"player_cell": player.current_cell})
print("Последний шаг отменён.")
else:
print("Путь не найден.")
if __name__ == "__main__":
print("Лабораторная работа: Поиск выхода из лабиринта")
print("1. Запустить эксперименты (сравнение алгоритмов)")
print("2. Интерактивный режим (загрузка своего лабиринта)")
mode = input("Выберите режим (1 или 2): ")
if mode == "1":
run_experiment()
elif mode == "2":
demo_interactive()
else:
print("Неверный выбор.")