diff --git a/src/simulator/api-gdext/src/replay.rs b/src/simulator/api-gdext/src/replay.rs index 1bef3c73..b70e3237 100644 --- a/src/simulator/api-gdext/src/replay.rs +++ b/src/simulator/api-gdext/src/replay.rs @@ -210,6 +210,14 @@ fn event_to_dict(evt: &TurnEvent) -> Dictionary { d.set("col", hex.q as i64); d.set("row", hex.r as i64); } + TurnEvent::CityBordersExpanded { turn, clan, city, hex } => { + d.set("kind", GString::from("CityBordersExpanded")); + d.set("turn", *turn as i64); + d.set("clan", clan.0 as i64); + d.set("city", GString::from(city.0.as_str())); + d.set("col", hex.q as i64); + d.set("row", hex.r as i64); + } TurnEvent::GameOver { turn, winner, reason_kind, condition, resigned_clan } => { d.set("kind", GString::from("GameOver")); d.set("turn", *turn as i64); diff --git a/src/simulator/crates/mc-player-api/src/dispatch.rs b/src/simulator/crates/mc-player-api/src/dispatch.rs index 7874da9e..069aceb6 100644 --- a/src/simulator/crates/mc-player-api/src/dispatch.rs +++ b/src/simulator/crates/mc-player-api/src/dispatch.rs @@ -979,9 +979,10 @@ fn translate_processor_events(events: &[mc_replay::TurnEvent]) -> Vec { | mc_replay::TurnEvent::EnvelopeIntercepted { .. } | mc_replay::TurnEvent::LinkSevered { .. } | mc_replay::TurnEvent::LinkRestored { .. } - // p3-29: surfaced for replay + the live UI (city growth), not the - // wire protocol — drop here to keep the match exhaustive. - | mc_replay::TurnEvent::CityGrew { .. } => {} + // p3-29: surfaced for replay + the live UI (city growth / border + // expansion), not the wire protocol — drop here to keep it exhaustive. + | mc_replay::TurnEvent::CityGrew { .. } + | mc_replay::TurnEvent::CityBordersExpanded { .. } => {} } } out diff --git a/src/simulator/crates/mc-replay/src/event.rs b/src/simulator/crates/mc-replay/src/event.rs index 0e58d5b4..5a2f648f 100644 --- a/src/simulator/crates/mc-replay/src/event.rs +++ b/src/simulator/crates/mc-replay/src/event.rs @@ -121,6 +121,18 @@ pub enum TurnEvent { /// Hex the city sits on, for the viewer to centre on. hex: TileCoord, }, + /// p3-29: a city expanded its borders, claiming a new tile (single-source + /// replacement for the GDScript turn's inline `city_border_expanded` signal). + CityBordersExpanded { + /// Turn the event fired on. + turn: u32, + /// Owning clan. + clan: ClanId, + /// Synthesised city name (`city__`). + city: CityName, + /// The newly-claimed tile. + hex: TileCoord, + }, /// A wonder finished construction. WonderBuilt { /// Turn the event fired on. @@ -514,6 +526,7 @@ impl TurnEvent { | Self::UnitCreated { turn, .. } | Self::CityBuildingCompleted { turn, .. } | Self::CityGrew { turn, .. } + | Self::CityBordersExpanded { turn, .. } | Self::WonderBuilt { turn, .. } | Self::WarDeclared { turn, .. } | Self::PeaceSigned { turn, .. } diff --git a/src/simulator/crates/mc-turn/src/processor.rs b/src/simulator/crates/mc-turn/src/processor.rs index db5b268a..fa19c9ca 100644 --- a/src/simulator/crates/mc-turn/src/processor.rs +++ b/src/simulator/crates/mc-turn/src/processor.rs @@ -434,7 +434,7 @@ impl TurnProcessor { self.process_city_production(state, pi, &mut result.events_emitted); self.try_found_city(state, pi, &mut result.events_emitted); self.try_spawn_unit(state, pi, &mut result); - self.process_culture(state, pi); + self.process_culture(state, pi, &mut result.events_emitted); self.process_culture_research(state, pi); self.process_science(state, pi, &mut result.events_emitted); } @@ -1129,7 +1129,12 @@ impl TurnProcessor { } } - fn process_culture(&self, state: &mut GameState, pi: usize) { + fn process_culture( + &self, + state: &mut GameState, + pi: usize, + events: &mut Vec, + ) { // Grid dims read before the &mut player borrow (needed for in-bounds // candidate filtering during border expansion). let (gw, gh) = state @@ -1137,6 +1142,10 @@ impl TurnProcessor { .as_ref() .map(|g| (g.width, g.height)) .unwrap_or((0, 0)); + let turn = state.turn; + // p3-29: tiles claimed this turn (city_idx, tile), flushed as + // `TurnEvent::CityBordersExpanded` so the live UI + replay see expansion. + let mut expanded: Vec<(usize, (i32, i32))> = Vec::new(); let player = &mut state.players[pi]; let culture_axis = *player.strategic_axes.get("culture").unwrap_or(&2); let per_city_yield = culture_axis as f64 * Self::BENCH_CULTURE_PER_AXIS_POINT; @@ -1197,11 +1206,22 @@ impl TurnProcessor { ot.push(pick); claimed.insert(pick); player.culture_pool.consume_expansion(ci); + expanded.push((ci, pick)); } } } player.culture_total = player.culture_pool.culture_total; + + // p3-29: flush border-expansion events (player no longer borrowed below). + for (ci, tile) in expanded { + events.push(mc_replay::TurnEvent::CityBordersExpanded { + turn, + clan: mc_replay::ClanId(pi as u32), + city: mc_replay::CityName(format!("city_{}_{}", pi, ci)), + hex: mc_replay::TileCoord::new(tile.0, tile.1), + }); + } } // ── Phase 1b': Culture-research accumulation ──────────────────────── @@ -2677,7 +2697,7 @@ impl TurnProcessor { self.process_city_production(state, pi, &mut result.events_emitted); self.try_found_city(state, pi, &mut result.events_emitted); self.try_spawn_unit(state, pi, &mut result); - self.process_culture(state, pi); + self.process_culture(state, pi, &mut result.events_emitted); self.process_culture_research(state, pi); self.process_science(state, pi, &mut result.events_emitted); } @@ -5837,10 +5857,19 @@ mod tests { state.players.push(p); let processor = TurnProcessor::new(1); + let mut events: Vec = Vec::new(); for _ in 0..80 { - processor.process_culture(&mut state, 0); + processor.process_culture(&mut state, 0, &mut events); } + // p3-29: each border claim surfaces a CityBordersExpanded event. + assert!( + events + .iter() + .any(|e| matches!(e, mc_replay::TurnEvent::CityBordersExpanded { .. })), + "CityBordersExpanded must fire when borders expand; got {events:?}" + ); + let owned = &state.players[0].cities[0].owned_tiles; assert!( owned.contains(&(5, 5)), diff --git a/src/simulator/crates/mc-turn/tests/event_collector_wiring.rs b/src/simulator/crates/mc-turn/tests/event_collector_wiring.rs index 723d65b3..723a1674 100644 --- a/src/simulator/crates/mc-turn/tests/event_collector_wiring.rs +++ b/src/simulator/crates/mc-turn/tests/event_collector_wiring.rs @@ -207,6 +207,7 @@ fn ten_turn_run_emits_each_wired_variant() { .map(|e| match e { TurnEvent::CityBuildingCompleted { .. } => "CityBuildingCompleted", TurnEvent::CityGrew { .. } => "CityGrew", + TurnEvent::CityBordersExpanded { .. } => "CityBordersExpanded", TurnEvent::CityFounded { .. } => "CityFounded", TurnEvent::WonderBuilt { .. } => "WonderBuilt", TurnEvent::CityCaptured { .. } => "CityCaptured", @@ -324,6 +325,7 @@ fn events_emitted_appears_on_turn_result() { .map(|e| match e { TurnEvent::CityBuildingCompleted { .. } => "CityBuildingCompleted", TurnEvent::CityGrew { .. } => "CityGrew", + TurnEvent::CityBordersExpanded { .. } => "CityBordersExpanded", TurnEvent::CityFounded { .. } => "CityFounded", TurnEvent::WonderBuilt { .. } => "WonderBuilt", TurnEvent::CityCaptured { .. } => "CityCaptured",