108 lines
3.5 KiB
Python
108 lines
3.5 KiB
Python
"""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)
|
||
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
|