2026-rff_mp/SobolevNS/docs/data/task2_maze/maze_solver/builder.py

93 lines
3.5 KiB
Python
Raw Normal View History

2026-05-22 10:42:42 +00:00
"""
maze_solver/builder.py - паттерн Builder для создания лабиринтов.
Зачем Builder: процесс построения лабиринта сложный (чтение файла, парсинг,
валидация символов, простановка флагов, поиск старта и выхода). Builder
изолирует эти подробности от клиента; для нового формата (JSON, бинарный)
достаточно реализовать ещё один builder с тем же интерфейсом.
"""
from abc import ABC, abstractmethod
from .model import Cell, Maze
class MazeBuilder(ABC):
"""Абстрактный билдер лабиринта."""
@abstractmethod
def build_from_file(self, filename) -> Maze:
"""Возвращает готовый Maze."""
class TextFileMazeBuilder(MazeBuilder):
"""Билдер из текстового формата.
Символы:
'#' - стена
' ' - проход (вес 1)
'S' - старт (проходим)
'E' - выход (проходим)
'.' - асфальт (вес 1) - то же, что пробел
',' - песок (вес 2)
'~' - болото (вес 3)
Лишние пробельные символы в начале/конце файла игнорируются,
но внутри строки пробелы значимы (это проходы).
"""
WEIGHT_MAP = {'.': 1, ',': 2, '~': 3}
def build_from_file(self, filename) -> Maze:
with open(filename, encoding="utf-8") as f:
raw = f.read().splitlines()
# отбрасываем пустые строки в конце - частая мелочь
while raw and raw[-1] == "":
raw.pop()
if not raw:
raise ValueError(f"Файл лабиринта {filename!r} пуст.")
height = len(raw)
width = max(len(line) for line in raw)
# выравниваем строки по ширине пробелами (если строки разной длины)
lines = [line.ljust(width, '#') for line in raw]
maze = Maze(width, height)
start_count = 0
exit_count = 0
for y, line in enumerate(lines):
for x, ch in enumerate(line):
cell = self._parse_char(x, y, ch)
maze.grid[y][x] = cell
if cell.is_start:
maze.start = cell
start_count += 1
if cell.is_exit:
maze.exit_ = cell
exit_count += 1
# валидация
if start_count != 1:
raise ValueError(
f"В лабиринте {filename!r} ожидался ровно 1 'S', нашли {start_count}.")
if exit_count != 1:
raise ValueError(
f"В лабиринте {filename!r} ожидался ровно 1 'E', нашли {exit_count}.")
return maze
def _parse_char(self, x, y, ch):
if ch == '#':
return Cell(x, y, is_wall=True)
if ch == 'S':
return Cell(x, y, is_start=True, weight=1)
if ch == 'E':
return Cell(x, y, is_exit=True, weight=1)
if ch in self.WEIGHT_MAP:
return Cell(x, y, weight=self.WEIGHT_MAP[ch])
if ch == ' ':
return Cell(x, y, weight=1)
raise ValueError(f"Неизвестный символ {ch!r} в позиции ({x},{y}).")