feat(mcts-service): ✨ Introduce MCTS protocol definitions, player action dispatch, and abstract turn projection logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
959d606ba7
commit
da4d665720
3 changed files with 48 additions and 15 deletions
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -277,7 +277,10 @@ fn apply_end_turn(state: &mut GameState, player: PlayerId) -> Result<Vec<Event>,
|
|||
// `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<Vec<Event>,
|
|||
/// 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<mc_turn::VictoryConfig> {
|
||||
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<Event> {
|
||||
let mut out: Vec<Event> = Vec::new();
|
||||
for ev in events {
|
||||
|
|
|
|||
|
|
@ -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<u32, &MapUnit>) -> 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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue