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:
parent
bd1f9aa011
commit
b6eed900ed
2 changed files with 110 additions and 0 deletions
109
.project/objectives/p1-60-fog-of-war-testing-ai-fairness.md
Normal file
109
.project/objectives/p1-60-fog-of-war-testing-ai-fairness.md
Normal 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
|
||||
|
||||
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`.
|
||||
|
|
@ -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) |
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue