把规则、架构与实现路线写成仓库文档。

后续改内容或结算顺序时有冻结依据,不必再翻原始稿才能对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-09 02:04:20 +08:00
co-authored by Cursor
parent 5c097caee1
commit 06eb360fbf
8 changed files with 669 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# Card Roguelike
Simulation-first PvE card roguelike. Godot comes after a headless rules engine.
Start here: `docs/FOUNDATION.md`, `docs/ARCHITECTURE.md`, `docs/DESIGN.md`, `docs/SURFACES.md` (headless vs interactive).
## Implementation roadmap
1. **CLI combat** — shared starting package, attack/activate, guaranteed field, guard/cult core.
2. **CLI full run (current)** — reward → Holder assignment → simple upgrades → next fight.
3. **Playtest client** — `Game.Client` browser UI; emoji/SVG placeholders. Godot still later.
4. **Art pass** — replace placeholders.
5. **Expansion card packs** — near the art pass.
## Status
CLI combat + CLI full run + browser playtest client. Core owns rules and in-run progression (`RunProgress`). Client only renders and posts legal actions. Placeholder art: emoji + SVG.
## Build
Requires [.NET 8 SDK](https://dotnet.microsoft.com/download).
```powershell
dotnet test CardGame.sln
dotnet run --project tools/HeadlessBattle -- --auto
dotnet run --project tools/HeadlessBattle -- --run --auto
dotnet run --project tools/HeadlessBattle -- --run --seed 42 --fights 3
dotnet run --project src/Game.Client
```
`--run`: fight → draft from unlocked packs → assign Holder → simple upgrade → next fight (default 3). `--auto` picks for both seats and between-fight choices.
Browser client: http://127.0.0.1:5088 — you play P0; P1 auto-resolves after each of your actions.
Interactive combat: enter action numbers for P0; P1 is AI. `a` = auto one step for P0, `q` = quit.
+40
View File
@@ -0,0 +1,40 @@
# AI
AI v1 consumes `Game.Core`. It does not reimplement rules.
## Scope
- Own current turn only
- Not: MCTS, minimax, multi-turn planning, predicting the player’s next turn, predicting player responses, LLM play
Pipeline: legal actions → simulate (`Clone` + `Execute`) → evaluate → shallow beam search → near-optimal pick.
Implemented in `TurnBeamSearch` (Game.Core). Playtest client shows each AI step’s top scored candidates.
## Core loop
```
state
→ GetLegalActions
→ simulate each action (CloneState + Execute)
→ evaluate
→ keep best candidates (beam width)
→ expand
→ stop at EndTurn or depth limit
```
After each **real** action, search again. Do not search the opponent’s next turn.
## Evaluation terms
`PlayerDamage`, `EnemyUnitRemoved`, `OwnBoardValue`, `EnemyBoardValue`, `Heal`, `Draw`, `Energy`, `HandValue`, `CountdownValue`, `StatusValue`.
Killing the player: `WIN_SCORE` far above ordinary terms. Weights per enemy and board (e.g. more draw weight on empty hand). Do not show weights in UI.
## Near-optimal randomness
Compute `BestScore`. Sample uniformly (via `GameRng`) among lines with `Score >= BestScore * Threshold`. Avoids both obvious blunders and fully deterministic puppeteering.
## Performance
Limit beam width and action depth. Dedupe identical/equivalent states. Do not simulate player responses in v1. Deep-copy until clone/`Execute` is the measured bottleneck.
+117
View File
@@ -0,0 +1,117 @@
# ARCHITECTURE
Current prototype implementation. May be rewritten.
Source: *AI-First Technical Architecture Plan · v0.1*, Layer 2.
## Stack
- Godot .NET — later Steam/desktop presentation
- `Game.Client` — current playtest browser; no rules
- C# / .NET 8 — rules and tools
- JSON + JSON Schema — content
- xUnit — tests
- First platform: Windows desktop / Steam prototype
## Repository layout
```
src/Game.Core
src/Game.AI
src/Game.Content
src/Game.Client
tests/Game.Core.Tests
tests/Game.AI.Tests
tests/Game.Content.Tests
content/cards
content/characters
content/enemies
content/encounters
schemas
tools
docs
AGENTS.md
```
## Game.Core
Pure C#; no Godot reference.
v1 types: `GameState`, `PlayerState`, `Entity`, `CardDefinition`, `CardInstance`, `Zone`, `GameAction`, `GameEvent`, `Effect`, `Condition`, `Trigger`, `ResponseWindow`, `GameRng`.
Do not build a universal framework. Implement only what current rules need.
## Core API
```
GetLegalActions(state)
Execute(state, action)
CloneState(state)
ResolveUntilStable(state)
```
AI and the player client must call this same pair of legal-action and execute paths.
## Action vs event
- `GameAction`: intent — `PlayCard`, `Attack`, `Activate`, `Respond`, `EndTurn`, `ChooseTarget`
- `GameEvent`: what happened — `Damage`, `Heal`, `Draw`, `Discard`, `Destroy`, `EnterField`, `LeaveField`, `CountdownChanged`, `SupplyGenerated`
Rules process actions. UI plays events.
## Atomic resolution and response
Complex effects split into atomic steps. An atomic step is an engine unit, not a player-response unit. Response windows open only at rule-defined timings.
Do not implement free “anywhere second-speed.” Responses bind to events (on attack, before damage, on target, on enter, about to destroy). Response is not a card type. Show a window only if a legal response exists.
## Content model
Card types v1: Creature / Spell / Field. Long-lived entities share one battlefield. Board size 10 is a test parameter.
## Standard effects
`Damage`, `Heal`, `Draw`, `Discard`, `Destroy`, `Summon`, `MoveCard`, `ModifyStat`, `ModifyCountdown`, `AddKeyword`, `RemoveKeyword`, `GenerateEnergy`, `AddStatus`, `RemoveStatus`.
Ordinary cards compose these. Custom C# handlers only when composition is unreasonable. Do not invent a card scripting language.
## Validation, RNG, replay
CLI validator (later): unique IDs, dangling refs, structure, required fields, numeric ranges, tags, effect args. PT and dead-ref reports later.
All randomness: `GameRng`. `GameState` stores RNG. Tests pin seed. A battle records initial state, seed, and action sequence for 100% replay.
## AI v1 (see `docs/AI.md`)
Current-turn only. No MCTS, minimax, multi-turn planning, opponent-future prediction, response prediction, or LLM AI.
Shallow beam search + near-optimal randomness among scores `≥ BestScore × Threshold`.
Performance depends on clone + `Execute` speed, not algorithm prestige. Deep-copy until it hurts.
## PT v1
Linear value table for damage, heal, draw, energy, attack, health. DelayedValue ≈ BaseValue × DelayDiscount. No forced PT for highly contextual effects.
## Godot client (Phase F)
`Render(GameState)` and `Input → GameAction → Core.Execute → GameEvents → Animation`. Never “fix” a rules bug in animation code.
## Prototype order
| Phase | Focus | Deliverable |
| --- | --- | --- |
| A | Core skeleton | Entities, zones, state, action, event, RNG, clone, tests; no UI |
| B | Basic combat | Draw, hand, energy, creatures, fields, attack, damage, heal, destroy, GY, turns |
| C | Special rules | Cost, effect text, trigger, response, countdown, supply, fatigue, tags |
| D | Headless battle | Full fight in CLI |
| E | AI | Random legal → utility → current-turn beam |
| F | Godot client | Hand, board, HP, energy, targeting, response prompt, basic anim |
| G | Content tools | Schema, validator, PT report, load tests |
| H | Vertical slice | First characters, cards, fields, enemies, one boss |
**Current milestone: Phase A.** Do not start Godot until Core + tests + a CLI fight can draw, play, attack, end turn, and run a simple AI.
## Success
Headless full combat; full replay; automated core tests; ordinary new cards usually need no code; AI legal turns and 2–4 card combos; sim fast enough for live decisions; Godot only presents the same simulation.
+105
View File
@@ -0,0 +1,105 @@
# CONTENT_SCHEMA
Draft formats. Change this file together with JSON Schema and any validator.
Ordinary cards are data + standard effects. `customEffect` is an escape hatch.
## Identity
- `id`: stable string, unique in its catalog
- `name`: display name
- `tags`: string array, `kebab-case`
## Cards (`content/cards/*.json`)
```json
{
"id": "card.example.spark",
"name": "Spark",
"type": "Spell",
"cost": 1,
"tags": ["damage"],
"effects": [
{ "op": "Damage", "amount": 1, "target": "ChosenEnemy" }
]
}
```
`type`: `Creature` | `Spell` | `Field`.
Creatures/fields may add `attack`, `health`, `countdown`, `triggers`, `activate`.
### Activate
```json
"activate": [{ "op": "TransferStatsThenDie" }],
"activateNeedsFriendly": true
```
`activateNeedsEnemy` for targeted enemy activates. Default: once per own turn; usable the turn the creature enters.
### Play targeting flags
`playNeedsFriendly`, `playNeedsEnemy` (e.g. Vile Fire needs both).
### Tags (v1, partial)
Only these are recognized by rules/content checks. Extra kebab-case tags may exist as data and are ignored by the engine.
`damage`, `heal`, `creature`, `field`, `supply`, `countdown`
Do not grow this list unless a rule actually reads the new tag.
### Triggers
`when`: `TurnStart` | `OnDestroyed`. Effects use the same `op` list as play effects.
`effects` on a card are on-play / on-cast. Do not duplicate them as `OnEnter` yet.
## Standard `op` values
`Damage`, `Heal`, `Draw`, `Discard`, `Destroy`, `Summon`, `MoveCard`, `ModifyStat`, `ModifyCountdown`, `AddKeyword`, `RemoveKeyword`, `GenerateEnergy`, `AddStatus`, `RemoveStatus`.
Unknown `op` is a content error unless `customEffect` names a registered C# handler.
## Characters (`content/characters/*.json`)
Each character is **starting deck + skill** (see `docs/DESIGN.md`). `coreActions` / `bridge` are design metadata.
```json
{
"id": "char.example.ash",
"name": "Ash",
"coreActions": ["Damage", "Heal"],
"bridge": "Damage+Heal",
"skill": { "id": "skill.ash.ember", "summary": "..." },
"startingDeck": ["card.example.spark"]
}
```
## Packs (`content/packs/*.json`)
In-run reward pools. `base: true` is always available this run. Other packs unlock when **all** `allOf` conditions are true. Evaluation is live against current run stats and current owned cards (not sticky, not meta).
```json
{
"id": "pack.example.vitality",
"name": "Vitality",
"base": false,
"coreActions": ["Heal"],
"cards": ["card.example.salve"],
"allOf": [{ "kind": "HealReceivedAtLeast", "amount": 2 }]
}
```
`kind`: `HealReceivedAtLeast`, `AttacksAtLeast`, `FriendlyDestroyedAtLeast`, `FatigueAtLeast`, `DeckTagAtLeast` (+ `tag`), `KillTagAtLeast` (+ `tag`).
World tags such as `cult` may appear in `KillTagAtLeast` without being v1 rules tags.
## Enemies / encounters
Same effect language as cards where possible. AI weights live in enemy data (`docs/AI.md`), not in the engine.
## Validation (Phase G)
Unique ids, existing references, required fields, numeric ranges, tag shape, effect arguments. PT report is optional and must not block load of contextual cards.
+89
View File
@@ -0,0 +1,89 @@
# DESIGN
Design freeze for content validation. Do not expand primitive rules in this phase.
Source: *Next-Step Design Direction · v0.6*.
## Stage conclusion
Current primitives are enough to validate content. Freeze world direction, the core action dictionary, and **in-run card-pack triggers**. Design characters, cards, packs, and a minimum playable slice on top of them. Do not keep inventing new base rules.
## World and art
Do not treat “classic xianxia” as a hard frame. Prefer Japanese-fantasy / Eastern-fantasy mix: keep cultivation, yao/mo, spirits, sects, martial arts, and rites; also allow machines, otherfolk, alchemy, ruins, and magitech.
Worldbuilding explains mechanics, characters, and fantasy. It does not veto mechanics. If a mechanic is fun, find a diegetic reading; do not delete it for genre purity.
## Core action dictionary (freeze)
These 11 should exist as primitive events or base state changes. Archetypes are combinations, conditions, or conversions — not new primitives.
| Action | Meaning |
| --- | --- |
| Damage taken | Life falls from a damage event. Paying life vs losing life may stay distinct in rules; decks may still connect them. |
| Heal | Restore life. Links to damage, half HP, overheal, max HP. |
| Attack | Creature or character attacks. May emit on-attack / after-attack / combat-destroy. |
| Destroy | Entity is destroyed by rule or effect, then leaves the field. Record combat vs effect destroy. |
| Enter field | Entity enters the battlefield. Summon, flash, special enter all map here. |
| Draw | Take a card from deck. Links to hand size, fatigue, GY, deck thickness. |
| Fatigue | Draw with empty deck: damage/penalty, not instant loss. Empty deck can be a strategy. |
| Discard | Hand → GY, active or forced. Equivalence with other “hand to GY” is a grammar question. |
| Supply | Generate cost at a timing. Not a land keyword; creatures, characters, and fields may have it. |
| Graveyard | A real zone for revive, recycle, copy, count, conditions — a resource domain, not only a log. |
| Countdown | Lifetime/progress that ticks down; at 0, follow that entity’s rule (often destroy). Fields, creatures, tokens. |
**Not primitives:** half-HP, last rites, combo, haste, self-harm, heal-deck, swarm, GY-deck, economy-deck. Those are conditions + core actions, combinations, keyword templates, or archetype names.
## Combination-first
Design from edges between core actions, not from “a complete archetype.”
- Damage + Heal → self-harm, low-life, yo-yo, heal-to-damage
- Enter + Destroy → swarm, sacrifice, last rites, deathrattle, combo
- Draw + Fatigue → thin deck, turbo draw, empty-deck payoff, fatigue tank
- Supply + Countdown → burst economy, long investment, borrow-from-future, lifespan extend, protect the engine
- GY + Enter / Destroy → revive, reuse, farm GY, copy, recycle
## In-run card packs
Character identity is **starting deck + character skill**, not a fixed class pool. Random rewards use **in-run pack unlocks**.
- The starting deck sets where the player sits on the action graph. The skill changes the value of those actions.
- The full card catalog is grouped into small packs. A pack has a few core-action cards and bridges. It enters this run’s reward pool only after its conditions are met.
- All pack state belongs to the current run and **resets when the run ends**. No cross-run unlock. This is not meta progression.
Conditions may AND together:
| Kind | Reads |
| --- | --- |
| Deck | Core-action tags present, counted, or combined |
| Combat | Heal received, attacks, friendly destroys, fatigue, … |
| World | Kill enemies with a world tag, factions, bosses, regions, events (world tags are data; not the v1 rules-tag allowlist) |
| Mix | e.g. GY-tagged deck + many friendly destroys + kills of a tagged enemy |
Examples: heal a threshold → vitality pack; kill N `cult` enemies → faction/loot pack. **Numeric thresholds are not frozen.**
Rewards draft from the always-on base pack plus unlocked packs. Multi-tag cards are bridges: they can satisfy new packs and grow this run’s pool.
Conditions describe how this run was built, fought, and traveled — not class gates. Internal thresholds may be exact; **do not show a progress bar** that invites grinding.
**Not frozen yet:** whether an unlocked pack stays on for the rest of the run; whether removing cards can lock it again; reward weights among active packs. Decide after Vertical Slice play.
## Vertical Slice 0.1
Goal: prove the action graph grows distinct builds. Not balance, not a full world.
1. Draw the 11-node graph: natural links, needed bridges, deliberate isolation.
2. Character = starting deck + skill → now **Faction = held cards + relic + upgrades**. Deck touches 2–3 core actions and at least one bridge.
3. Small content: two starting factions (官兵/魔教), guaranteed field, later in-run packs.
4. Validate: same start + different pack paths → different builds; multi-tag cards open packs; combat/world conditions do not invite empty grinding.
**Expansion pack factions** (death / draw / GY / enter) are scheduled with the later content/art pass — after CLI combat and CLI full-run work.
## Current non-goals
- Full class system and final class pool ratios
- World map, long-run event pool, **cross-run pack unlocks**, full meta progression
- Final art, factions, lore wiki
- Exact card balance and final PT
- Advanced AI (multi-turn, opponent-response simulation)
+108
View File
@@ -0,0 +1,108 @@
# FOUNDATION
Long-lived engineering principles. Change only if the project itself changes.
Source: *AI-First Technical Architecture Plan · v0.1*, Layer 1.
## Project nature
Single-player PvE card roguelike: light presentation, heavy rules, data, and simulation.
Serve:
- Fast add/edit of cards, characters, enemies, and fields
- Dense rule interaction and deterministic resolution
- AI simulating action sequences within the current turn
- Automated tests, headless batch simulation, and balance analysis
- High-frequency AI coding, refactor, and iteration
Do not prioritize AAA graphics, physics, multiplayer sync, or open-world systems.
## Simulation vs presentation
| World | Owns | Must not own |
| --- | --- | --- |
| Simulation | Rules, entities, zones, cost, events, triggers, responses, AI, RNG, combat state | Godot, UI, animation, audio, scene nodes |
| Presentation | Display `GameState`, input, play `GameEvent`, UI, VFX, SFX | Rules |
UI may express rules. UI must not possess rules.
## Technology direction
- Godot .NET: client / presentation
- C#: primary language and a Godot-free simulation core
- JSON (or similar text) for content
- Git for code and data
- Automated tests on core rules
Engine, library, and tool versions are implementation details, not this document.
## Deterministic simulation
Same initial state, seed, and action sequence → identical result. All randomness comes from game RNG state.
Must support `Seed + Action Log →` full combat replay. Determinism underpins AI search, bug reproduction, tests, balance sims, and playback.
## Simulation first
Every core mechanic must run without Godot, graphics, or animation. The AI simulator and the player rules engine are the same code. Do not ship a simplified second ruleset for AI.
## State must be simulatable
`GameState` must copy, mutate, compare, enumerate legal actions, and simulate a short distance.
v1 may deep-copy. Snapshot, copy-on-write, undo, or structural sharing only after a real performance need.
The goal is future-optimizable semantics, not premature optimization.
## Data-driven content
Most content is data + standard effects/events/conditions + tags. Rare designs may use `CustomEffect` / C# handlers.
Not “100% zero-code cards.” Ordinary content should be cheap; special content may still need code.
## Rules before cards
Do not change primitives to ship one card. A new mechanic must state: event type, atomic step, whether it is an effect, whether it opens a response window, who can listen, and how it is tested.
## AI consumes the rules engine
AI does not own rules. The engine exposes visible state and legal actions; AI only simulates and chooses.
v1 is not global-optimal play. Enemies should notice simple in-turn combos and express style through result weights.
## AI evaluation language
Score **outcomes**, not card names: player damage, board clear, own board, heal, draw, energy, hand value, countdown, status, etc.
Per-enemy weights may shift with the board (e.g. draw weight rises when the hand is empty). Weights are not shown to the player.
## Content value model (PT)
Internal PT / value model for easy-to-quantify effects (damage, heal, draw, stats, cost, delayed value). PT is a balance reference, not an auto-card formula. Context-heavy copy, conversion, and open combos need not have exact PT.
## Tests are design work
Core rules have automated tests. Important bugfixes should leave a reproduction test. Prefer coverage of zones, event order, cost, effects, text, responses, destroy, graveyard, countdown, energy, and RNG determinism.
## AI coding
Prefer strong types, clear module edges, small APIs, tests, searchability, little implicit behavior, little global state.
The human does not need to read every implementation file, but must understand data, rule docs, test names, logs, and debug output.
## Documentation hierarchy
| Doc | Role |
| --- | --- |
| `FOUNDATION.md` | Stable principles |
| `ARCHITECTURE.md` | Current implementation; may be rewritten |
| `RULES.md` | Game rule grammar |
| `CONTENT_SCHEMA.md` | Data formats |
| `AI.md` | AI v1 |
| `DESIGN.md` | Design freeze (world + action dictionary) |
| `AGENTS.md` | Coding-agent constraints |
## Engineering loop
Design mechanic → change data/code → compile → tests → content validate → headless sim → client play → tune data → repeat.
+54
View File
@@ -0,0 +1,54 @@
# RULES
Game grammar for the simulation. Aligned with *Core Game Rules · v0.1* and the CLI combat slice.
## Entities and zones
Long-lived combat entities share one battlefield (`Board`, capacity 10 including system fields).
Zones: deck, hand, board, graveyard. Card type 场域 is `FieldCard`.
**CardInstance** keeps current `Cost` / stats / `HolderId` across zone moves. `CardDefinition` is the template only.
## Faction & Holder
A player’s held cards from all factions merge into one shared deck/hand/graveyard for combat. `HolderId` records which faction owns the instance (for upgrades). Who may draw/play is not limited by Holder.
## In-run progression (CLI full run)
`RunProgress` owns the persistent card list between fights. Combat mutations do not write back; only reward adds and explicit simple upgrades (`ReduceCost` / `+Attack` / `+Health`) change owned copies. `RunStats` and pack unlock evaluation are live for the current run and reset when the run ends.
## Economy
No automatic mana ramp. Energy persists and increases when effects/fields **GenerateEnergy** (supply).
**Guaranteed system field** (`card.system.base_field`): 0C, TurnStart supply 1, countdown 3. Placed on board at match start; not part of the 8+8 held cards; not in the draw pile.
## Attack & Activate
Independent once-per-turn resources (`AttacksRemaining`, `ActivatesRemaining`).
- Enter on your turn: attack count 0, activate count 1 (if any).
- Enter via opponent-turn-end summon: next BeginTurn grants attack count.
- Attack requires effective attack > 0.
- Activate does not consume attack and may be used the turn a creature enters.
## Damage / Heal
Events record Requested and Applied amounts. Applied may be 0.
## Countdown
At end of controller’s turn, countdown > 0 decreases by 1; at 0 the entity is destroyed (including system field).
## Triggers (v1)
`TurnStart`, `OnDestroyed`, `OnEnterBoard` (listeners when a **creature** enters), `OpponentTurnEnd`, `OnSupply`.
## Core actions
Unchanged 11-action dictionary in `docs/DESIGN.md`.
## Randomness
Only `GameRng`. Same seed + action log must replay.
+121
View File
@@ -0,0 +1,121 @@
# Surfaces
How the project ships two usable fronts on **one** rules engine.
This is product/engineering framing, not a second ruleset.
## Verdict
Yes. We maintain **two surfaces**:
| # | Surface | Who uses it | Job |
| --- | --- | --- | --- |
| 1 | **Headless** | Dev, AI, batch sims, CI, balance | Fast, no UI (or terminal-only). Same rules as the player game. |
| 2 | **Interactive** | Human players (and playtesters) | Click/type legal actions; see `GameState` and play `GameEvent`s. |
They are not two games. They are two **adapters** over `Game.Core` (+ in-run progression such as `RunProgress`).
```
┌─────────────────────┐
│ Game.Core │
│ rules · RNG · AI │
│ RunProgress · packs │
└──────────┬──────────┘
┌───────────────┼───────────────┐
▼ ▼
Headless surface Interactive surface
tools / tests / batch playtest client → later Godot
```
## 1. Headless surface
**Purpose:** AI search, auto fights, full-run loops, regression tests, future balance batches. Anything that must be cheap to run and easy to pin with a seed.
**Today:**
- `tools/HeadlessBattle` — single fight and `--run` full run (terminal I/O is convenience, not a second rules layer)
- `tests/Game.Core.Tests` — pinned scenarios
- In-process AI (`TurnBeamSearch`) calling `GetLegalActions` / `Execute` / `Clone`
**Later (same surface family):**
- Batch sims / PT reports / seed sweeps
- Headless full-run without printing every step
- Replay from `seed + action log`
**Must:**
- Depend only on Core (and content JSON), never on Godot or browser UI
- Stay deterministic: same seed + choices → same result
- Be the path AI and automated tests use in production of balance data
**Must not:**
- Reimplement card text, pack unlocks, or combat resolution “for speed”
- Become a simplified AI-only ruleset
## 2. Interactive surface
**Purpose:** What a human actually plays. Input becomes `GameAction`; screen shows state and events. Presentation only.
**Today:**
- `Game.Client` — browser playtest (emoji/SVG placeholders)
- Optional: interactive mode of `HeadlessBattle` (numbered legal actions) as a thin terminal client — still surface 2 in spirit, but not the player-facing target
**Later:**
- Godot .NET desktop / Steam presentation
- Art pass replaces placeholders; rules stay in Core
**Must:**
- Call the same Core APIs as headless (`GetLegalActions`, `Execute`, …)
- Own layout, input, animation, audio, UX copy
- Refuse to “fix” rules bugs in the UI
**Must not:**
- Encode damage formulas, pack conditions, legal-move filters, or win checks locally
- Drift into a parallel progression model for “the real game”
## Shared core (the only rules owner)
Both surfaces consume:
- Combat: `GameState`, `GameAction`, `GameEvent`, `GameRng`
- Progression: `RunProgress`, rewards, Holder, simple upgrades, in-run packs
- AI: evaluation + current-turn search that **reads** Core, does not own it
If a feature matters for “how the game works,” it lands in Core (and tests) first. Surfaces only expose or automate it.
## How work usually splits
| Change type | Land in | Headless | Interactive |
| --- | --- | --- | --- |
| New rule / card effect / pack condition | Core + tests | exercise via CLI/tests | render/input only if needed |
| Faster AI / batch balance | Core + tools | primary | optional “show AI candidates” |
| UX, targeting UX, animation, art | Client (later Godot) | no | primary |
| Full-run loop (reward → upgrade → next fight) | Core + headless first | prove it | then wire the same loop in the client |
Default order stays **simulation first**: headless (or tests) prove the loop; interactive follows.
## Roadmap mapping
1. CLI combat / CLI full run → headless surface (also a crude interactive terminal)
2. Playtest client → interactive surface v1
3. Art pass / Godot → interactive surface v2 (same Core)
4. Expansion packs → content + Core; both surfaces pick them up automatically
## Non-goals
- Two rulebooks (player vs AI)
- Meta / cross-run unlocks on either surface
- Making the browser client a permanent architecture requirement (it is a playtest skin; Godot may replace it)
## Related docs
- Principles: `docs/FOUNDATION.md` (simulation vs presentation)
- Layout & phases: `docs/ARCHITECTURE.md`
- AI constraints: `docs/AI.md`
- Run / Holder grammar: `docs/RULES.md`