docs(@projects/@magic-civilization): 🧭 agent tooling — add code-layering decision guide + fix rust-source-of-truth drift
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) <noreply@anthropic.com>
This commit is contained in:
parent
e307db755d
commit
40dbe84415
6 changed files with 120 additions and 19 deletions
|
|
@ -38,6 +38,7 @@ Modules live at `.claude/instructions/<file>.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` |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
84
tooling/claude/dot-claude/instructions/code-layering.md
Normal file
84
tooling/claude/dot-claude/instructions/code-layering.md
Normal file
|
|
@ -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/<world>/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.
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue