docs(simulation-report): 📝 Add detailed log entry for iteration 7l summarizing player.gd restoration, per-lair tier mapping, and contract test results

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-08 20:15:53 -07:00
parent 0e8100069c
commit 90e9dcb6bf

View file

@ -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<N>` 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<N>` 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.