2026-rff_mp/starikovta/maze_solver.py
2026-05-25 10:14:49 +03:00

444 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 csv
import os
from collections import deque
import heapq
from abc import ABC, abstractmethod
# ========== ЭТАП 1: МОДЕЛЬ ЛАБИРИНТА ==========
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 x in range(width)] for y in range(height)]
self.start = None
self.exit = None
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[y][x]
return None
def get_neighbors(self, cell):
"""Возвращает список проходимых соседей (вверх, вниз, влево, вправо)"""
neighbors = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # вверх, вниз, влево, вправо
for dx, dy in directions:
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
def set_cell(self, x, y, cell):
if 0 <= x < self.width and 0 <= y < self.height:
self.grid[y][x] = cell
# ========== ЭТАП 2: ПАТТЕРН BUILDER ==========
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 = f.readlines()
# Убираем лишние пробелы и переносы строк
lines = [line.rstrip('\n\r') for line in lines]
height = len(lines)
width = len(lines[0]) if height > 0 else 0
maze = Maze(width, height)
start = None
exit_cell = None
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
cell = Cell(x, y, is_wall=True)
elif ch == 'S':
cell = Cell(x, y, is_start=True)
start = cell
elif ch == 'E':
cell = Cell(x, y, is_exit=True)
exit_cell = cell
else: # пробел или '.' — проход
cell = Cell(x, y, is_wall=False)
maze.set_cell(x, y, cell)
maze.start = start
maze.exit = exit_cell
# Валидация: проверяем, что есть и старт, и выход
if maze.start is None or maze.exit is None:
raise ValueError("В лабиринте должны быть S (старт) и E (выход)")
return maze
# ========== ЭТАП 3: ПАТТЕРН STRATEGY ==========
class PathFindingStrategy(ABC):
"""Интерфейс стратегии поиска пути"""
@abstractmethod
def find_path(self, maze, start, exit_cell):
pass
def reconstruct_path(parents, start, exit_cell):
"""Восстанавливает путь от выхода до старта"""
path = []
current = exit_cell
while current != start:
path.append(current)
current = parents.get((current.x, current.y))
if current is None:
return []
path.append(start)
path.reverse()
return path
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину (гарантирует кратчайший путь)"""
def find_path(self, maze, start, exit_cell):
queue = deque()
queue.append(start)
visited = {(start.x, start.y)}
parents = {}
while queue:
current = queue.popleft()
if current == exit_cell:
return reconstruct_path(parents, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if (neighbor.x, neighbor.y) not in visited:
visited.add((neighbor.x, neighbor.y))
parents[(neighbor.x, neighbor.y)] = current
queue.append(neighbor)
return [] # Путь не найден
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину (быстрый, но не гарантирует кратчайший путь)"""
def find_path(self, maze, start, exit_cell):
stack = [start]
visited = {(start.x, start.y)}
parents = {}
while stack:
current = stack.pop()
if current == exit_cell:
return reconstruct_path(parents, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if (neighbor.x, neighbor.y) not in visited:
visited.add((neighbor.x, neighbor.y))
parents[(neighbor.x, neighbor.y)] = current
stack.append(neighbor)
return []
class AStarStrategy(PathFindingStrategy):
"""A* с манхэттенской эвристикой"""
def _heuristic(self, cell, exit_cell):
"""Манхэттенское расстояние"""
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def find_path(self, maze, start, exit_cell):
# Приоритетная очередь: (f_score, counter, клетка)
counter = 0
open_set = [(0, counter, start)]
heapq.heapify(open_set)
came_from = {}
g_score = {(start.x, start.y): 0}
f_score = {(start.x, start.y): self._heuristic(start, exit_cell)}
while open_set:
current = heapq.heappop(open_set)[2]
if current == exit_cell:
return reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
tentative_g = g_score.get((current.x, current.y), float('inf')) + 1
if tentative_g < g_score.get((neighbor.x, neighbor.y), float('inf')):
came_from[(neighbor.x, neighbor.y)] = current
g_score[(neighbor.x, neighbor.y)] = tentative_g
f = tentative_g + self._heuristic(neighbor, exit_cell)
f_score[(neighbor.x, neighbor.y)] = f
counter += 1
heapq.heappush(open_set, (f, counter, neighbor))
return []
# ========== ЭТАП 4: КЛАСС-ОРКЕСТРАТОР ==========
class SearchStats:
"""Статистика поиска"""
def __init__(self, time_ms=0, visited_cells=0, path_length=0, path_found=False):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
self.path_found = path_found
def __repr__(self):
return f"Stats(time={self.time_ms:.3f}ms, visited={self.visited_cells}, length={self.path_length}, found={self.path_found})"
class MazeSolver:
"""Оркестратор: принимает лабиринт и стратегию, выполняет поиск"""
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
self.observers = []
def set_strategy(self, strategy):
self.strategy = strategy
def add_observer(self, observer):
self.observers.append(observer)
def _notify(self, event):
for observer in self.observers:
observer.update(event)
def solve(self):
if self.strategy is None:
raise ValueError("Стратегия не установлена")
if self.maze.start is None or self.maze.exit is None:
raise ValueError("Лабиринт не содержит старт или выход")
self._notify("Начало поиска пути...")
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000
stats = SearchStats(
time_ms=time_ms,
visited_cells=len(path) if path else 0,
path_length=len(path),
path_found=bool(path)
)
self._notify(f"Поиск завершён. Путь найден: {stats.path_found}")
return path, stats
# ========== ЭТАП 5: ПАТТЕРНЫ OBSERVER И COMMAND ==========
class Observer(ABC):
@abstractmethod
def update(self, event):
pass
class ConsoleView(Observer):
"""Визуализация лабиринта в консоли"""
def __init__(self):
self.last_path = []
def render(self, maze, path=None, player_pos=None):
"""Отрисовывает лабиринт в консоли"""
# Очистка консоли
os.system('cls' if os.name == 'nt' else 'clear')
symbols = {
'wall': '#',
'path': '.',
'start': 'S',
'exit': 'E',
'player': 'P',
'way': 'O'
}
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if player_pos and player_pos == (x, y):
line += symbols['player']
elif cell == maze.start:
line += symbols['start']
elif cell == maze.exit:
line += symbols['exit']
elif cell.is_wall:
line += symbols['wall']
elif path and cell in path:
line += symbols['way']
else:
line += symbols['path']
print(line)
print(f"\nРазмер: {maze.width}x{maze.height}")
def update(self, event):
print(f"[ConsoleView] {event}")
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class Player:
"""Игрок, который может перемещаться по лабиринту"""
def __init__(self, start_cell):
self.current = start_cell
self.previous = start_cell
def move_to(self, cell):
self.previous = self.current
self.current = cell
def undo_move(self):
self.current, self.previous = self.previous, self.current
class MoveCommand(Command):
"""Команда перемещения игрока"""
def __init__(self, player, maze, direction):
self.player = player
self.maze = maze
self.direction = direction
self.executed = False
def execute(self):
dx, dy = self.direction
new_cell = self.maze.get_cell(self.player.current.x + dx, self.player.current.y + dy)
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
self.executed = True
return True
return False
def undo(self):
if self.executed:
self.player.undo_move()
self.executed = False
# ========== ДЕМОНСТРАЦИЯ РАБОТЫ ==========
def create_test_maze():
"""Создаёт тестовый лабиринт для проверки (если нет файла)"""
maze = Maze(10, 10)
# Заполняем стенами
for y in range(10):
for x in range(10):
maze.set_cell(x, y, Cell(x, y, is_wall=True))
# Создаём коридор
for x in range(10):
maze.set_cell(x, 5, Cell(x, 5, is_wall=False))
maze.set_cell(0, 5, Cell(0, 5, is_start=True))
maze.set_cell(9, 5, Cell(9, 5, is_exit=True))
maze.start = maze.get_cell(0, 5)
maze.exit = maze.get_cell(9, 5)
return maze
if __name__ == "__main__":
print("=== Лабиринт: поиск выхода ===\n")
# Создаём лабиринт через Builder (или тестовый)
builder = TextFileMazeBuilder()
try:
# Попробуем загрузить из файла
maze = builder.build_from_file("maze.txt")
print("Лабиринт загружен из файла maze.txt")
except FileNotFoundError:
print("Файл maze.txt не найден. Использую тестовый лабиринт.")
maze = create_test_maze()
# Создаём визуализацию
view = ConsoleView()
# Тестируем стратегии
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy()
}
solver = MazeSolver(maze)
solver.add_observer(view)
results = []
for name, strategy in strategies.items():
print(f"\n--- {name} ---")
solver.set_strategy(strategy)
path, stats = solver.solve()
results.append([name, stats.time_ms, stats.visited_cells, stats.path_length, stats.path_found])
view.render(maze, path)
print(f"Время: {stats.time_ms:.3f} мс")
print(f"Длина пути: {stats.path_length}")
input("\nНажми Enter для продолжения...")
# Сохраняем результаты
with open("maze_results.csv", "w", newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(["Стратегия", "Время(мс)", "Посещено клеток", "Длина пути", "Путь найден"])
writer.writerows(results)
print("\n Результаты сохранены в maze_results.csv")