forked from UNN/2026-rff_mp
121 lines
4.4 KiB
Python
121 lines
4.4 KiB
Python
|
|
# maze_builder.py
|
|||
|
|
from abc import ABC, abstractmethod
|
|||
|
|
from maze import Maze, Cell
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MazeBuilder(ABC):
|
|||
|
|
"""Интерфейс строителя лабиринта (Builder pattern)."""
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def build_from_file(self, filename: str) -> Maze:
|
|||
|
|
"""Принимает путь к файлу, возвращает готовый Maze."""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TextFileMazeBuilder(MazeBuilder):
|
|||
|
|
"""
|
|||
|
|
Строитель лабиринта из текстового файла.
|
|||
|
|
|
|||
|
|
Формат файла:
|
|||
|
|
# — стена
|
|||
|
|
. или пробел — проход
|
|||
|
|
S — старт
|
|||
|
|
E — выход
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
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_found = False
|
|||
|
|
exit_found = False
|
|||
|
|
|
|||
|
|
for y, line in enumerate(lines):
|
|||
|
|
for x, char in enumerate(line):
|
|||
|
|
is_wall = (char == '#')
|
|||
|
|
is_start = (char == 'S')
|
|||
|
|
is_exit = (char == 'E')
|
|||
|
|
|
|||
|
|
if is_start:
|
|||
|
|
start_found = True
|
|||
|
|
if is_exit:
|
|||
|
|
exit_found = True
|
|||
|
|
|
|||
|
|
cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
|
|||
|
|
maze.set_cell(x, y, cell)
|
|||
|
|
|
|||
|
|
if not start_found or not exit_found:
|
|||
|
|
raise ValueError("Лабиринт должен содержать старт (S) и выход (E)")
|
|||
|
|
|
|||
|
|
return maze
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RandomMazeBuilder(MazeBuilder):
|
|||
|
|
"""
|
|||
|
|
Строитель случайного лабиринта.
|
|||
|
|
Алгоритм: рекурсивный бэктрекинг.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, width: int, height: int):
|
|||
|
|
self.width = width
|
|||
|
|
self.height = height
|
|||
|
|
|
|||
|
|
def build_from_file(self, filename: str = None) -> Maze:
|
|||
|
|
"""
|
|||
|
|
filename игнорируется — лабиринт генерируется случайно.
|
|||
|
|
Название метода общее для всех Builder'ов.
|
|||
|
|
"""
|
|||
|
|
import random
|
|||
|
|
|
|||
|
|
maze = Maze(self.width, self.height)
|
|||
|
|
|
|||
|
|
# Шаг 1: Заполняем всё стенами
|
|||
|
|
for y in range(self.height):
|
|||
|
|
for x in range(self.width):
|
|||
|
|
maze.set_cell(x, y, Cell(x, y, is_wall=True))
|
|||
|
|
|
|||
|
|
# Шаг 2: Рекурсивно прокладываем пути
|
|||
|
|
visited = set()
|
|||
|
|
|
|||
|
|
def carve(x, y):
|
|||
|
|
"""Прокладывает проход из точки (x, y)."""
|
|||
|
|
visited.add((x, y))
|
|||
|
|
maze.set_cell(x, y, Cell(x, y, is_wall=False))
|
|||
|
|
|
|||
|
|
# 4 направления, перемешанные случайно
|
|||
|
|
directions = [(0, -2), (0, 2), (-2, 0), (2, 0)]
|
|||
|
|
random.shuffle(directions)
|
|||
|
|
|
|||
|
|
for dx, dy in directions:
|
|||
|
|
nx, ny = x + dx, y + dy
|
|||
|
|
|
|||
|
|
# Проверяем границы и что ещё не посещали
|
|||
|
|
if (0 <= nx < self.width and 0 <= ny < self.height
|
|||
|
|
and (nx, ny) not in visited):
|
|||
|
|
|
|||
|
|
# Убираем стену между текущей и новой клеткой
|
|||
|
|
wall_x = x + dx // 2
|
|||
|
|
wall_y = y + dy // 2
|
|||
|
|
maze.set_cell(wall_x, wall_y, Cell(wall_x, wall_y, is_wall=False))
|
|||
|
|
|
|||
|
|
# Рекурсия в новую клетку
|
|||
|
|
carve(nx, ny)
|
|||
|
|
|
|||
|
|
# Начинаем с (1, 1) — нечётные координаты для коридоров
|
|||
|
|
carve(1, 1)
|
|||
|
|
|
|||
|
|
# Шаг 3: Устанавливаем старт и выход в углах лабиринта
|
|||
|
|
maze.set_cell(1, 1, Cell(1, 1, is_wall=False, is_start=True))
|
|||
|
|
maze.set_cell(
|
|||
|
|
self.width - 2, self.height - 2,
|
|||
|
|
Cell(self.width - 2, self.height - 2, is_wall=False, is_exit=True)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return maze
|