93 lines
3.5 KiB
Python
93 lines
3.5 KiB
Python
"""
|
||
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}).")
|