test(@projects/@magic-civilization): 🧪 p3-16 self-play war-dec gate (explore→discover→declare)
Adds the deterministic end-to-end proof p3-16's last bullet needed: in AI-vs-AI self-play, once frontier-seek exploration brings a militarist into view of a *weaker* rival, it dispatches a war-dec and the relation flips to War within 60 turns. Asymmetric armies (slot 2 weakened) so the aggressor clears its superiority threshold on discovery. Pairs with the existing decide_diplomacy unit cases (cautious-holds-at-parity / warmonger-strikes) to demonstrate personality-driven war-decs manifest in actual play — the integration that was blocked purely on discovery (p3-17). Shared self-play setup helper extracted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a3b2a2324f
commit
3aaa524bf1
1 changed files with 68 additions and 26 deletions
|
|
@ -1,13 +1,15 @@
|
|||
//! p3-17 / p3-16 — deterministic self-play first-contact via exploration.
|
||||
//! p3-17 / p3-16 — deterministic self-play: exploration → first contact → war-dec.
|
||||
//!
|
||||
//! 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.
|
||||
//! self-play run" (p3-17: earlier first-contact; p3-16: personality-driven
|
||||
//! war-decs). The sim is deterministic (fixed seed → identical outcome), so these
|
||||
//! are reproducible cargo tests rather than apricot-only smoke batches:
|
||||
//! - `..._first_contact`: the frontier-seek (p3-17) expands fogged-apart AI
|
||||
//! footprints until one sees another within a bounded turn budget — the
|
||||
//! discovery feed `decide_diplomacy` needs.
|
||||
//! - `..._declares_war`: once a militarist discovers a *weaker* rival it
|
||||
//! dispatches a war-dec (p3-16), flipping the relation to War. Without
|
||||
//! exploration this never fired in AI-vs-AI play.
|
||||
|
||||
mod common;
|
||||
|
||||
|
|
@ -18,6 +20,7 @@ use common::{
|
|||
use mc_player_api::action::PlayerAction;
|
||||
use mc_player_api::{apply_action, project_tactical_with_vision};
|
||||
use mc_state::game_state::GameState;
|
||||
use mc_trade::relation::Relation;
|
||||
use mc_vision::{compute_vision, VisionCatalog};
|
||||
|
||||
fn grassland_grid(w: i32, h: i32) -> mc_core::grid::GridState {
|
||||
|
|
@ -28,8 +31,26 @@ fn grassland_grid(w: i32, h: i32) -> mc_core::grid::GridState {
|
|||
g
|
||||
}
|
||||
|
||||
/// Has player `viewer` discovered any unit or city of any *other* player
|
||||
/// (fog-aware projection redacts the unseen)?
|
||||
/// Three militarists on a compact 12x12 map. Slot 0 is a passive "human" that
|
||||
/// only EndTurns; slots 1 + 2 are AI driven by the dispatch EndTurn. They start
|
||||
/// fogged-apart, so any contact is earned by exploration.
|
||||
fn build_self_play_state() -> GameState {
|
||||
let mut state = GameState::default();
|
||||
state.turn = 1;
|
||||
state.units_catalog = build_runtime_units_catalog();
|
||||
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));
|
||||
state
|
||||
}
|
||||
|
||||
/// Has `viewer` discovered any unit or city of another player (fog-aware)?
|
||||
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 {
|
||||
|
|
@ -41,30 +62,22 @@ fn has_first_contact(state: &GameState, viewer: u8) -> bool {
|
|||
.any(|p| p.index != viewer && (!p.units.is_empty() || !p.cities.is_empty()))
|
||||
}
|
||||
|
||||
fn any_pair_at_war(state: &GameState) -> bool {
|
||||
state
|
||||
.players
|
||||
.iter()
|
||||
.any(|p| p.relations.values().any(|r| r.relation == Relation::War))
|
||||
}
|
||||
|
||||
#[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));
|
||||
let mut state = build_self_play_state();
|
||||
|
||||
// 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");
|
||||
|
|
@ -80,3 +93,32 @@ fn ai_self_play_makes_first_contact_via_exploration() {
|
|||
got none — exploration regressed or the discovery feed is broken"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_self_play_declares_war_on_discovered_weaker_rival() {
|
||||
let mut state = build_self_play_state();
|
||||
// Asymmetry: weaken slot 2's army (drain two of its three warriors; the
|
||||
// founder at index 3 stays) so slot 1 — an aggressive militarist — clears
|
||||
// its superiority threshold and war-decs slot 2 once it discovers it.
|
||||
state.players[2].units.drain(1..3);
|
||||
|
||||
assert!(
|
||||
!any_pair_at_war(&state),
|
||||
"everyone must start at peace (courier-diplomacy default)"
|
||||
);
|
||||
|
||||
let mut war = false;
|
||||
for _turn in 1..=60u32 {
|
||||
apply_action(&mut state, 0, &PlayerAction::EndTurn).expect("end turn dispatch");
|
||||
if any_pair_at_war(&state) {
|
||||
war = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
war,
|
||||
"a militarist must dispatch a war-dec against a discovered weaker rival \
|
||||
within 60 turns — exploration→discovery→decide_diplomacy chain broken"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue