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}).")
|