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 |