Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions crates/engine/src/game/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,14 +1184,12 @@ fn drain_pending_change_zone_iteration(state: &mut GameState, events: &mut Vec<G
)
.expect("replacement-resumed ChangeZone delivery retains its exact segment");
}
if let Some(terminal_completion) = paused_current.terminal_completion {
logical_zone_change_group
.record_delivery_completion(
paused_current.member.object_id,
terminal_completion,
)
.expect("resumed ChangeZone member records its exact terminal outcome");
}
logical_zone_change_group
.record_delivery_completion(
paused_current.member.object_id,
paused_current.terminal_completion_after_resume(),
)
.expect("resumed ChangeZone member records its exact terminal outcome");
if matches!(
paused_current.count,
crate::types::game_state::PausedZoneChangeDeliveryCount::NeedsCount
Expand Down
9 changes: 5 additions & 4 deletions crates/engine/src/game/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16107,10 +16107,11 @@ mod stage2_injector_tests {
// three producers: `:6212/:6289/:9477 => :6228/:6305/:9493`.
// shifts combine with #6958's paid-cast outcome exclusion and
// #6976's conditional-branch exclusions. None creates an
// `OptionalEffect` prompt. Re-pinned against the merged source.
"game/effects/mod.rs:6300".to_string(),
"game/effects/mod.rs:6377".to_string(),
"game/effects/mod.rs:9572".to_string(),
// `OptionalEffect` prompt. #7268 removes two lines above all three,
// preserving the same producers at `:6298/:6375/:9570`.
"game/effects/mod.rs:6298".to_string(),
"game/effects/mod.rs:6375".to_string(),
"game/effects/mod.rs:9570".to_string(),
// UNMOVED across the rebase, and that is itself evidence the SET did not
// move: a census that had gained or lost a producer would not leave this
// entry both byte-identical AND at the same coordinate.
Expand Down
14 changes: 1 addition & 13 deletions crates/engine/src/game/zone_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,19 +676,7 @@ pub(crate) fn zone_move_completion_from_delivery(
member: ObjectIncarnationRef,
delivery_events: &[GameEvent],
) -> ZoneMoveCompletion {
if delivery_events.iter().any(|event| {
matches!(
event,
GameEvent::ZoneChanged { record, .. }
if record
.trigger_source_context()
.is_some_and(|context| context.identity.reference == member)
)
}) {
ZoneMoveCompletion::Moved
} else {
ZoneMoveCompletion::Remained
}
PendingZoneChangeDelivery::completion_from_delivery_events(member, delivery_events)
}

pub(crate) enum ZoneDeliveryResult {
Expand Down
69 changes: 69 additions & 0 deletions crates/engine/src/types/game_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3446,6 +3446,46 @@ impl PendingZoneChangeDelivery {
Ok(())
}

/// CR 400.7: A zone change creates a new object, so the retained delivery
/// event slice — not current object state — identifies whether this exact
/// pre-delivery incarnation moved.
///
/// Returns the terminal completion recorded by the delivery pipeline, or
/// derives it from this paused delivery's exact event slice once its
/// resolution has returned to the owning resume driver.
///
/// A returned prompt is terminal for this delivery. If an older pause path
/// omitted the sidecar classification, its own retained events remain the
/// authoritative witness; do not inspect live object state or global event
/// history, either of which could observe a later incarnation.
pub fn terminal_completion_after_resume(&self) -> ZoneMoveCompletion {
self.terminal_completion.unwrap_or_else(|| {
Self::completion_from_delivery_events(self.member, &self.delivery_events)
})
}

/// Classifies one explicit delivery slice against its pre-delivery
/// incarnation. Callers use this when a completed move does not have a
/// separately captured terminal sidecar.
pub fn completion_from_delivery_events(
member: ObjectIncarnationRef,
delivery_events: &[GameEvent],
) -> ZoneMoveCompletion {
if delivery_events.iter().any(|event| {
matches!(
event,
GameEvent::ZoneChanged { record, .. }
if record
.trigger_source_context()
.is_some_and(|context| context.identity.reference == member)
)
}) {
ZoneMoveCompletion::Moved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
ZoneMoveCompletion::Remained
}
}

pub fn mark_counted(&mut self) {
self.count = PausedZoneChangeDeliveryCount::AlreadyCounted;
}
Expand Down Expand Up @@ -22709,6 +22749,35 @@ mod tests {
);
}

#[test]
fn paused_zone_change_delivery_derives_a_terminal_outcome_after_resume() {
let member = ObjectIncarnationRef::of(ObjectId(71), 4);
let mut delivery = PendingZoneChangeDelivery::new(
member,
ProposedEvent::zone_change(
member.object_id,
Zone::Battlefield,
Zone::Graveyard,
Some(ObjectId(72)),
),
);

assert_eq!(
delivery.terminal_completion_after_resume(),
ZoneMoveCompletion::Remained,
"an answered pause with no original-incarnation event completed without moving"
);

delivery
.record_terminal_completion(ZoneMoveCompletion::Prevented)
.expect("the explicit replacement outcome is recorded once");
assert_eq!(
delivery.terminal_completion_after_resume(),
Comment on lines +22752 to +22779

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Add an end-to-end integration test for resumed completion.

This test constructs PendingZoneChangeDelivery directly. It does not execute replacement pause, retained-event capture, and drain_pending_change_zone_iteration. Add an integration test that validates Moved, Prevented, and Remained through the real resume path.

As per path instructions, “Add or extend focused engine tests in crates/engine/tests/integration/ with the existing integration module registration.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/types/game_state.rs` around lines 22748 - 22775, Extend the
focused integration tests under the existing integration module registration to
exercise the real replacement-pause resume flow through
drain_pending_change_zone_iteration, rather than constructing
PendingZoneChangeDelivery directly. Cover all three resumed outcomes—Moved,
Prevented, and Remained—while preserving the existing terminal outcome
assertions and using the actual retained-event and replacement-pause path.

Source: Path instructions

ZoneMoveCompletion::Prevented,
"an explicit replacement outcome remains authoritative over slice inference"
);
}

#[test]
fn persisted_batched_zone_change_pairs_migrate_in_raw_and_trusted_envelopes() {
let mut state = GameState::new_two_player(42);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,18 @@ fn rhythm_of_the_wild_grants_riot_on_library_to_battlefield_put() {
resolve_ability_chain(runner.state_mut(), &ability, &mut events, 0).unwrap();

assert_riot_replacement_choice(&runner);
assert!(
runner.state().active_change_zone_frame().is_some(),
"the real ChangeZone delivery must park its resume frame at the replacement choice"
);

runner
.act(GameAction::ChooseReplacement { index: 0 })
.expect("choose Riot counter");
assert!(
runner.state().active_change_zone_frame().is_none(),
"answering the replacement choice must drain the parked ChangeZone iteration"
);
assert_eq!(
runner.state().objects[&wurm].zone,
Zone::Battlefield,
Expand Down
Loading