Scrypt / scrypt /engine /cards.py
IMJONEZZ's picture
SCRYPT: initial commit — game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
4.14 kB
"""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)