from __future__ import annotations from abc import ABC, abstractmethod from pathlib import Path from .models import Cell, Maze class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename: str | Path) -> Maze: raise NotImplementedError def buildFromFile(self, filename: str | Path) -> Maze: return self.build_from_file(filename) class TextFileMazeBuilder(MazeBuilder): WALL = "#" START = "S" EXIT = "E" PASSAGES = {" ", "."} WEIGHTS = {"1": 1, "2": 2, "3": 3, "~": 3} def build_from_file(self, filename: str | Path) -> Maze: path = Path(filename) rows = path.read_text(encoding="utf-8").splitlines() if not rows: raise ValueError(f"Maze file is empty: {path}") width = len(rows[0]) if width == 0: raise ValueError("Maze width must be greater than zero") if any(len(row) != width for row in rows): raise ValueError("All maze rows must have the same width") cells: list[list[Cell]] = [] start: Cell | None = None exit: Cell | None = None for y, row in enumerate(rows): cell_row: list[Cell] = [] for x, char in enumerate(row): cell = self._create_cell(x, y, char) if cell.is_start: if start is not None: raise ValueError("Maze must contain exactly one start cell") start = cell if cell.is_exit: if exit is not None: raise ValueError("Maze must contain exactly one exit cell") exit = cell cell_row.append(cell) cells.append(cell_row) if start is None: raise ValueError("Maze must contain a start cell marked with 'S'") if exit is None: raise ValueError("Maze must contain an exit cell marked with 'E'") return Maze(cells, start, exit) def _create_cell(self, x: int, y: int, char: str) -> Cell: if char == self.WALL: return Cell(x=x, y=y, is_wall=True, symbol=char) if char == self.START: return Cell(x=x, y=y, is_start=True, symbol=char) if char == self.EXIT: return Cell(x=x, y=y, is_exit=True, symbol=char) if char in self.PASSAGES: return Cell(x=x, y=y, symbol=" ") if char in self.WEIGHTS: return Cell(x=x, y=y, weight=self.WEIGHTS[char], symbol=char) raise ValueError(f"Unsupported maze symbol {char!r} at ({x}, {y})")