test(@projects/@magic-civilization): 🧪 p3-17 deterministic self-play first-contact gate
Closes p3-17's "measurable improvement in self-play: earlier first-contact" as a reproducible cargo test (the sim is deterministic, so this needs no apricot smoke batch). Two AI militarists start fogged-apart on a 12x12 map; the frontier-seek exploration must bring one into view of another within 40 turns. Asserts no contact at game start (fog intact) and contact by the budget. This is the discovery feed p3-16's decide_diplomacy (declares only on a *discovered* rival) depends on — it never fired reliably before exploration landed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c8896d50c9
commit
a3b2a2324f
1 changed files with 82 additions and 0 deletions
|
|
@ -0,0 +1,82 @@
|
|||
//! p3-17 / p3-16 — deterministic self-play first-contact via exploration.
|
||||
//!
|
||||
//! The original objectives asked for "measurable improvement in a hotseat
|
||||
//! self-play run: earlier first-contact". The sim is deterministic (fixed seed →
|
||||
//! identical outcome), so this is a reproducible cargo test, not an apricot-only
|
||||
//! smoke batch: two AI militarists start fogged-apart on a small map; once the
|
||||
//! frontier-seek exploration (p3-17) expands their footprints, one must come into
|
||||
//! view of the other within a bounded turn budget. First contact is the discovery
|
||||
//! feed p3-16's war-dec gate (`decide_diplomacy` only declares on a *discovered*
|
||||
//! rival) depends on — without exploration this never happened reliably.
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{
|
||||
add_player_militarist_inline, build_building_catalog, build_runtime_units_catalog,
|
||||
build_unit_catalog, stamp_personality,
|
||||
};
|
||||
use mc_player_api::action::PlayerAction;
|
||||
use mc_player_api::{apply_action, project_tactical_with_vision};
|
||||
use mc_state::game_state::GameState;
|
||||
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
|
||||
}
|
||||
|
||||
/// Has player `viewer` discovered any unit or city of any *other* player
|
||||
/// (fog-aware projection redacts the unseen)?
|
||||
fn has_first_contact(state: &GameState, viewer: u8) -> bool {
|
||||
let vs = compute_vision(state, &VisionCatalog::default(), None);
|
||||
let Some(pv) = vs.for_player(viewer) else {
|
||||
return false;
|
||||
};
|
||||
let tac = project_tactical_with_vision(state, viewer, Some(pv));
|
||||
tac.players
|
||||
.iter()
|
||||
.any(|p| p.index != viewer && (!p.units.is_empty() || !p.cities.is_empty()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_self_play_makes_first_contact_via_exploration() {
|
||||
let mut state = GameState::default();
|
||||
state.turn = 1;
|
||||
state.units_catalog = build_runtime_units_catalog();
|
||||
// Slot 0 is a passive "human" that only EndTurns; slots 1 + 2 are AI driven
|
||||
// by the dispatch EndTurn. Start them fogged-apart on a compact 12x12 map.
|
||||
add_player_militarist_inline(&mut state, 2, 2);
|
||||
add_player_militarist_inline(&mut state, 9, 2);
|
||||
add_player_militarist_inline(&mut state, 5, 9);
|
||||
stamp_personality(&mut state, 1, "blackhammer");
|
||||
stamp_personality(&mut state, 2, "deepforge");
|
||||
state.ai_unit_catalog = build_unit_catalog();
|
||||
state.ai_building_catalog = build_building_catalog();
|
||||
state.ai_difficulty_threshold_mult = 1.0;
|
||||
state.grid = Some(grassland_grid(12, 12));
|
||||
|
||||
// Sanity: nobody has seen anyone else at game start (fog intact).
|
||||
assert!(
|
||||
!has_first_contact(&state, 1),
|
||||
"slot 1 must start fogged — no enemy visible before exploration"
|
||||
);
|
||||
|
||||
// Drive deterministic self-play. Slot-0 EndTurn runs the AI for slots 1 + 2.
|
||||
let mut contact_turn: Option<u32> = None;
|
||||
for turn in 1..=40u32 {
|
||||
apply_action(&mut state, 0, &PlayerAction::EndTurn).expect("end turn dispatch");
|
||||
if has_first_contact(&state, 1) || has_first_contact(&state, 2) {
|
||||
contact_turn = Some(turn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
contact_turn.is_some(),
|
||||
"AI frontier-seek exploration must produce first contact within 40 turns; \
|
||||
got none — exploration regressed or the discovery feed is broken"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue