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)