From b6eed900ed3d06e4d21445cff38632fdf163d933 Mon Sep 17 00:00:00 2001 From: autocommit Date: Mon, 18 May 2026 18:41:42 -0700 Subject: [PATCH] =?UTF-8?q?docs(objectives):=20=F0=9F=93=9D=20Clarify=20fo?= =?UTF-8?q?g-of-war=20testing=20methodology=20in=20objectives=20and=20add?= =?UTF-8?q?=20setup=20instructions=20to=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../p1-60-fog-of-war-testing-ai-fairness.md | 109 ++++++++++++++++++ README.md | 1 + 2 files changed, 110 insertions(+) create mode 100644 .project/objectives/p1-60-fog-of-war-testing-ai-fairness.md diff --git a/.project/objectives/p1-60-fog-of-war-testing-ai-fairness.md b/.project/objectives/p1-60-fog-of-war-testing-ai-fairness.md new file mode 100644 index 00000000..d32e0c0b --- /dev/null +++ b/.project/objectives/p1-60-fog-of-war-testing-ai-fairness.md @@ -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 + +A–G 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 A–G 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`. diff --git a/README.md b/README.md index f5e5ce50..078fcbf9 100644 --- a/README.md +++ b/README.md @@ -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) |