test(@projects/@magic-civilization): 🧪 p3-17 end-to-end exploration gate (fog → projection → decision)
Guards the wiring the movement unit tests can't: compute_vision (fog) → project_tactical_with_vision (sets TacticalTile.explored) → decide_tactical_actions. An idle warrior with no visible enemy on a 20x20 foggy map must issue a MoveUnit toward the fog rather than idling — the discovery feed p3-16's war-dec gate needs. Asserts the far corner projects as unexplored and the unit steps off its start tile. Reproducible (no MCP), runs under cargo test --workspace. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d4cf465236
commit
6a4d87a029
1 changed files with 96 additions and 0 deletions
96
src/simulator/crates/mc-player-api/tests/ai_exploration.rs
Normal file
96
src/simulator/crates/mc-player-api/tests/ai_exploration.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//! p3-17 end-to-end — idle military units frontier-seek toward real fog.
|
||||
//!
|
||||
//! Exercises the full chain the headless self-play path runs every turn:
|
||||
//! `compute_vision` (fog) → `project_tactical_with_vision` (sets
|
||||
//! `TacticalTile::explored`) → `decide_tactical_actions`. An idle warrior with no
|
||||
//! visible enemy must issue a `MoveUnit` that heads toward unexplored territory
|
||||
//! rather than idling — the discovery feed p3-16's war-dec gate depends on. The
|
||||
//! movement-layer unit tests cover the scorer in isolation; this guards the
|
||||
//! projection→decision wiring (explored actually reaches the AI).
|
||||
|
||||
use mc_ai::mcts::XorShift64;
|
||||
use mc_ai::tactical::TacticalMemory;
|
||||
use mc_ai::{decide_tactical_actions, Action, ScoringWeights};
|
||||
use mc_player_api::project_tactical_with_vision;
|
||||
use mc_state::game_state::{GameState, MapUnit, PlayerState};
|
||||
use mc_vision::{compute_vision, VisionCatalog};
|
||||
|
||||
fn grassland_grid(w: i32, h: i32) -> mc_core::grid::GridState {
|
||||
let mut g = mc_core::grid::GridState::new(w, h);
|
||||
for t in &mut g.tiles {
|
||||
t.biome_label_id = "grassland".into();
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
fn warrior(id: u32, col: i32, row: i32) -> MapUnit {
|
||||
let mut u = MapUnit::default();
|
||||
u.id = id;
|
||||
u.unit_id = "warrior".into();
|
||||
u.col = col;
|
||||
u.row = row;
|
||||
u.hp = 100;
|
||||
u.max_hp = 100;
|
||||
// movement_remaining drives the projected `moves_left`; without it the unit
|
||||
// can't step and the frontier scorer has nothing to emit.
|
||||
u.base_moves = 2;
|
||||
u.movement_remaining = 2;
|
||||
u
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_military_frontier_seeks_toward_unexplored_with_real_fog() {
|
||||
let mut state = GameState::default();
|
||||
state.turn = 1;
|
||||
state.grid = Some(grassland_grid(20, 20));
|
||||
|
||||
// Player 0: a lone warrior in the (2,2) corner. Its sight covers a small
|
||||
// radius; the rest of the 20x20 map is unexplored fog.
|
||||
let mut p0 = PlayerState::default();
|
||||
p0.player_index = 0;
|
||||
p0.units.push(warrior(1, 2, 2));
|
||||
state.players.push(p0);
|
||||
|
||||
// Player 1: a warrior in the far (17,17) corner — hidden by fog, so player 0
|
||||
// has no visible enemy and its warrior is idle-military (must explore).
|
||||
let mut p1 = PlayerState::default();
|
||||
p1.player_index = 1;
|
||||
p1.units.push(warrior(2, 17, 17));
|
||||
state.players.push(p1);
|
||||
|
||||
let vs = compute_vision(&state, &VisionCatalog::default(), None);
|
||||
let pv = vs.for_player(0).expect("player 0 vision");
|
||||
let mut tactical = project_tactical_with_vision(&state, 0, Some(pv));
|
||||
tactical.current_player = 0;
|
||||
|
||||
// Fog is real and reaches the projection: the far corner is unexplored.
|
||||
let far = tactical
|
||||
.map
|
||||
.tiles
|
||||
.iter()
|
||||
.find(|t| t.hex == (17, 17))
|
||||
.map(|t| t.explored);
|
||||
assert_eq!(far, Some(false), "the far corner must project as unexplored");
|
||||
assert!(
|
||||
tactical.map.tiles.iter().any(|t| !t.explored),
|
||||
"the tactical map must carry fog for the frontier scorer to act on"
|
||||
);
|
||||
|
||||
let mut rng = XorShift64::new(42);
|
||||
let mut memory = TacticalMemory::default();
|
||||
let weights = ScoringWeights::default();
|
||||
let actions = decide_tactical_actions(&tactical, &weights, &mut rng, None, &mut memory);
|
||||
|
||||
// The idle warrior must issue an exploration move, not idle/fortify.
|
||||
let to_hex = actions
|
||||
.iter()
|
||||
.find_map(|a| match a {
|
||||
Action::MoveUnit { unit_id: 1, to_hex } => Some(*to_hex),
|
||||
_ => None,
|
||||
})
|
||||
.expect("idle military unit must issue an exploration MoveUnit toward the fog");
|
||||
|
||||
// The step must leave the start tile (head into the map toward unexplored
|
||||
// territory), not stay put.
|
||||
assert_ne!(to_hex, (2, 2), "exploration move must advance off the start tile");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue