magicciv/.project/objectives/p1-60-fog-of-war-testing-ai-fairness.md
2026-05-26 02:21:13 -07:00

13 KiB
Raw Blame History

id title priority status scope category owner created updated_at blocked_by follow_ups related evidence
p1-60 Fog-of-war end-to-end test coverage + AI fairness fix p1 partial game1 simulation simulator-infra 2026-05-18 2026-05-18
p0-13
p2-67
p2-70
src/simulator/crates/mc-vision/src/lib.rs:570-789 — 11 pre-existing tests + 4 p1-60 tests appended (multi_unit_unions, stale_snapshot_frozen, los_endpoint_behind_two_blockers, wrap_mode_disk_clipped)
src/simulator/crates/mc-player-api/src/projection.rs:917-949 — project_tactical_with_vision new vision-aware variant
src/simulator/crates/mc-player-api/src/projection.rs:1044-1090 — project_tactical_player threads fog filter on enemy units + cities
src/simulator/crates/mc-player-api/src/projection.rs:1004-1042 — project_tactical_map strips resources outside explored set
src/simulator/crates/mc-player-api/src/dispatch.rs:535-555 — drive_ai_slot computes vision and passes to project_tactical_with_vision
src/simulator/api-gdext/src/ai.rs:253-268 — decide_strategic_kind computes vision and passes to project_tactical_with_vision
src/simulator/crates/mc-player-api/tests/ai_fairness.rs — 6 tests (hidden-stack redaction, scout-reveals, omniscient compat, enemy-city redaction, resource stripping)
src/simulator/crates/mc-player-api/tests/projection_redaction.rs — 6 tests (enemy unit/city/tile omission, stale tile explored-but-not-visible, omniscient flag preserved, default-path parity)
src/simulator/crates/mc-save/src/format.rs:50-66 — SaveFile.vision_state Option<serde_json::Value> with #[serde(default)]
src/simulator/crates/mc-save/tests/round_trip.rs:63-130 — vision_state_round_trips_byte_equal + vision_state_missing_in_old_save_reads_as_none
src/simulator/crates/mc-vision/benches/compute_vision.rs — criterion bench, small_map at ~90 µs / large_map runnable via cargo bench
src/simulator/crates/mc-vision/Cargo.toml:13-22 — criterion dev-dep + bench entry pinned to v0.5 (matches mc-ai)
src/game/engine/tests/unit/test_vision_parity.gd — 5 GUT tests verifying GDScript matches Rust spiral_count and one-hex-move math; requires ./run gut on RUN host
src/game/engine/tests/integration/test_fog_renderer_consumes_vision.gd — 8 GUT tests exercising real fog_renderer.gd headlessly
docs/modding/ai-controller.md ## Fog of war — mod-author contract: TacticalState is pre-filtered, CP_OMNISCIENT=1 is debug-only
cargo test results post-H/I/J: mc-vision 29/29 (1 ignored, Phase 2), mc-player-api 138/138 across 11 test binaries, mc-save 10/10 + doctest, workspace cargo build clean
src/simulator/crates/mc-core/src/grid/mod.rs:418-449 — WrapMode enum + GridState.wrap_mode field (workstream H)
src/simulator/crates/mc-vision/src/lib.rs wrap_coord + tile_in_bounds + tile_at + accumulate_visible_from_with_pierce + has_line_of_sight_with_pierce (workstreams H + I)
src/simulator/crates/mc-vision/src/lib.rs VisionCatalog peak_elevation_threshold / peak_sight_bonus / peak_pierce_blockers (workstream I)
src/simulator/crates/mc-turn/src/game_state.rs:265-273 — GameState.alliances: BTreeSet<(u8,u8)> with serde(default) (workstream J)
src/simulator/crates/mc-vision/src/lib.rs apply_allied_vision (workstream J)

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. (Initially failed; Communications Phase 1 author landed visible_snapshots mechanism in parallel and the test now passes.)
  • 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 (resources on stale tiles surface via the producer's LastSeen::Fresh.improvement_set payload — tech-gate verification deferred to a Phase 2 follow-up since mc-observation tech gates now live on the producer side).

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. (Test file landed; requires ./run gut tests/unit/test_vision_parity.gd on RUN host to validate. Direct Rust↔GDScript cross-call test deferred — GameState JSON authoring inside GUT is impractical; the landed tests verify both layers obey the same 1 + 3·R·(R+1) formula and constants.)

D. AI fairness — code change + tests:

  • ✓ New project_tactical_with_vision(state, player, Option<&PlayerVision>) threads a vision argument through project_tactical_map / project_tactical_player. project_tactical(state, player) kept as backward-compat omniscient wrapper for existing tests/fixtures.
  • dispatch.rs:540 (drive_ai_slot) computes vision once via mc_vision::compute_vision and passes the active player's PlayerVision to the new variant.
  • api-gdext/src/ai.rs:260 (decide_strategic_kind) does the same for the GDExtension entry point.
  • mc-ai audit: cross-player reads happen via state.players on the projected TacticalState, so filtering at the projection boundary is sufficient — no further changes needed inside mc-ai. (Verified by ai_fairness tests passing: behaviour changes when hidden entities are filtered out, proving mc-ai reads from the projected slice not a side channel.)
  • mc-player-api/tests/ai_fairness.rs — 6 tests landed: hidden-warrior-behind-mountain redaction, scout-reveals-warrior, 2-arg compat omniscient, vision=None omniscient, enemy-city redaction, resources-on-unexplored stripped.

E. Save/load VisionState:

  • SaveFile.vision_state: Option<serde_json::Value> with #[serde(default)]. Opaque JSON payload — keeps mc-save decoupled from mc-vision's dependency graph (mc-turn, mc-replay). Producers serialise via serde_json::to_value, consumers via from_value.
  • mc-save/tests/round_trip.rsvision_state_round_trips_byte_equal (typed VisionState materialises identically after save/load) + vision_state_missing_in_old_save_reads_as_none (back-compat default).
  • Loader-side compute_vision(state, &catalog, Some(&prior)) carry-forward — wiring belongs in the production save-load callers (api-gdext save bridge), not in mc-save itself. Tracked: integrate into the GdSave restore path when that landing happens.

F. Performance bench (new mc-vision/benches/compute_vision.rs, criterion):

  • ✓ 60×60 map / 4 players / 8 units each — measured at ~90 µs median (~55× headroom on the 5 ms target).
  • ✓ 200×200 map / 8 players / 50 units each — runnable via cargo bench -p mc-vision; quick spot-run not measured to keep CI runtime short.

G. GDScript fog-renderer integration smoke (new tests/integration/test_fog_renderer_consumes_vision.gd):

  • ✓ Hand-built PlayerVision drives the real fog_renderer.gd (no proof-scene fallback needed). 8 assertions cover constant alignment, polygon-per-tile creation, VIS_VISIBLE/SEEN_STALE/UNSEEN colour + visibility, edge-fade vertices on stale-adjacent-to-visible, and live update_tile_fog transitions. Requires ./run gut tests/integration/test_fog_renderer_consumes_vision.gd on RUN host to execute.

H. Wrap-mode vision:

  • WrapMode enum (None / Horizontal) added to GridState (mc-core/src/grid/mod.rs). #[serde(default)] keeps old saves byte-equal. New wrap_coord helper normalises col modulo width when Horizontal; tile_in_bounds / tile_at route through it. accumulate_visible_from stores wrapped canonical coords in the visible set; LoS walks use the raw (pre-wrap) goal so cube-line interpolation crosses the seam correctly.
  • ✓ Tests: wrap_horizontal_disk_crosses_seam, wrap_los_through_seam_respects_blockers, bounded_mode_unchanged_after_wrap_field_added.

I. Elevation / peak vision bonus:

  • VisionCatalog gained peak_elevation_threshold: f32 = 0.7 (data-driven), peak_sight_bonus: i32 = 0, peak_pierce_blockers: u32 = 0 (all #[serde(default)]). When a unit stands on a tile with elevation >= threshold, vision contribution uses base_radius + bonus AND the new has_line_of_sight_with_pierce ignores up to pierce intermediate blockers (see over the ridge).
  • ✓ Default values keep behaviour identical until a catalog turns the bonus on — no pre-existing test regressed.
  • ✓ 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:

  • GameState.alliances: BTreeSet<(u8, u8)> added with #[serde(default)] (canonical (min, max) keying mirrors PlayerState.relations). New apply_allied_vision step in compute_vision unions visible and explored between every allied pair after per-player refresh. last_seen is NOT shared — info-decay stays per-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.