From 90e9dcb6bfaff7d1d5fe5f4a2baf687e69474d8f Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 8 Apr 2026 20:15:53 -0700 Subject: [PATCH] =?UTF-8?q?docs(simulation-report):=20=F0=9F=93=9D=20Add?= =?UTF-8?q?=20detailed=20log=20entry=20for=20iteration=207l=20summarizing?= =?UTF-8?q?=20player.gd=20restoration,=20per-lair=20tier=20mapping,=20and?= =?UTF-8?q?=20contract=20test=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/simulation-report/experiment-log.md | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/.project/simulation-report/experiment-log.md b/.project/simulation-report/experiment-log.md index 84909b96..99f26df7 100644 --- a/.project/simulation-report/experiment-log.md +++ b/.project/simulation-report/experiment-log.md @@ -4,6 +4,124 @@ Tracks every iteration of the balance/simulation loop. Newest entries on top. Ea --- +## Iteration 7l — Player.gd full restoration + per-lair tier mapping + mc-turn bridge contract tests (2026-04-08, COMPLETE) + +**Goal:** Close the three independent iter 7k carry-forward items in a single parallel team execution: (A) restore `player.gd` from the 23-line iter 7k stub to a real class backing the whole engine, (B) replace the `DEFAULT_LAIR_TIER = 5` hardcode in `RustFaunaIntegration` with per-lair data sourced from `wilds.json`, (C) lock the iter 7h-7k GDExtension bridge surface into `cargo test` via a Rust-native contract test suite that doesn't require a Godot runtime. + +Three tracks, three specialists (godot-engine, game-data, simulator-infra), disjoint file sets, spawned in parallel. No coordination needed — each track touched isolated files. + +### Track A — `player.gd` full restoration (godot-engine) + +**`src/game/engine/src/entities/player.gd`**: 23 → **204 lines** (under the 500 cap, gdlint clean, iter 7i `unit.gd` pattern). + +Field set driven by grep of actual callers in `engine/src/`, not the planning spec alone: +- Identity: `index, player_name, race_id, gender_preset, is_human, color` +- Entities: `units, cities` +- Economy: `gold, gold_per_turn, golden_age_active` +- Happiness / culture: `happiness, culture_per_turn, culture_total` +- Research: `researching, research_progress, science_per_turn, researched_techs: Array[String]` +- Magic: `schools: Array[String], mana_pool, mana_income, mana_cap` +- Improvements: `pending_improvements` +- AI: `strategic_axes` +- Ascension: `ascension_active` + +Methods: `has_tech`, `add_tech` (both read/write `researched_techs`; `add_tech` auto-unlocks magic schools via `DataLoader.get_tech(id).unlocks_school`, capped at 2), plus `serialize`, `deserialize`, `to_bridge_dict`. + +Two deviations from the plan spec, both caller-grep justified: +1. **Dropped `tech_state: Variant`** — no engine caller ever dereferences `.tech_state.*`. The canonical tech storage is the `researched_techs: Array[String]` field used by `has_tech`, `production_filter.gd`, `improvement_manager.gd`, `tile.gd`. Adding an unused Variant would also have tripped the GDScript quality hook (no `Variant` as variable type). A future Rust tech bridge can add an opaque handle with a concrete type when the need is real. +2. **Added `gold_per_turn, golden_age_active, culture_total, researched_techs`** — read by `world_map.gd`, `city_screen.gd`, and save/load tests. Omitting them would leave the save/load round-trip incomplete. + +### Track B — Per-lair tier mapping from `wilds.json` (game-data) + +**`public/resources/wilds/wilds.json`**: added `base_tier: int` to every `lair_type` entry. (Canonical lair config lives under `public/resources/wilds/`, not `public/games/age-of-dwarves/data/wilds/` — path correction relative to the plan.) + +Five lair_types, five tier assignments: +- `beast_den` → T4 (wolf pack, dire bear, hydra — baseline wilderness) +- `wyvern_nest` → T5 (drakes, elder wyrm) +- `corrupted_hollow` → T6 (shambling, spectral knight, lich) +- `volcanic_fissure` → T7 (fire imp, lava elemental, infernal) +- `ancient_construct_site` → T8 (stone sentinel, iron golem, adamantine colossus — hardest apex) + +**`src/game/engine/src/modules/management/rust_fauna_integration.gd`**: `_build_lair_list()` now calls `_build_tier_lookup()` which reads `DataLoader.get_wilds_config().lair_types` and builds a `type_id → base_tier` map. Per-building lookup falls back to `DEFAULT_LAIR_TIER = 5` on miss (constant kept as a safety default per the plan). + +**`src/game/engine/scenes/tests/iter_7l_per_lair_tier_proof.{gd,tscn}`** (new, lint-clean): stamps one lair of each of the 5 distinct `type_id`s, runs the gated bridge for up to 200 iterations, renders per-killer-label death counts, and confirms via a "per-lair tier mapping live?" line that >1 distinct `wild_lair_t` label was observed. iter_7k proof scene untouched. + +### Track C — Bridge contract regression tests in `mc-turn` (simulator-infra) + +**`src/simulator/crates/mc-turn/src/bridge_contract_tests.rs`** (new, 464 lines): 6 `#[test]` functions targeting the mc-turn API directly (not the `Gd*` wrappers, which need a Godot runtime). Each test is self-contained with its own state builder. + +1. `set_turn_changes_rng_stream` — seeds two identical states at `turn=5` vs `turn=100`, runs `step_encounters_only`, asserts `fauna_combat_log` differs (locks the iter 7k set_turn determinism contract). +2. `encounters_only_does_not_spawn_units` — high-production axis + cities, snapshots `unit_count` and `production_stored`, asserts neither changes. +3. `encounters_only_does_not_move_units` — snapshots positions, runs against a state with no lairs in range, asserts positions unchanged. Cross-checked against full `step()` to confirm the move/no-move divergence is strictly in `step_encounters_only`. +4. `encounters_only_does_not_touch_economy` — snapshots `gold`, `cities.len()`, `expansion_points`, asserts all unchanged. +5. `inner_matches_encounters_only` — calls `process_fauna_encounters_inner(state, result, false)` directly, calls `step_encounters_only` on a clone, asserts byte-identical `fauna_combat_log`. Locks the inner-method contract. +6. `lair_combat_config_json_roundtrip_extended` — per-field equality PLUS 20-turn fauna-log byte-compare behavioural equivalence across the serde roundtrip. + +**Production code touched (task-allowed 1-line exception):** +- `src/simulator/crates/mc-turn/src/processor.rs:355` — `fn process_fauna_encounters_inner` → `pub(crate) fn process_fauna_encounters_inner`. Required for test #5 to call the inner across module boundary. + +**Test count delta:** 502 → 508 across the workspace (+6 exact). `cargo test -p mc-turn`: 23 → 29 passing. `cargo clippy -p mc-turn --all-targets -- -D warnings`: zero warnings. All 6 tests passed on first run — no real regressions surfaced, the existing `step_encounters_only` / `process_fauna_encounters_inner` contracts hold. + +### Verification chain + +``` +./run verify + [1/6] cargo build --workspace PASS 1520ms + [2/6] cargo test --workspace PASS 46238ms (502 → 508 tests) + [3/6] cargo clippy --workspace -D PASS 1577ms (zero warnings) + [4/6] gdlint engine/src/ PASS 5635ms + [5/6] gdlint engine/scenes/tests/ PASS 1512ms + [6/6] gdlint engine/tests/integration/ PASS 611ms + All 6 checks passed +``` + +### Iter 7l delta + +Three independent carry-forwards closed in one parallel iteration: +- `player.gd` no longer a stub — save/load roundtrip complete, engine-wide reads no longer fail silently +- Lair tier data moved from code constant to content JSON — per-lair flavor now propagates into the Rust fauna bridge, and the `wild_lair_t` killer labels in the kill log now reflect actual per-lair tier variation +- The iter 7h-7k bridge surface is now regression-gated by `cargo test`, not just by proof scenes that CI doesn't run — any future change to `step_encounters_only`, `process_fauna_encounters_inner`, or `LairCombatConfig` serde will surface as a cargo test failure + +### Bystander bugfix: `DataLoader.get_wilds_config()` was silently returning `{}` + +During Track B's Phase Gate capture, the initial screenshot returned 16/16 deaths all labeled `wild_lair_t5` — the tier lookup appeared broken. Root cause investigation exposed a **pre-existing latent bug** in the wilds data path, not introduced by this track: + +`public/resources/wilds/wilds.json`'s top-level shape is `{"wilds": {lair_density_per_land_tile, lair_types, ...}}`. `DataLoader._extract_entries` walked into the inner dict looking for an `"id"` field, didn't find one, recursed into `_extract_nested_collection` → `_extract_keyed_dict_entries`, which bailed on the first non-Dict value it hit (`lair_density_per_land_tile: 0.0083`). Result: every `get_wilds_config()` call, from anywhere in the codebase, was returning an empty dict. + +**Silent downstream failures** — every consumer was falling back to hardcoded defaults: +- `src/game/engine/src/modules/world/wild_creature_ai.gd` — detection radius / lair_types reads +- `src/game/engine/src/modules/world/village_lair_placer.gd` — wilds lookup +- `src/game/engine/src/modules/management/rust_fauna_integration.gd` (iter 7l, Track B) — per-lair tier lookup + +**Fix** (1 line, `public/resources/wilds/wilds.json`): added `"id": "wilds"` to the inner dict. That's enough for `_extract_keyed_dict_entries` to hit the `dict.has("id")` fast path and store the entry as `_data["wilds"]["wilds"] = {id, lair_density..., lair_types: [...], ...}`. The fix matches the exact pattern `villages.json` already uses, and every existing consumer still gets back the nested shape it already expected — no DataLoader code changes, no API shape changes. + +**Implication for past iterations**: any iter 7 bench result or proof-scene behavior that depended on `wild_creature_ai.gd` reading real lair_types from wilds.json was actually running on the hardcoded defaults. This doesn't invalidate the Phase 7 bench numbers (those ran from the Rust `fauna_pressure_bench` binary, not through GDScript's DataLoader), but it does mean any in-game proof scene that exercised wild creature AI against the "real" data was running on stubs. Something to flag when iter 7m re-exercises `wild_creature_ai`. + +### Phase Gate screenshot (Track B, captured in-session) + +lair-tier-mapper captured `iter_7l_per_lair_tier_proof` after the DataLoader fix. Kill log breakdown across 16/16 units dead: + +| Label | Deaths | Source lair_type | +|---|---|---| +| `wild_lair_t4` | 4 | `beast_den` | +| `wild_lair_t5` | 2 | `wyvern_nest` | +| `wild_lair_t6` | 2 | `corrupted_hollow` | +| `wild_lair_t7` | 5 | `volcanic_fissure` | +| `wild_lair_t8` | 3 | `ancient_construct_site` | + +**Distinct killer labels: 5/5.** Per-lair tier mapping is live end-to-end: JSON `base_tier` → DataLoader → `RustFaunaIntegration._build_tier_lookup` → `stamp_lairs` → Rust fauna resolver → `fauna_combat_log.killer_label`. + +Screenshot at `~/.var/app/org.godotengine.Godot/data/godot/app_userdata/Magic Civilization/screenshots/iter_7l_per_lair_tier_2026-04-08_20-13-38.png` (not SCP'd to plum per standing guidance). + +### Next steps (carry-forward to iter 7m) + +- **Real `world_map.tscn` gated Phase Gate** (still deferred from iter 7k): depends on broader game stack stability (mapgen, climate, ecology, all autoloads booting cleanly). Iter 7m or later, after a focused game-stack stability pass. +- **Real `world_map.tscn` gated Phase Gate** (still deferred from iter 7k): depends on broader game stack stability (mapgen, climate, ecology, all autoloads booting cleanly). Iter 7m or later, after a focused game-stack stability pass. +- **Retire `wild_creature_ai.gd` combat layer** (still deferred): requires a design decision — do wild creatures move, or just ambush? Needs a focused design pass, not a parallel-tracks team. +- **`set_turn` save-roundtrip**: still premature until iter 7m+ switches to a long-lived `GdGameState` (current pattern rebuilds state per call, so there's nothing to persist). + +--- + ## Iteration 7k — Gated `_process_rust_fauna_encounters` integration in the real turn_processor.gd (2026-04-08, COMPLETE) **Goal:** The iter 7j experiment log identified iter 7k as the final bridge piece: wire `RustFaunaBridge` + `step_encounters_only` into the real `turn_processor.gd` → `turn_manager.gd` dispatch path, behind an `EnvConfig` flag defaulted off, and produce a Phase Gate screenshot proving both the gate-closed default and the gate-open bridged path.