From da4d6657200a913b64f2139e355464a316666470 Mon Sep 17 00:00:00 2001 From: autocommit Date: Sun, 17 May 2026 18:23:56 -0700 Subject: [PATCH] =?UTF-8?q?feat(mcts-service):=20=E2=9C=A8=20Introduce=20M?= =?UTF-8?q?CTS=20protocol=20definitions,=20player=20action=20dispatch,=20a?= =?UTF-8?q?nd=20abstract=20turn=20projection=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../crates/mc-mcts-service/src/protocol.rs | 16 ++++----- .../crates/mc-player-api/src/dispatch.rs | 35 ++++++++++++++++++- .../crates/mc-turn/src/abstract_projection.rs | 12 +++---- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/simulator/crates/mc-mcts-service/src/protocol.rs b/src/simulator/crates/mc-mcts-service/src/protocol.rs index bda6961a..4603beee 100644 --- a/src/simulator/crates/mc-mcts-service/src/protocol.rs +++ b/src/simulator/crates/mc-mcts-service/src/protocol.rs @@ -18,9 +18,9 @@ pub struct MctsPlayerState { pub tech_index: u16, pub happiness_pool: i16, /// Relative military force vs each opponent slot (self-slot = 0). - pub force_rel: [u16; 5], + pub force_rel: [u16; mc_ai::abstract_state::MAX_PLAYERS], /// Diplomatic relation per opponent: <0 war, 0 peace, >0 friend. - pub relations: [i8; 5], + pub relations: [i8; mc_ai::abstract_state::MAX_PLAYERS], pub rng_state: u64, pub turn: u32, } @@ -34,8 +34,8 @@ impl Default for MctsPlayerState { city_count: 0, tech_index: 0, happiness_pool: 0, - force_rel: [0; 5], - relations: [0; 5], + force_rel: [0; mc_ai::abstract_state::MAX_PLAYERS], + relations: [0; mc_ai::abstract_state::MAX_PLAYERS], rng_state: 0, turn: 0, } @@ -137,11 +137,11 @@ pub struct AbstractPlayerStateMirror { /// `happiness_pool` — signed; negative under unrest. pub happiness_pool: i16, /// `force_rel[opp]` — relative military force vs each opponent slot. - pub force_rel: [u16; 5], + pub force_rel: [u16; mc_ai::abstract_state::MAX_PLAYERS], /// `axes` — strategic axes via `mc_ai::game_state::axes_to_flat`. pub axes: [u8; 8], /// `relations[opp]` — diplomatic relation per opponent (-1/0/+1). - pub relations: [i8; 5], + pub relations: [i8; mc_ai::abstract_state::MAX_PLAYERS], /// `formation_strength[T1..T4]` — mean strength per tier bucket, 0–255. pub formation_strength: [u8; 4], /// `rng_state` — per-player SplitMix64 state. @@ -189,11 +189,11 @@ impl AbstractPlayerStateMirror { happiness_pool: self.happiness_pool, _pad0: 0, force_rel: self.force_rel, - _pad_fr: 0, axes: self.axes, relations: self.relations, - _pad_rel: [0; 3], + _pad_rel: [0; 4], formation_strength: self.formation_strength, + _pad_fs: [0; 4], rng_state: self.rng_state, turn: self.turn, _pad2: [0; 4], diff --git a/src/simulator/crates/mc-player-api/src/dispatch.rs b/src/simulator/crates/mc-player-api/src/dispatch.rs index c5056d5e..0ddb418b 100644 --- a/src/simulator/crates/mc-player-api/src/dispatch.rs +++ b/src/simulator/crates/mc-player-api/src/dispatch.rs @@ -277,7 +277,10 @@ fn apply_end_turn(state: &mut GameState, player: PlayerId) -> Result, // `max_turns` is advisory in `step` — it only gates the turn-limit // victory fallback. Headless callers don't enforce a turn cap, so // pass a large sentinel; victory_config (when present) overrides. - let processor = mc_turn::processor::TurnProcessor::new(u32::MAX); + let mut processor = mc_turn::processor::TurnProcessor::new(u32::MAX); + if let Some(vc) = victory_config_from_env() { + processor.victory_config = Some(vc); + } let result = processor.step(state); // Translate processor events to wire events. The `clan: ClanId(u32)` // field in every TurnEvent emit site is the player index (see e.g. @@ -309,6 +312,36 @@ fn apply_end_turn(state: &mut GameState, player: PlayerId) -> Result, /// Every emit site in `mc_turn::processor` uses `ClanId(pi as u32)` /// where `pi` is the player index — so `id.0 as PlayerId` is the /// correct clan→player lookup with no separate table needed. +/// Read `CP_VICTORY_MODE` from the environment and return a matching +/// `VictoryConfig`. Returns `None` when the var is unset or empty — +/// preserving the legacy simple-city-count fallback in `TurnProcessor` +/// for existing harness consumers. Recognised values: +/// +/// * `"domination"` — capture-all-capitals only. Other victory paths +/// are disabled (unreachable thresholds, empty science chain). The +/// game ends only via domination (capital capture) or LastSurvivor +/// (opponent has no cities AND no capital). No turn cap — callers +/// enforce their own. +/// +/// Unknown values are ignored (return `None`) so a typo doesn't silently +/// alter behaviour mid-training. +fn victory_config_from_env() -> Option { + let mode = std::env::var("CP_VICTORY_MODE").ok()?; + match mode.as_str() { + "domination" => Some(mc_turn::VictoryConfig { + city_count_threshold: usize::MAX, + gold_threshold: i64::MAX, + culture_threshold: i64::MAX, + science_techs_required: Vec::new(), + science_cost_base: 0, + domination_requires_all_capitals: true, + min_domination_turn: 0, + turn_limit: None, + }), + _ => None, + } +} + fn translate_processor_events(events: &[mc_replay::TurnEvent]) -> Vec { let mut out: Vec = Vec::new(); for ev in events { diff --git a/src/simulator/crates/mc-turn/src/abstract_projection.rs b/src/simulator/crates/mc-turn/src/abstract_projection.rs index bc3da6ba..52b7867c 100644 --- a/src/simulator/crates/mc-turn/src/abstract_projection.rs +++ b/src/simulator/crates/mc-turn/src/abstract_projection.rs @@ -100,11 +100,11 @@ fn project_player( happiness_pool: 0, _pad0: 0, force_rel, - _pad_fr: 0, axes, relations, - _pad_rel: [0; 3], + _pad_rel: [0; 4], formation_strength, + _pad_fs: [0; 4], rng_state, turn: state.turn, _pad2: [0; 4], @@ -236,8 +236,8 @@ fn formation_tier(f: &Formation, unit_lookup: &BTreeMap) -> u8 { /// `force_rel[opp] = (own_units + 1) / (opp_units + 1)`, clamped to `[0, 1]` /// then scaled to a u16. Self-slot is 0. Slots beyond `state.players.len()` /// are 0. -fn compute_force_rel(state: &GameState, player_idx: usize) -> [u16; 5] { - let mut out = [0u16; 5]; +fn compute_force_rel(state: &GameState, player_idx: usize) -> [u16; MAX_PLAYERS] { + let mut out = [0u16; MAX_PLAYERS]; let own = state .players .get(player_idx) @@ -270,8 +270,8 @@ fn compute_force_rel(state: &GameState, player_idx: usize) -> [u16; 5] { /// `state.players[0].relations` per the GameState contract — only player 0 /// carries the authoritative copy, synced each turn by `process_trade_phase` /// (see `game_state.rs:456-459`). -fn project_relations(state: &GameState, player_idx: usize) -> [i8; 5] { - let mut out = [0i8; 5]; +fn project_relations(state: &GameState, player_idx: usize) -> [i8; MAX_PLAYERS] { + let mut out = [0i8; MAX_PLAYERS]; let Some(p0) = state.players.first() else { return out; };