feat(@projects/@magic-civilization): 🌅 p3-29 T3 — Rust turn emits GoldenAgeStarted/Ended
The live GDScript turn emitted `golden_age_started`/`golden_age_ended` inline; the headless happiness phase flipped `golden_age_active` silently. Detect the false→true / true→false edge in `process_happiness_phase`, buffer it into a new transient `GameState.pending_golden_age_events` (registry-has-no-event-sink pattern), and drain it in `step()` into `GoldenAgeStarted`/`GoldenAgeEnded`. Both edges emitted since the live UI consumes both signals (event_bus.gd). No wire surface — dispatch drops them; live UI reads the kind-tagged event_to_dict. Verified headless: mc-replay 20/0 (golden_age_events_serde), mc-turn 291/0 (golden_age_start_edge_buffers_started_event + golden_age_end_edge_buffers_ended_event + event_collector_wiring). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
158ef4d1bd
commit
a87ea9f4d4
7 changed files with 143 additions and 5 deletions
|
|
@ -162,6 +162,16 @@ pub(crate) fn event_to_dict(evt: &TurnEvent) -> Dictionary {
|
|||
d.set("col", hex.q as i64);
|
||||
d.set("row", hex.r as i64);
|
||||
}
|
||||
TurnEvent::GoldenAgeStarted { turn, clan } => {
|
||||
d.set("kind", GString::from("GoldenAgeStarted"));
|
||||
d.set("turn", *turn as i64);
|
||||
d.set("clan", clan.0 as i64);
|
||||
}
|
||||
TurnEvent::GoldenAgeEnded { turn, clan } => {
|
||||
d.set("kind", GString::from("GoldenAgeEnded"));
|
||||
d.set("turn", *turn as i64);
|
||||
d.set("clan", clan.0 as i64);
|
||||
}
|
||||
TurnEvent::AmbientEncounterFired { turn, clan, hex, species, group_size } => {
|
||||
d.set("kind", GString::from("AmbientEncounterFired"));
|
||||
d.set("turn", *turn as i64);
|
||||
|
|
|
|||
|
|
@ -993,10 +993,12 @@ fn translate_processor_events(events: &[mc_replay::TurnEvent]) -> Vec<Event> {
|
|||
| mc_replay::TurnEvent::CityGrew { .. }
|
||||
| mc_replay::TurnEvent::CityBordersExpanded { .. }
|
||||
| mc_replay::TurnEvent::FloraSuccession { .. }
|
||||
// p3-29 (T2): no wire `Event::UnitHealed` surface — consumed by the
|
||||
// live UI via the kind-tagged `event_to_dict` dict, not this wire
|
||||
// path. Drop here to keep the match exhaustive.
|
||||
| mc_replay::TurnEvent::UnitHealed { .. } => {}
|
||||
// p3-29 (T2/T3): no wire surface — consumed by the live UI via the
|
||||
// kind-tagged `event_to_dict` dict, not this wire path. Drop here to
|
||||
// keep the match exhaustive.
|
||||
| mc_replay::TurnEvent::UnitHealed { .. }
|
||||
| mc_replay::TurnEvent::GoldenAgeStarted { .. }
|
||||
| mc_replay::TurnEvent::GoldenAgeEnded { .. } => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
|
|
|
|||
|
|
@ -175,6 +175,23 @@ pub enum TurnEvent {
|
|||
/// Hex the unit healed on.
|
||||
hex: TileCoord,
|
||||
},
|
||||
/// p3-29 (T3): a clan entered a Golden Age (the happiness phase's
|
||||
/// `golden_age_active` flipped false→true). Single-source replacement for
|
||||
/// the GDScript turn's inline `golden_age_started` signal.
|
||||
GoldenAgeStarted {
|
||||
/// Turn the event fired on.
|
||||
turn: u32,
|
||||
/// Clan that entered the Golden Age.
|
||||
clan: ClanId,
|
||||
},
|
||||
/// p3-29 (T3): a clan's Golden Age ended (`golden_age_active` flipped
|
||||
/// true→false). Single-source replacement for `golden_age_ended`.
|
||||
GoldenAgeEnded {
|
||||
/// Turn the event fired on.
|
||||
turn: u32,
|
||||
/// Clan whose Golden Age ended.
|
||||
clan: ClanId,
|
||||
},
|
||||
/// A wonder finished construction.
|
||||
WonderBuilt {
|
||||
/// Turn the event fired on.
|
||||
|
|
@ -572,6 +589,8 @@ impl TurnEvent {
|
|||
| Self::FloraSuccession { turn, .. }
|
||||
| Self::CultureResearched { turn, .. }
|
||||
| Self::UnitHealed { turn, .. }
|
||||
| Self::GoldenAgeStarted { turn, .. }
|
||||
| Self::GoldenAgeEnded { turn, .. }
|
||||
| Self::WonderBuilt { turn, .. }
|
||||
| Self::WarDeclared { turn, .. }
|
||||
| Self::PeaceSigned { turn, .. }
|
||||
|
|
@ -763,6 +782,29 @@ mod tests {
|
|||
assert_eq!(decoded, ev);
|
||||
}
|
||||
|
||||
/// p3-29 (T3): verify `GoldenAgeStarted`/`GoldenAgeEnded` survive a JSON +
|
||||
/// bincode serde round-trip and `turn()` returns their turn.
|
||||
#[test]
|
||||
fn golden_age_events_serde() {
|
||||
let started = TurnEvent::GoldenAgeStarted { turn: 11, clan: ClanId(0) };
|
||||
let ended = TurnEvent::GoldenAgeEnded { turn: 21, clan: ClanId(0) };
|
||||
assert_eq!(started.turn(), 11);
|
||||
assert_eq!(ended.turn(), 21);
|
||||
|
||||
for ev in [&started, &ended] {
|
||||
let json = serde_json::to_string(ev).expect("serialize");
|
||||
let back: TurnEvent = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(&back, ev);
|
||||
}
|
||||
|
||||
let cfg = bincode::config::standard();
|
||||
let events = vec![started, ended];
|
||||
let bytes = bincode::serde::encode_to_vec(&events, cfg).expect("encode");
|
||||
let (decoded, _): (Vec<TurnEvent>, usize) =
|
||||
bincode::serde::decode_from_slice(&bytes, cfg).expect("decode");
|
||||
assert_eq!(decoded, events);
|
||||
}
|
||||
|
||||
/// p2-55: verify that `UnitCaptured`, `UnitRansomOffered`, and
|
||||
/// `CivilianDestroyed` survive a JSON serde round-trip, and that
|
||||
/// `turn()` returns the correct value for each.
|
||||
|
|
|
|||
|
|
@ -447,6 +447,14 @@ pub struct GameState {
|
|||
/// `#[serde(skip)]` — cleared/drained every turn.
|
||||
#[serde(skip)]
|
||||
pub pending_heal_events: Vec<(usize, u32, i32, i32, i32)>,
|
||||
/// p3-29 (T3): transient buffer of golden-age transitions `(player_index,
|
||||
/// started)` the happiness phase produced this turn — `started == true` for
|
||||
/// a false→true edge (GoldenAgeStarted), `false` for true→false
|
||||
/// (GoldenAgeEnded). `step()` drains it into the matching turn events
|
||||
/// (same registry-has-no-event-sink buffer pattern as the heal buffer).
|
||||
/// `#[serde(skip)]` — cleared/drained every turn.
|
||||
#[serde(skip)]
|
||||
pub pending_golden_age_events: Vec<(usize, bool)>,
|
||||
/// p3-26 B3: improvement definitions (`id → {build_turns, food, production}`),
|
||||
/// boot-loaded from `public/resources/improvements/*.json`. `#[serde(skip)]`
|
||||
/// static content like the other catalogs; drives both the build-tick
|
||||
|
|
|
|||
|
|
@ -53,7 +53,12 @@ use crate::game_state::GameState;
|
|||
pub fn process_happiness_phase(state: &mut GameState) {
|
||||
let config = HappinessConfig::default();
|
||||
|
||||
for player in &mut state.players {
|
||||
// p3-29 (T3): record golden-age edges here, push into the transient buffer
|
||||
// after the loop releases its `&mut state.players` borrow. `step()` drains
|
||||
// it into GoldenAgeStarted/Ended events.
|
||||
let mut golden_edges: Vec<(usize, bool)> = Vec::new();
|
||||
|
||||
for (pi, player) in state.players.iter_mut().enumerate() {
|
||||
let total_citizens: i32 = player
|
||||
.cities
|
||||
.iter()
|
||||
|
|
@ -86,6 +91,7 @@ pub fn process_happiness_phase(state: &mut GameState) {
|
|||
player.happiness = breakdown.total;
|
||||
player.happiness_status = breakdown.status;
|
||||
|
||||
let was_active = player.golden_age_active;
|
||||
let mut ga_state = GoldenAgeState {
|
||||
golden_age_active: player.golden_age_active,
|
||||
golden_age_turns: player.golden_age_turns,
|
||||
|
|
@ -97,7 +103,13 @@ pub fn process_happiness_phase(state: &mut GameState) {
|
|||
player.golden_age_turns = ga_state.golden_age_turns;
|
||||
player.golden_age_progress = ga_state.golden_age_progress;
|
||||
player.golden_age_count = ga_state.golden_age_count;
|
||||
|
||||
if ga_state.golden_age_active != was_active {
|
||||
golden_edges.push((pi, ga_state.golden_age_active));
|
||||
}
|
||||
}
|
||||
|
||||
state.pending_golden_age_events.extend(golden_edges);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -185,6 +197,52 @@ mod tests {
|
|||
assert_eq!(player.golden_age_count, 1);
|
||||
}
|
||||
|
||||
/// p3-29 (T3): the false→true golden-age edge buffers exactly one
|
||||
/// `(player, started=true)` entry for `step()` to drain into GoldenAgeStarted.
|
||||
#[test]
|
||||
fn golden_age_start_edge_buffers_started_event() {
|
||||
let mut state = GameState::default();
|
||||
let mut p = player_with(
|
||||
1,
|
||||
1,
|
||||
&[("diamond", 4), ("emerald", 4), ("ruby", 4), ("jade", 4)],
|
||||
"balanced",
|
||||
);
|
||||
p.golden_age_progress = 95; // surplus pushes meter past 100 this tick
|
||||
state.players.push(p);
|
||||
|
||||
process_happiness_phase(&mut state);
|
||||
|
||||
assert!(state.players[0].golden_age_active);
|
||||
assert_eq!(
|
||||
state.pending_golden_age_events,
|
||||
vec![(0usize, true)],
|
||||
"false→true edge buffers one started event"
|
||||
);
|
||||
}
|
||||
|
||||
/// p3-29 (T3): the true→false edge (Golden Age expiring) buffers one
|
||||
/// `(player, started=false)` entry → GoldenAgeEnded; a still-active Golden
|
||||
/// Age buffers nothing (no edge).
|
||||
#[test]
|
||||
fn golden_age_end_edge_buffers_ended_event() {
|
||||
let mut state = GameState::default();
|
||||
let mut p = player_with(1, 1, &[], "balanced");
|
||||
p.golden_age_active = true;
|
||||
p.golden_age_turns = 1; // expires this tick → true→false edge
|
||||
p.golden_age_count = 1;
|
||||
state.players.push(p);
|
||||
|
||||
process_happiness_phase(&mut state);
|
||||
|
||||
assert!(!state.players[0].golden_age_active, "golden age expired this tick");
|
||||
assert_eq!(
|
||||
state.pending_golden_age_events,
|
||||
vec![(0usize, false)],
|
||||
"true→false edge buffers one ended event"
|
||||
);
|
||||
}
|
||||
|
||||
/// An active Golden Age ticks down each turn.
|
||||
#[test]
|
||||
fn active_golden_age_counts_down() {
|
||||
|
|
|
|||
|
|
@ -558,6 +558,20 @@ impl TurnProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
// p3-29 (T3): drain the happiness phase's golden-age edge buffer into
|
||||
// GoldenAgeStarted/Ended events.
|
||||
if !state.pending_golden_age_events.is_empty() {
|
||||
let turn_now = state.turn;
|
||||
for (pi, started) in state.pending_golden_age_events.drain(..) {
|
||||
let clan = mc_replay::ClanId(pi as u32);
|
||||
result.events_emitted.push(if started {
|
||||
mc_replay::TurnEvent::GoldenAgeStarted { turn: turn_now, clan }
|
||||
} else {
|
||||
mc_replay::TurnEvent::GoldenAgeEnded { turn: turn_now, clan }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// now-awoken unit's state is consistent when combat checks fire.
|
||||
|
|
|
|||
|
|
@ -211,6 +211,8 @@ fn ten_turn_run_emits_each_wired_variant() {
|
|||
TurnEvent::FloraSuccession { .. } => "FloraSuccession",
|
||||
TurnEvent::CultureResearched { .. } => "CultureResearched",
|
||||
TurnEvent::UnitHealed { .. } => "UnitHealed",
|
||||
TurnEvent::GoldenAgeStarted { .. } => "GoldenAgeStarted",
|
||||
TurnEvent::GoldenAgeEnded { .. } => "GoldenAgeEnded",
|
||||
TurnEvent::CityFounded { .. } => "CityFounded",
|
||||
TurnEvent::WonderBuilt { .. } => "WonderBuilt",
|
||||
TurnEvent::CityCaptured { .. } => "CityCaptured",
|
||||
|
|
@ -332,6 +334,8 @@ fn events_emitted_appears_on_turn_result() {
|
|||
TurnEvent::FloraSuccession { .. } => "FloraSuccession",
|
||||
TurnEvent::CultureResearched { .. } => "CultureResearched",
|
||||
TurnEvent::UnitHealed { .. } => "UnitHealed",
|
||||
TurnEvent::GoldenAgeStarted { .. } => "GoldenAgeStarted",
|
||||
TurnEvent::GoldenAgeEnded { .. } => "GoldenAgeEnded",
|
||||
TurnEvent::CityFounded { .. } => "CityFounded",
|
||||
TurnEvent::WonderBuilt { .. } => "WonderBuilt",
|
||||
TurnEvent::CityCaptured { .. } => "CityCaptured",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue