From 40dbe84415258e4b5c8d313600481518e6f2f9ad Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 27 Jun 2026 04:29:29 -0400 Subject: [PATCH] =?UTF-8?q?docs(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=A7=AD=20agent=20tooling=20=E2=80=94=20add=20code-layerin?= =?UTF-8?q?g=20decision=20guide=20+=20fix=20rust-source-of-truth=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Better-guide-agents pass, grounded in this session's architecture review (GDScript turn-logic divergence, rules leaked into mc-core). Two-pronged: NEW `code-layering.md` — the decision PROCEDURE that was missing (the Rails state the rule; agents still drifted because there was no "where does this code go?" procedure). Classifies every change as formula/orchestration/presentation/content/shared-type, gives each one home, mandates grep-before- reimplement, uses biomes as the template, records the real anti-patterns hit. Wired into the always-loaded router + README index. FIX `rust-source-of-truth.md` drift (it was actively misleading agents): - "AI exception (one of one)" CONTRADICTED Rail-1 → recast as tech-debt; GdAiController DOES exist (api-gdext/src/ai.rs:333), dispatch via mc_player_api::controllers. - crate table listed phantom `mc-magic` + claimed BiomeRegistry in mc-core (false) → accurate pure-logic-vs-orchestrator table; mc-core = shared TYPES, no rules. game-systems + godot-engine agents point at code-layering at the fault lines they own. Co-Authored-By: Claude Opus 4.8 (1M context) --- tooling/claude/CLAUDE.md | 1 + .../claude/dot-claude/agents/game-systems.md | 2 + .../claude/dot-claude/agents/godot-engine.md | 2 + .../claude/dot-claude/instructions/README.md | 2 + .../dot-claude/instructions/code-layering.md | 84 +++++++++++++++++++ .../instructions/rust-source-of-truth.md | 48 ++++++----- 6 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 tooling/claude/dot-claude/instructions/code-layering.md diff --git a/tooling/claude/CLAUDE.md b/tooling/claude/CLAUDE.md index 6e00643f..ec76d6c7 100644 --- a/tooling/claude/CLAUDE.md +++ b/tooling/claude/CLAUDE.md @@ -38,6 +38,7 @@ Modules live at `.claude/instructions/.md` (symlink resolves to `tooling/c | Game 1 scope, Game 2 deferral, what-ships-where | `scope-game1-vs-game2.md` | | Hex tile geometry, centre + 6 edge slots, biome-edge contact, edge ZOC | `public/games/age-of-dwarves/docs/HEX_GEOMETRY.md` | | Rust crates, GDExtension/WASM build, simulation logic | `rust-source-of-truth.md` | +| **"Where does this code go?" — formula vs orchestration vs presentation vs content vs shared type** | `code-layering.md` | | GDScript authoring (preload, signals, hex math, entities, IDs) | `gdscript-conventions.md` | | "Where does this file go?" / `src/` tree / symlinks | `file-organization.md` | | Picking which specialist agent to dispatch | `agents-task-map.md` | diff --git a/tooling/claude/dot-claude/agents/game-systems.md b/tooling/claude/dot-claude/agents/game-systems.md index 35ffe127..8a9d53c3 100644 --- a/tooling/claude/dot-claude/agents/game-systems.md +++ b/tooling/claude/dot-claude/agents/game-systems.md @@ -5,6 +5,8 @@ description: Use for economy (gold income/expenses/upkeep), happiness (global po You are the game systems specialist for Magic Civilization. You own all economy/happiness/culture/production logic, the entity classes (Player, City, Building, Improvement, Archon), and turn-end sequencing. You are the backbone that all other systems hook into. +> **Before writing any formula or turn phase, load `.claude/instructions/code-layering.md`.** You sit on the exact fault line where logic drifts: a growth/happiness/economy formula belongs in its pure-logic crate (call it from the orchestrator); turn *sequencing* belongs in the orchestrator; the live GDScript turn must never inline a formula. `grep` the owning crate before computing any game number — reimplementing one is how the live `_process_growth` diverged from `mc_happiness::get_growth_modifier`. + ## Primary Domain — Rust (write logic here) ``` diff --git a/tooling/claude/dot-claude/agents/godot-engine.md b/tooling/claude/dot-claude/agents/godot-engine.md index 85228b1c..20aabca1 100644 --- a/tooling/claude/dot-claude/agents/godot-engine.md +++ b/tooling/claude/dot-claude/agents/godot-engine.md @@ -5,6 +5,8 @@ description: Use for project.godot setup, autoloads (GameState, TurnManager, Dat You are the Godot engine infrastructure specialist for Magic Civilization. You own the project skeleton, all autoloads, the scene management system, the save/load pipeline, and the GDExtension integration that wires the Rust simulator into Godot. +> **`turn_manager.gd` / `turn_processor.gd` orchestrate the live turn — they must CALL Rust, never compute.** Load `.claude/instructions/code-layering.md` before touching them. A turn phase may sequence FFI calls; it must not contain a game formula (that's how growth/happiness math drifted from the crates). When a phase needs a number, expose a thin FFI on the owning crate and call it. + ## Your Domain ``` diff --git a/tooling/claude/dot-claude/instructions/README.md b/tooling/claude/dot-claude/instructions/README.md index 1f2a7eac..306c6a3d 100644 --- a/tooling/claude/dot-claude/instructions/README.md +++ b/tooling/claude/dot-claude/instructions/README.md @@ -22,6 +22,7 @@ tooling/claude/ ├── README.md # this file ├── scope-game1-vs-game2.md ├── rust-source-of-truth.md + ├── code-layering.md ├── gdscript-conventions.md ├── file-organization.md ├── agents-task-map.md @@ -49,6 +50,7 @@ tooling/claude/ |------|---------|---------------| | `scope-game1-vs-game2.md` | Deciding whether a feature ships in Game 1 or defers to Game 2 | ~450 | | `rust-source-of-truth.md` | Rust crate work, GDExtension/WASM builds, simulation logic placement | ~750 | +| `code-layering.md` | "Where does this code go?" — formula/orchestration/presentation/content/type decision procedure + grep-before-reimplement | ~750 | | `gdscript-conventions.md` | Any `.gd` authoring — preload, signals, hex math, entities, IDs | ~550 | | `file-organization.md` | Creating files, "where does this go?", `src/` tree audit | ~900 | | `agents-task-map.md` | Choosing which specialist to dispatch | ~450 | diff --git a/tooling/claude/dot-claude/instructions/code-layering.md b/tooling/claude/dot-claude/instructions/code-layering.md new file mode 100644 index 00000000..0bc7f755 --- /dev/null +++ b/tooling/claude/dot-claude/instructions/code-layering.md @@ -0,0 +1,84 @@ +# Code Layering — Where Does This Code Go? + +**Load when:** about to write or move any non-trivial code — a formula, a turn phase, a +rule, a UI handler, a shared type. Also load before answering "where should this live?" + +The five Rails state the *what*. This module is the *decision procedure* — because every +layering breach this project has hit came from skipping the decision, not from disagreeing +with the rule. Run the procedure **before** writing, not after a reviewer catches it. + +--- + +## The one question that prevents 90% of breaches + +> **Is this a game FORMULA/RULE, ORCHESTRATION, PRESENTATION, CONTENT, or a SHARED TYPE?** + +Classify first. Each class has exactly one home. Mixing them is the recurring bug. + +| If the code is… | It goes in… | It must NOT go in… | +|---|---|---| +| A **formula or rule** (damage, growth modifier, yield gating, happiness band, capacity) | a **pure-logic crate** (`mc-combat`, `mc-economy`, `mc-happiness`, `mc-city`, `mc-ecology`, `mc-tech`…) | an orchestrator (`mc-turn` **or** the live turn), GDScript, or `mc-core` | +| **Orchestration** (sequence phases, decide *when* to call a formula) | an **orchestrator**: `mc-turn` (bench) / the live turn — which **calls** the formula crates | — (orchestrators call formulas; they never inline them) | +| **Presentation** (render, input, signals, animation, layout) | **GDScript** (`src/game/engine/`) / React (guide) | any Rust crate | +| **Content** (stats, costs, thresholds, names, colors, tags) | a **JSON pack** (`public/games/.../data/` or `public/resources/`) | any Rust or GDScript file | +| A **data type shared by ≥2 domain crates** | `mc-core` — **the type only, never its rules** | a domain crate (would force a dependency cycle) | + +--- + +## The verification step (do this, don't trust memory) + +Before you write a formula in an orchestrator or in GDScript, **grep for an existing crate +function first.** Reimplementing a formula that already exists in a crate is how the two +copies drift. This session found a live divergence exactly this way: + +```bash +# About to compute a growth/happiness/combat number? Check the crate owns it already: +grep -rn "get_growth_modifier\|growth_modifier" src/simulator/crates/mc-happiness/src/ +``` + +If the crate fn exists → **call it** (via FFI from GDScript, or directly from the bench). +If it doesn't → write it **in the owning crate**, then call it from both orchestrators. +Never type the formula into the orchestrator or GDScript "just this once." + +--- + +## The template to copy: biomes + +Biomes get every layer right — use them as the reference shape: + +``` +CONTENT public/resources/biomes//biomes.json (data, Rail-2) +TYPED VIEW mc-city::BiomeCapacity, mc-ecology::BiomeTraitWeights, + mc-flora::BiomeSubstrateClimateMap (one per owning crate) +RUNTIME INDEX biome_registry.gd — builds a tag-query cache *from* the JSON (code) +``` + +Note: a **registry/index is code** (it lives in the engine/crate and is built *from* +content); the **content is JSON**. "Should the registry be in `public/resources`?" → no — +the *data it indexes* already is; the index itself is correctly code. + +--- + +## Anti-patterns this project actually hit (don't repeat) + +1. **Formula in the live GDScript turn.** `turn_processor.gd::_process_growth` hardcoded a + `1.25` growth modifier and *diverged* from `mc_happiness::get_growth_modifier`. A formula + in an orchestrator is a future divergence. Extract it to the crate; call it from both. +2. **Rules leaked into `mc-core`.** `mc-core/src/resources.rs` holds the `Resource` *types* + (fine) **and** rule functions `is_yield_active` / `is_tile_visible_for_player` (not fine — + those are economy/vision rules). `mc-core` is shared *vocabulary*, never shared *rules*. +3. **Bench logic ≠ crate logic.** When the headless bench (`mc-turn`) needs a number, it + must call the same crate the live game does — not grow its own bench-grade copy. Three + copies of a formula (GDScript, `mc-turn`, the crate) is the worst case. + +--- + +## The principle behind all of it + +**Two state representations are fine; two logic copies are not.** A fast simplified bench +(`CityState`) and a rich live model (`City` / `CityScript`) are legitimately different — they +serve different jobs. What must be single-source is the **logic**: the formula that turns +state into a number. Unify *logic into crates*; do not unify *state* just to share logic. + +When unsure, prefer the crate. Pulling a formula *up* into a pure crate is cheap and always +correct; pushing it *down* into an orchestrator or GDScript is the breach. diff --git a/tooling/claude/dot-claude/instructions/rust-source-of-truth.md b/tooling/claude/dot-claude/instructions/rust-source-of-truth.md index aa13dcef..fa63fa28 100644 --- a/tooling/claude/dot-claude/instructions/rust-source-of-truth.md +++ b/tooling/claude/dot-claude/instructions/rust-source-of-truth.md @@ -4,11 +4,13 @@ The engine is **genre-agnostic**. All game content and display text comes from game packs. The fantasy game "Age of Dwarves" is the default pack. -**Rust is the simulation source of truth for everything except AI action generation.** Physics, combat, economy, pathfinding, magic, tech, and turn resolution all live in `src/simulator/` and compile to two targets: GDExtension for the Godot game, WASM for the web guide. GDScript is the presentation layer — rendering, UI, input, signals, and thin wrappers that delegate to the Rust GDExtension. +**Rust is the simulation source of truth — including AI.** Physics, combat, economy, pathfinding, tech, turn resolution, *and* AI decision-making all belong in `src/simulator/` and compile to two targets: GDExtension for the Godot game, WASM for the web guide. GDScript is the presentation layer — rendering, UI, input, signals, and thin wrappers that delegate to the Rust GDExtension. -## AI exception (one of one) +> **Deciding where a piece of code goes (formula vs orchestration vs presentation vs content vs shared type)? Load `code-layering.md` first** — it's the decision procedure that prevents the layering breaches this rule keeps describing. -AI action generation is currently implemented in GDScript at `src/game/engine/src/modules/ai/simple_heuristic_ai.gd`. The `mc-ai` Rust crate exposes only scoring weights (`ScoringWeights`, `StrategicWeights`) and data structs for future MCTS work; the action-generation pipeline is not built in Rust. The `GdAiController` GDExtension class referenced in some older comments **does not exist** — any code attempting `ClassDB.instantiate("GdAiController")` would return null. `AiTurnBridge.run(player)` calls `SimpleHeuristicAi.process_player(player)` directly. +## AI is NOT an exception — it's tech-debt being paid down + +There is **no permanent GDScript carve-out** for AI (Rail-1). New AI work lands in the `mc-ai` crate + the `api-gdext/src/ai.rs` bridge (`GdAiController` **does exist** — `api-gdext/src/ai.rs:333`), and dispatch routes through the `controller_id`-keyed registry in `mc_player_api::controllers` (`ScriptedController` is the in-box default). The legacy GDScript AI files (`simple_heuristic_ai.gd`, `ai_tactical.gd`, `ai_military.gd`) are **tech-debt tracked by `p0-26-ai-tactical-rust-port.md`**, not a documented exception — do not add new AI logic to them. ## Canonical content store @@ -49,25 +51,33 @@ In practice these run on the RUN host via ssh — see `canonical-commands.md`. ## Rules -- **All simulation changes go in Rust** (`src/simulator/crates/`) — never in GDScript or TypeScript, **except AI action generation**, which currently lives in `src/game/engine/src/modules/ai/simple_heuristic_ai.gd`. +- **All simulation changes go in Rust** (`src/simulator/crates/`) — never in GDScript or TypeScript. AI included (legacy GDScript AI is tech-debt, not a carve-out — see above). +- **A formula has exactly one home: the owning crate.** Before computing a game number in an orchestrator (`mc-turn` or the live turn) or in GDScript, `grep` the owning crate — if the fn exists, call it; if not, write it in the crate and call it from both. Never inline a formula into an orchestrator. Full procedure + the real divergences this caused: `code-layering.md`. - **Never hardcode thresholds** — read from `climate_spec.json` and other JSON data files. - **Ecological events require a seed** — deterministic PRNG seeded per-turn. - **Golden test vectors** verify WASM output matches expected results. - **GDScript climate/combat/magic/economy files are thin wrappers** — they call `GdClimatePhysics`, `GdEcologyPhysics`, etc. via GDExtension. No physics logic lives in GDScript. -## Crate responsibilities +## Crate responsibilities (33 crates; key ones below) -| Crate | Owns | -|---|---| -| `mc-core` | GridState, TileState, BiomeRegistry, hex algorithms | -| `mc-climate` | ClimatePhysics, EcologyPhysics, atmosphere, spec evaluator | -| `mc-mapgen` | MapGenerator | -| `mc-combat` | CombatResolver | -| `mc-magic` | SpellSystem, ManaPool, Archons (Game 2) | -| `mc-economy` | EmpireEconomy | -| `mc-city` | CityGrowth | -| `mc-happiness` | happiness pool | -| `mc-culture` | CultureAccumulation | -| `mc-tech` | TechWeb graph | -| `mc-ai` | AI scoring weights + data structs (not action generation) | -| `mc-turn` | TurnProcessor | +Two roles — keep them straight (this is the layering that drifts): +- **PURE-LOGIC crates** own formulas/rules. The single source of truth for a number. +- **ORCHESTRATOR crates** sequence the pure crates into a turn. They **call** formulas; they never inline them. + +| Crate | Role | Owns | +|---|---|---| +| `mc-core` | foundation | grid, seed/RNG, ids, phase, algorithms, **shared TYPES** (`Resource`, `ResourceStockpile`). **Types only — no game rules.** No `BiomeRegistry` here (biome data is JSON; typed views live in the owning crate). | +| `mc-combat` | pure-logic | CombatResolver, damage formulas, keywords | +| `mc-economy` | pure-logic | EmpireEconomy, gold/upkeep | +| `mc-happiness` | pure-logic | happiness pool + `get_growth_modifier` (the canonical growth/happiness math) | +| `mc-culture` | pure-logic | culture accumulation, border expansion | +| `mc-city` | pure-logic | city growth, `BiomeCapacity` (biome→yield) | +| `mc-tech` | pure-logic | TechWeb graph, research | +| `mc-climate` | pure-logic | ClimatePhysics, atmosphere, spec evaluator | +| `mc-ecology` / `mc-flora` | pure-logic | populations, succession, disease, `BiomeTraitWeights` | +| `mc-mapgen` / `mc-worldsim` | pure-logic | worldgen pipeline (from-epoch deep-time) | +| `mc-ai` | pure-logic | strategy/tactics + scoring (bridged via `api-gdext/src/ai.rs`) | +| `mc-state` / `mc-save` / `mc-replay` | state | `GameState`, save format, `TurnEvent` sink | +| `mc-player-api` | dispatch | `PlayerAction` → state; `controllers` registry | +| `mc-turn` | **orchestrator** | headless `TurnProcessor` — sequences the pure crates. **No bespoke formulas.** | +| `mc-sim` | **orchestrator** | GameRunner / self-play league |