Spaces:
Running on Zero
Running on Zero
| """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 | |
| } | |
| ) | |
| 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}" | |
| 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) | |
| 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 | |
| def power(self) -> int: | |
| return max(0, self.spec.power + self.power_bonus) | |
| def name(self) -> str: | |
| return self.spec.name | |
| def has(self, sigil: str) -> bool: | |
| return sigil in self.sigils | |
| def sigils(self) -> tuple[str, ...]: | |
| return self.spec.sigils + self.sigil_bonus | |
| 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) | |