refactor(mc-state): 🏗️ Phase 3b — move GameState into mc-state behind a shim

p2-65 foundation milestone. `game_state.rs` (1356 lines: GameState +
PlayerState + MapUnit + TechState + PendingCaptureEvents + 8 action-request
structs + RallyCommand/BuildingRallyPoint/CityEcology + custom serde helpers)
relocated from mc-turn to mc-state. Decouples the canonical full-simulation
state shape from the turn-step mutation logic (Rail 1 cleanup).

- `git mv mc-turn/src/game_state.rs → mc-state/src/game_state.rs`; mc-turn's
  `game_state.rs` is now `pub use mc_state::game_state::*;` so all ~30 consumer
  sites (mc-ai, mc-player-api, mc-mod-host, api-gdext, mc-sim, mc-replay) +
  mc-turn's lib.rs `pub use game_state::{GameState,…}` re-export resolve
  unchanged for one cycle (Phase 4 sweeps them).
- Re-paths inside the moved file: `crate::combat_balance::CombatBalance` →
  `mc_core::CombatBalance` (the only non-sibling code ref); the 5
  sibling-module field types (ransom/capture/patrol/combat_event) resolve as
  `crate::` since they're now co-located in mc-state. Broken `[crate::…]`
  intra-doc links demoted to plain `mc_turn::…` backtick prose.
- `PendingCaptureEvents::drain_into` peeled off into the mc-turn-local
  `DrainCaptureEvents` extension trait (`capture_drain.rs`): it embeds
  `mc_replay::TurnEvent` + `mc_turn::combat_event::TurnResult`, neither movable
  to the data crate without a cycle. Local-trait-for-foreign-type, orphan-rule
  legal. 4 call sites (processor.rs + 3 tests) add the trait `use`.

SAVE-FORMAT GATE (the byte-identical proof): mc-turn serde_roundtrip 6/6 +
full_turn_golden 3/3 green — assembled GameState round-trips identically
post-move (serde shapes invariant; module paths were never on disk).

Parity (apricot): workspace --no-run exit 0; mc-state 12/12 (8 + the 4
game_state unit tests that moved with the file); mc-turn lib 234/234
(1 ignored, pre-existing — was 238 before the 4 moved out); mc-ai 268/268;
mc-player-api 126/126.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
autocommit 2026-06-04 19:16:00 -07:00
parent 45e9adea92
commit 0ed21945c1
8 changed files with 1404 additions and 1362 deletions

File diff suppressed because it is too large Load diff

View file

@ -17,5 +17,6 @@
pub mod capture;
pub mod combat_event;
pub mod game_state;
pub mod patrol;
pub mod ransom;

View file

