Spaces:
Running on Zero
Running on Zero
File size: 4,135 Bytes
9fca766 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | """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)
|