diff --git a/src/simulator/crates/mc-player-api/src/dispatch.rs b/src/simulator/crates/mc-player-api/src/dispatch.rs index 99b72de6..e621da92 100644 --- a/src/simulator/crates/mc-player-api/src/dispatch.rs +++ b/src/simulator/crates/mc-player-api/src/dispatch.rs @@ -976,7 +976,12 @@ fn translate_processor_events(events: &[mc_replay::TurnEvent]) -> Vec { /// counted but DO NOT abort the rest of the turn — a single /// unknown unit or illegal move must not collapse the entire AI /// side (matches the GDScript dispatch behaviour). -fn drive_ai_slot(state: &mut GameState, ai_slot: u8) -> u32 { +/// Drive one AI slot for a turn: project vision, run the slot's controller +/// (`run_ai_turn` by default), and apply the chosen actions. Public so headless +/// multi-clan benches (`dominion_bench`) can drive the *real* AI through the +/// same path the interactive game uses (`apply_end_turn`), instead of relying +/// on `mc-turn`'s inline fallback movement. Returns the action count. +pub fn drive_ai_slot(state: &mut GameState, ai_slot: u8) -> u32 { let pi: usize = ai_slot as usize; if pi >= state.players.len() { return 0; diff --git a/src/simulator/crates/mc-sim/Cargo.toml b/src/simulator/crates/mc-sim/Cargo.toml index 21f4d750..3524c67f 100644 --- a/src/simulator/crates/mc-sim/Cargo.toml +++ b/src/simulator/crates/mc-sim/Cargo.toml @@ -23,6 +23,7 @@ mc-city = { path = "../mc-city" } mc-culture = { path = "../mc-culture" } mc-economy = { path = "../mc-economy" } mc-ai = { path = "../mc-ai" } +mc-player-api = { path = "../mc-player-api" } serde.workspace = true serde_json.workspace = true rayon = "1" diff --git a/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs b/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs index ef38321e..14a33063 100644 --- a/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs +++ b/src/simulator/crates/mc-sim/src/bin/dominion_bench.rs @@ -355,7 +355,20 @@ fn run_scenario_with_profiles(num_players: usize, all_profiles: &[ProfileJson]) ..Default::default() }], unit_upkeep: vec![], strategic_axes: make_axes(profile), - scoring_weights: ScoringWeights::default(), + // Real per-personality scoring weights (military/expansion/etc.), + // the same source the live game uses (`ScoringWeights::from_personality`). + // Without these the AI scores every action ~0 and `drive_ai_slot` + // produces no moves — the inert `default()` was why the bench showed + // zero PvP. Falls back to default if the personality file is absent. + scoring_weights: ScoringWeights::from_personality( + &profile.id, + &Path::new(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .expect("workspace root") + .join("public/resources/ai/personalities"), + ) + .unwrap_or_default(), expansion_points: 0, city_buildings: vec![vec![]], city_improvements: Default::default(), @@ -413,6 +426,14 @@ fn run_scenario_with_profiles(num_players: usize, all_profiles: &[ProfileJson]) let mut victory_turn: Option = None; for turn_num in 1..=TOTAL_TURNS { + // Drive every slot's REAL AI (`run_ai_turn` via the controller registry) + // before the turn resolves — the same `drive_ai_slot` the interactive + // game runs inside `apply_end_turn`. Without this the bench relied on + // `mc-turn`'s inline `nearest_lair` fallback (PvE only → zero PvP). All + // slots are AI here (no human), so we drive all of them. + for slot in 0..state.players.len() as u8 { + mc_player_api::dispatch::drive_ai_slot(&mut state, slot); + } let result = processor.step(&mut state); // Track unit production per player.