From 023c6624d28d05226611faacc52645cf201c0d5b Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 9 May 2026 02:05:04 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20ransom=20event=20bridge=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../objectives/p2-55e-richer-ransom-events.md | 2 +- src/simulator/crates/mc-turn/tests/ransom.rs | 77 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/.project/objectives/p2-55e-richer-ransom-events.md b/.project/objectives/p2-55e-richer-ransom-events.md index d01a1699..8822321e 100644 --- a/.project/objectives/p2-55e-richer-ransom-events.md +++ b/.project/objectives/p2-55e-richer-ransom-events.md @@ -25,7 +25,7 @@ Chronicle currently distinguishes "expired-then-captured" from "fresh capture" b - [x] `TurnResult` gains `ransom_offers_accepted: Vec` and `ransom_offers_expired: Vec`. ✓ Added at `combat_event.rs:189-198` with `#[serde(default)]` for save migration. - [x] `mc-turn::process_ransom_expiry` pushes `UnitRansomExpiredEvent` in addition to the existing `UnitCapturedEvent` for the conversion. ✓ `processor.rs:2244-2253` adds the expired event after the capture event. - [x] `accept_ransom_offer` / `refuse_ransom_offer` push the corresponding event into `pending_capture_events.ransom_offers_accepted` / `_expired` so the next `step()` drains them onto TurnResult. ✓ `api-gdext/src/lib.rs:3791-3804` (accept) and `:3894-3905` (refuse). `PendingCaptureEvents.drain_into` updated at `mc-turn/src/game_state.rs:407-414` to drain both new vecs onto TurnResult. -- [-] ◐ api-gdext bridge surfaces both as `Array[Dictionary]` on the `step()` result. Bridge writes through `pending_capture_events` and they arrive on `TurnResult.ransom_offers_accepted` / `_expired` already; the `step()` Dictionary projection of those Vecs into `Array[Dictionary]` shape is the remaining piece (mirror of `units_captured` dict shape). +- [x] api-gdext bridge surfaces both as `Array[Dictionary]` on the `step()` result. ✓ `api-gdext/src/lib.rs:4317-4344` projects `result.ransom_offers_accepted` → `step_dict["ransom_offers_accepted"]` as `Array[Dictionary]` (`turn`, `offer_id`, `unit_id`, `captor`, `owner`, `price_paid`); `result.ransom_offers_expired` → `step_dict["ransom_offers_expired"]` (`turn`, `offer_id`, `unit_id`, `captor`, `prior_owner`). `cargo check --workspace` clean on apricot. - [ ] godot-engine chronicle subscriber reads from these new arrays directly; cross-reference workaround removed. - [ ] Test in `mc-turn/tests/ransom.rs` asserts both events appear in `TurnResult` after the corresponding state transitions. diff --git a/src/simulator/crates/mc-turn/tests/ransom.rs b/src/simulator/crates/mc-turn/tests/ransom.rs index c35be923..38507291 100644 --- a/src/simulator/crates/mc-turn/tests/ransom.rs +++ b/src/simulator/crates/mc-turn/tests/ransom.rs @@ -100,3 +100,80 @@ fn tick_drains_only_expired_leaves_others() { let remaining: Vec<_> = q.iter().map(|o| o.unit_id).collect(); assert_eq!(remaining, vec![102]); } + +// ── p2-55e: integration tests for typed accept/expired events ──────────────── + +use mc_turn::combat_event::{ + UnitRansomAcceptedEvent, UnitRansomExpiredEvent, +}; +use mc_turn::game_state::PendingCaptureEvents; +use mc_turn::combat_event::TurnResult; + +#[test] +fn pending_capture_events_drain_accept_into_turn_result() { + // p2-55e bullet 4 + bullet 2: pending_capture_events.ransom_offers_accepted + // drains onto TurnResult.ransom_offers_accepted. + let mut pending = PendingCaptureEvents::default(); + pending.ransom_offers_accepted.push(UnitRansomAcceptedEvent { + turn: 12, + offer_id: 7, + unit_id: 99, + captor: 1, + owner: 2, + price_paid: 50, + }); + let mut result = TurnResult::default(); + pending.drain_into(&mut result); + + assert!(pending.ransom_offers_accepted.is_empty(), + "drain_into should empty the pending vec"); + assert_eq!(result.ransom_offers_accepted.len(), 1); + let ev = &result.ransom_offers_accepted[0]; + assert_eq!(ev.offer_id, 7); + assert_eq!(ev.unit_id, 99); + assert_eq!(ev.captor, 1); + assert_eq!(ev.owner, 2); + assert_eq!(ev.price_paid, 50); +} + +#[test] +fn pending_capture_events_drain_expired_into_turn_result() { + // p2-55e bullet 3 + bullet 2: pending_capture_events.ransom_offers_expired + // drains onto TurnResult.ransom_offers_expired. + let mut pending = PendingCaptureEvents::default(); + pending.ransom_offers_expired.push(UnitRansomExpiredEvent { + turn: 20, + offer_id: 11, + unit_id: 42, + captor: 3, + prior_owner: 4, + }); + let mut result = TurnResult::default(); + pending.drain_into(&mut result); + + assert!(pending.ransom_offers_expired.is_empty()); + assert_eq!(result.ransom_offers_expired.len(), 1); + let ev = &result.ransom_offers_expired[0]; + assert_eq!(ev.offer_id, 11); + assert_eq!(ev.unit_id, 42); + assert_eq!(ev.captor, 3); + assert_eq!(ev.prior_owner, 4); +} + +#[test] +fn pending_capture_events_is_empty_considers_new_vecs() { + // p2-55e: is_empty must consider the new accepted/expired vecs. + let mut pending = PendingCaptureEvents::default(); + assert!(pending.is_empty(), "fresh PendingCaptureEvents should be empty"); + + pending.ransom_offers_accepted.push(UnitRansomAcceptedEvent { + turn: 1, offer_id: 1, unit_id: 1, captor: 0, owner: 1, price_paid: 10, + }); + assert!(!pending.is_empty(), "non-empty accepted should report not-empty"); + + let mut pending = PendingCaptureEvents::default(); + pending.ransom_offers_expired.push(UnitRansomExpiredEvent { + turn: 1, offer_id: 1, unit_id: 1, captor: 0, prior_owner: 1, + }); + assert!(!pending.is_empty(), "non-empty expired should report not-empty"); +}