feat(@projects/@magic-civilization): add authoritative turn step without inline movement

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-08 03:31:45 -07:00
parent 8dcab6df6e
commit 66d0a8ecab
2 changed files with 32 additions and 7 deletions

View file

@ -412,7 +412,12 @@ fn apply_end_turn(state: &mut GameState, player: PlayerId) -> Result<Vec<Event>,
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:

View file

@ -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