@ -0,0 +1,89 @@
//! Capture-event drain (p2-65 Phase 3b).
//!
//! `PendingCaptureEvents` is a data shape that lives in [`mc_state::game_state`],
//! but draining it into a [`crate::combat_event::TurnResult`] is turn-step event
//! translation: it builds `mc_replay::TurnEvent` variants and appends them to
//! `result.events_emitted`. Both `TurnResult` and `mc_replay::TurnEvent` are
//! `mc-turn` / replay shapes that cannot move into the data crate without a
//! cycle, so this logic stays in `mc-turn` as an extension trait over the
//! foreign `PendingCaptureEvents` type (local-trait-for-foreign-type — legal
//! under the orphan rule because the trait is local).
use crate::combat_event::TurnResult;
use mc_state::game_state::PendingCaptureEvents;
/// Drain staged capture/PvP events into the turn's `TurnResult`.
pub trait DrainCaptureEvents {
/// Move accumulated events into the supplied `TurnResult` and clear the
/// scratch buffer. Called once per `process_pvp_combat`.
///
/// Also translates each p2-55 event into a `mc_replay::TurnEvent` variant
/// and appends it to `result.events_emitted` so the chronicle pipeline
/// picks them up without an extra pass.
fn drain_into(&mut self, result: &mut TurnResult);
}
impl DrainCaptureEvents for PendingCaptureEvents {
fn drain_into(&mut self, result: &mut TurnResult) {
// Emit replay variants before moving the vecs (we borrow from them).
for ev in &self.units_captured {
result.events_emitted.push(mc_replay::TurnEvent::UnitCaptured {
turn: ev.turn,
unit_id: ev.unit_id,
captor: mc_replay::ClanId(ev.captor as u32),
prior_owner: mc_replay::ClanId(ev.prior_owner as u32),
hex: mc_replay::TileCoord::new(ev.col, ev.row),
unit_kind: mc_replay::UnitKind(ev.unit_kind.clone()),
});
}
for ev in &self.ransom_offers_created {
result.events_emitted.push(mc_replay::TurnEvent::UnitRansomOffered {
turn: ev.turn,
offer_id: ev.offer_id,
unit_id: ev.unit_id,
captor: mc_replay::ClanId(ev.captor as u32),
owner: mc_replay::ClanId(ev.owner as u32),
price: ev.price,
expires_turn: ev.expires_turn,
});
}
for ev in &self.civilians_destroyed {
result.events_emitted.push(mc_replay::TurnEvent::CivilianDestroyed {
turn: ev.turn,
unit_id: ev.unit_id,
destroyer: mc_replay::ClanId(ev.destroyer as u32),
owner: mc_replay::ClanId(ev.owner as u32),
hex: mc_replay::TileCoord::new(ev.col, ev.row),
unit_kind: mc_replay::UnitKind(ev.unit_kind.clone()),
});
}
// p2-67 Bug 3: queued-PvP kill events were silent before this drain
// existed — the inline `swap_remove` in `resolve_single_pvp_attack`
// happened with no `TurnEvent::UnitKilled` push. Translate each
// staged `UnitKilledEvent` to the chronicle variant now.
for ev in &self.units_killed {
result.events_emitted.push(mc_replay::TurnEvent::UnitKilled {
turn: ev.turn,
attacker: mc_replay::ClanId(ev.attacker as u32),
defender: mc_replay::ClanId(ev.defender as u32),
unit_id: ev.unit_id,
unit_kind: mc_replay::UnitKind(ev.unit_kind.clone()),
hex: mc_replay::TileCoord::new(ev.col, ev.row),
});
}
// `units_killed` does not have a matching `TurnResult` Vec field —
// the canonical surface is `events_emitted` above. Just clear.
self.units_killed.clear();
result.units_captured.append(&mut self.units_captured);
result.ransom_offers_created.append(&mut self.ransom_offers_created);
result.civilians_destroyed.append(&mut self.civilians_destroyed);
// p2-55e: drain accepted/expired into TurnResult so chronicle reads
// them directly without prior-turn cross-reference.
result
.ransom_offers_accepted
.append(&mut self.ransom_offers_accepted);
result
.ransom_offers_expired
.append(&mut self.ransom_offers_expired);
}
}

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@ pub mod abstract_projection;
pub mod action;
pub mod action_handlers;
pub mod capture;
pub mod capture_drain;
pub mod combat_balance;
pub mod ransom;
pub mod building_action_handlers;

View file

@ -26,6 +26,7 @@ use crate::combat_event::{
CivilianDestroyedEvent, FaunaCombatEvent, PvpCombatEvent, SiegeEvent, StrategicGateRejection,
TurnResult, UnitCapturedEvent, UnitKilledEvent, UnitRansomOfferedEvent,
};
use crate::capture_drain::DrainCaptureEvents;
use crate::game_state::{BuildingRallyPoint, GameState, MapUnit, RallyCommand};
use crate::spatial_index::LairIndex;
use mc_core::formation::{Formation, FormationShape};

View file

@ -41,6 +41,7 @@ use mc_ai::evaluator::ScoringWeights;
use mc_city::CityState;
use mc_replay::TurnEvent;
use mc_turn::{
capture_drain::DrainCaptureEvents,
combat_event::{
CivilianDestroyedEvent, TurnResult, UnitCapturedEvent,
UnitRansomAcceptedEvent, UnitRansomExpiredEvent, UnitRansomOfferedEvent,

View file

@ -106,6 +106,7 @@ fn tick_drains_only_expired_leaves_others() {
use mc_turn::combat_event::{
UnitRansomAcceptedEvent, UnitRansomExpiredEvent,
};
use mc_turn::capture_drain::DrainCaptureEvents;
use mc_turn::game_state::PendingCaptureEvents;
use mc_turn::combat_event::TurnResult;