docs(objectives): 📝 Clarify fog-of-war testing methodology in objectives and add setup instructions to README

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-05-18 18:41:42 -07:00
parent bd1f9aa011
commit b6eed900ed
2 changed files with 110 additions and 0 deletions

View file

@ -0,0 +1,109 @@
---
id: p1-60
title: "Fog-of-war end-to-end test coverage + AI fairness fix"
priority: p1
status: open
scope: game1
category: simulation
owner: simulator-infra
created: 2026-05-18
updated_at: 2026-05-18
blocked_by: []
follow_ups: []
related: [p0-13, p2-67, p2-70]
---
## Context
`p0-13` (tri-state fog) and `p2-70` (Rust `mc-vision` producer) both shipped, but the contract *between* the Rust producer and its consumers — the player-API projection, the GDScript renderer, the save format, and the AI — is not under test. Today:
- `mc-vision` has 11 inline tests covering radius math, mountain/dense-forest LoS, `last_seen` carry-forward, per-player isolation, and serde determinism (`src/simulator/crates/mc-vision/src/lib.rs:570-789`).
- GDScript GUT covers tri-state transitions and ring counts (`tests/unit/test_fog_of_war.gd`, `test_fog_of_war_vision.gd`).
- Nothing asserts the Rust and GDScript visibility sets agree on a seeded map.
- `mc-player-api::projection::project_view_with_vision` is the security boundary, but every existing call site uses `omniscient=true` (`tests/legal_actions_round_trip.rs:35`); the redaction path itself has no unit tests.
- **`api-gdext/src/ai.rs:260` calls `project_tactical(state, …)` which (`mc-player-api/src/projection.rs:917`) hands the AI the raw `GameState`. The AI sees unexplored tiles, hidden enemy units, hidden cities.** This violates the Game-1 design contract and invalidates any AI-vs-AI tournament for balance purposes (see `feedback_balance_philosophy.md`).
- `SaveFile` (`mc-save/src/format.rs:28`) does not persist `VisionState`; fog memory is silently rebuilt at load time from current unit positions, which destroys "stale" memory.
- `last_seen` snapshots are not proven frozen until re-observation. No test for movement-revealed enemies, multi-unit union, wrap, or perf regression.
Intended outcome: every fog-of-war seam (sim → projection → save/load → renderer → AI consumer) has a regression test, and the AI plays the same fog-of-war game the human plays.
## Source-of-truth rails
- Rust `mc-vision::compute_vision` (already public, `src/simulator/crates/mc-vision/src/lib.rs:225`) remains the only producer. GDScript `world_map_vision.gd` is already marked deprecated and must become a pure consumer.
- `mc-player-api::projection::project_view_with_vision` (`projection.rs:104`) is the only public read path for non-omniscient clients. Threading vision through `project_tactical` (917) closes the AI omniscience hole.
- `CP_OMNISCIENT` env flag kept for debug/repro only; default off in AI.
- No new JSON content; sight ranges already in `public/games/age-of-dwarves/data/units/*.json`.
## Acceptance
**A. Rust sim — `mc-vision` gap fill** (extend inline tests in `lib.rs:538-789`):
- `multi_unit_vision_unions_not_double_counted` — two overlapping scouts produce a union.
- `stale_snapshot_is_frozen_until_reobserved` — biome mutation on a stale tile does NOT update `last_seen` until unit returns.
- `los_endpoint_behind_two_blockers` — two mountains in a row: only the first is visible.
- `wrap_mode_disk_clipped_on_bounded_map` — disk at corner produces no negative/out-of-bounds hexes (nails current clip behaviour).
**B. Rust projection — redaction unit tests** (new `mc-player-api/tests/projection_redaction.rs`):
- Player 0 view omits player 1's units/cities/tiles outside `visible`.
- Stale tiles report `last_seen` snapshot, not live grid values.
- `omniscient=true` still sees both players (env-flag path preserved).
- Resources on unseen tiles omitted; on stale tiles, gated through `mc-observation` tech rules.
**C. Sim↔presentation parity** (new `src/game/engine/tests/unit/test_vision_parity.gd`):
- For seeded maps (flat grass; one mountain on LoS line; one-hex move post-reveal), `GdVision` output and `WorldMapVisionScript.recalculate_vision` produce identical per-tile `(player, visibility)` maps.
**D. AI fairness — code change + tests**:
- `mc-player-api::projection::project_tactical` / `project_tactical_map` / `project_tactical_player` accept a `&PlayerVision` and redact enemy entities/tiles outside it. Stale tiles use `last_seen`, not live grid.
- `api-gdext/src/ai.rs:260` computes vision once and passes the active player's `PlayerVision` into `project_tactical`. `CP_OMNISCIENT` retained as debug toggle.
- `mc-ai/` audit: no direct `state.players[i].units` reads for `i != active_player` in any decision function.
- New `mc-ai/tests/ai_fairness.rs`: hidden-stack-behind-mountain test asserts AI decision matches a control where the hidden stack is absent; scout-reveals-stack test asserts decision differs.
**E. Save/load `VisionState`**:
- `SaveFile` gains `vision_state: VisionState` with `#[serde(default)]`.
- Loader calls `compute_vision(state, &catalog, Some(&save.vision_state))` to carry forward `last_seen`.
- `mc-save/tests/round_trip.rs` extended: `vision_round_trips_byte_equal` and `stale_memory_survives_save_load`.
**F. Performance bench** (new `mc-vision/benches/compute_vision.rs`, criterion):
- 60×60 map / 4 players / 8 units each — median < 5 ms.
- 200×200 map / 8 players / 50 units each — median < 50 ms.
**G. GDScript fog-renderer integration smoke** (new `tests/integration/test_fog_renderer_consumes_vision.gd`):
- Hand-built `PlayerVision` drives `fog_renderer.gd`; assert the painted cells match `VIS_VISIBLE` / `VIS_SEEN_STALE` / `VIS_UNSEEN` correctly.
**H. Wrap-mode vision** (follow-up):
- `WrapMode` enum added to `GridState`. Disk expansion and LoS walks wrap correctly.
- Tests: `wrap_horizontal_disk_crosses_seam`, `wrap_los_through_seam_respects_blockers`, `bounded_mode_unchanged` regression.
**I. Elevation / peak vision bonus** (follow-up):
- `TileMeta.elevation` consulted by `compute_vision`; unit on peak gets +1 sight and sees over one blocker ring.
- Threshold is data-driven (JSON game-pack constant), not hardcoded.
- Tests: `unit_on_peak_sees_over_one_mountain_ring`, `unit_on_plains_does_not_see_over_mountain`, `elevation_threshold_data_driven`.
**J. Shared / allied vision** (follow-up):
- `GameState.alliances: BTreeSet<(PlayerId, PlayerId)>`. `compute_vision` unions allied vision into the refreshing player.
- Tests: `allied_pair_shares_visible_set`, `non_allied_pair_does_not_share`, `breaking_alliance_drops_shared_vision_next_turn`.
**End-to-end proof** (`phase-gate-protocol.md`):
- Proof scene: two AI players on opposite sides of a 60×60 map with a mountain ridge. Screenshots at turns 1 / 20 / 50 show fog growing from scouts only; no early discovery of the opposing capital; AI move logs contain only explored target hexes.
## Non-goals
- Spell-revealed / scrying tile gates — Game 3 (magic schools).
- Per-tile weather or time-of-day vision modulators — future.
## Sequencing
AG land first (core fog correctness + AI fairness, single PR set). H, I, J each get their own PR set; can land in any order once AG is in.
## Verification
```
cd src/simulator && cargo test -p mc-vision -p mc-player-api -p mc-save -p mc-ai
cd src/simulator && cargo bench -p mc-vision # spot-check, not CI-gated
./run verify
./run gut tests/unit/test_fog_of_war.gd
./run gut tests/unit/test_fog_of_war_vision.gd
./run gut tests/unit/test_vision_parity.gd
./run gut tests/integration/test_fog_renderer_consumes_vision.gd
```
Plan companion: `/var/home/lilith/.claude/plans/update-plan-moonlit-bentley.md`.

View file

@ -49,6 +49,7 @@ tools/ — sprite generation, transpiler, screenshot capture
| [GOVERNMENTS.md](public/games/age-of-dwarves/docs/GOVERNMENTS.md) | Government types and mechanics |
| [GLOSSARY.md](public/games/age-of-dwarves/docs/GLOSSARY.md) | Term definitions |
| COMBAT_SYSTEM.md | Combat mechanics (written when M8 is built) |
| [military/COMMUNICATIONS.md](public/games/age-of-dwarves/docs/military/COMMUNICATIONS.md) | Comms tiers, first-contact, courier envelopes, perceived state, war-dec semantics |
| SPELL_SYSTEM.md | Spell/mana system (written when M9 is built) |
| ERA_SYSTEM.md | Era progression (written when needed) |