7.2 KiB
| id | title | priority | status | scope | category | owner | created | updated_at | blocked_by | follow_ups | related | |||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| p1-60 | Fog-of-war end-to-end test coverage + AI fairness fix | p1 | open | game1 | simulation | simulator-infra | 2026-05-18 | 2026-05-18 |
|
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-visionhas 11 inline tests covering radius math, mountain/dense-forest LoS,last_seencarry-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_visionis the security boundary, but every existing call site usesomniscient=true(tests/legal_actions_round_trip.rs:35); the redaction path itself has no unit tests.api-gdext/src/ai.rs:260callsproject_tactical(state, …)which (mc-player-api/src/projection.rs:917) hands the AI the rawGameState. 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 (seefeedback_balance_philosophy.md).SaveFile(mc-save/src/format.rs:28) does not persistVisionState; fog memory is silently rebuilt at load time from current unit positions, which destroys "stale" memory.last_seensnapshots 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. GDScriptworld_map_vision.gdis 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 throughproject_tactical(917) closes the AI omniscience hole.CP_OMNISCIENTenv 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 updatelast_seenuntil 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_seensnapshot, not live grid values. omniscient=truestill sees both players (env-flag path preserved).- Resources on unseen tiles omitted; on stale tiles, gated through
mc-observationtech 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),
GdVisionoutput andWorldMapVisionScript.recalculate_visionproduce identical per-tile(player, visibility)maps.
D. AI fairness — code change + tests:
mc-player-api::projection::project_tactical/project_tactical_map/project_tactical_playeraccept a&PlayerVisionand redact enemy entities/tiles outside it. Stale tiles uselast_seen, not live grid.api-gdext/src/ai.rs:260computes vision once and passes the active player'sPlayerVisionintoproject_tactical.CP_OMNISCIENTretained as debug toggle.mc-ai/audit: no directstate.players[i].unitsreads fori != active_playerin 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:
SaveFilegainsvision_state: VisionStatewith#[serde(default)].- Loader calls
compute_vision(state, &catalog, Some(&save.vision_state))to carry forwardlast_seen. mc-save/tests/round_trip.rsextended:vision_round_trips_byte_equalandstale_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
PlayerVisiondrivesfog_renderer.gd; assert the painted cells matchVIS_VISIBLE/VIS_SEEN_STALE/VIS_UNSEENcorrectly.
H. Wrap-mode vision (follow-up):
WrapModeenum added toGridState. Disk expansion and LoS walks wrap correctly.- Tests:
wrap_horizontal_disk_crosses_seam,wrap_los_through_seam_respects_blockers,bounded_mode_unchangedregression.
I. Elevation / peak vision bonus (follow-up):
TileMeta.elevationconsulted bycompute_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_visionunions 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.