From 06eb360fbf3bb8bd35dae2e9b59d0ce5f0c548a8 Mon Sep 17 00:00:00 2001 From: moranzhi Date: Fri, 9 Oct 2026 02:04:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=8A=E8=A7=84=E5=88=99=E3=80=81=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=B8=8E=E5=AE=9E=E7=8E=B0=E8=B7=AF=E7=BA=BF=E5=86=99?= =?UTF-8?q?=E6=88=90=E4=BB=93=E5=BA=93=E6=96=87=E6=A1=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后续改内容或结算顺序时有冻结依据,不必再翻原始稿才能对齐。 Co-authored-by: Cursor --- README.md | 35 ++++++++++++ docs/AI.md | 40 ++++++++++++++ docs/ARCHITECTURE.md | 117 +++++++++++++++++++++++++++++++++++++++ docs/CONTENT_SCHEMA.md | 105 +++++++++++++++++++++++++++++++++++ docs/DESIGN.md | 89 ++++++++++++++++++++++++++++++ docs/FOUNDATION.md | 108 ++++++++++++++++++++++++++++++++++++ docs/RULES.md | 54 ++++++++++++++++++ docs/SURFACES.md | 121 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 669 insertions(+) create mode 100644 README.md create mode 100644 docs/AI.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CONTENT_SCHEMA.md create mode 100644 docs/DESIGN.md create mode 100644 docs/FOUNDATION.md create mode 100644 docs/RULES.md create mode 100644 docs/SURFACES.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..b31f285 --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/docs/AI.md b/docs/AI.md new file mode 100644 index 0000000..e3c004b --- /dev/null +++ b/docs/AI.md @@ -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. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..93ca74b --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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. diff --git a/docs/CONTENT_SCHEMA.md b/docs/CONTENT_SCHEMA.md new file mode 100644 index 0000000..e61cc97 --- /dev/null +++ b/docs/CONTENT_SCHEMA.md @@ -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. diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..0820125 --- /dev/null +++ b/docs/DESIGN.md @@ -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) diff --git a/docs/FOUNDATION.md b/docs/FOUNDATION.md new file mode 100644 index 0000000..475ab56 --- /dev/null +++ b/docs/FOUNDATION.md @@ -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. diff --git a/docs/RULES.md b/docs/RULES.md new file mode 100644 index 0000000..07002fa --- /dev/null +++ b/docs/RULES.md @@ -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. diff --git a/docs/SURFACES.md b/docs/SURFACES.md new file mode 100644 index 0000000..e7322ad --- /dev/null +++ b/docs/SURFACES.md @@ -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`