From 66d0a8ecab823800d4fb94a133907e01e0578e7c Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 8 Jun 2026 03:31:45 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20authoritative=20turn=20step=20without=20inlin?= =?UTF-8?q?e=20movement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../crates/mc-player-api/src/dispatch.rs | 7 +++- src/simulator/crates/mc-turn/src/processor.rs | 32 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/simulator/crates/mc-player-api/src/dispatch.rs b/src/simulator/crates/mc-player-api/src/dispatch.rs index 99b72de6..65d37152 100644 --- a/src/simulator/crates/mc-player-api/src/dispatch.rs +++ b/src/simulator/crates/mc-player-api/src/dispatch.rs @@ -412,7 +412,12 @@ fn apply_end_turn(state: &mut GameState, player: PlayerId) -> Result, if let Some(vc) = victory_config_from_env() { processor.victory_config = Some(vc); } - let mut result = processor.step(state); + // Authoritative path: units were already moved by the real tactical AI + // (`drive_ai_slot` → `run_ai_turn`) earlier in this `apply_end_turn`, so we + // run the turn WITHOUT mc-turn's inline bench-grade movement heuristic — + // otherwise the dumb nearest-enemy/lair seek re-moves what the real AI + // positioned. `step_authoritative` runs the full turn minus Phase-5 movement. + let mut result = processor.step_authoritative(state); // Communications Phase 6 — end-of-turn comms passes. // Runs after the processor step (so `state.turn` is the new turn // and `step_comms` evaluates deliveries against it). Order: diff --git a/src/simulator/crates/mc-turn/src/processor.rs b/src/simulator/crates/mc-turn/src/processor.rs index c2892cb0..7b2e4c09 100644 --- a/src/simulator/crates/mc-turn/src/processor.rs +++ b/src/simulator/crates/mc-turn/src/processor.rs @@ -386,10 +386,29 @@ impl TurnProcessor { self.culture_web_parsed.as_ref() } - /// Advance `state` by one turn. Deterministic: same state in → same state - /// out (the processor uses a hash-mixed deterministic RNG derived from - /// `state.turn`). + /// Advance `state` by one turn with the **inline bench-grade movement AI** + /// (Phase-5 nearest enemy/city/lair seek). Used by the headless self-play + /// benches (`dominion_bench`, `solo_dominion`, …) where the inline heuristic + /// is the only mover. Deterministic: same state in → same state out. pub fn step(&self, state: &mut GameState) -> TurnResult { + self.step_impl(state, true) + } + + /// Advance `state` by one turn WITHOUT the inline movement AI — the + /// **authoritative** path. Movement is owned by the real tactical AI + /// (`mc_ai::tactical::run_ai_turn`, applied via + /// `mc_player_api::dispatch::drive_ai_slot` *before* this call), so the + /// inline nearest-enemy/lair heuristic must NOT re-move what the real AI + /// positioned. Runs the full turn (economy / production / culture / combat / + /// victory) exactly like [`step`], skipping only the Phase-5 movement loop. + pub fn step_authoritative(&self, state: &mut GameState) -> TurnResult { + self.step_impl(state, false) + } + + /// Shared turn body. `move_units` gates the Phase-5 inline movement loop + /// (see [`process_fauna_encounters_inner`]). Deterministic: same state + + /// same `move_units` in → same state out (hash-mixed RNG from `state.turn`). + fn step_impl(&self, state: &mut GameState, move_units: bool) -> TurnResult { state.turn += 1; let mut result = TurnResult::default(); @@ -486,9 +505,10 @@ impl TurnProcessor { self.process_escort_requests(state); // Phase 5a: movement + fauna encounters. - // Movement runs unconditionally; fauna encounters only fire - // when a grid with lairs exists. - self.process_fauna_encounters_inner(state, &mut result, true); + // Movement runs only when `move_units` (bench path); the authoritative + // path drives movement via run_ai_turn before the turn and passes false. + // Fauna encounters fire whenever a grid with lairs exists. + self.process_fauna_encounters_inner(state, &mut result, move_units); // Phase 5a-sentry: wake sentrying units that have enemies in vision range (2 hex). // Runs after movement so positions are current; runs before PvP so the