Spaces:
Running on Zero
Running on Zero
| """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)) | |