"""Load authored game content (cards, decks, encounters) from YAML. All IO lives here; the engine stays pure. Content errors (unknown sigils, bad costs, decks referencing missing cards) raise at load time. """ from __future__ import annotations from importlib import resources import yaml from scrypt.engine.cards import Card, Cost, CostType from scrypt.engine.combat import EncounterScript, ScriptedPlay def _parse_cost(raw: dict) -> Cost: type_ = CostType(raw["type"]) amount = raw.get("amount", 0) if type_ is not CostType.FREE and amount < 1: raise ValueError(f"{type_.value} cost needs an amount >= 1") return Cost(type_, amount) def _parse_card(raw: dict) -> tuple[Card, bool]: card = Card( id=raw["id"], name=raw["name"], power=raw["power"], health=raw["health"], cost=_parse_cost(raw["cost"]), sigils=tuple(raw.get("sigils", ())), flavor=raw.get("flavor", ""), art=raw.get("art", "").rstrip("\n"), ) return card, raw.get("draftable", True) class Content: """Parsed cards.yaml: card pool, starter decks, encounter scripts.""" def __init__(self, raw: dict): self.pool: dict[str, Card] = {} self.draftable: list[Card] = [] for raw_card in raw["cards"]: card, draftable = _parse_card(raw_card) if card.id in self.pool: raise ValueError(f"duplicate card id {card.id!r}") self.pool[card.id] = card # Bits are fodder, not a draft prize. if draftable and card.id != "bit": self.draftable.append(card) self.starter_decks: dict[str, dict] = {} for deck_id, deck in raw.get("starter_decks", {}).items(): self.starter_decks[deck_id] = { "name": deck["name"], "description": deck.get("description", ""), "cards": [self.card(cid) for cid in deck["cards"]], } self.encounters: dict[str, dict] = {} for enc_id, enc in raw.get("encounters", {}).items(): script: EncounterScript = [ [ScriptedPlay(lane=p["lane"], card=self.card(p["card"])) for p in turn] for turn in enc["script"] ] self.encounters[enc_id] = {"name": enc["name"], "script": script} self.forks: dict[str, dict] = {} for fork_id, fork in raw.get("forks", {}).items(): options = [] for opt in fork["options"]: if opt["encounter"] not in self.encounters: raise ValueError( f"fork {fork_id!r} references unknown encounter {opt['encounter']!r}" ) bounty = opt.get("bounty", {}) if bounty and bounty.get("kind") not in ("cycles", "draft"): raise ValueError(f"fork {fork_id!r} has unknown bounty kind") options.append( { "encounter": opt["encounter"], "label": opt["label"], "blurb": opt.get("blurb", ""), "bounty": bounty, } ) self.forks[fork_id] = {"prompt": fork.get("prompt", ""), "options": options} def card(self, card_id: str) -> Card: if card_id not in self.pool: raise KeyError(f"unknown card id {card_id!r}") return self.pool[card_id] def load_content() -> Content: text = (resources.files("scrypt.data") / "cards.yaml").read_text(encoding="utf-8") return Content(yaml.safe_load(text))