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