"""Einlesen der Koffersequenzen aus ``case_sequences.txt``. Dateiformat ----------- Sequenzen sind durch Leerzeilen getrennt. Jede Zeile beschreibt einen Koffer:: breite,höhe,rotiert * ``breite`` / ``höhe`` sind Kasten-Einheiten (1 Einheit = 100x100 Pixel). * ``rotiert=True`` heisst: der Koffer wird beim Einlaufen um 90° gedreht, Breite und Höhe werden also getauscht. Beispiel:: 3,1,False 1,2,True -> Koffer ist effektiv 2 Einheiten breit und 1 hoch """ from __future__ import annotations import re from dataclasses import dataclass from pathlib import Path from . import config _TRUE_VALUES = {"true", "1", "ja", "yes", "x"} @dataclass(frozen=True, slots=True) class CaseSpec: """Ein Koffer aus der Sequenzdatei – bereits in Einlauf-Orientierung.""" units_w: int units_h: int def __post_init__(self) -> None: if self.units_w < 1 or self.units_h < 1: raise ValueError(f"Koffer muss mindestens 1x1 Einheiten gross sein: {self!r}") @property def label(self) -> str: return f"{self.units_w}x{self.units_h}" #: Notfall-Sequenz, falls die Datei fehlt oder unlesbar ist. FALLBACK_SEQUENCE: tuple[CaseSpec, ...] = (CaseSpec(2, 1), CaseSpec(1, 2)) def parse_sequences(raw: str) -> tuple[list[list[CaseSpec]], list[str]]: """Zerlegt den Dateiinhalt in Sequenzen. Rückgabe: ``(sequenzen, warnungen)``. Ungültige Zeilen werden gemeldet und übersprungen – ein Tippfehler in der Datei soll das Spiel nicht verhindern. """ sequences: list[list[CaseSpec]] = [] warnings: list[str] = [] for block_number, block in enumerate(re.split(r"\n\s*\n", raw.strip()), start=1): specs: list[CaseSpec] = [] for line_number, line in enumerate(block.splitlines(), start=1): text = line.strip() if not text: continue parts = [part.strip() for part in text.split(",")] if len(parts) != 3: warnings.append( f"Block {block_number}, Zeile {line_number}: " f"'breite,höhe,rotiert' erwartet, gefunden {text!r}" ) continue try: units_w, units_h = int(parts[0]), int(parts[1]) except ValueError: warnings.append( f"Block {block_number}, Zeile {line_number}: keine ganzen Zahlen in {text!r}" ) continue rotated = parts[2].lower() in _TRUE_VALUES if rotated: units_w, units_h = units_h, units_w if units_w < 1 or units_h < 1: warnings.append( f"Block {block_number}, Zeile {line_number}: ungültige Masse {text!r}" ) continue specs.append(CaseSpec(units_w, units_h)) if specs: sequences.append(specs) if not sequences: warnings.append("Keine gültige Sequenz gefunden – es wird die Notfall-Sequenz benutzt.") return sequences, warnings def load_sequences(path: Path | None = None) -> tuple[list[list[CaseSpec]], list[str]]: """Liest die Sequenzdatei und liefert immer mindestens eine Sequenz.""" path = path or config.SEQUENCES_FILE try: raw = path.read_text(encoding="utf-8") except OSError as exc: return [list(FALLBACK_SEQUENCE)], [f"Sequenzdatei {path} nicht lesbar: {exc}"] sequences, warnings = parse_sequences(raw) if not sequences: sequences = [list(FALLBACK_SEQUENCE)] return sequences, warnings