"""Card definitions: immutable card specs and mutable in-combat instances. The engine is data-driven: sigils are string ids whose behavior lives in combat.py. New sigils added to YAML with an id the engine doesn't know are rejected at load time so content errors surface early. """ from __future__ import annotations import itertools from dataclasses import dataclass, field, replace from enum import Enum class CostType(Enum): FREE = "free" MEM = "mem" # paid by killing your own running processes (sacrifice) DUMPS = "dumps" # core dumps, left behind when your processes die # Sigils the combat resolver implements. Keep in sync with combat.py. KNOWN_SIGILS = frozenset( { "tunneling", # attacks the face directly, unless blocked by packet_filter "packet_filter", # blocks tunneling attackers "forked", # attacks the two lanes adjacent to the opposing slot "null_pointer", # any damage dealt to a card kills it "honeypot", # direct attackers take 1 damage back "privileged", # worth 3 mem when sacrificed "auto_restart", # survives being sacrificed "scavenger_loop", # owner gains 1 core dump at end of their turn "self_replicating", # when played, a copy is added to its owner's hand } ) @dataclass(frozen=True) class Cost: type: CostType amount: int = 0 def __str__(self) -> str: if self.type is CostType.FREE: return "free" return f"{self.amount} {self.type.value}" @dataclass(frozen=True) class Card: """An immutable card spec, as authored in data/cards.yaml.""" id: str name: str power: int health: int cost: Cost sigils: tuple[str, ...] = () flavor: str = "" art: str = "" # ≤3 lines, ≤9 cols; drawn inside the card frame def __post_init__(self) -> None: unknown = set(self.sigils) - KNOWN_SIGILS if unknown: raise ValueError(f"card {self.id!r} has unknown sigils: {sorted(unknown)}") if self.health < 1: raise ValueError(f"card {self.id!r} must have at least 1 health") def has(self, sigil: str) -> bool: return sigil in self.sigils _instance_ids = itertools.count(1) @dataclass class CardInstance: """A card on the board or in a hand. Mutable combat state lives here.""" spec: Card health: int = field(default=0) power_bonus: int = 0 uid: int = field(default_factory=lambda: next(_instance_ids)) sigil_bonus: tuple[str, ...] = () def __post_init__(self) -> None: if self.health == 0: self.health = self.spec.health @property def power(self) -> int: return max(0, self.spec.power + self.power_bonus) @property def name(self) -> str: return self.spec.name def has(self, sigil: str) -> bool: return sigil in self.sigils @property def sigils(self) -> tuple[str, ...]: return self.spec.sigils + self.sigil_bonus @property def alive(self) -> bool: return self.health > 0 def make_card( id: str, name: str | None = None, power: int = 0, health: int = 1, cost: Cost | None = None, sigils: tuple[str, ...] = (), flavor: str = "", art: str = "", ) -> Card: """Convenience constructor used by tests and the YAML loader.""" return Card( id=id, name=name or id.replace("-", " "), power=power, health=health, cost=cost or Cost(CostType.FREE), sigils=sigils, flavor=flavor, art=art, ) def mem_value(card: CardInstance) -> int: """How much mem killing this process yields.""" return 3 if card.has("privileged") else 1 def upgraded(card: Card, *, power: int = 0, health: int = 0, sigil: str | None = None) -> Card: """A modified copy of a spec (campfire buffs, Warden tampering).""" new_sigils = card.sigils + ((sigil,) if sigil and sigil not in card.sigils else ()) return replace(card, power=card.power + power, health=card.health + health, sigils=new_sigils)