From a789202d99cf3bc5458ff4f13d849c84b2d7628d Mon Sep 17 00:00:00 2001 From: Nishad Date: Thu, 13 Aug 2026 00:28:49 -0700 Subject: [PATCH 01/11] fix(engine): route reflexive triggers through the stack Reflexive ("when you do") triggers from mana-ability costs and resolutions now materialize as real deferred triggers released through the stack at their owner boundary (payment, cast announcement, or resolution settlement) with CR 603.3b APNAP batching, instead of a completed mana frame self-releasing its own batch. - A completed mana micro-frame queues its reflexive + observer trigger contexts in `deferred_triggers` (durable, serde round-tripped state) and returns to the owner prompt; only the owner boundary releases the batch onto the stack. - Nested parent mana topologies keep per-frame cost-event ledgers disjoint; a suspended parent's retained prefix joins the batch via parent-snapshot suffix augmentation. - Targetless/nonmodal `ManaAdded` observers are classified as triggered mana abilities and resolve inline inside the frame; optional and opponent-may bodies pause and resume the frame on readiness. - Delayed one-shot and whenever-event triggers matching TapsForMana join the frame batch under the same ordering. - `build_resolved_from_def` replaces four inline ResolvedAbility literals in vote tally resolution. - Eliminated players' queued contexts are cleaned up; the client envelope never carries the sidecar or construction-recipient state. Known gap, documented in the tests rather than encoded as a contract: a rider-free unless-payment has no settled-Priority convergence yet, so its queued batch is released only by the next owner boundary. Co-Authored-By: Claude Fable 5 --- crates/engine/src/game/ability_rw.rs | 13 + crates/engine/src/game/ability_scan.rs | 19 + crates/engine/src/game/ability_utils.rs | 132 +- crates/engine/src/game/casting.rs | 22 +- crates/engine/src/game/derived_views.rs | 135 + .../src/game/effects/additional_phase.rs | 1 + crates/engine/src/game/effects/double.rs | 1 + crates/engine/src/game/effects/extra_turn.rs | 1 + .../grant_extra_loyalty_activations.rs | 1 + crates/engine/src/game/effects/mana.rs | 10 +- crates/engine/src/game/effects/mod.rs | 460 ++- .../engine/src/game/effects/player_counter.rs | 2 + .../src/game/effects/reverse_turn_order.rs | 1 + .../engine/src/game/effects/skip_next_step.rs | 1 + .../engine/src/game/effects/skip_next_turn.rs | 1 + crates/engine/src/game/effects/vote.rs | 262 +- crates/engine/src/game/elimination.rs | 268 ++ crates/engine/src/game/engine.rs | 190 +- crates/engine/src/game/engine_modes.rs | 12 +- .../engine/src/game/engine_payment_choices.rs | 116 +- crates/engine/src/game/engine_priority.rs | 130 +- crates/engine/src/game/engine_stack.rs | 15 +- crates/engine/src/game/mana_abilities.rs | 856 +++- crates/engine/src/game/resolution_prompt.rs | 35 + crates/engine/src/game/stack.rs | 471 ++- crates/engine/src/game/triggers.rs | 2718 ++++++++++++- crates/engine/src/game/visibility.rs | 161 + .../src/parser/oracle_effect/sequence.rs | 34 +- .../engine/src/parser/oracle_nom/condition.rs | 28 +- .../engine/src/parser/oracle_replacement.rs | 80 +- crates/engine/src/types/ability.rs | 19 +- crates/engine/src/types/game_state.rs | 216 +- .../tests/fixtures/integration_cards.json.gz | Bin 1737662 -> 1737677 bytes .../ancient_brass_dragon_roll_d20.rs | 7 + .../tests/integration/cost_zone_pipeline.rs | 3430 ++++++++++++++++- .../cr733_resolved_trigger_collection.rs | 61 +- .../reflexive_body_token_referent.rs | 13 + .../the_chain_veil_loyalty_grants.rs | 1 + 38 files changed, 9146 insertions(+), 777 deletions(-) diff --git a/crates/engine/src/game/ability_rw.rs b/crates/engine/src/game/ability_rw.rs index a5578a07be..4edd3c30cd 100644 --- a/crates/engine/src/game/ability_rw.rs +++ b/crates/engine/src/game/ability_rw.rs @@ -3754,6 +3754,7 @@ fn walk_ability( copy_count_status: _, forward_result: _, distribution: _, + distribute: _, // announcement unit, no state read/write axis chosen_x: _, cost_paid_object: _, noted_mana_payment: _, // concrete captured payment snapshot, no read/write effect @@ -6698,6 +6699,18 @@ mod tests { fn ra(effect: Effect) -> ResolvedAbility { ResolvedAbility::new(effect, vec![], ObjectId(1), PlayerId(0)) } + + #[test] + fn unassigned_distribution_unit_is_rw_inert() { + let base = ra(Effect::NoOp); + let mut divided = base.clone(); + divided.distribute = Some(crate::types::game_state::DistributionUnit::Damage); + + assert_eq!( + format!("{:?}", ability_rw_profile(&base)), + format!("{:?}", ability_rw_profile(÷d)) + ); + } fn cond(mut a: ResolvedAbility, c: AbilityCondition) -> ResolvedAbility { a.condition = Some(c); a diff --git a/crates/engine/src/game/ability_scan.rs b/crates/engine/src/game/ability_scan.rs index dec21a96a1..bdd6c510e0 100644 --- a/crates/engine/src/game/ability_scan.rs +++ b/crates/engine/src/game/ability_scan.rs @@ -251,6 +251,7 @@ fn resolved_ability_axes(a: &ResolvedAbility, mode: ScanMode) -> Axes { copy_count_status: _, // status tag forward_result: _, // bool distribution: _, // concrete pre-assigned (TargetRef, u32) portions + distribute: _, // announcement unit tag/string, no resolution-time dynamic read chosen_x: _, // concrete cast-time X cost_paid_object: _, // concrete captured-object snapshot cost_paid_object_ids: _, // concrete captured-object ids (issue #4948) @@ -6285,6 +6286,24 @@ mod tests { ) } + #[test] + fn unassigned_distribution_unit_adds_no_dynamic_read_axis() { + let base = fixed_drain(); + let mut divided = base.clone(); + divided.distribute = Some(crate::types::game_state::DistributionUnit::Life); + + let base_axes = resolved_ability_axes(&base, ScanMode::Conservative); + let divided_axes = resolved_ability_axes(÷d, ScanMode::Conservative); + assert_eq!( + (base_axes.event, base_axes.sibling, base_axes.projected), + ( + divided_axes.event, + divided_axes.sibling, + divided_axes.projected + ) + ); + } + // ---- P0/P2: the ScanMode split + descending object-growth firewall ---- /// A read-free vanilla token (Presence of Gond's "1/1 green Elf Warrior"): diff --git a/crates/engine/src/game/ability_utils.rs b/crates/engine/src/game/ability_utils.rs index a160856922..2a2060a36d 100644 --- a/crates/engine/src/game/ability_utils.rs +++ b/crates/engine/src/game/ability_utils.rs @@ -142,6 +142,9 @@ pub fn build_resolved_from_def_with_targets( resolved.description = def.description.clone(); resolved.forward_result = def.forward_result; resolved.unless_pay = def.unless_pay.clone(); + // CR 601.2d + CR 603.3d: Preserve the unassigned division unit until the + // ordinary stack-announcement authority assigns concrete portions. + resolved.distribute = def.distribute.clone(); resolved.player_scope = def.player_scope.clone(); // CR 101.4 + CR 800.4: Propagate the turn-order override for `player_scope` // iteration. The iteration driver in `effects/mod.rs` reads this and calls @@ -168,7 +171,7 @@ pub fn build_resolved_from_def_with_targets( // keyword list collapses after the first false gate. resolved.sibling_condition = def.sibling_condition; // CR 700.2b + CR 603.3c: Carry the reflexive modal choice + per-mode abilities - // through so try_begin_reflexive_target_selection can route a gated modal + // through so try_materialize_reflexive_trigger can route a gated modal // trigger (Caesar) to AbilityModeChoice instead of resolving the modes // unconditionally. resolved.modal = def.modal.clone(); @@ -192,7 +195,8 @@ pub fn build_resolved_from_def_with_targets( /// Fields from `sub`: effect, duration, sub_ability, else_ability, /// player_scope, optional, optional_for, optional_targeting, multi_target, /// target_constraints, target_choice_timing, description, repeat_for, -/// min_x_value, forward_result, unless_pay, distribution, target_selection_mode. +/// min_x_value, forward_result, unless_pay, distribution, distribute, +/// target_selection_mode. /// /// Fields preserved from `parent`: controller, source_id, kind, context, /// original_controller, scoped_player, chosen_x, cost_paid_object, @@ -245,6 +249,7 @@ pub(crate) fn apply_instead_swap( overridden.forward_result = sub.forward_result; overridden.unless_pay = sub.unless_pay.clone(); overridden.distribution = sub.distribution.clone(); + overridden.distribute = sub.distribute.clone(); overridden.target_selection_mode = sub.target_selection_mode; overridden.target_chooser = sub.target_chooser.clone(); // CR 608.2b + CR 601.2c: a swapped-in effect with its own declared target @@ -900,6 +905,35 @@ pub fn compute_unavailable_modes( unavailable } +/// CR 700.2a / CR 700.2e: every player the modal's `chooser` admits, in APNAP +/// order. +/// +/// `PlayerFilter::Controller` — every standard modal and the `you choose —` +/// alias — is the controller alone, without consulting +/// `effects::matches_player_scope`. Any other filter (CR 700.2e, "an opponent +/// chooses …") is resolved through that canonical authority over APNAP order. +/// +/// Spell announcement wants only the first admitted player, which is what +/// `casting::resolve_modal_chooser` takes; trigger construction needs the whole +/// set, because more than one non-controller candidate makes the controller's +/// CR 700.2e chooser selection a real choice rather than a derivation. +pub(crate) fn modal_chooser_candidates( + state: &GameState, + modal: &ModalChoice, + controller: PlayerId, + source_id: ObjectId, +) -> Vec { + if modal.chooser == PlayerFilter::Controller { + return vec![controller]; + } + players::apnap_order(state) + .into_iter() + .filter(|&p| { + super::effects::matches_player_scope(state, p, &modal.chooser, controller, source_id) + }) + .collect() +} + /// CR 700.2a-b: Mode indices a modal spell cannot choose — repeat constraints /// plus modes whose targeting requirements have no legal assignment. pub fn spell_modal_unavailable_modes( @@ -7714,6 +7748,75 @@ mod tests { use super::*; use crate::game::zones::create_object; + /// CR 700.2a / CR 700.2e: `modal_chooser_candidates` is the one authority + /// both spell announcement and trigger construction read. + /// + /// Announcement is single-valued and takes `.first()`, so this row proves + /// the head of the returned order is byte-identical to the historic + /// `resolve_modal_chooser` result on both branches, and that the tail — the + /// part only trigger construction consumes — really is the complete + /// admitted set rather than that same single value. A regression that + /// truncates the extraction back to one candidate fails the three-player + /// length assertion while leaving both head assertions green. + #[test] + fn modal_chooser_candidates_are_the_complete_admitted_set_in_apnap_order() { + let mut state = GameState::new(crate::types::format::FormatConfig::free_for_all(), 3, 42); + state.active_player = PlayerId(0); + let source = create_object( + &mut state, + crate::types::identifiers::CardId(1), + PlayerId(0), + "Modal chooser source".to_string(), + Zone::Battlefield, + ); + + let mut modal = ModalChoice { + min_choices: 1, + max_choices: 1, + mode_count: 2, + ..Default::default() + }; + + // CR 700.2a: the controller branch never consults `matches_player_scope` + // and never admits anyone else, in any seat count. + modal.chooser = PlayerFilter::Controller; + assert_eq!( + modal_chooser_candidates(&state, &modal, PlayerId(1), source), + vec![PlayerId(1)], + "the controller branch is the controller alone" + ); + + // CR 700.2e: "an opponent chooses —" with two opponents is a real + // choice, and APNAP order decides which one announcement would take. + modal.chooser = PlayerFilter::Opponent; + let candidates = modal_chooser_candidates(&state, &modal, PlayerId(0), source); + assert_eq!( + candidates, + vec![PlayerId(1), PlayerId(2)], + "every opponent is admitted, in APNAP order" + ); + assert_eq!( + candidates.first().copied(), + Some(PlayerId(1)), + "announcement's single-valued head is the first APNAP opponent" + ); + + // Two-player: the same authority collapses to the unambiguous opponent. + let mut two = GameState::new_two_player(42); + two.active_player = PlayerId(0); + let two_source = create_object( + &mut two, + crate::types::identifiers::CardId(1), + PlayerId(0), + "Modal chooser source".to_string(), + Zone::Battlefield, + ); + assert_eq!( + modal_chooser_candidates(&two, &modal, PlayerId(0), two_source), + vec![PlayerId(1)] + ); + } + /// Matrix rows 5 + 6 — the slot/spec mirror must agree in COUNT **and** /// ORDER, and the context-ref skip must agree between the two sites. /// @@ -9090,6 +9193,7 @@ mod tests { sub.player_scope = Some(crate::types::ability::PlayerFilter::Opponent); sub.optional = true; sub.description = Some("override description".to_string()); + sub.distribute = Some(crate::types::game_state::DistributionUnit::Damage); let swapped = apply_instead_swap(&parent, &sub); @@ -9105,6 +9209,11 @@ mod tests { ); assert!(swapped.optional, "swap must preserve sub.optional"); assert_eq!(swapped.description.as_deref(), Some("override description")); + assert_eq!( + swapped.distribute, + Some(crate::types::game_state::DistributionUnit::Damage), + "swap must preserve the sub-ability's unassigned distribution unit" + ); // Identity / runtime-context fields come from parent. assert_eq!( swapped.controller, @@ -9218,6 +9327,25 @@ mod tests { ); } + #[test] + fn build_resolved_from_def_preserves_unassigned_distribution_unit() { + let mut def = AbilityDefinition::new( + AbilityKind::Database, + Effect::DealDamage { + amount: QuantityExpr::Fixed { value: 4 }, + target: TargetFilter::Any, + damage_source: None, + excess: None, + }, + ); + def.distribute = Some(crate::types::game_state::DistributionUnit::Damage); + + let resolved = build_resolved_from_def(&def, ObjectId(1), PlayerId(0)); + + assert_eq!(resolved.distribute, def.distribute); + assert!(resolved.distribution.is_none()); + } + #[test] fn build_resolved_from_def_preserves_unless_pay_modifier() { let modifier = UnlessPayModifier { diff --git a/crates/engine/src/game/casting.rs b/crates/engine/src/game/casting.rs index 7c3eb781e4..d212f67bfe 100644 --- a/crates/engine/src/game/casting.rs +++ b/crates/engine/src/game/casting.rs @@ -13252,26 +13252,20 @@ fn continue_with_prepared( /// `effects::matches_player_scope` authority filtered over APNAP order. In the /// 2-player engine this is unambiguous. Falls back to the controller if no /// player matches (defensive — cannot happen in a live 2-player game). +/// +/// Spell announcement is single-valued by construction: it opens exactly one +/// `WaitingFor::ModeChoice`, so it takes the first admitted candidate from the +/// shared `ability_utils::modal_chooser_candidates` authority. Trigger +/// construction consumes that same authority's full set. fn resolve_modal_chooser( state: &GameState, modal: &crate::types::ability::ModalChoice, controller: PlayerId, source_id: ObjectId, ) -> PlayerId { - if modal.chooser == crate::types::ability::PlayerFilter::Controller { - return controller; - } - crate::game::players::apnap_order(state) - .into_iter() - .find(|&p| { - crate::game::effects::matches_player_scope( - state, - p, - &modal.chooser, - controller, - source_id, - ) - }) + super::ability_utils::modal_chooser_candidates(state, modal, controller, source_id) + .first() + .copied() .unwrap_or(controller) } diff --git a/crates/engine/src/game/derived_views.rs b/crates/engine/src/game/derived_views.rs index a7b294e186..3c5167e6ed 100644 --- a/crates/engine/src/game/derived_views.rs +++ b/crates/engine/src/game/derived_views.rs @@ -819,6 +819,11 @@ fn client_state_wire_value( root.remove("stack_trigger_firings"); root.remove("resolving_trigger_firing"); root.remove("resolved_rules_journal"); + // CR 605.4a + CR 117.3c: Defense in depth for direct `ClientGameStateRef` + // callers that did not first run `visibility::filter_state_for_viewer`. + // Both are trusted persistence authorities, never client schema. + root.remove("pending_triggered_mana_resume"); + root.remove("pending_trigger_construction_priority_recipient"); redact_private_trigger_firing(&mut value); @@ -6155,4 +6160,134 @@ mod tests { loyalty, the battlefield set and the `∞` store — zero new information" ); } + + /// CR 605.4a + CR 117.3c (plan Step 6, boundary 2): `client_state_wire_value` + /// is the defence-in-depth boundary for direct `ClientGameStateRef::wrap` + /// callers that never ran `visibility::filter_state_for_viewer`. + /// + /// All four projections — direct `wrap` for the prompt owner and for an + /// opponent, and `wrap_filtered` over both filtered states — must omit both + /// root keys and every private sentinel, while the public prompt survives. + /// The structural half fails if either field is renamed without updating + /// `client_state_wire_value`: the trusted root must contain exactly the + /// snake-case key, the client root must not. + #[test] + fn triggered_mana_sidecar_and_construction_recipient_never_reach_the_client_envelope() { + use crate::types::ability::QuantityExpr; + use crate::types::game_state::{ + ManaTriggerFixedPointResume, TriggeredManaResume, TriggeredManaStage, + }; + use crate::types::resolved_commands::{RulesExecutionNodeRef, SettlementNodeOrdinal}; + + const MARKER: &str = "WIRE-PRIVATE-ORACLE-SENTINEL"; + + let mut state = GameState::new(FormatConfig::standard(), 3, 42); + let hidden = create_object( + &mut state, + CardId(70_601), + PlayerId(0), + "Hidden Wire Source".to_string(), + Zone::Battlefield, + ); + let mut pending = PendingTrigger::ordinary( + hidden, + PlayerId(0), + None, + Box::new(ResolvedAbility::new( + Effect::Draw { + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Controller, + }, + Vec::new(), + hidden, + PlayerId(0), + )), + 1, + ); + pending.description = Some(MARKER.to_string()); + state.pending_triggered_mana_resume = Some(Box::new(TriggeredManaResume { + current: Box::new(PendingTriggerContext::single(pending)), + current_override: None, + rules_execution_node: RulesExecutionNodeRef::TriggeredMana(SettlementNodeOrdinal(5)), + accepted_tail: Vec::new(), + collected_batches: Vec::new(), + outer_resume: ManaTriggerFixedPointResume::Parent, + stage: TriggeredManaStage::ChoosingModes, + })); + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + state.waiting_for = WaitingFor::AbilityModeChoice { + player: PlayerId(2), + modal: ModalChoice { + min_choices: 1, + max_choices: 1, + mode_count: 2, + ..Default::default() + }, + source_id: hidden, + mode_abilities: Vec::new(), + is_activated: false, + ability_index: None, + ability_cost: None, + unavailable_modes: Vec::new(), + }; + + let trusted = serde_json::to_value(&state).expect("serialize trusted state"); + let trusted_root = trusted.as_object().expect("trusted root object"); + assert!( + trusted_root.contains_key("pending_triggered_mana_resume") + && trusted_root.contains_key("pending_trigger_construction_priority_recipient"), + "test precondition: trusted persistence keeps both authorities under their exact \ + snake-case keys" + ); + + let owner_view = crate::game::visibility::filter_state_for_viewer(&state, PlayerId(2)); + let opponent_view = crate::game::visibility::filter_state_for_viewer(&state, PlayerId(1)); + let projections = [ + serde_json::to_value(ClientGameStateRef::wrap(&state, Some(PlayerId(2)))) + .expect("direct owner wrap serializes"), + serde_json::to_value(ClientGameStateRef::wrap(&state, Some(PlayerId(1)))) + .expect("direct opponent wrap serializes"), + serde_json::to_value(ClientGameStateRef::wrap_filtered( + &state, + &owner_view, + Some(PlayerId(2)), + )) + .expect("filtered owner wrap serializes"), + serde_json::to_value(ClientGameStateRef::wrap_filtered( + &state, + &opponent_view, + Some(PlayerId(1)), + )) + .expect("filtered opponent wrap serializes"), + ]; + + for (index, projection) in projections.iter().enumerate() { + let client_state = &projection["state"]; + assert!( + client_state.get("pending_triggered_mana_resume").is_none(), + "projection {index} leaked the triggered-mana continuation key" + ); + assert!( + client_state + .get("pending_trigger_construction_priority_recipient") + .is_none(), + "projection {index} leaked the construction recipient key" + ); + let text = serde_json::to_string(projection).expect("projection serializes"); + assert!( + !text.contains(MARKER), + "projection {index} leaked the private sidecar payload" + ); + assert!( + client_state["waiting_for"] != serde_json::Value::Null, + "projection {index} must retain the public prompt" + ); + } + + assert!( + state.pending_triggered_mana_resume.is_some() + && state.pending_trigger_construction_priority_recipient == Some(PlayerId(1)), + "projection must not alter the authoritative carriers" + ); + } } diff --git a/crates/engine/src/game/effects/additional_phase.rs b/crates/engine/src/game/effects/additional_phase.rs index db6c404301..f922d05cd8 100644 --- a/crates/engine/src/game/effects/additional_phase.rs +++ b/crates/engine/src/game/effects/additional_phase.rs @@ -306,6 +306,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/double.rs b/crates/engine/src/game/effects/double.rs index 149061ef06..0bd1fa5df4 100644 --- a/crates/engine/src/game/effects/double.rs +++ b/crates/engine/src/game/effects/double.rs @@ -366,6 +366,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/extra_turn.rs b/crates/engine/src/game/effects/extra_turn.rs index be995ef991..a05e44b11d 100644 --- a/crates/engine/src/game/effects/extra_turn.rs +++ b/crates/engine/src/game/effects/extra_turn.rs @@ -111,6 +111,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs b/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs index 1dc37343fd..fd18c89ea2 100644 --- a/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs +++ b/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs @@ -129,6 +129,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/mana.rs b/crates/engine/src/game/effects/mana.rs index 91840a2972..2a08ed57e0 100644 --- a/crates/engine/src/game/effects/mana.rs +++ b/crates/engine/src/game/effects/mana.rs @@ -188,10 +188,12 @@ pub fn resolve( // "first player target" quantity resolution cannot read the recipient. let count_ability = count_scoped_ability(state, ability, mana_role.as_ref()); let count_ability = &count_ability; - let is_triggered_mana_inline = crate::game::mana_abilities::is_triggered_mana_ability( - ability, - state.current_trigger_event.as_ref(), - ); + // CR 605.4a: read back the acceptance decision for the occurrence that is + // actually executing rather than re-answering CR 605.1b from a clone the + // resolver may already have bound a context referent onto. With no accepted + // occurrence live this is byte-for-byte the baseline raw classifier call. + let is_triggered_mana_inline = + crate::game::mana_abilities::is_resolving_triggered_mana(state, ability); let mana_choice = (!is_triggered_mana_inline) .then(|| { crate::game::mana_abilities::mana_choice_prompt( diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 35a097a7ff..4fb0e7f3c0 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -2180,10 +2180,79 @@ fn try_begin_deferred_else_branch_target_selection( Ok(false) } -/// CR 603.12: Begin reflexive target selection for a `WhenYouDo` / -/// `QuantityCheck` ability whose targets were deferred to resolution time. -/// Returns `true` when `WaitingFor::TriggerTargetSelection` (or inline random -/// resolution) was entered. +/// CR 603.12: Consume the `WhenYouDo` creation gate across a whole reflexive +/// BODY — the root plus every `sub_ability` / `else_ability` clause the created +/// trigger carries with it. +/// +/// The parser stamps `WhenYouDo` on EVERY clause of a reflexive body, not only +/// its first sentence: it is a membership marker for "this instruction belongs +/// to the reflexive ability", which is why Ratonhnhaké꞉ton's +/// `ChangeZone{forward_result} -> Attach{LastCreated}` body carries the +/// condition on both links. CR 603.12 creates ONE triggered ability from that +/// whole body, so consuming only the root would leave each later sentence as a +/// live creation gate: resolving the stack object would materialize a SECOND +/// trigger for its own tail, which both violates CR 603.12a occurrence +/// semantics and severs the intra-body `forward_result` linkage the tail +/// depends on (the Equipment returns but attaches to nothing). +/// +/// Every OTHER chain condition — `EffectOutcome`, `QuantityCheck`, intervening +/// `if` gates — is left intact and is still re-checked at resolution (CR 603.4). +fn consume_reflexive_creation_gate(ability: &mut ResolvedAbility) { + if ability.condition == Some(AbilityCondition::WhenYouDo) { + ability.condition = None; + } + if let Some(sub) = ability.sub_ability.as_deref_mut() { + consume_reflexive_creation_gate(sub); + } + if let Some(alt) = ability.else_ability.as_deref_mut() { + consume_reflexive_creation_gate(alt); + } +} + +/// CR 603.12 + CR 603.3: Build one complete synthetic reflexive trigger from an +/// already context-bound runtime chain. The `WhenYouDo` creation gate is +/// consumed across the whole body on the clone so resolving the resulting stack +/// object cannot create itself again; every other chain condition remains +/// intact. +fn build_reflexive_pending_trigger( + state: &mut GameState, + reflexive: &ResolvedAbility, + parent: Option<&ResolvedAbility>, +) -> crate::game::triggers::PendingTrigger { + let mut ability = reflexive.clone(); + debug_assert_eq!(ability.condition, Some(AbilityCondition::WhenYouDo)); + consume_reflexive_creation_gate(&mut ability); + + let source_id = parent.map_or(ability.source_id, |parent| parent.source_id); + let controller = parent.map_or(ability.controller, |parent| parent.controller); + let description = ability + .description + .clone() + .or_else(|| parent.and_then(|parent| parent.description.clone())); + let timestamp = u32::try_from(state.next_timestamp()).unwrap_or(u32::MAX); + + crate::game::triggers::PendingTrigger { + source_id, + controller, + condition: None, + target_constraints: ability.target_constraints.clone(), + distribute: ability.distribute.clone(), + trigger_event: state.current_trigger_event.clone(), + modal: ability.modal.clone(), + mode_abilities: ability.mode_abilities.clone(), + description, + may_trigger_origin: ability.may_trigger_origin.clone(), + subject_match_count: freeze_reflexive_event_count(state, controller, source_id), + die_result: state.die_result_this_resolution, + provenance: None, + ability: Box::new(ability), + timestamp, + } +} + +/// CR 603.12: Materialize a `WhenYouDo` reflexive trigger, or preserve the +/// existing target-selection behavior for a `QuantityCheck` resolution gate. +/// Returns `true` when the reflexive was queued/pushed or a target choice began. /// /// The whole body runs inside `with_reflexive_resolution_scope` so every /// `EventContextAmount` read reached from here — via `build_target_slots`, @@ -2193,7 +2262,7 @@ fn try_begin_deferred_else_branch_target_selection( /// this scope a paused enclosing trigger (whose event is restored on a /// `PendingContinuation` resume) would leak its own "that many" into the /// reflexive ability's target-slot count. -fn try_begin_reflexive_target_selection( +fn try_materialize_reflexive_trigger( state: &mut GameState, reflexive: &ResolvedAbility, parent: Option<&ResolvedAbility>, @@ -2202,7 +2271,7 @@ fn try_begin_reflexive_target_selection( depth: u32, ) -> Result { crate::game::quantity::with_reflexive_resolution_scope(|| { - try_begin_reflexive_target_selection_inner( + try_materialize_reflexive_trigger_inner( state, reflexive, parent, @@ -2213,7 +2282,7 @@ fn try_begin_reflexive_target_selection( }) } -fn try_begin_reflexive_target_selection_inner( +fn try_materialize_reflexive_trigger_inner( state: &mut GameState, reflexive: &ResolvedAbility, parent: Option<&ResolvedAbility>, @@ -2221,7 +2290,8 @@ fn try_begin_reflexive_target_selection_inner( events: &mut Vec, depth: u32, ) -> Result { - if !reflexive.targets.is_empty() { + let creates_reflexive_trigger = reflexive.condition == Some(AbilityCondition::WhenYouDo); + if !creates_reflexive_trigger && !reflexive.targets.is_empty() { return Ok(false); } @@ -2233,6 +2303,43 @@ fn try_begin_reflexive_target_selection_inner( let reflexive_context_owned; let reflexive = if let Some(parent) = parent { let mut owned = reflexive.clone(); + // CR 608.2c + CR 603.12: an INHERITED referent ("…, exile that card") + // carries no declared target slot of its own — it reads the parent's + // bound target through `TargetFilter::ParentTarget`. The generic + // sub-chain descent below this function binds that referent with + // `should_propagate_parent_targets` immediately before it resolves the + // sub, and at HEAD every inherited-referent reflexive reached the chain + // that way because the slot-less shape returned `Ok(false)` here. + // + // A materialized reflexive resolves LATER, from its own stack object, + // where no parent frame exists — so the referent must be bound NOW, on + // the clone the pending trigger carries, using the SAME authority the + // descent uses. Without this, Superior Spider-Man's "exile that card" + // and the `BecomeCopy` post-replacement rider both resolve against an + // empty target list and silently exile nothing. + // + // Gated three ways, and each gate is load-bearing: + // * `creates_reflexive_trigger` — the `QuantityCheck` gate still + // returns `Ok(false)` for slot-less shapes and reaches the descent's + // own propagation, so binding here would double-bind it. + // * `ability_refs_parent_target` — bind ONLY a rider that actually + // reads the referent. The descent propagates to every slot-less sub + // because it resolves that sub immediately, inside the parent's own + // frame, where a stray `targets` entry is inert for an effect that + // never reads it. A materialized trigger CARRIES its targets onto + // the stack, where they are no longer inert: The Fourteenth Doctor's + // "it gains haste" rider (a `GenericEffect` whose static is + // `affected: SelfRef`) would sail to the stack holding the copy + // SOURCE in the graveyard as a target. The declined-branch + // dispatcher above ANDs the same predicate for the same reason. + // * `should_propagate_parent_targets` — the shared authority, so the + // `ExiledBySource` / resolution-timing carve-outs stay in one place. + if creates_reflexive_trigger + && ability_refs_parent_target(&owned) + && should_propagate_parent_targets(parent, &owned) + { + owned.targets = parent.targets.clone(); + } apply_parent_chain_context(&mut owned, parent, effect_context_object, state); reflexive_context_owned = owned; &reflexive_context_owned @@ -2248,35 +2355,11 @@ fn try_begin_reflexive_target_selection_inner( // pending trigger carrying the modal + per-mode abilities, then defer to the // shared modal-trigger router, which prompts `WaitingFor::AbilityModeChoice` // and only then collects each chosen mode's targets. - if reflexive.modal.is_some() && !reflexive.mode_abilities.is_empty() { - let reflexive_clone = reflexive.clone(); - let trigger_description = reflexive_clone - .description - .clone() - .or_else(|| parent.and_then(|p| p.description.clone())); - let source_id = parent.map(|p| p.source_id).unwrap_or(reflexive.source_id); - let controller = parent.map(|p| p.controller).unwrap_or(reflexive.controller); - - let pending = crate::game::triggers::PendingTrigger { - source_id, - controller, - condition: None, - ability: Box::new(reflexive_clone), - timestamp: state.turn_number, - target_constraints: reflexive.target_constraints.clone(), - distribute: None, - trigger_event: state.current_trigger_event.clone(), - modal: reflexive.modal.clone(), - mode_abilities: reflexive.mode_abilities.clone(), - description: trigger_description, - may_trigger_origin: None, - // CR 603.12 + CR 601.2c: freeze the live event count (e.g. number - // sacrificed) so an "up to that many target ..." bound survives - // into the later fresh-`apply()` target-assign. - subject_match_count: freeze_reflexive_event_count(state, controller, source_id), - die_result: state.die_result_this_resolution, - provenance: None, - }; + if creates_reflexive_trigger + && reflexive.modal.is_some() + && !reflexive.mode_abilities.is_empty() + { + let pending = build_reflexive_pending_trigger(state, reflexive, parent); let trigger_events = crate::game::triggers::take_pending_trigger_event_batch(state, &pending); let pending_for_state = pending.clone(); @@ -2304,9 +2387,21 @@ fn try_begin_reflexive_target_selection_inner( } } - let target_slots = crate::game::ability_utils::build_target_slots(state, reflexive) - .map_err(|e| EffectError::InvalidParam(e.to_string()))?; + let target_slots = match crate::game::ability_utils::build_target_slots(state, reflexive) { + Ok(slots) => slots, + Err(_) if creates_reflexive_trigger => { + let pending = build_reflexive_pending_trigger(state, reflexive, parent); + crate::game::triggers::defer_pending_trigger(state, pending); + return Ok(true); + } + Err(error) => return Err(EffectError::InvalidParam(error.to_string())), + }; if target_slots.is_empty() { + if creates_reflexive_trigger { + let pending = build_reflexive_pending_trigger(state, reflexive, parent); + crate::game::triggers::defer_pending_trigger(state, pending); + return Ok(true); + } return Ok(false); } @@ -2314,6 +2409,11 @@ fn try_begin_reflexive_target_selection_inner( reflexive.target_selection_mode, crate::types::ability::TargetSelectionMode::Random ) { + if creates_reflexive_trigger { + let pending = build_reflexive_pending_trigger(state, reflexive, parent); + crate::game::triggers::defer_pending_trigger(state, pending); + return Ok(true); + } // CR 115.1d + CR 603.12: Random-mode reflexive triggers still choose // the targets for the reflexive triggered ability; the seeded RNG // supplies that choice without entering an interactive prompt. @@ -2330,6 +2430,29 @@ fn try_begin_reflexive_target_selection_inner( return Ok(true); } + if creates_reflexive_trigger { + let pending = build_reflexive_pending_trigger(state, reflexive, parent); + let trigger_events = + crate::game::triggers::take_pending_trigger_event_batch(state, &pending); + let pending_for_state = pending.clone(); + let entry_id = crate::game::triggers::push_pending_trigger_to_stack_with_event_batch( + state, + pending, + trigger_events, + events, + ); + state.pending_trigger = Some(Box::new(pending_for_state)); + state.pending_trigger_firing = Some(crate::types::identifiers::TriggerFiring::Ordinary); + state.pending_trigger_entry = Some(entry_id); + if let Some(waiting_for) = + crate::game::engine::begin_pending_trigger_target_selection(state) + .map_err(|error| EffectError::InvalidParam(error.to_string()))? + { + state.waiting_for = waiting_for; + } + return Ok(true); + } + let selection = crate::game::ability_utils::begin_target_selection_for_ability( state, reflexive, @@ -9390,7 +9513,7 @@ fn resolve_chain_body( if matches!( condition, AbilityCondition::WhenYouDo | AbilityCondition::QuantityCheck { .. } - ) && try_begin_reflexive_target_selection(state, ability, None, None, events, depth)? + ) && try_materialize_reflexive_trigger(state, ability, None, None, events, depth)? { return Ok(()); } @@ -10984,7 +11107,7 @@ fn resolve_chain_body( if matches!( condition, AbilityCondition::WhenYouDo | AbilityCondition::QuantityCheck { .. } - ) && try_begin_reflexive_target_selection( + ) && try_materialize_reflexive_trigger( state, sub, Some(ability), @@ -12919,6 +13042,207 @@ mod tests { use crate::types::triggers::TriggerMode; use crate::types::zones::Zone; + fn reflexive_test_creature( + state: &mut GameState, + controller: PlayerId, + name: &str, + ) -> ObjectId { + let id = create_object( + state, + CardId(state.next_object_id), + controller, + name.to_string(), + Zone::Battlefield, + ); + let object = state.objects.get_mut(&id).unwrap(); + object.card_types.core_types.push(CoreType::Creature); + object.base_power = Some(2); + object.base_toughness = Some(2); + object.power = Some(2); + object.toughness = Some(2); + id + } + + fn reflexive_counter_ability(source_id: ObjectId) -> ResolvedAbility { + ResolvedAbility::new( + Effect::PutCounter { + counter_type: CounterType::Plus1Plus1, + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Typed(TypedFilter::creature()), + }, + Vec::new(), + source_id, + PlayerId(0), + ) + .condition(AbilityCondition::WhenYouDo) + } + + #[test] + fn targetless_reflexive_is_deferred_and_root_gate_is_consumed() { + let mut state = GameState::new_two_player(42); + state.waiting_for = WaitingFor::Priority { + player: PlayerId(0), + }; + let mut reflexive = ResolvedAbility::new( + Effect::LoseLife { + amount: QuantityExpr::Fixed { value: 2 }, + target: None, + }, + Vec::new(), + ObjectId(100), + PlayerId(0), + ) + .condition(AbilityCondition::WhenYouDo); + reflexive.player_scope = Some(PlayerFilter::Opponent); + let life_before = state.players[1].life; + let mut events = Vec::new(); + + assert!(try_materialize_reflexive_trigger( + &mut state, + &reflexive, + None, + None, + &mut events, + 0, + ) + .unwrap()); + assert_eq!(state.players[1].life, life_before); + assert!(state.stack.is_empty()); + assert_eq!(state.deferred_triggers.len(), 1); + assert!(state.deferred_triggers[0] + .pending + .ability + .condition + .is_none()); + + assert!( + crate::game::triggers::drain_deferred_trigger_queue(&mut state, &mut events).is_none() + ); + assert_eq!(state.stack.len(), 1); + assert_eq!(state.players[1].life, life_before); + + let mut safety = 4; + while !state.stack.is_empty() && safety > 0 { + crate::game::engine::apply_as_current(&mut state, GameAction::PassPriority) + .expect("resolve reflexive trigger through priority"); + safety -= 1; + } + assert_eq!(state.players[1].life, life_before - 2); + } + + #[test] + fn random_reflexive_materializes_on_stack_before_effect() { + let mut state = GameState::new_two_player(42); + state.waiting_for = WaitingFor::Priority { + player: PlayerId(0), + }; + let a = reflexive_test_creature(&mut state, PlayerId(0), "A"); + let b = reflexive_test_creature(&mut state, PlayerId(0), "B"); + let mut reflexive = reflexive_counter_ability(ObjectId(100)); + reflexive.target_selection_mode = TargetSelectionMode::Random; + let mut events = Vec::new(); + + try_materialize_reflexive_trigger(&mut state, &reflexive, None, None, &mut events, 0) + .unwrap(); + crate::game::triggers::drain_deferred_trigger_queue(&mut state, &mut events); + + assert_eq!(state.stack.len(), 1); + assert!(state.objects[&a].counters.is_empty()); + assert!(state.objects[&b].counters.is_empty()); + let StackEntryKind::TriggeredAbility { ability, .. } = &state.stack[0].kind else { + panic!("expected random reflexive trigger on stack"); + }; + assert_eq!(ability.targets.len(), 1); + } + + #[test] + fn reflexive_with_no_legal_required_target_is_dropped_by_shared_dispatch() { + let mut state = GameState::new_two_player(42); + state.waiting_for = WaitingFor::Priority { + player: PlayerId(0), + }; + let reflexive = reflexive_counter_ability(ObjectId(100)); + let mut events = Vec::new(); + + try_materialize_reflexive_trigger(&mut state, &reflexive, None, None, &mut events, 0) + .unwrap(); + crate::game::triggers::drain_deferred_trigger_queue(&mut state, &mut events); + + assert!(state.deferred_triggers.is_empty()); + assert!(state.stack.is_empty()); + } + + #[test] + fn reflexive_target_chooser_differs_from_stack_controller() { + let mut state = GameState::new_two_player(42); + state.waiting_for = WaitingFor::Priority { + player: PlayerId(0), + }; + let target = reflexive_test_creature(&mut state, PlayerId(0), "Only target"); + let mut reflexive = reflexive_counter_ability(ObjectId(100)); + // CR 601.2c + CR 603.3d: `Opponent` is the ONLY non-`ScopedPlayer` chooser + // the parser ever stamps (`oracle_target.rs:3540`, "of an opponent's + // choice"), and it is the shape `resolve_effect_player_ref` resolves + // through the shared authority. A synthetic `SpecificPlayer` chooser is + // unreachable from any card and falls into that resolver's event-context + // catch-all, which returns `None` with no trigger event — the prompt would + // then fall back to the controller and the row would prove nothing about + // the chooser seam. + reflexive.target_chooser = Some(TargetFilter::Opponent); + let mut events = Vec::new(); + + try_materialize_reflexive_trigger(&mut state, &reflexive, None, None, &mut events, 0) + .unwrap(); + assert!(matches!( + state.waiting_for, + WaitingFor::TriggerTargetSelection { + player: PlayerId(1), + trigger_controller: Some(PlayerId(0)), + .. + } + )); + assert!( + crate::game::engine::apply( + &mut state, + PlayerId(0), + GameAction::ChooseTarget { + target: Some(TargetRef::Object(target)), + }, + ) + .is_err(), + "the controller cannot answer another player's target prompt" + ); + crate::game::engine::apply( + &mut state, + PlayerId(1), + GameAction::ChooseTarget { + target: Some(TargetRef::Object(target)), + }, + ) + .expect("the designated chooser may select the target"); + + let StackEntryKind::TriggeredAbility { ability, .. } = &state.stack[0].kind else { + panic!("expected reflexive trigger entry"); + }; + assert_eq!(state.stack[0].controller, PlayerId(0)); + assert_eq!(ability.targets, [TargetRef::Object(target)]); + } + + #[test] + fn reflexive_constructor_preserves_unassigned_distribution_metadata() { + let mut state = GameState::new_two_player(42); + let mut reflexive = reflexive_counter_ability(ObjectId(100)); + reflexive.distribute = Some(crate::types::game_state::DistributionUnit::Counters( + "+1/+1".to_string(), + )); + + let pending = build_reflexive_pending_trigger(&mut state, &reflexive, None); + + assert_eq!(pending.distribute, reflexive.distribute); + assert_eq!(pending.ability.distribute, reflexive.distribute); + assert!(pending.ability.condition.is_none()); + } + // CR 608.2h (#6486): Volcanic Vision — "Return target instant or sorcery card // from your graveyard to your hand. ~ deals damage equal to that card's mana // value to each creature your opponents control. Exile ~." The card is @@ -17303,9 +17627,22 @@ mod tests { /// Issue #418 (Guide of Souls): when the embedded `{E}{E}{E}` cost IS paid, /// the `WhenYouDo` reflexive sub-ability runs and energy is deducted. + /// + /// CR 603.12 + CR 603.3b: the rider is a REFLEXIVE TRIGGERED ABILITY, so + /// "runs" means it is CREATED during this resolution and put on the stack at + /// the next priority point — not applied inline inside the parent's event + /// vector. This row therefore asserts the cost half inline (energy is spent + /// during `resolve_ability_chain`, exactly as before) and the rider half + /// after the trigger actually resolves, with an explicit no-inline-effect + /// assertion in between so the deferral itself is discriminated: reverting + /// the materializer to inline resolution puts the counters on before the + /// drain and fails the middle assertion. #[test] fn when_you_do_runs_when_embedded_energy_cost_paid() { let mut state = GameState::new_two_player(42); + state.waiting_for = WaitingFor::Priority { + player: PlayerId(0), + }; // Controller has exactly enough energy to pay {E}{E}{E}. state.players[0].energy = 3; @@ -17363,15 +17700,40 @@ mod tests { .any(|e| matches!(e, GameEvent::EnergyChanged { delta: -3, .. })), "energy payment must emit EnergyChanged delta -3" ); + + // CR 603.2: the reflexive ability triggered, and "does nothing at this + // time" — it is queued, not applied. + assert_eq!( + state.deferred_triggers.len(), + 1, + "the paid cost must CREATE the reflexive trigger" + ); assert!( - events.iter().any(|e| matches!( - e, - GameEvent::CounterAdded { - counter_type: CounterType::Plus1Plus1, - count: 2, - .. - } - )), + state.objects[&target].counters.is_empty(), + "the reflexive must not put its counters on during the parent resolution" + ); + assert!( + !events + .iter() + .any(|e| matches!(e, GameEvent::CounterAdded { .. })), + "no CounterAdded may be emitted into the parent's event vector" + ); + + // CR 603.3b: it goes on the stack at the next priority point, then resolves. + crate::game::triggers::drain_deferred_trigger_queue(&mut state, &mut events); + assert_eq!(state.stack.len(), 1, "the reflexive is a stack object"); + let mut safety = 4; + while !state.stack.is_empty() && safety > 0 { + crate::game::engine::apply_as_current(&mut state, GameAction::PassPriority) + .expect("resolve the reflexive trigger through priority"); + safety -= 1; + } + assert_eq!( + state.objects[&target] + .counters + .get(&CounterType::Plus1Plus1) + .copied(), + Some(2), "WhenYouDo reflexive sub-ability must run when the embedded cost was paid" ); } diff --git a/crates/engine/src/game/effects/player_counter.rs b/crates/engine/src/game/effects/player_counter.rs index b564934717..9152d6d9c4 100644 --- a/crates/engine/src/game/effects/player_counter.rs +++ b/crates/engine/src/game/effects/player_counter.rs @@ -479,6 +479,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, @@ -676,6 +677,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/reverse_turn_order.rs b/crates/engine/src/game/effects/reverse_turn_order.rs index b851afa6d9..65269c06bc 100644 --- a/crates/engine/src/game/effects/reverse_turn_order.rs +++ b/crates/engine/src/game/effects/reverse_turn_order.rs @@ -84,6 +84,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/skip_next_step.rs b/crates/engine/src/game/effects/skip_next_step.rs index 37bfb8c4ac..37cf3b5804 100644 --- a/crates/engine/src/game/effects/skip_next_step.rs +++ b/crates/engine/src/game/effects/skip_next_step.rs @@ -147,6 +147,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/skip_next_turn.rs b/crates/engine/src/game/effects/skip_next_turn.rs index 0f6d74987a..b0155e6fff 100644 --- a/crates/engine/src/game/effects/skip_next_turn.rs +++ b/crates/engine/src/game/effects/skip_next_turn.rs @@ -125,6 +125,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: crate::types::ability::TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, diff --git a/crates/engine/src/game/effects/vote.rs b/crates/engine/src/game/effects/vote.rs index 78d00004d8..b9a6e9c225 100644 --- a/crates/engine/src/game/effects/vote.rs +++ b/crates/engine/src/game/effects/vote.rs @@ -30,6 +30,7 @@ use crate::types::identifiers::ObjectId; use crate::types::player::PlayerId; use super::resolve_ability_chain; +use crate::game::ability_utils::build_resolved_from_def; /// CR 701.38a + CR 101.4: Initiate a vote. Builds the APNAP voter queue /// starting from `starting_with` (resolved against the ability controller), @@ -348,65 +349,7 @@ pub fn resolve_tally( if per_choice_player_scope.is_some() { // player_scope path — single dispatch, fan-out handled by // resolve_ability_chain's player_scope driver. - let chain = ResolvedAbility { - effect: (*per_choice_effect[idx].effect).clone(), - targets: Vec::new(), - source_id, - source_incarnation: None, - trigger_source: None, - trigger_definition_ref: None, - force_block_attacker: None, - target_incarnations: Vec::new(), - selected_target_incarnations: Vec::new(), - controller, - original_controller: None, - scoped_player: None, - target_chooser: None, - kind: per_choice_effect[idx].kind, - sub_ability: per_choice_effect[idx] - .sub_ability - .as_ref() - .map(|sub| Box::new(resolved_from_def(sub, source_id, controller))), - else_ability: None, - duration: per_choice_effect[idx].duration.clone(), - condition: per_choice_effect[idx].condition.clone(), - context: Default::default(), - optional_targeting: per_choice_effect[idx].optional_targeting, - optional: per_choice_effect[idx].optional, - optional_for: None, - multi_target: None, - target_constraints: Vec::new(), - target_choice_timing: per_choice_effect[idx].target_choice_timing, - description: per_choice_effect[idx].description.clone(), - selected_mode_labels: Vec::new(), - repeat_for: None, - min_x_value: per_choice_effect[idx].min_x_value, - announced_x: per_choice_effect[idx].announced_x.clone(), - cant_be_copied: per_choice_effect[idx].cant_be_copied, - copy_count_status: crate::types::ability::CopyCountStatus::Pending, - forward_result: per_choice_effect[idx].forward_result, - unless_pay: None, - distribution: None, - player_scope: per_choice_player_scope, - starting_with: per_choice_effect[idx].starting_with.clone(), - chosen_x: None, - cost_paid_object: None, - noted_mana_payment: None, - cost_paid_object_ids: Vec::new(), - effect_context_object: None, - amassed_army_object: None, - ability_index: None, - may_trigger_origin: None, - target_selection_mode: per_choice_effect[idx].target_selection_mode, - chosen_players: Vec::new(), - repeat_until: None, - replacement_applied: Default::default(), - sub_link: crate::types::ability::SubAbilityLink::ContinuationStep, - sibling_condition: crate::types::ability::SiblingCondition::Dependent, - modal: None, - mode_abilities: vec![], - parent_target_missing_reason: None, - }; + let chain = build_resolved_from_def(&per_choice_effect[idx], source_id, controller); resolve_ability_chain(state, &chain, events, 1)?; } else if per_choice_effect[idx] .effect @@ -418,65 +361,7 @@ pub fn resolve_tally( // `QuantityRef::VoteCount`, so the effect resolves as ONE aggregate // event whose `resolve_ref` sums the full tally — do NOT repeat it // per ballot, which would multiply the tally by itself. - let chain = ResolvedAbility { - effect: (*per_choice_effect[idx].effect).clone(), - targets: Vec::new(), - source_id, - source_incarnation: None, - trigger_source: None, - trigger_definition_ref: None, - force_block_attacker: None, - target_incarnations: Vec::new(), - selected_target_incarnations: Vec::new(), - controller, - original_controller: None, - scoped_player: None, - target_chooser: None, - kind: per_choice_effect[idx].kind, - sub_ability: per_choice_effect[idx] - .sub_ability - .as_ref() - .map(|sub| Box::new(resolved_from_def(sub, source_id, controller))), - else_ability: None, - duration: per_choice_effect[idx].duration.clone(), - condition: per_choice_effect[idx].condition.clone(), - context: Default::default(), - optional_targeting: per_choice_effect[idx].optional_targeting, - optional: per_choice_effect[idx].optional, - optional_for: None, - multi_target: None, - target_constraints: Vec::new(), - target_choice_timing: per_choice_effect[idx].target_choice_timing, - description: per_choice_effect[idx].description.clone(), - selected_mode_labels: Vec::new(), - repeat_for: None, - min_x_value: per_choice_effect[idx].min_x_value, - announced_x: per_choice_effect[idx].announced_x.clone(), - cant_be_copied: per_choice_effect[idx].cant_be_copied, - copy_count_status: crate::types::ability::CopyCountStatus::Pending, - forward_result: per_choice_effect[idx].forward_result, - unless_pay: None, - distribution: None, - player_scope: None, - starting_with: per_choice_effect[idx].starting_with.clone(), - chosen_x: None, - cost_paid_object: None, - noted_mana_payment: None, - cost_paid_object_ids: Vec::new(), - effect_context_object: None, - amassed_army_object: None, - ability_index: None, - may_trigger_origin: None, - target_selection_mode: per_choice_effect[idx].target_selection_mode, - chosen_players: Vec::new(), - repeat_until: None, - replacement_applied: Default::default(), - sub_link: crate::types::ability::SubAbilityLink::ContinuationStep, - sibling_condition: crate::types::ability::SiblingCondition::Dependent, - modal: None, - mode_abilities: vec![], - parent_target_missing_reason: None, - }; + let chain = build_resolved_from_def(&per_choice_effect[idx], source_id, controller); resolve_ability_chain(state, &chain, events, 1)?; } else { // CR 701.38d + CR 608.2c: Per-ballot iteration. Each ballot that @@ -595,7 +480,7 @@ fn resolve_top_votes_tally( // winner is exiled (not a battlefield rescan). Some(template) => { if let Some(&winner_obj) = candidate_objects.get(winner as usize) { - let mut chain = resolved_from_def(template, source_id, controller); + let mut chain = build_resolved_from_def(template, source_id, controller); chain.targets = vec![TargetRef::Object(winner_obj)]; resolve_ability_chain(state, &chain, events, 1)?; } @@ -603,7 +488,7 @@ fn resolve_top_votes_tally( // Named vote: `per_choice_effect` is populated. None => { if let Some(winning_effect) = per_choice_effect.get(winner as usize) { - let chain = resolved_from_def(winning_effect, source_id, controller); + let chain = build_resolved_from_def(winning_effect, source_id, controller); resolve_ability_chain(state, &chain, events, 1)?; } } @@ -628,7 +513,8 @@ fn resolve_top_votes_tally( // rescan). Some(template) => { if let Some(&winner_obj) = candidate_objects.get(idx) { - let mut chain = resolved_from_def(template, source_id, controller); + let mut chain = + build_resolved_from_def(template, source_id, controller); chain.targets = vec![TargetRef::Object(winner_obj)]; resolve_ability_chain(state, &chain, events, 1)?; } @@ -637,7 +523,7 @@ fn resolve_top_votes_tally( None => { if let Some(winning_effect) = per_choice_effect.get(idx) { let chain = - resolved_from_def(winning_effect, source_id, controller); + build_resolved_from_def(winning_effect, source_id, controller); resolve_ability_chain(state, &chain, events, 1)?; } } @@ -655,81 +541,6 @@ fn resolve_top_votes_tally( Ok(()) } -/// Convert a stored `AbilityDefinition` (typically a sub-effect) into a -/// `ResolvedAbility` carrying the same source/controller as the parent Vote. -fn resolved_from_def( - def: &AbilityDefinition, - source_id: crate::types::identifiers::ObjectId, - controller: PlayerId, -) -> ResolvedAbility { - ResolvedAbility { - effect: (*def.effect).clone(), - targets: Vec::new(), - source_id, - source_incarnation: None, - trigger_source: None, - trigger_definition_ref: None, - force_block_attacker: None, - target_incarnations: Vec::new(), - selected_target_incarnations: Vec::new(), - controller, - original_controller: None, - scoped_player: None, - target_chooser: None, - kind: def.kind, - sub_ability: def - .sub_ability - .as_ref() - .map(|sub| Box::new(resolved_from_def(sub, source_id, controller))), - else_ability: None, - duration: def.duration.clone(), - condition: def.condition.clone(), - context: Default::default(), - optional_targeting: def.optional_targeting, - optional: def.optional, - optional_for: None, - multi_target: None, - target_constraints: Vec::new(), - target_choice_timing: def.target_choice_timing, - description: def.description.clone(), - selected_mode_labels: Vec::new(), - repeat_for: None, - min_x_value: def.min_x_value, - announced_x: def.announced_x.clone(), - cant_be_copied: def.cant_be_copied, - copy_count_status: crate::types::ability::CopyCountStatus::Pending, - forward_result: def.forward_result, - unless_pay: None, - distribution: None, - player_scope: None, - // CR 101.4 + CR 800.4: Carry through the parent def's turn-order - // override so vote sub-effects resolve with consistent iteration - // semantics. None for non-Join-Forces vote chains. - starting_with: def.starting_with.clone(), - chosen_x: None, - cost_paid_object: None, - noted_mana_payment: None, - cost_paid_object_ids: Vec::new(), - effect_context_object: None, - amassed_army_object: None, - ability_index: None, - may_trigger_origin: None, - target_selection_mode: def.target_selection_mode, - chosen_players: Vec::new(), - repeat_until: None, - replacement_applied: Default::default(), - // CR 608.2c: Carry the parent-link kind through to the resolved ability. - sub_link: def.sub_link, - // CR 608.2c: Carry the replication marker through (Dependent for vote sub-effects). - sibling_condition: def.sibling_condition, - // CR 700.2b + CR 603.3c: Carry the reflexive modal choice + per-mode - // abilities through (None for vote sub-effects). - modal: def.modal.clone(), - mode_abilities: def.mode_abilities.clone(), - parent_target_missing_reason: None, - } -} - /// CR 701.38a: Resolve `ControllerRef::You` (and friends) to the concrete /// starting voter PlayerId. Falls back to `controller` if the ref doesn't /// resolve to a non-eliminated player. @@ -820,7 +631,7 @@ fn build_per_ballot_ability( source_id: crate::types::identifiers::ObjectId, controller: PlayerId, ) -> ResolvedAbility { - let mut ability = resolved_from_def(template, source_id, controller); + let mut ability = build_resolved_from_def(template, source_id, controller); ability.scoped_player = Some(voter); ability.original_controller = Some(controller); ability @@ -908,6 +719,47 @@ mod tests { use crate::types::resolution::{FrameKind, ResolutionFrame}; use crate::types::zones::Zone; + #[test] + fn vote_definition_routes_preserve_unassigned_distribution_unit() { + let source_id = ObjectId(1); + let controller = PlayerId(0); + + let mut player_scope = AbilityDefinition::new(AbilityKind::Database, Effect::NoOp) + .player_scope(crate::types::ability::PlayerFilter::VotedFor { choice_index: 0 }); + player_scope.distribute = Some(crate::types::game_state::DistributionUnit::Damage); + let player_scope_runtime = build_resolved_from_def(&player_scope, source_id, controller); + + let mut aggregate = AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Ref { + qty: crate::types::ability::QuantityRef::VoteCount { choice_index: 0 }, + }, + player: TargetFilter::Controller, + }, + ); + aggregate.distribute = Some(crate::types::game_state::DistributionUnit::Life); + let aggregate_runtime = build_resolved_from_def(&aggregate, source_id, controller); + + let mut per_ballot = AbilityDefinition::new(AbilityKind::Database, Effect::NoOp); + per_ballot.distribute = Some(crate::types::game_state::DistributionUnit::Counters( + "charge".to_string(), + )); + let per_ballot_runtime = + build_per_ballot_ability(&per_ballot, PlayerId(1), source_id, controller); + + assert_eq!(player_scope_runtime.distribute, player_scope.distribute); + assert_eq!(aggregate_runtime.distribute, aggregate.distribute); + assert_eq!(per_ballot_runtime.distribute, per_ballot.distribute); + + let ordinary = build_resolved_from_def( + &AbilityDefinition::new(AbilityKind::Database, Effect::NoOp), + source_id, + controller, + ); + assert!(ordinary.distribute.is_none()); + } + /// CR 701.38a + CR 101.4: Initiating a Vote sets `WaitingFor::VoteChoice` /// for the controller, queuing the opponent next, with no extra-vote /// granters present (so each player gets exactly 1 vote). @@ -963,6 +815,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, player_scope: None, starting_with: None, chosen_x: None, @@ -1074,6 +927,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, player_scope: None, starting_with: None, chosen_x: None, @@ -1510,6 +1364,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, player_scope: None, starting_with: None, chosen_x: None, @@ -1678,6 +1533,7 @@ mod tests { forward_result: false, unless_pay: None, distribution: None, + distribute: None, player_scope: None, starting_with: None, chosen_x: None, @@ -2375,7 +2231,7 @@ mod tests { .core_types .push(CoreType::Creature); - let ability = resolved_from_def(&vote_def, source_id, controller); + let ability = build_resolved_from_def(&vote_def, source_id, controller); let mut events = Vec::new(); resolve_ability_chain(&mut state, &ability, &mut events, 0).unwrap(); @@ -2452,7 +2308,7 @@ mod tests { let mut state = GameState::new_two_player(42); let controller = state.players[0].id; let opp = state.players[1].id; - let ability = resolved_from_def(&def, ObjectId(1), controller); + let ability = build_resolved_from_def(&def, ObjectId(1), controller); let mut events = Vec::new(); resolve_ability_chain(&mut state, &ability, &mut events, 0).unwrap(); @@ -2554,7 +2410,7 @@ mod tests { .core_types .push(CoreType::Creature); - let ability = resolved_from_def(&vote_def, source_id, controller); + let ability = build_resolved_from_def(&vote_def, source_id, controller); let mut events = Vec::new(); resolve_ability_chain(&mut state, &ability, &mut events, 0).unwrap(); let voter = match &state.waiting_for { @@ -2632,7 +2488,7 @@ mod tests { let opp_b = make_creature(&mut state, 3, "Bear B"); let opp_c = make_creature(&mut state, 4, "Bear C"); - let ability = resolved_from_def(&vote_def, source_id, controller); + let ability = build_resolved_from_def(&vote_def, source_id, controller); let mut events = Vec::new(); resolve_ability_chain(&mut state, &ability, &mut events, 0).unwrap(); @@ -2754,7 +2610,7 @@ mod tests { } let last_bear = *opp_ids.last().unwrap(); - let ability = resolved_from_def(&vote_def, source_id, controller); + let ability = build_resolved_from_def(&vote_def, source_id, controller); let mut events = Vec::new(); resolve_ability_chain(&mut state, &ability, &mut events, 0).unwrap(); diff --git a/crates/engine/src/game/elimination.rs b/crates/engine/src/game/elimination.rs index 3df9dfa88d..c28771a75d 100644 --- a/crates/engine/src/game/elimination.rs +++ b/crates/engine/src/game/elimination.rs @@ -155,6 +155,13 @@ pub fn eliminate_players_simultaneously( state.pending_trigger = None; state.pending_trigger_entry = None; state.pending_trigger_event_batch.clear(); + // CR 117.3c: The construction priority recipient is scheduling state for + // a batch that no longer exists. Leaving it installed would durably + // serialize a departed player into a terminal `GameOver` snapshot — the + // exact leak the surrounding comment already calls out for the reused + // singleton engine. The terminal arm needs no re-point, because there is + // no later construction to route. + state.pending_trigger_construction_priority_recipient = None; for firing in terminal_firings { crate::game::lifecycle::record_delayed_terminal( firing, @@ -236,6 +243,20 @@ pub fn eliminate_players_simultaneously( state.waiting_for = WaitingFor::Priority { player: next }; } } + + // CR 800.4a: A live trigger-construction batch can carry a priority + // recipient who is not the prompt's controller, so neither cursor- + // clearing site above fires when that recipient alone leaves. Priority + // passes to the next player in turn order who is still in the game + // (`docs/MagicCompRules.txt:6424`), so the carried recipient is + // re-pointed rather than stranded — the same authority and remedy the + // `waiting_for` re-point directly above uses for a dead acting player. + if let Some(recipient) = state.pending_trigger_construction_priority_recipient { + if !players::is_alive(state, recipient) { + state.pending_trigger_construction_priority_recipient = + Some(players::next_player(state, recipient)); + } + } } } @@ -818,6 +839,12 @@ fn do_eliminate( state.pending_trigger_entry = None; state.pending_trigger = None; state.pending_trigger_event_batch.clear(); + // CR 117.3c: The batch this recipient was scheduled for has ceased with + // its tracked entry. Clear it with the cursors — unconditionally, not + // only when the departing player happens to be the recipient — so the + // next construction cannot consume a stale carrier and mis-route its + // terminal priority. + state.pending_trigger_construction_priority_recipient = None; } // CR 800.4a: Abandon any not-yet-resolved cast this player controls. A spell @@ -3425,4 +3452,245 @@ mod tests { "an unrelated control by a living controller survives (non-vacuous)" ); } + + /// Put a real in-construction triggered-ability entry on the stack for + /// `controller`, park the construction cursors on it, and open a live + /// `AbilityModeChoice` prompt for that same player. Returns the entry id. + fn open_live_trigger_construction_prompt( + state: &mut GameState, + controller: PlayerId, + ) -> ObjectId { + let source = create_object( + state, + CardId(state.next_object_id), + controller, + "Construction prompt source".to_string(), + crate::types::zones::Zone::Battlefield, + ); + let trigger = crate::game::triggers::PendingTrigger::ordinary( + source, + controller, + None, + Box::new(ResolvedAbility::new( + Effect::Draw { + count: crate::types::ability::QuantityExpr::Fixed { value: 1 }, + target: crate::types::ability::TargetFilter::Controller, + }, + Vec::new(), + source, + controller, + )), + state.turn_number, + ); + let mut events = Vec::new(); + let entry = crate::game::triggers::push_pending_trigger_to_stack( + state, + trigger.clone(), + &mut events, + ); + state.pending_trigger = Some(Box::new(trigger)); + state.pending_trigger_entry = Some(entry); + state.pending_trigger_firing = Some(crate::types::identifiers::TriggerFiring::Ordinary); + state.waiting_for = WaitingFor::AbilityModeChoice { + player: controller, + modal: crate::types::ability::ModalChoice { + min_choices: 1, + max_choices: 1, + mode_count: 2, + ..Default::default() + }, + source_id: source, + mode_abilities: Vec::new(), + is_activated: false, + ability_index: None, + ability_cost: None, + unavailable_modes: Vec::new(), + }; + entry + } + + fn concede(state: &mut GameState, player_id: PlayerId) { + crate::game::engine::apply( + state, + player_id, + crate::types::actions::GameAction::Concede { player_id }, + ) + .expect("concession is always legal"); + } + + /// CR 800.4a (plan Step 5, elimination site 1): `do_eliminate`'s + /// tracked-entry-gone cleanup clears the construction priority recipient + /// beside the three construction cursors it already clears — and it does so + /// **with the cursors**, not only when the departing player happens to be + /// the carried recipient. Both clones below exercise the same site. + /// + /// Revert discriminator: with only this site's clearing removed, the + /// uncleared `Some(P1)` survives into the game-continues arm, where the + /// re-point branch sees a no-longer-alive P1 and installs `Some(P2)` — so + /// the `== None` assertion reads `Some(P2)` and fails. + #[test] + fn tracked_entry_cleanup_clears_the_construction_priority_recipient() { + // Clone A: the leaver is both the prompt controller and the recipient. + let mut state = setup_three_player(); + let entry = open_live_trigger_construction_prompt(&mut state, PlayerId(1)); + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + assert!( + state.stack.iter().any(|e| e.id == entry), + "positive reach guard: the tracked entry is really on the stack" + ); + + concede(&mut state, PlayerId(1)); + + assert!( + !state.stack.iter().any(|e| e.id == entry), + "the leaver's trigger entry ceases to exist (CR 800.4a)" + ); + assert_eq!(state.pending_trigger, None); + assert_eq!(state.pending_trigger_entry, None); + assert!(state.pending_trigger_event_batch.is_empty()); + assert_eq!( + state.pending_trigger_construction_priority_recipient, None, + "the recipient is cleared beside the three construction cursors" + ); + let restored: GameState = + serde_json::from_value(serde_json::to_value(&state).expect("serialize")) + .expect("trusted round trip"); + assert_eq!( + restored.pending_trigger_construction_priority_recipient, None, + "the cleared recipient must survive trusted serde" + ); + assert!( + matches!(state.waiting_for, WaitingFor::Priority { player } if player != PlayerId(1)), + "CR 800.4a hands the wait to a surviving player, got {:?}", + state.waiting_for + ); + + // Clone B (non-carrier): the eliminated prompt controller is NOT the + // carried recipient, so the site must still clear with the cursors. + let mut state = setup_three_player(); + let entry = open_live_trigger_construction_prompt(&mut state, PlayerId(2)); + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + assert!( + state.stack.iter().any(|e| e.id == entry), + "positive reach guard: the tracked entry is really on the stack" + ); + assert!( + players::is_alive(&state, PlayerId(1)), + "positive reach guard: the carried recipient is a DIFFERENT, still-living \ + player, so any clearing observed below came from this site rather than \ + from the departed-recipient re-point" + ); + + concede(&mut state, PlayerId(2)); + + assert!(!state.stack.iter().any(|e| e.id == entry)); + assert_eq!(state.pending_trigger_entry, None); + assert_eq!( + state.pending_trigger_construction_priority_recipient, None, + "the site clears the recipient with the cursors, not only when the two coincide" + ); + } + + /// CR 800.4a (plan Step 5, elimination site 2): the terminal `GameOver` + /// cleanup clears the recipient beside the same three cursors, in a fixture + /// where **no leaving player controls the tracked entry** — so this site is + /// provably the only one that can clear it. + /// + /// Revert discriminator: with only this site's clearing removed, a player + /// who has left the game stays installed in a serialized `GameOver` state. + /// No other branch masks it — site 1 never fires here, and the + /// game-continues re-point cannot run on the terminal path. + #[test] + fn terminal_game_over_cleanup_clears_the_construction_priority_recipient() { + let mut state = setup_three_player(); + let entry = open_live_trigger_construction_prompt(&mut state, PlayerId(0)); + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + + concede(&mut state, PlayerId(2)); + + // Pre-terminal reach guard: P1 controls no stack entry, so the + // tracked-entry-gone site is false and this row provably exercises the + // terminal branch rather than site 1. + assert_eq!( + state.pending_trigger_construction_priority_recipient, + Some(PlayerId(1)), + "a still-living recipient is neither cleared nor re-pointed" + ); + assert_eq!(state.pending_trigger_entry, Some(entry)); + assert!( + state.stack.iter().any(|e| e.id == entry), + "the tracked entry is still on the stack before the final concession" + ); + + concede(&mut state, PlayerId(1)); + + assert!( + matches!( + state.waiting_for, + WaitingFor::GameOver { + winner: Some(PlayerId(0)) + } + ), + "the single game-over check crowns P0, got {:?}", + state.waiting_for + ); + assert_eq!(state.pending_trigger, None); + assert_eq!(state.pending_trigger_entry, None); + assert!(state.pending_trigger_event_batch.is_empty()); + assert_eq!( + state.pending_trigger_construction_priority_recipient, None, + "a departed player must not stay installed in a terminal snapshot" + ); + let restored: GameState = + serde_json::from_value(serde_json::to_value(&state).expect("serialize")) + .expect("trusted round trip"); + assert_eq!( + restored.pending_trigger_construction_priority_recipient, + None + ); + } + + /// CR 800.4a (plan Step 5, elimination site 3 — the new game-continues + /// re-point): when the carried recipient alone leaves and neither cursor- + /// clearing site fires, priority passes to the next player still in the + /// game rather than stranding a departed recipient. + /// + /// Revert discriminator: without the re-point, `Some(P1)` stays installed + /// with P1 out of the game, and the finisher would later return + /// `WaitingFor::Priority { player: P1 }` for a departed player. Neither + /// cursor-clearing row detects this; both still pass. + #[test] + fn game_continues_repoints_a_departed_construction_priority_recipient() { + let mut state = setup_three_player(); + let entry = open_live_trigger_construction_prompt(&mut state, PlayerId(0)); + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + + concede(&mut state, PlayerId(1)); + + // Neither clearing site fired: the tracked entry is P0's and survives, + // and the game continues so the terminal arm was never entered. + assert!( + state.stack.iter().any(|e| e.id == entry), + "P0's tracked entry survives an opponent's departure" + ); + assert_eq!(state.pending_trigger_entry, Some(entry)); + assert!( + matches!(state.waiting_for, WaitingFor::AbilityModeChoice { player, .. } if player == PlayerId(0)), + "the construction prompt is still live and unchanged for P0, got {:?}", + state.waiting_for + ); + assert_eq!( + state.pending_trigger_construction_priority_recipient, + Some(PlayerId(2)), + "the departed recipient is re-pointed to the next living player, not stranded" + ); + let restored: GameState = + serde_json::from_value(serde_json::to_value(&state).expect("serialize")) + .expect("trusted round trip"); + assert_eq!( + restored.pending_trigger_construction_priority_recipient, + Some(PlayerId(2)), + "the re-pointed recipient must survive trusted serde" + ); + } } diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 87050a07e4..516cfca452 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -6867,6 +6867,11 @@ fn apply_action( let mut events = Vec::new(); let mut triggers_processed_inline = false; let skip_deferred_trigger_drain = false; + // The trigger-construction finisher runs at most once per reducer action, at + // the outermost handler return of its enumerated seams. Reset the witness + // here rather than at the outer boundary so a direct `apply_action` caller + // (drive loops, injector harnesses) is measured per action too. + state.trigger_construction_finisher_ran_this_action = false; // CancelAutoPass works from any WaitingFor state (player may cancel during // interactive choices). Routed by `actor` — previously used @@ -8476,6 +8481,17 @@ fn apply_action( }, ) => { let events_before = events.len(); + // CR 605.4a: which typed half of the colour seam this action is. + // A `ManaAbility` choice is a completed mana frame and has already + // recorded its exact occurrences through + // `mana_abilities::collect_completed_mana_frame_events` (the + // original source in `handle_choose_mana_color`, each sibling on its + // own finish path). A `ResolvingEffect` choice is not a mana frame + // at all and keeps its historical immediate scan. + let is_completed_mana_frame = matches!( + context, + crate::types::game_state::ManaChoiceContext::ManaAbility(_) + ); let wf = match context { crate::types::game_state::ManaChoiceContext::ManaAbility(pending_mana_ability) => { // CR 605.3a: validate the requested batch size BEFORE any mana @@ -8543,15 +8559,22 @@ fn apply_action( )? } }; - // CR 603.2c + CR 605.4a: A mana color choice produces mana inline. - // Scan its events for TapsForMana mana multipliers and for - // cost-payment triggers HERE, because for `ManaPayment` / - // `UnlessPayment` resumes the post-action pipeline is skipped - // (it is guarded by `matches!(waiting_for, WaitingFor::Priority)`), - // so this is the only scan site — and CR 605.4a requires the bonus - // mana to enter the pool before the spell's payment step continues. - // Do NOT "simplify" this scan away for non-Priority resumes. - if events.len() > events_before { + // CR 603.2c + CR 605.4a: A NON-mana `ResolvingEffect` colour choice + // produces mana inline. Scan its events for TapsForMana mana + // multipliers and for cost-payment triggers HERE, because for + // `ManaPayment` / `UnlessPayment` resumes the post-action pipeline is + // skipped (it is guarded by `matches!(waiting_for, + // WaitingFor::Priority)`), so this is the only scan site — and + // CR 605.4a requires the bonus mana to enter the pool before the + // spell's payment step continues. Do NOT "simplify" this scan away + // for non-Priority resumes. + // + // The `ManaAbility` half owns no aggregate scan: every source and + // sibling already recorded exact occurrences through the typed + // completed-frame seam, so a second scan here would rediscover + // events the frame already claimed and dispatch an ordinary cost + // observer separately from that frame's synthetic reflexive. + if !is_completed_mana_frame && events.len() > events_before { let mana_events: Vec<_> = events[events_before..].to_vec(); super::triggers::process_triggers(state, &mana_events); } @@ -8576,7 +8599,12 @@ fn apply_action( // Claim the scan via `triggers_processed_inline` — the same // mechanism `DeclareAttackers` uses — so the pipeline runs SBAs, // delayed/state triggers, and layers but skips the trigger re-scan. - if matches!(wf, WaitingFor::Priority { .. }) { + // + // The `ManaAbility` half must NOT use this broad suppression: its + // occurrence journal already narrows the pipeline's scan to exactly + // the events the mana frames did not claim, and the pipeline's + // guarded deferred drain is what releases their queued contexts once. + if !is_completed_mana_frame && matches!(wf, WaitingFor::Priority { .. }) { triggers_processed_inline = true; } wf @@ -9352,7 +9380,6 @@ fn apply_action( if ability_index < obj.abilities.len() && mana_abilities::is_mana_ability(&obj.abilities[ability_index]) { - let events_before = events.len(); let ability_def = obj.abilities[ability_index].clone(); let wf = mana_abilities::activate_mana_ability( state, @@ -9367,18 +9394,14 @@ fn apply_action( }, None, )?; - // CR 605.1b: Process TapsForMana triggers inline during mana payment - // (same rationale as the TapLandForMana arm below). - // CR 605.3b + CR 616.1 + CR 603.3b: A paused costed mana - // ability serializes its unscanned events in its typed cursor. - // The cursor is their single settlement authority, so do not - // scan them here and again when the replacement choice resumes. - if events.len() > events_before - && !casting::mana_ability_cost_payment_is_paused(state) - { - let mana_events: Vec<_> = events[events_before..].to_vec(); - super::triggers::process_triggers(state, &mana_events); - } + // CR 605.1b + CR 605.4a: no outer scan. `activate_mana_ability` + // builds a real typed cursor here, and its completed frame has + // already run `collect_completed_mana_frame_events` — collecting + // TapsForMana multipliers and ordinary cost observers together + // and journaling the exact live occurrences it claimed. Rescanning + // the same range would rediscover them. A paused costed mana + // ability keeps owning its unscanned events in that cursor, and + // still settles them when the replacement choice resumes. if let Some(order_wf) = super::triggers::preserve_order_triggers_resume(state, wf.clone()) { @@ -9414,21 +9437,39 @@ fn apply_action( }, &mut events, )?; - super::triggers::resolve_tap_mana_triggers_inline( - state, - &mut events, - events_before, - ); - // CR 605.1b: TapsForMana triggered mana abilities (Wild Growth, Vorinclex, - // Fertile Ground, Mana Flare class) must resolve inline when mana is - // produced during cost payment. The ManaPayment path does not flow through - // run_post_action_pipeline, so process triggers explicitly here so the - // bonus mana reaches the pool before the payment check. - if events.len() > events_before - && !casting::mana_ability_cost_payment_is_paused(state) - { - let mana_events: Vec<_> = events[events_before..].to_vec(); - super::triggers::process_triggers(state, &mana_events); + // CR 605.1b + CR 605.4a: the manual land tap has no cursor wrapper + // of its own — `activate_mana_source_option`'s no-ability branch + // taps and produces directly — so this arm IS its completed mana + // frame and runs the same typed empty-ledger preparation the cursor + // path runs internally. It resolves the TapsForMana triggered mana + // abilities inline (Wild Growth, Vorinclex, Fertile Ground, Mana + // Flare class) so the bonus mana reaches the pool before the payment + // check, and collects the ordinary tap observers into the same + // release group instead of dispatching them separately. The + // `ManaPayment` path does not flow through + // `run_post_action_pipeline`, so this remains the only seam. Frames + // whose ability branch already collected are protected by the + // helper's own consumed-occurrence filter. + if !casting::mana_ability_cost_payment_is_paused(state) { + if let Some(pause) = mana_abilities::collect_completed_mana_frame_events( + state, + Vec::new(), + &mut events, + events_before, + crate::types::game_state::ManaTriggerFixedPointResume::Root { + player: *player, + resume: Box::new(ManaAbilityResume::ManaPayment { + outer_player: Some(*player), + convoke_mode: *convoke_mode, + }), + }, + ) { + return Ok(ActionResult { + events, + waiting_for: pause, + log_entries: vec![], + }); + } } if let Some(order_wf) = super::triggers::preserve_order_triggers_resume(state, wf.clone()) @@ -9864,7 +9905,12 @@ fn apply_action( // `actor` is already authorized as the prompted player by // `check_actor_authorization` (via `WaitingFor::acting_player`). (WaitingFor::OrderTriggers { .. }, GameAction::OrderTriggers { order }) => { - triggers::handle_order_triggers(state, order)? + // Round-20 seam 1: this arm is the outermost handler return for the + // whole ordered batch, so it is where the construction finisher runs + // — covering the multi-group re-prompt, both early returns after + // `pending_trigger_order.take()`, and the terminal resume. + let produced = triggers::handle_order_triggers(state, order)?; + triggers::finish_trigger_construction_action(state, &mut events, produced) } // CR 707.9: Player chose a permanent to copy for "enter as a copy of" replacement. ( @@ -11300,7 +11346,10 @@ fn apply_action( // `pending_trigger_entry` so the resolver may now fire it. pending_trigger.ability.distribution = Some(distribution.iter().map(|(t, a)| (t.clone(), *a)).collect()); - if !triggers::finalize_pending_trigger_entry(state, &pending_trigger.ability) { + let produced = if !triggers::finalize_pending_trigger_entry( + state, + &pending_trigger.ability, + ) { // Unexpected dangling cursor: the entry is no longer on the // stack. Recover per CR 608.2b / CR 800.4a (a stack object // that has left the stack does not resolve) — record the @@ -11326,7 +11375,13 @@ fn apply_action( } else { WaitingFor::Priority { player: p } } - } + }; + // Round-20 seam 4: the trigger-owned division arm's produced + // wait — dangling recovery, deferred sibling, or success — goes + // through the construction finisher exactly once. The + // resolution-time and cast-time distribution arms below are not + // trigger-owned and are deliberately untouched. + triggers::finish_trigger_construction_action(state, &mut events, produced) } else { // Resolution-time distribution continuation path. state.waiting_for = WaitingFor::Priority { player: p }; @@ -15997,19 +16052,39 @@ mod stage2_injector_tests { assert_eq!( producers.len() + readers.len() + in_test, - 37, + 40, "CR 603.5 prompt census drifted. A new PRODUCER must have its recipient bound \ somewhere — the mint's conjunct (a) covers exactly ONE of them. A new READER is \ the benign case (U4's own consumption arm was one): adjudicate it in this doc and \ name the site, do not merely move the number.\n\ producers={producers:#?}\nreaders={readers:#?}" ); + // TEST-FIXTURE DRIFT, adjudicated on THIS branch (plan-v23 Step 5, session 4), + // `27 ⇒ 28`. One new line, again in the third (benign) partition: + // `game/triggers.rs::approved_construction_prompts` — the fixture that enumerates one + // instance of each of the six approved trigger-construction prompts, which the + // construction-finisher contract rows iterate. It mints nothing in production and + // reads `state.waiting_for` nowhere. The PRODUCER half is unchanged at 5 and the + // READER half unchanged at 7, both with byte-identical per-file lists. + // TEST-FIXTURE DRIFT, adjudicated on THIS branch (plan-v23 Steps 6/7), `25 ⇒ 27`. + // Both new lines are `#[cfg(test)]` fixture waits, i.e. the third partition — the + // benign class. The PRODUCER half is unchanged at 5 with a byte-identical per-file + // list, and the READER half is unchanged at 7, which is what this row's claim is + // about. The two lines are: + // - `game/triggers.rs::observer_helper_fixture` — the paused wait the + // `park_observer_triggers_if_paused` rows need in order to reach that helper's + // collecting branch at all; + // - `game/visibility.rs::triggered_mana_projection_fixture` — the live public + // prompt the Step-6 redaction rows assert survives viewer filtering. + // Neither mints a prompt in production; neither reads `state.waiting_for` in + // production. Adjudicated, not relaxed: a sixth PRODUCER would still red the + // partition assert below before this total could absorb it. assert_eq!( (producers.len(), readers.len(), in_test), - (5, 7, 25), + (5, 7, 28), "the partition, not just the total: five PRODUCTION producers, seven PRODUCTION \ readers (they read `state.waiting_for` and never write it — the seventh is U4's \ - `inject_pinned_answer` arm), 25 `#[cfg(test)]` lines.\nproducers={producers:#?}\n\ + `inject_pinned_answer` arm), 28 `#[cfg(test)]` lines.\nproducers={producers:#?}\n\ readers={readers:#?}" ); assert_eq!( @@ -16106,9 +16181,20 @@ mod stage2_injector_tests { // 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:6306".to_string(), - "game/effects/mod.rs:6383".to_string(), - "game/effects/mod.rs:9578".to_string(), + // THIS BRANCH (reflexive materializer), REBASED ONTO `117b430c2`: + // `:6306/:6383/:9578 ⇒ :6429/:6506/:9701`, uniform +123 above all three. + // Cause is LOCAL — `build_reflexive_pending_trigger` plus the deferral arms + // inside `try_materialize_reflexive_trigger`, all of which sit above `:6306` + // in `game/effects/mod.rs` — so the CI-vs-local diagnosis in the header does + // not apply. Coordinates re-derived from this row's OWN failure output at the + // rebased tip, not carried over from either side of the rebase conflict. + // Identity re-established rather than assumed: each producer at its new + // coordinate is byte-identical to `117b430c2:game/effects/mod.rs` at its old + // one, and `scoped_library_search.rs:452` did not move at all — the + // set-preservation evidence that no producer was gained or lost. + "game/effects/mod.rs:6429".to_string(), + "game/effects/mod.rs:6506".to_string(), + "game/effects/mod.rs:9701".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. @@ -16426,7 +16512,15 @@ mod stage2_injector_tests { // // SET PRESERVATION: unchanged. Upstream adds no line matching the needle to this file and // neither does this branch — total still 37, partition still 5/7/25. - "game/engine.rs:12004".to_string(), + // + // THIS BRANCH (reflexive materializer), REBASED ONTO `117b430c2`: `:12004 ⇒ + // :12059`, +55. LOCAL, not upstream — this branch's own engine.rs insertions + // (the deferred-drain gate and the trigger-construction finisher witness) + // land above this producer. Coordinate re-derived from this row's own + // failure output at the rebased tip; the line at `:12059` is byte-identical + // to `117b430c2:engine.rs:12004` and still inside + // `begin_pending_trigger_target_selection`. + "game/engine.rs:12059".to_string(), ], "the five production producers, NAMED: the CR 603.5 gate in `resolve_chain_body` \ plus the two repeated-optional-payment drivers, the per-player acceptance cursor \ diff --git a/crates/engine/src/game/engine_modes.rs b/crates/engine/src/game/engine_modes.rs index 0afbbebe86..1dbdf5a0b2 100644 --- a/crates/engine/src/game/engine_modes.rs +++ b/crates/engine/src/game/engine_modes.rs @@ -61,7 +61,12 @@ pub(super) fn handle_ability_mode_choice( events, ) } else { - handle_triggered_mode_choice( + // Round-20 seam 2: the finisher wraps the result HERE, at the public + // `SelectModes` entry, and never inside `handle_triggered_mode_choice`. + // That function is re-entered inside trigger dispatch (via + // `resolve_random_modal_trigger`), where a `Priority` result is + // discarded — consuming the recipient there would lose it mid-batch. + let produced = handle_triggered_mode_choice( state, TriggeredModeChoice { player, @@ -72,7 +77,10 @@ pub(super) fn handle_ability_mode_choice( indices, }, events, - ) + )?; + Ok(triggers::finish_trigger_construction_action( + state, events, produced, + )) }?; Ok(waiting_for) diff --git a/crates/engine/src/game/engine_payment_choices.rs b/crates/engine/src/game/engine_payment_choices.rs index ab1c48b282..410b8e66b0 100644 --- a/crates/engine/src/game/engine_payment_choices.rs +++ b/crates/engine/src/game/engine_payment_choices.rs @@ -28,10 +28,38 @@ use super::engine_priority; use super::mana_abilities; use super::zone_pipeline::{self, ZoneMoveRequest, ZoneMoveResult}; +/// CR 605.4a: the sidecar-aware wrapper around the unmodified baseline handler. +/// +/// The baseline body — including its `park_observer_triggers_if_paused` call, +/// which fails closed under the typed ownership guard — runs inside the resumed +/// occurrence's own node/marker/production-override scope. Readiness runs only +/// after that scope has restored, so combined collection and child discovery see +/// the ambient authority they would have seen synchronously. With no live +/// carrier both hooks are exact no-ops and this is baseline byte-for-byte. pub(super) fn handle_optional_effect_choice( state: &mut GameState, accept: bool, events: &mut Vec, +) -> Result { + let events_before = events.len(); + let produced = super::triggers::with_accepted_triggered_mana_action_scope(state, |state| { + handle_optional_effect_choice_inner(state, accept, events) + })?; + if let super::triggers::TriggeredManaReadiness::Resumed { wait, .. } = + super::triggers::finish_accepted_triggered_mana_action(state, events, events_before)? + { + // This arm falls through to the reducer's ordinary epilogue, which owns + // release for the resumed frame; no settled-Priority convergence here. + state.waiting_for = wait.clone(); + return Ok(wait); + } + Ok(produced) +} + +fn handle_optional_effect_choice_inner( + state: &mut GameState, + accept: bool, + events: &mut Vec, ) -> Result { let events_before = events.len(); state.cost_payment_failed_flag = false; @@ -102,22 +130,32 @@ pub(super) fn handle_optional_effect_choice( // CR 608.2c + CR 700.2b: Optional triggered modal ("you may choose N") — // the decline/accept gate runs before mode selection while the stack // entry is still mid-construction. - if accept { + let produced = if accept { super::engine::clear_pending_trigger_optional(state); if let Some(waiting) = super::engine::begin_pending_trigger_target_selection(state)? { - state.waiting_for = waiting; + waiting } else { - state.waiting_for = WaitingFor::Priority { + WaitingFor::Priority { player: state.active_player, - }; + } } } else { super::engine::drop_mid_construction_pending_trigger(state); - state.waiting_for = WaitingFor::Priority { + WaitingFor::Priority { player: state.active_player, - }; - } + } + }; + // Round-20 seam 5: the optional-modal branch's assigned wait goes + // through the construction finisher BEFORE + // `resume_pending_continuation_if_priority` and + // `park_observer_triggers_if_paused` run below, so those baseline + // calls observe the final wait. With no carried recipient the + // finisher returns `produced` byte-for-byte, preserving baseline's + // `Priority { player: state.active_player }` fallback exactly — the + // active player, not the trigger controller. + state.waiting_for = + super::triggers::finish_trigger_construction_action(state, events, produced); } } @@ -155,12 +193,58 @@ pub(super) fn handle_optional_effect_choice_and_remember( handle_optional_effect_choice(state, matches!(choice, AutoMayChoice::Accept), events) } +/// CR 605.4a: the sidecar-aware wrapper around the unmodified baseline handler, +/// exactly as [`handle_optional_effect_choice`] above. The inner body keeps every +/// early return it already had — an intermediate `OpponentMayChoice` re-prompt is +/// a repeated pause of the same accepted occurrence, and readiness recognizes it +/// as one. pub(super) fn handle_opponent_may_choice( state: &mut GameState, waiting_for: WaitingFor, accept: bool, events: &mut Vec, ) -> Result { + let events_before = events.len(); + let produced = super::triggers::with_accepted_triggered_mana_action_scope(state, |state| { + handle_opponent_may_choice_inner(state, waiting_for, accept, events) + })?; + if let super::triggers::TriggeredManaReadiness::Resumed { + wait, + settled_direct_priority_root, + } = super::triggers::finish_accepted_triggered_mana_action(state, events, events_before)? + { + // CR 117.5 + CR 605.4a: this reducer arm returns its `ActionResult` + // directly, so the ordinary epilogue never runs. A resumed direct + // `Priority` root with no live owner therefore has to converge here or + // its own frame's settled batch would sit undrained until some later + // action — that is exactly the gap the settled wrapper closes. Every + // other owner keeps its queue and returns the frame's wait unchanged. + let wait = if settled_direct_priority_root { + engine_priority::run_post_action_pipeline_from_settled_priority( + state, + events, + events_before, + &wait, + )? + } else { + wait + }; + state.waiting_for = wait.clone(); + return Ok(action_result(events, wait)); + } + Ok(action_result(events, produced)) +} + +/// The unmodified baseline body, returning its wait rather than an +/// `ActionResult`: `action_result` takes the event vector, and the wrapper above +/// must still read this action's own emitted range afterwards. Every early +/// return is preserved exactly. +fn handle_opponent_may_choice_inner( + state: &mut GameState, + waiting_for: WaitingFor, + accept: bool, + events: &mut Vec, +) -> Result { let events_before = events.len(); let WaitingFor::OpponentMayChoice { player: promptee, @@ -258,7 +342,7 @@ pub(super) fn handle_opponent_may_choice( max_targets: 1, pending_ability: ability, }; - return Ok(action_result(events, state.waiting_for.clone())); + return Ok(state.waiting_for.clone()); } if !remaining.is_empty() { @@ -274,7 +358,7 @@ pub(super) fn handle_opponent_may_choice( description, remaining: rest, }; - return Ok(action_result(events, state.waiting_for.clone())); + return Ok(state.waiting_for.clone()); } state @@ -309,7 +393,7 @@ pub(super) fn handle_opponent_may_choice( description, remaining: rest, }; - return Ok(action_result(events, state.waiting_for.clone())); + return Ok(state.waiting_for.clone()); } else { set_active_priority(state); if let Some(frame) = state @@ -322,7 +406,7 @@ pub(super) fn handle_opponent_may_choice( resume_pending_continuation_if_priority(state, events)?; super::triggers::collect_and_drain_observer_triggers_if_settled(state, events, events_before); - Ok(action_result(events, state.waiting_for.clone())) + Ok(state.waiting_for.clone()) } fn resolve_all_declined_opponent_may( @@ -1635,6 +1719,13 @@ pub(super) fn handle_unless_payment_activate_ability( } let ability_def = object.abilities[ability_index].clone(); + // CR 605.3b + CR 118.12: propagate the activation's OWN returned wait. A + // colour choice, a hybrid mana-sub-cost choice, or the completed-frame + // seam's whitelisted pause is raised by return value, not by writing + // `state.waiting_for`; reading the field back therefore silently dropped + // those prompts and left the unless payment reprompted with the interactive + // step never taken. Every pause that DOES write the field (a replacement + // choice) returns the same value it wrote, so this is a strict improvement. mana_abilities::activate_mana_ability( state, source_id, @@ -1651,8 +1742,7 @@ pub(super) fn handle_unless_payment_activate_ability( remaining, }, None, - )?; - Ok(state.waiting_for.clone()) + ) } pub(super) fn handle_ward_discard_choice( diff --git a/crates/engine/src/game/engine_priority.rs b/crates/engine/src/game/engine_priority.rs index 55f7db7a61..10bdc28345 100644 --- a/crates/engine/src/game/engine_priority.rs +++ b/crates/engine/src/game/engine_priority.rs @@ -1,12 +1,14 @@ use crate::types::events::GameEvent; use crate::types::game_state::{GameState, WaitingFor}; use crate::types::identifiers::ObjectId; +use crate::types::player::PlayerId; use super::engine::{begin_pending_trigger_target_selection, check_exile_returns, EngineError}; use super::match_flow; use super::players; use super::sba; use super::triggers; +use super::triggers::DeferredTriggerDrainPolicy; pub(super) fn run_post_action_pipeline( state: &mut GameState, @@ -35,9 +37,95 @@ pub(crate) fn run_post_action_pipeline_from( default_wf: &WaitingFor, skip_trigger_scan: bool, skip_deferred_trigger_drain: bool, +) -> Result { + run_post_action_pipeline_from_with_policy( + state, + events, + event_start, + default_wf, + skip_trigger_scan, + if skip_deferred_trigger_drain { + DeferredTriggerDrainPolicy::Skip + } else { + DeferredTriggerDrainPolicy::ResolutionSafe + }, + None, + ) +} + +/// CR 117.3c + CR 117.5 + CR 605.4a: the settled-Priority convergence wrapper. +/// +/// The one caller family is a handler that returns its `ActionResult` directly, +/// bypassing the reducer's ordinary epilogue, after a sidecar-owned accepted +/// triggered-mana occurrence resumed a **direct `ManaAbilityResume::Priority` +/// root** with no live cast/resolution/payment owner left. Its `settled_priority` +/// is that root's own exact reconstructed wait, and it is exhaustively validated +/// here rather than trusted. +/// +/// It differs from the ordinary wrappers in exactly three ways: the drain policy +/// permits a passive announced spell to remain on the stack (the batch below it +/// is fully settled, so its observers belong above it); the carried recipient +/// governs the no-choice stack-growth exit and is persisted before any ordering +/// or construction step can pause; and its own return is a finisher call, which +/// is Round-20 seam 7 — this wrapper must never hand back a non-prompt +/// `Priority` with the recipient still installed. +pub(crate) fn run_post_action_pipeline_from_settled_priority( + state: &mut GameState, + events: &mut Vec, + event_start: usize, + settled_priority: &WaitingFor, +) -> Result { + let WaitingFor::Priority { player } = *settled_priority else { + debug_assert!( + false, + "settled-Priority convergence requires the root's exact Priority wait, \ + got {settled_priority:?}", + ); + return Ok(settled_priority.clone()); + }; + triggers::preserve_trigger_construction_priority_recipient(state, player); + let produced = run_post_action_pipeline_from_with_policy( + state, + events, + event_start, + settled_priority, + false, + DeferredTriggerDrainPolicy::SettledPriority, + Some(player), + )?; + Ok(triggers::finish_trigger_construction_action( + state, events, produced, + )) +} + +/// The shared post-action settlement core. Every ordinary wrapper reaches it +/// through the boolean form above, with `carried_priority_recipient == None`, +/// which preserves their current stack-growth fallback and every current +/// non-sidecar ordering behavior byte-for-byte. +/// +/// `carried_priority_recipient` is `Some(player)` only for a settled-Priority +/// convergence whose activator was not the active player (CR 117.3c + CR 117.5). +/// It changes exactly two things: the wait computed at the no-choice +/// stack-growth exit, and the fact that any ordering or construction prompt the +/// pipeline opens persists that player so the construction finisher can hand +/// priority back to them once the batch is fully announced. +fn run_post_action_pipeline_from_with_policy( + state: &mut GameState, + events: &mut Vec, + event_start: usize, + default_wf: &WaitingFor, + skip_trigger_scan: bool, + drain_policy: DeferredTriggerDrainPolicy, + carried_priority_recipient: Option, ) -> Result { stage_pending_activation_trigger_events(state, events, event_start); + // CR 117.3c + CR 117.5: the wait a completed no-choice pass hands back. With + // no carried recipient this is exactly baseline's active-player wait. + let settled_priority_wait = WaitingFor::Priority { + player: carried_priority_recipient.unwrap_or(state.active_player), + }; + // Capture stack depth before any trigger/SBA processing so we can detect // whether new triggered abilities were added during this pipeline pass. let stack_before = state.stack.len(); @@ -192,6 +280,7 @@ pub(crate) fn run_post_action_pipeline_from( // next SBA pass. if let Some(waiting_for) = begin_pending_trigger_target_selection(state)? { state.waiting_for = waiting_for.clone(); + persist_carried_recipient_across_prompt(state, carried_priority_recipient); state.consumed_before_priority_trigger_events.clear(); return Ok(waiting_for); } @@ -241,6 +330,7 @@ pub(crate) fn run_post_action_pipeline_from( ); if let Some(waiting_for) = outcome.prompt { state.waiting_for = waiting_for.clone(); + persist_carried_recipient_across_prompt(state, carried_priority_recipient); state.consumed_before_priority_trigger_events.clear(); return Ok(waiting_for); } @@ -262,9 +352,10 @@ pub(crate) fn run_post_action_pipeline_from( } } else if matches!(state.waiting_for, WaitingFor::Priority { .. }) && !state.deferred_triggers.is_empty() - && !skip_deferred_trigger_drain { - if let Some(wf) = triggers::drain_deferred_trigger_queue(state, events) { + if let Some(wf) = + triggers::drain_deferred_trigger_queue_with_policy(state, events, drain_policy) + { state.waiting_for = wf; } } @@ -273,6 +364,7 @@ pub(crate) fn run_post_action_pipeline_from( if matches!(state.waiting_for, WaitingFor::GameOver { .. }) { match_flow::handle_game_over_transition(state); } + persist_carried_recipient_across_prompt(state, carried_priority_recipient); state.consumed_before_priority_trigger_events.clear(); return Ok(state.waiting_for.clone()); } @@ -300,6 +392,7 @@ pub(crate) fn run_post_action_pipeline_from( // sets pending_trigger and is re-derived at begin_pending_trigger_target_selection) // is untouched. if matches!(state.waiting_for, WaitingFor::OrderTriggers { .. }) { + persist_carried_recipient_across_prompt(state, carried_priority_recipient); return Ok(state.waiting_for.clone()); } @@ -310,15 +403,14 @@ pub(crate) fn run_post_action_pipeline_from( if let Some(waiting_for) = begin_pending_trigger_target_selection(state)? { state.waiting_for = waiting_for.clone(); + persist_carried_recipient_across_prompt(state, carried_priority_recipient); return Ok(waiting_for); } if state.stack.len() > stack_before { let outgoing = flush_pending_priority_intercepts( state, - WaitingFor::Priority { - player: state.active_player, - }, + settled_priority_wait.clone(), default_wf.acting_player(), ); return Ok(outgoing); @@ -333,6 +425,34 @@ pub(crate) fn run_post_action_pipeline_from( )) } +/// CR 117.3c + CR 117.5: persist a carried priority recipient across any +/// ordering or trigger-construction prompt this pipeline pass just installed, so +/// the construction finisher can hand priority back to that player once the +/// batch has finished announcing. +/// +/// With `carried_priority_recipient == None` — every ordinary wrapper — this is +/// a no-op, and the existing active-player/controller fallbacks at each +/// construction seam are untouched. When ordering opens, the same player is +/// stored in both authorities: `PendingTriggerOrder::resume_after_ordering` +/// (which `handle_order_triggers` consumes and then drops) and the durable +/// recipient (which stays authoritative across every early return after that +/// order carrier is taken). +fn persist_carried_recipient_across_prompt( + state: &mut GameState, + carried_priority_recipient: Option, +) { + let Some(player) = carried_priority_recipient else { + return; + }; + if !triggers::is_trigger_construction_prompt(&state.waiting_for) { + return; + } + if matches!(state.waiting_for, WaitingFor::OrderTriggers { .. }) { + triggers::preserve_order_triggers_resume(state, WaitingFor::Priority { player }); + } + triggers::preserve_trigger_construction_priority_recipient(state, player); +} + /// Route events emitted while a target-bearing activation remains pending into /// its private trigger transaction before the ordinary post-action collector /// can observe them. diff --git a/crates/engine/src/game/engine_stack.rs b/crates/engine/src/game/engine_stack.rs index e5d574e0ee..aa65d742b2 100644 --- a/crates/engine/src/game/engine_stack.rs +++ b/crates/engine/src/game/engine_stack.rs @@ -194,8 +194,13 @@ pub(super) fn handle_trigger_target_selection_select_targets( .take() .ok_or_else(|| EngineError::InvalidAction("No pending trigger".to_string()))?; - Ok(finalize_trigger_target_selection( - state, trigger, ability, events, + let produced = finalize_trigger_target_selection(state, trigger, ability, events); + // Round-20 seam 3: wrapping at the action seam — not per return inside + // `finalize_trigger_target_selection` — covers all five of its returns + // uniformly and keeps the `engine_modes` delegation, which runs inside + // trigger dispatch, from consuming the recipient. + Ok(triggers::finish_trigger_construction_action( + state, events, produced, )) } @@ -351,8 +356,10 @@ pub(super) fn handle_trigger_target_selection_choose_target( .take() .ok_or_else(|| EngineError::InvalidAction("No pending trigger".to_string()))?; - Ok(finalize_trigger_target_selection( - state, trigger, ability, events, + let produced = finalize_trigger_target_selection(state, trigger, ability, events); + // Round-20 seam 3, step-by-step walk completion. + Ok(triggers::finish_trigger_construction_action( + state, events, produced, )) } } diff --git a/crates/engine/src/game/mana_abilities.rs b/crates/engine/src/game/mana_abilities.rs index 15bdc8fceb..f6cd90ae8e 100644 --- a/crates/engine/src/game/mana_abilities.rs +++ b/crates/engine/src/game/mana_abilities.rs @@ -13,8 +13,9 @@ use crate::types::events::{GameEvent, ManaTapState}; use crate::types::game_state::{ CostResume, GameState, ManaAbilityCostCursor, ManaAbilityCostParent, ManaAbilityCostParentLifecycle, ManaAbilityCostResolutionMode, ManaAbilityResume, ManaChoice, - ManaChoiceContext, ManaChoicePrompt, PayCostKind, PayableResource, PendingCostMoveResume, - PendingManaAbility, ProductionOverride, WaitingFor, + ManaChoiceContext, ManaChoicePrompt, ManaColorChoiceResume, ManaTriggerFixedPointResume, + PayCostKind, PayableResource, PendingCostMoveResume, PendingManaAbility, ProductionOverride, + WaitingFor, }; use crate::types::identifiers::ObjectId; use crate::types::mana::{ManaColor, ManaCost, ManaPool, ManaType, PaymentContext}; @@ -322,6 +323,42 @@ pub fn is_triggered_mana_ability( ) } +/// CR 605.1b + CR 605.4a: the **resolver-facing** counterpart of +/// [`is_triggered_mana_ability`]. +/// +/// [`is_triggered_mana_ability`] is the *acceptance-time* gate: it answers +/// "does this classification-time graph qualify?" and is deliberately raw — +/// any target anywhere in the graph makes it false. That is the right question +/// once, when an occurrence is accepted. +/// +/// It is the wrong question during resolution. `resolve_ability_chain` may +/// materialize an engine resolution-context referent (a `chosen_players` +/// member surfacing through `ControllerRef::ChosenPlayer`, for instance) into +/// the overloaded `ResolvedAbility.targets` vector. That referent is not a +/// CR 115.1d announcement target — `build_target_slots` surfaces no slot for +/// it — so the already-accepted ability does not stop being a triggered mana +/// ability partway through its own resolution. CR 605.4a keeps the occurrence +/// stackless and owned by the immediate fixed point. +/// +/// So: while the accepted-occurrence marker is live, the classification +/// decision has already been made and is simply read back. Outside such an +/// occurrence there is no marker and this delegates to the raw classifier with +/// the ambient `current_trigger_event`, which is exactly baseline. Ordinary +/// callers — including the compatibility `resolve_triggered_mana_ability_inline` +/// wrapper, which deliberately installs no marker — are unaffected. +pub(crate) fn is_resolving_triggered_mana(state: &GameState, ability: &ResolvedAbility) -> bool { + if let Some(node) = state.active_accepted_triggered_mana_node { + debug_assert_eq!( + Some(node), + state.active_rules_execution_node, + "an accepted triggered-mana occurrence marker must name the ambient rules-execution \ + node; a mismatch means a scope was entered or restored without its partner" + ); + return true; + } + is_triggered_mana_ability(ability, state.current_trigger_event.as_ref()) +} + /// True iff every reachable link (via `sub_ability` and `else_ability` per /// CR 608.2c) has `Effect::Mana`. The "every link is mana" rule is the /// conservative reading of CR 605.1b(c) — inline resolution skips priority, @@ -571,6 +608,13 @@ pub(super) fn resolve_mana_ability_excluding( // its historical default-output semantics even when a cost move pauses. // The cursor serializes that resolution mode with the exact outer resume. let cost_event_start = events.len(); + // CR 603.2 + CR 603.3b: Prepare a fresh child-facing snapshot of the live + // synchronous parent carrying that parent frame's current unscanned suffix, + // including every earlier sibling that already completed synchronously. The + // live parent cursor is never mutated, so a synchronous child drops this + // snapshot without duplicating the root's eventual scan. + let prepared_parent = + parent.map(|parent| parent_snapshot_with_current_cost_events(parent, events)); let waiting_for = continue_mana_ability_cost_payment( state, pending, @@ -579,7 +623,7 @@ pub(super) fn resolve_mana_ability_excluding( excluded_sources, sub_cost_demand, ManaAbilityCostResolutionMode::AutoResolved, - parent, + prepared_parent.as_ref(), ), events, cost_event_start, @@ -1247,6 +1291,10 @@ pub fn handle_choose_mana_color( let node = pending .rules_execution_node .unwrap_or_else(|| state.begin_activated_mana_journal_node(pending.source_id)); + // CR 605.4a: the choice action's own live-collection start. The already-paid + // cost range was collected by the frame that returned this prompt; this + // action owns only what production/completion emits from here. + let choice_action_start = events.len(); state.with_rules_execution_node(node, |state| { produce_mana_from_ability( state, @@ -1267,6 +1315,26 @@ pub fn handle_choose_mana_color( ); }); + // CR 603.2 + CR 605.3c + CR 605.4a: the second typed half of the colour + // seam. The frame this choice completes is a completed mana frame with an + // empty durable ledger, so it runs the SAME collection helper before + // returning its resume owner — and before `batch_activate_mana_siblings` + // begins the next sibling, each of which performs the same helper on its own + // finish path. That is the CR 605.4a pre-pass between siblings, in place of + // one aggregate scan after the loop. + if let Some(pause) = collect_completed_mana_frame_events( + state, + Vec::new(), + events, + choice_action_start, + ManaTriggerFixedPointResume::Root { + player: pending.player, + resume: Box::new(pending.resume.clone()), + }, + ) { + return Ok(pause); + } + Ok(resume_waiting_for(pending.player, pending.resume.clone())) } @@ -2119,6 +2187,42 @@ fn mana_ability_definition( .ok_or_else(|| EngineError::InvalidAction("Mana ability no longer exists".to_string())) } +/// CR 603.2 + CR 603.3b: Build the child-facing snapshot of a live synchronous +/// parent frame. Only this clone's ledger gains the parent's current unscanned +/// suffix; the live parent cursor keeps its own `cost_event_start` and its +/// events remain in the reducer vector, so a synchronous child that drops the +/// snapshot cannot duplicate the root's eventual scan. Preparation runs at every +/// child entry, so a later pausing sibling inherits the parent prefix plus all +/// earlier synchronously completed siblings in chronological order. +fn parent_snapshot_with_current_cost_events( + parent: &ManaAbilityCostParent, + events: &[GameEvent], +) -> ManaAbilityCostParent { + debug_assert!( + matches!( + parent.lifecycle, + ManaAbilityCostParentLifecycle::Synchronous + ), + "only a live synchronous parent may be augmented; a suspended parent's prefix is already durable" + ); + debug_assert!( + parent.current_action_event_start <= events.len(), + "parent event marker must index the live reducer event vector" + ); + let mut prepared = parent.clone(); + if matches!( + parent.lifecycle, + ManaAbilityCostParentLifecycle::Synchronous + ) { + let start = parent.current_action_event_start.min(events.len()); + prepared + .cursor + .deferred_cost_events + .extend_from_slice(&events[start..]); + } + prepared +} + fn mana_ability_cost_cursor( cost: &Option, excluded_sources: &HashSet, @@ -2144,13 +2248,10 @@ fn mana_ability_cost_cursor( next_sacrificed: 0, selected_exile_remaining: None, selected_sacrifice_remaining: None, - // CR 603.2 + CR 603.3b: A nested child takes over every unscanned - // event already owned by its suspended parent. It appends only newly - // emitted events if it pauses, then hands the one batch back on - // completion; no ancestor batch is copied twice. - deferred_cost_events: parent.map_or_else(Vec::new, |parent| { - parent.cursor.deferred_cost_events.clone() - }), + // CR 603.2 + CR 603.3b: A nested child starts with an empty frame-local + // ledger. Its parent snapshot retains every ancestor-owned event + // opaquely until a suspended child moves its own events upward. + deferred_cost_events: Vec::new(), current_action_deferred_start: 0, parent: parent.cloned().map(Box::new), } @@ -2265,6 +2366,19 @@ fn ensure_mana_ability_selection_cursor_consumed( Ok(()) } +/// CR 603.2 + CR 603.3b: Move one suspended child's frame-local trigger-event +/// ledger into its parent without replacing the parent's earlier events. +fn append_suspended_child_cost_events( + parent: &mut ManaAbilityCostCursor, + child: &mut ManaAbilityCostCursor, + current: &[GameEvent], +) { + parent + .deferred_cost_events + .extend(std::mem::take(&mut child.deferred_cost_events)); + parent.deferred_cost_events.extend_from_slice(current); +} + fn advance_mana_ability_selection_cursor( cursor: &mut ManaAbilityCostCursor, cost: &AbilityCost, @@ -2657,15 +2771,19 @@ fn pay_mana_ability_cost_component( .skip(cursor.next_sacrificed); // CR 605.3b + CR 605.3c: A nested source that pauses while // funding this Mana component must retain this exact parent cursor. - // The child cursor inherits the parent's existing deferred batch - // when it is constructed. Do not also copy this action's events - // into the parent: if the child pauses, its own cursor appends them - // exactly once and becomes the temporary settlement authority. + // The child's frame-local deferred ledger starts empty; ancestor + // events stay opaque in this parent snapshot until upward handoff. let parent_cursor = cursor.clone(); let parent = ManaAbilityCostParent { pending: Box::new(pending.clone()), cursor: Box::new(parent_cursor), lifecycle: ManaAbilityCostParentLifecycle::Synchronous, + // CR 603.2 + CR 603.3b: Record where this parent frame's own + // unscanned events begin so each nested child entry can prepare + // a snapshot carrying that prefix plus any earlier synchronous + // sibling. The live cursor and its `cost_event_start` are not + // modified. + current_action_event_start: cost_event_start, }; let prior_waiting_for = state.waiting_for.clone(); let component_progress = match pay_mana_ability_cost_with_choices( @@ -2735,7 +2853,7 @@ fn pay_mana_ability_cost_component( fn finish_mana_ability_cost_payment( state: &mut GameState, mut pending: PendingManaAbility, - cursor: ManaAbilityCostCursor, + mut cursor: ManaAbilityCostCursor, events: &mut Vec, cost_event_start: usize, ) -> Result { @@ -2744,9 +2862,32 @@ fn finish_mana_ability_cost_payment( ManaAbilityCostResolutionMode::AutoResolved ); let has_deferred_cost_events = !cursor.deferred_cost_events.is_empty(); + let is_ultimate_root = cursor.parent.is_none(); + // CR 605.3b + CR 605.4a: The `AutoResolved` direct resolver + // (`resolve_mana_ability_excluding`) is not an action's completed root mana + // frame — it is the synchronous auto-tap/probe entry, whose events belong to + // the OUTER cost owner already on the Rust call stack and whose caller + // restores its own `waiting_for`. It keeps its historical default-output + // semantics: with an empty durable ledger it performs no collection at all, + // exactly as baseline. Only a frame that already owns replacement-paused + // events settles from this path. + let settles_completed_frame = + is_ultimate_root && (has_deferred_cost_events || !resolves_automatically); + if matches!( + cursor.parent.as_deref(), + Some(ManaAbilityCostParent { + lifecycle: ManaAbilityCostParentLifecycle::Synchronous, + .. + }) + ) { + debug_assert!( + cursor.deferred_cost_events.is_empty(), + "a synchronous mana child cannot own deferred ancestor events" + ); + } let parent = cursor .parent - .clone() + .take() .filter(|parent| matches!(parent.lifecycle, ManaAbilityCostParentLifecycle::Suspended)); let ability_def = mana_ability_definition(state, &pending)?; if !resolves_automatically && pending.color_override.is_none() { @@ -2771,32 +2912,42 @@ fn finish_mana_ability_cost_payment( pending.batch_siblings = batch_eligible_siblings(state, pending.player, pending.source_id, &ability_def); } + let choice_player = pending.player; + let context = ManaChoiceContext::ManaAbility(Box::new(pending)); let resume = WaitingFor::ChooseManaColor { - player: pending.player, - choice, - context: ManaChoiceContext::ManaAbility(Box::new(pending)), + player: choice_player, + choice: choice.clone(), + context: context.clone(), }; - if has_deferred_cost_events { - settle_mana_ability_cost_events( + if settles_completed_frame { + // CR 603.2 + CR 603.3b + CR 605.3b + CR 605.4a: A mana-color + // choice returns before the ordinary post-action pipeline + // runs, so the already-paid cost range needs its one normal + // trigger collection here — through the SAME typed + // completed-frame seam the durable-ledger root uses, whether + // or not that ledger is empty. The empty-ledger shape used to + // fall through to a bare `process_triggers`, which dispatched + // an ordinary cost observer separately from the frame's own + // synthetic reflexive. + debug_assert!(cursor.parent.is_none()); + if let Some(pause) = collect_completed_mana_frame_events( state, cursor.deferred_cost_events, events, cost_event_start, - ); + ManaTriggerFixedPointResume::ColorChoice(Box::new(ManaColorChoiceResume { + player: choice_player, + choice, + context, + })), + ) { + return Ok(pause); + } if let Some(order_wf) = super::triggers::preserve_order_triggers_resume(state, resume.clone()) { return Ok(order_wf); } - } else if events.len() > cost_event_start { - // CR 603.2 + CR 603.3b + CR 605.3b: A mana-color choice - // returns before the ordinary post-action pipeline runs. - // Its already-paid cost events therefore need their one - // normal trigger collection here. Replacement-paused events - // are deliberately excluded: their typed cursor owns them - // until `settle_mana_ability_cost_events` above. - let cost_events = events[cost_event_start..].to_vec(); - super::triggers::process_triggers(state, &cost_events); } return Ok(resume); } @@ -2828,13 +2979,15 @@ fn finish_mana_ability_cost_payment( super::triggers::resolve_tap_mana_triggers_inline(state, events, production_events_start); if let Some(parent) = parent { let mut parent_cursor = *parent.cursor; - // CR 603.2 + CR 603.3b: The child now owns the complete old batch; - // append its current action exactly once, then transfer that batch to - // the parent before it retries its still-unpaid Mana component. - parent_cursor.deferred_cost_events = cursor.deferred_cost_events; - parent_cursor - .deferred_cost_events - .extend_from_slice(&events[cost_event_start..]); + // CR 603.2 + CR 603.3b: Move the suspended child's frame-local batch + // upward without replacing the ancestor's earlier ledger. Append the + // child's current action exactly once before retrying the parent's + // still-unpaid Mana component. + append_suspended_child_cost_events( + &mut parent_cursor, + &mut cursor, + &events[cost_event_start..], + ); let parent_event_start = events.len(); return continue_mana_ability_cost_payment( state, @@ -2844,45 +2997,68 @@ fn finish_mana_ability_cost_payment( parent_event_start, ); } - let resume = resume_mana_ability_root(state, pending.player, pending.resume, events)?; - if super::casting::mana_ability_cost_payment_is_paused(state) { - defer_cost_events_into_active_mana_root( - state, - cursor.deferred_cost_events, - &events[cost_event_start..], - ); - return Ok(resume); - } - - if has_deferred_cost_events { - settle_mana_ability_cost_events( + // CR 603.2 + CR 603.3b + CR 605.3b + CR 605.4a: EVERY completed root mana + // frame settles through the one typed seam, whether or not its durable + // ledger holds replacement-paused events and whatever it resumes to — and it + // settles **before** `resume_mana_ability_root`, not after. + // + // Baseline had two bypasses here. The durable-ledger branch called the + // typed seam; a `ManaPayment`/`UnlessPayment` resume with an empty ledger + // called `process_triggers` directly (because the post-action pipeline is + // guarded by `waiting_for == Priority` and would otherwise drop every + // already-paid cost observer — Scavenger's Talent, Korvold, Mayhem Devil, + // ...; #5963); and a `Priority` resume with an empty ledger fell through to + // the pipeline's own generic scan. The first bypass could dispatch an + // ordinary cost observer separately from this frame's synthetic reflexive; + // the second could let the pipeline rediscover events the frame already + // owns. The typed seam closes both: it journals every live occurrence it + // claims into `consumed_before_priority_trigger_events`, so the pipeline's + // scan is narrowed by the journal rather than by excluding the `Priority` + // resume, and nothing double-fires (e.g. Kilo's becomes-tapped proliferate + // under a standalone Relic activation). + // + // Settlement-before-resume is what makes CR 605.4a hold at a durable-ledger + // root: this frame's accepted triggered mana must be spendable BY the thing + // it resumes into (the automatic payment finalizer, the unless-payment + // poll, the pay-to-end permission), exactly as it already is at the colour + // and `TapLandForMana` roots. It is also the only ordering at which + // `pending.resume` still exists, so the frame can name its own root in + // `ManaTriggerFixedPointResume::Root` for an accepted pause instead of the + // `Parent` variant, which is factually wrong for a parentless root. + if settles_completed_frame { + debug_assert!(cursor.parent.is_none()); + if let Some(pause) = settle_mana_ability_cost_events( state, - cursor.deferred_cost_events, + std::mem::take(&mut cursor.deferred_cost_events), events, cost_event_start, - ); - return Ok( - super::triggers::preserve_order_triggers_resume(state, resume.clone()) - .unwrap_or(resume), - ); + ManaTriggerFixedPointResume::Root { + player: pending.player, + resume: Box::new(pending.resume.clone()), + }, + ) { + return Ok(pause); + } } - // CR 603.2 + CR 603.3b + CR 605.3b: A direct (non-paused) mana-ability - // action normally reaches the ordinary post-action trigger pipeline, which - // remains its settlement authority. But that pipeline only runs for a - // `Priority` resume (it is guarded by `waiting_for == Priority`). A mana - // ability activated during mana payment or an unless-cost payment resumes - // to `ManaPayment`/`UnlessPayment`, which the pipeline skips — so its - // already-paid cost events (sacrifice, discard, exile) would never be - // scanned and every observer would be dropped (Scavenger's Talent, Korvold, - // Mayhem Devil, ...; #5963). Scan them here, the single settlement - // authority, exactly as the `ChooseManaColor` branch above already does for - // an interrupted mana-color choice. A `Priority` resume stays EXCLUDED so - // the pipeline remains the sole scan site and nothing double-fires (e.g. - // Kilo's becomes-tapped proliferate under a standalone Relic activation). - if !matches!(resume, WaitingFor::Priority { .. }) && events.len() > cost_event_start { - let cost_events = events[cost_event_start..].to_vec(); - super::triggers::process_triggers(state, &cost_events); + let resume = resume_mana_ability_root(state, pending.player, pending.resume, events)?; + if super::casting::mana_ability_cost_payment_is_paused(state) { + debug_assert!(is_ultimate_root); + // A settled frame has nothing left to hand upward: its ledger was taken + // and its live occurrences are already journaled, so re-deferring them + // into the next root would let that root's durable segment — which the + // journal does NOT filter — collect them a second time. + if !settles_completed_frame { + defer_cost_events_into_active_mana_root( + state, + cursor.deferred_cost_events, + &events[cost_event_start..], + ); + } + return Ok(resume); + } + + if settles_completed_frame { return Ok( super::triggers::preserve_order_triggers_resume(state, resume.clone()) .unwrap_or(resume), @@ -2897,33 +3073,72 @@ fn finish_mana_ability_cost_payment( /// pause. Deferred events were never scanned by their initiating action; current /// events are scanned here even when the nominal resume is Priority. fn settle_mana_ability_cost_events( + state: &mut GameState, + deferred: Vec, + events: &mut Vec, + current_start: usize, + outer_resume: ManaTriggerFixedPointResume, +) -> Option { + debug_assert!( + !matches!(outer_resume, ManaTriggerFixedPointResume::Parent), + "a parentless root must name its own resume, never the Parent variant" + ); + collect_completed_mana_frame_events(state, deferred, events, current_start, outer_resume) +} + +/// CR 603.2 + CR 603.3b + CR 605.4a: prepare the exact durable-plus-unconsumed-live +/// batch for one completed mana frame and drive it through the single +/// classifier/dispatcher fixed point. +/// +/// Used for **every** completed mana frame, whether or not its durable ledger is +/// empty, so the no-ledger direct/colour/`TapLandForMana` shapes stop falling +/// through to a separate aggregate scan. +/// +/// The historical inline output of the durable segment appears once in the +/// logical batch and once only as a public copy in the returned event vector; +/// the copies are excluded from the live logical segment while their live +/// occurrences are still journaled, so the durable tail and the copied public +/// tail are never concatenated into the trigger batch twice. +pub(crate) fn collect_completed_mana_frame_events( state: &mut GameState, mut deferred: Vec, events: &mut Vec, current_start: usize, -) { + outer_resume: ManaTriggerFixedPointResume, +) -> Option { let deferred_original_len = deferred.len(); super::triggers::resolve_tap_mana_triggers_inline(state, &mut deferred, 0); + let historical_copy_start = events.len(); events.extend(deferred[deferred_original_len..].iter().cloned()); + let historical_copy_end = events.len(); super::triggers::resolve_tap_mana_triggers_inline(state, events, current_start); - deferred.extend_from_slice(&events[current_start..]); - if !deferred.is_empty() { - super::triggers::process_triggers(state, &deferred); - } - // The typed cursor has already collected the events emitted by this action. - // Claim their exact occurrences through the normal post-action authority so - // a Priority resume cannot scan the same events a second time. - let occurrences = events - .iter() - .enumerate() - .skip(current_start) + + // One chronological logical batch: the durable segment exactly once, then + // every live event this frame owns that an earlier synchronous child or a + // completed resume prefix has not already claimed. + let consumed = state.consumed_before_priority_trigger_events.clone(); + let live_indices: Vec = (current_start..events.len()) + .filter(|index| !(historical_copy_start..historical_copy_end).contains(index)) + .filter(|index| { + let occurrence = super::triggers::trigger_event_occurrence(events, *index); + !consumed + .iter() + .any(|claimed| claimed.event == events[*index] && claimed.occurrence == occurrence) + }) + .collect(); + let mut batch = deferred; + batch.extend(live_indices.iter().map(|index| events[*index].clone())); + + let live_end = events.len(); + let pause = super::triggers::collect_mana_action_trigger_batch(state, &batch, outer_resume); + + // Only after the combined collection result is durably owned: claim every + // exact live occurrence this frame is responsible for, by full-action index. + let occurrences = (current_start..live_end) .map( - |(index, event)| crate::game::triggers::ConsumedTriggerEventOccurrence { - event: event.clone(), - occurrence: events[..index] - .iter() - .filter(|prior| *prior == event) - .count(), + |index| crate::game::triggers::ConsumedTriggerEventOccurrence { + event: events[index].clone(), + occurrence: super::triggers::trigger_event_occurrence(events, index), }, ) .collect(); @@ -2936,6 +3151,7 @@ fn settle_mana_ability_cost_events( .expect( "mana-ability cost-settlement consumed-before-priority trigger journal cause must be live", ); + pause } /// CR 603.2 + CR 603.3b: A parent payment can immediately pause again after @@ -2960,8 +3176,55 @@ fn defer_cost_events_into_active_mana_root( cursor .deferred_cost_events .extend_from_slice(¤t[..inherited_current_len]); + cursor.current_action_deferred_start = cursor.deferred_cost_events.len(); cursor.deferred_cost_events.extend(local_events); - cursor.current_action_deferred_start = 0; +} + +/// CR 605.4a: convert one completed fixed point's typed outer continuation back +/// into the suspended mana frame's own wait, exactly once. +/// +/// This is the resumed-occurrence counterpart of the tails +/// `finish_mana_ability_cost_payment` and `handle_choose_mana_color` run +/// synchronously, and it mirrors them exactly — including the paused-payment +/// early exit and the `preserve_order_triggers_resume` wrap. It never re-defers +/// a cost-event ledger: the frame that installed this continuation already took +/// its ledger and journaled its live occurrences before pausing. +pub(crate) fn resume_settled_mana_frame( + state: &mut GameState, + outer_resume: ManaTriggerFixedPointResume, + events: &mut Vec, +) -> Result, EngineError> { + match outer_resume { + // A nested child frame's suspended parent cursor is the authority; no + // wait is reconstructed here. + ManaTriggerFixedPointResume::Parent => Ok(None), + ManaTriggerFixedPointResume::Root { player, resume } => { + let resumed = resume_mana_ability_root(state, player, *resume, events)?; + if super::casting::mana_ability_cost_payment_is_paused(state) { + return Ok(Some(resumed)); + } + Ok(Some( + super::triggers::preserve_order_triggers_resume(state, resumed.clone()) + .unwrap_or(resumed), + )) + } + ManaTriggerFixedPointResume::ColorChoice(choice) => { + let ManaColorChoiceResume { + player, + choice, + context, + } = *choice; + let resume = WaitingFor::ChooseManaColor { + player, + choice, + context, + }; + Ok(Some( + super::triggers::preserve_order_triggers_resume(state, resume.clone()) + .unwrap_or(resume), + )) + } + } } pub(crate) fn resume_mana_ability_root( @@ -4520,6 +4783,107 @@ mod tests { use crate::game::test_fixtures::mana_fixture_roles; + /// **CR 605.4a — the acceptance decision is THREADED, not re-derived.** + /// + /// `is_triggered_mana_ability` answers CR 605.1b about a + /// *classification-time* graph, and it is deliberately raw: a target + /// anywhere makes it false. `is_resolving_triggered_mana` answers a + /// different question at a different time — "is the occurrence currently + /// executing an accepted triggered mana ability?" — because + /// `resolve_ability_chain` may materialize an engine resolution-context + /// referent into the overloaded `targets` vector partway through the very + /// resolution whose status is being asked about. CR 605.4a says that + /// occurrence stays stackless; re-asking the raw predicate would say + /// otherwise and hand the body to the ordinary prompt path. + /// + /// Rows: + /// + /// * **(a) no marker, qualifying graph** ⇒ true, delegated; + /// * **(b) no marker, targeted graph** ⇒ false, delegated. (a)+(b) are the + /// two-sided reach guard that the delegation is real rather than a + /// constant; + /// * **(c) no marker, wrong firing event** ⇒ false, so the ambient + /// `current_trigger_event` is genuinely consulted; + /// * **(d) marker live, targeted graph** ⇒ **true** — the delta. This is + /// exactly the clone shape (b) rejects, so the two rows differ only by + /// the marker; + /// * **(e) marker cleared again** ⇒ (b)'s answer returns, proving the + /// marker is scope-shaped rather than sticky. + /// + /// REVERT-PROBE: delete the marker short-circuit so the helper always + /// delegates ⇒ (d) flips to false while (a), (b), (c) and (e) still pass. + /// The inverse probe — returning `true` whenever the marker is `None` — is + /// caught by (b) and (c). + #[test] + fn the_accepted_occurrence_marker_is_the_resolution_time_classification_authority() { + use crate::types::ability::{ManaProduction, QuantityExpr}; + use crate::types::resolved_commands::{RulesExecutionNodeRef, SettlementNodeOrdinal}; + + let mana_effect = || Effect::Mana { + produced: ManaProduction::Colorless { + count: QuantityExpr::Fixed { value: 1 }, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }; + let untargeted = ResolvedAbility::new(mana_effect(), vec![], ObjectId(1), PlayerId(0)); + // The post-injection clone: identical body, but the resolver has + // written a context referent into the overloaded `targets` vector. + let injected = ResolvedAbility::new( + mana_effect(), + vec![crate::types::ability::TargetRef::Player(PlayerId(1))], + ObjectId(1), + PlayerId(0), + ); + + let mut state = GameState::new_two_player(42); + state.current_trigger_event = Some(GameEvent::ManaAdded { + player_id: PlayerId(0), + mana_type: ManaType::Colorless, + source_id: ObjectId(1), + tap_state: ManaTapState::default(), + }); + + // (a) + (b): both truth values are reachable through delegation. + assert!( + is_resolving_triggered_mana(&state, &untargeted), + "(a) with no accepted occurrence live this must BE the raw classifier" + ); + assert!( + !is_resolving_triggered_mana(&state, &injected), + "(b) CR 605.1b at classification time: a target in the graph rejects" + ); + + // (c) the ambient firing event is genuinely part of the delegation. + let restore_event = state.current_trigger_event.take(); + assert!( + !is_resolving_triggered_mana(&state, &untargeted), + "(c) CR 605.1b(b): no qualifying firing event, no triggered mana ability" + ); + state.current_trigger_event = restore_event; + + // (d) THE DELTA: inside an accepted occurrence the same rejected clone + // is still the accepted occurrence's own body. + let node = RulesExecutionNodeRef::TriggeredMana(SettlementNodeOrdinal(1)); + state.active_rules_execution_node = Some(node); + state.active_accepted_triggered_mana_node = Some(node); + assert!( + is_resolving_triggered_mana(&state, &injected), + "(d) CR 605.4a: a resolution-context referent injected AFTER acceptance \ + does not make an already-accepted occurrence begin using the stack" + ); + + // (e) the marker is scope-shaped: restoring it restores (b)'s answer. + state.active_accepted_triggered_mana_node = None; + state.active_rules_execution_node = None; + assert!( + !is_resolving_triggered_mana(&state, &injected), + "(e) outside the occurrence the raw classification-time answer returns" + ); + } + /// Matrix rows 15c + 20 — CR 605.1a classification is unchanged. This reader /// also bypasses `Effect::target_filter()`. /// @@ -4662,6 +5026,67 @@ mod tests { ); } + #[test] + fn targetless_mana_reflexive_produces_mana_now_and_waits_on_stack() { + let mut state = GameState::new_two_player(42); + let player = PlayerId(0); + state.active_player = player; + state.priority_player = player; + state.waiting_for = WaitingFor::Priority { player }; + let source = create_object( + &mut state, + CardId(9901), + player, + "Rubble Rouser fixture".to_string(), + Zone::Battlefield, + ); + let mut reflexive = AbilityDefinition::new( + AbilityKind::Database, + Effect::LoseLife { + amount: QuantityExpr::Fixed { value: 2 }, + target: None, + }, + ); + reflexive.condition = Some(AbilityCondition::WhenYouDo); + reflexive.player_scope = Some(PlayerFilter::Opponent); + let mana = make_mana_ability(ManaProduction::Fixed { + colors: vec![ManaColor::Green], + contribution: ManaContribution::Base, + }) + .sub_ability(reflexive); + Arc::make_mut(&mut state.objects.get_mut(&source).unwrap().abilities).push(mana); + let opponent_life = state.players[1].life; + + crate::game::engine::apply_as_current( + &mut state, + crate::types::actions::GameAction::ActivateAbility { + source_id: source, + ability_index: 0, + }, + ) + .expect("activate targetless reflexive mana ability"); + + assert_eq!(state.players[0].mana_pool.count_color(ManaType::Green), 1); + assert_eq!(state.players[1].life, opponent_life); + assert_eq!(state.stack.len(), 1); + assert!(matches!( + state.stack[0].kind, + crate::types::game_state::StackEntryKind::TriggeredAbility { .. } + )); + assert!(matches!(state.waiting_for, WaitingFor::Priority { .. })); + + let mut safety = 4; + while !state.stack.is_empty() && safety > 0 { + crate::game::engine::apply_as_current( + &mut state, + crate::types::actions::GameAction::PassPriority, + ) + .expect("pass priority to resolve reflexive"); + safety -= 1; + } + assert_eq!(state.players[1].life, opponent_life - 2); + } + #[test] fn scoped_mana_ability_tap_event_aggregates_recipient_dependent_production() { let mut state = GameState::new(crate::types::format::FormatConfig::standard(), 3, 42); @@ -13338,4 +13763,253 @@ mod tests { // selection: the gate returns None even though the sole detector matched it. assert!(discard_cost_choice(&state, PlayerId(0), source, &Some(random_leg)).is_none()); } + + fn ledger_event(source_id: u64) -> GameEvent { + GameEvent::EffectResolved { + kind: crate::types::ability::EffectKind::NoOp, + source_id: ObjectId(source_id), + subject: None, + } + } + + #[test] + fn nested_mana_cursor_starts_with_empty_frame_local_ledger() { + let mut parent_cursor = mana_ability_cost_cursor( + &None, + &HashSet::new(), + None, + ManaAbilityCostResolutionMode::AutoResolved, + None, + ); + parent_cursor.deferred_cost_events.push(ledger_event(1)); + let parent = ManaAbilityCostParent { + pending: Box::new(pending_for(ObjectId(10))), + cursor: Box::new(parent_cursor), + lifecycle: ManaAbilityCostParentLifecycle::Synchronous, + current_action_event_start: 0, + }; + + let child = mana_ability_cost_cursor( + &None, + &HashSet::new(), + None, + ManaAbilityCostResolutionMode::AutoResolved, + Some(&parent), + ); + + assert!(child.deferred_cost_events.is_empty()); + assert_eq!( + child + .parent + .as_deref() + .expect("child must retain its parent snapshot") + .cursor + .deferred_cost_events, + [ledger_event(1)] + ); + } + + #[test] + fn suspended_child_ledger_extends_parent_without_loss_or_overwrite() { + let mut parent = mana_ability_cost_cursor( + &None, + &HashSet::new(), + None, + ManaAbilityCostResolutionMode::AutoResolved, + None, + ); + parent.deferred_cost_events = vec![ledger_event(1), ledger_event(2)]; + let mut child = mana_ability_cost_cursor( + &None, + &HashSet::new(), + None, + ManaAbilityCostResolutionMode::AutoResolved, + None, + ); + child.deferred_cost_events = vec![ledger_event(3)]; + + append_suspended_child_cost_events(&mut parent, &mut child, &[ledger_event(4)]); + + assert_eq!( + parent.deferred_cost_events, + [ + ledger_event(1), + ledger_event(2), + ledger_event(3), + ledger_event(4) + ] + ); + assert!(child.deferred_cost_events.is_empty()); + } + + #[test] + fn repeated_pause_transfer_preserves_local_suffix_boundary() { + let mut active_cursor = mana_ability_cost_cursor( + &None, + &HashSet::new(), + None, + ManaAbilityCostResolutionMode::AutoResolved, + None, + ); + active_cursor.deferred_cost_events = vec![ledger_event(5), ledger_event(8)]; + active_cursor.current_action_deferred_start = 1; + let mut state = GameState::new_two_player(42); + state.pending_cost_move_resume = Some(PendingCostMoveResume::ManaAbilityPayment { + pending: Box::new(pending_for(ObjectId(10))), + cursor: active_cursor, + }); + + defer_cost_events_into_active_mana_root( + &mut state, + vec![ledger_event(1), ledger_event(2)], + &[ledger_event(7), ledger_event(8)], + ); + + let Some(PendingCostMoveResume::ManaAbilityPayment { cursor, .. }) = + state.pending_cost_move_resume.as_ref() + else { + panic!("expected active mana root"); + }; + assert_eq!( + cursor.deferred_cost_events, + [ + ledger_event(5), + ledger_event(1), + ledger_event(2), + ledger_event(7), + ledger_event(8) + ] + ); + assert_eq!(cursor.current_action_deferred_start, 4); + } + + fn synchronous_parent_at(start: usize, ledger: Vec) -> ManaAbilityCostParent { + let mut cursor = mana_ability_cost_cursor( + &None, + &HashSet::new(), + None, + ManaAbilityCostResolutionMode::AutoResolved, + None, + ); + cursor.deferred_cost_events = ledger; + ManaAbilityCostParent { + pending: Box::new(pending_for(ObjectId(10))), + cursor: Box::new(cursor), + lifecycle: ManaAbilityCostParentLifecycle::Synchronous, + current_action_event_start: start, + } + } + + /// CR 603.2 + CR 603.3b: The prepared child-facing snapshot owns the live + /// parent frame's current unscanned suffix, while the live parent cursor is + /// never mutated. A synchronous child that drops the snapshot therefore + /// leaves exactly one representation of those events — the reducer vector. + #[test] + fn prepared_parent_snapshot_carries_current_suffix_without_mutating_live_parent() { + let parent = synchronous_parent_at(0, Vec::new()); + let events = vec![ledger_event(1), ledger_event(2)]; + + let prepared = parent_snapshot_with_current_cost_events(&parent, &events); + + assert_eq!( + prepared.cursor.deferred_cost_events, + [ledger_event(1), ledger_event(2)], + "prepared snapshot must carry the parent's current unscanned suffix" + ); + assert!( + parent.cursor.deferred_cost_events.is_empty(), + "the live parent cursor must not be mutated by preparation" + ); + + let child = mana_ability_cost_cursor( + &None, + &HashSet::new(), + None, + ManaAbilityCostResolutionMode::AutoResolved, + Some(&prepared), + ); + assert!( + child.deferred_cost_events.is_empty(), + "a nested child's top-level ledger starts empty" + ); + assert_eq!( + child + .parent + .as_deref() + .expect("child retains its prepared parent") + .cursor + .deferred_cost_events, + [ledger_event(1), ledger_event(2)] + ); + } + + /// CR 603.2 + CR 603.3b: Preparation runs at every child entry from the + /// unchanged ephemeral parent, so a later child sees the parent's own prefix + /// plus every earlier synchronously completed sibling exactly once. Extending + /// a previously prepared snapshot instead would double-append the prefix. + #[test] + fn later_child_snapshot_includes_earlier_synchronous_sibling_events_once() { + let parent = synchronous_parent_at(0, Vec::new()); + let mut events = vec![ledger_event(1)]; + + let first = parent_snapshot_with_current_cost_events(&parent, &events); + assert_eq!(first.cursor.deferred_cost_events, [ledger_event(1)]); + + // The first child completed synchronously and emitted its own event. + events.push(ledger_event(2)); + let second = parent_snapshot_with_current_cost_events(&parent, &events); + + assert_eq!( + second.cursor.deferred_cost_events, + [ledger_event(1), ledger_event(2)], + "a later child prepares from the unchanged ephemeral parent, not from an earlier snapshot" + ); + } + + /// CR 603.2 + CR 603.3b: The parent frame's own `cost_event_start` is the + /// marker, so a parent that already scanned an earlier prefix contributes + /// only its unscanned suffix to the child snapshot. + #[test] + fn prepared_parent_snapshot_starts_at_the_parent_frame_marker() { + let parent = synchronous_parent_at(1, Vec::new()); + let events = vec![ledger_event(1), ledger_event(2), ledger_event(3)]; + + let prepared = parent_snapshot_with_current_cost_events(&parent, &events); + + assert_eq!( + prepared.cursor.deferred_cost_events, + [ledger_event(2), ledger_event(3)] + ); + } + + /// CR 603.2 + CR 603.3b: A pause makes the prepared prefix durable, so the + /// ephemeral marker is never consulted again and is deliberately not + /// serialized. Round-tripping a suspended parent keeps the prefix while the + /// marker resets to its default. + #[test] + fn suspended_parent_prefix_survives_serde_while_marker_is_skipped() { + let mut parent = synchronous_parent_at(0, Vec::new()); + parent = parent_snapshot_with_current_cost_events(&parent, &[ledger_event(1)]); + parent.lifecycle = ManaAbilityCostParentLifecycle::Suspended; + parent.current_action_event_start = 7; + + let json = serde_json::to_string(&parent).expect("serialize suspended parent"); + assert!( + !json.contains("current_action_event_start"), + "the ephemeral marker must not be serialized: {json}" + ); + let restored: ManaAbilityCostParent = + serde_json::from_str(&json).expect("deserialize suspended parent"); + + assert_eq!( + restored.cursor.deferred_cost_events, + [ledger_event(1)], + "the durable prefix must survive suspension" + ); + assert_eq!(restored.current_action_event_start, 0); + assert!(matches!( + restored.lifecycle, + ManaAbilityCostParentLifecycle::Suspended + )); + } } diff --git a/crates/engine/src/game/resolution_prompt.rs b/crates/engine/src/game/resolution_prompt.rs index 0df0fa0bef..ea745ac3a5 100644 --- a/crates/engine/src/game/resolution_prompt.rs +++ b/crates/engine/src/game/resolution_prompt.rs @@ -547,6 +547,7 @@ pub(crate) fn chain_offers_choice(a: &ResolvedAbility) -> bool { multi_target: _, // announce-time variable-count bounds (Resolution case caught by timing) target_constraints: _, // announce-time cross-target legality, no resolution prompt distribution: _, // CR 601.2d concrete pre-assigned portions (announce-time) + distribute: _, // CR 601.2d/603.3d unassigned division is an announce-time choice targets: _, // concrete announced target refs (already resolved) source_id: _, // object id source_incarnation: _, // self-transform epoch latch, no resolution-time choice @@ -708,6 +709,40 @@ mod tests { ProbeBudget::for_test(PROBE_BUDGET) } + /// CR 601.2d + CR 603.3d: an unassigned division UNIT is announcement metadata. + /// The division itself is answered while the object is announced (the trigger's + /// `DistributeAmong` prompt), never during resolution, so toggling only + /// `distribute` may not move the resolution-choice verdict. + /// + /// The base ability must be an ALLOW-LISTED choice-free effect with a fixed + /// quantity. `Effect::NoOp` is NOT one: `effect_offers_choice` fail-closes every + /// unclassified variant to `true`, so a `NoOp` base reports `MayPrompt` before + /// `distribute` is even read and the row would pass for the wrong reason in the + /// negative direction and fail outright in the positive one. + #[test] + fn unassigned_distribution_unit_is_not_a_resolution_choice() { + let base = ResolvedAbility::new( + Effect::DealDamage { + amount: fixed(3), + target: TargetFilter::Typed(crate::types::ability::TypedFilter::creature()), + damage_source: None, + excess: None, + }, + Vec::new(), + ObjectId(1), + PlayerId(0), + ); + assert!( + !chain_offers_choice(&base), + "reach guard: the undivided base must already be choice-free, otherwise the \ + divided clone below proves nothing" + ); + + let mut divided = base.clone(); + divided.distribute = Some(crate::types::game_state::DistributionUnit::Damage); + assert!(!chain_offers_choice(÷d)); + } + /// Snapshot every axis the witness reads. fn board_axes(state: &GameState) -> BoardAxes { BoardAxes { diff --git a/crates/engine/src/game/stack.rs b/crates/engine/src/game/stack.rs index ef74710c04..a03faec6fd 100644 --- a/crates/engine/src/game/stack.rs +++ b/crates/engine/src/game/stack.rs @@ -932,59 +932,104 @@ pub(crate) fn bind_resolution_scope( entry: &StackEntry, trigger_event_batch: Option>, ) -> bool { - // CR 603.4: Intervening-if condition rechecked at resolution time. - if let StackEntryKind::TriggeredAbility { - condition: Some(ref condition), - source_id: _, - ref trigger_event, - .. - } = &entry.kind - { - let trigger_source = entry - .ability() - .and_then(|ability| ability.trigger_source.as_ref()); - if !super::triggers::check_trigger_condition_with_source( - state, + let triggered = match &entry.kind { + StackEntryKind::TriggeredAbility { condition, - entry.controller, - trigger_source, - trigger_event.as_ref(), - ) { - return false; + trigger_event, + subject_match_count, + die_result, + .. + } => Some(TriggeredResolutionScope { + condition: condition.as_ref(), + controller: entry.controller, + trigger_source: entry + .ability() + .and_then(|ability| ability.trigger_source.as_ref()), + trigger_event: trigger_event.as_ref(), + subject_match_count: *subject_match_count, + die_result: *die_result, + }), + _ => None, + }; + bind_triggered_resolution_scope(state, triggered, trigger_event_batch) +} + +/// The facts a triggered ability contributes to its own resolution scope, +/// lifted out of `StackEntryKind::TriggeredAbility` so a resolution that owns +/// no stack entry can bind exactly the same scope. +/// +/// CR 605.4a triggered mana abilities are the motivating second consumer: they +/// resolve without ever creating a stack object, yet they still need the +/// CR 603.4 recheck, the CR 608.2k event context, the CR 603.2c subject count, +/// and the CR 706.2 die-roll re-stamp to be bound in exactly the order and with +/// exactly the semantics stack resolution uses. Reimplementing that binding +/// beside the immediate dispatcher would be a second authority for CR 603.4. +pub(crate) struct TriggeredResolutionScope<'a> { + pub condition: Option<&'a TriggerCondition>, + pub controller: PlayerId, + pub trigger_source: Option<&'a TriggerSourceContext>, + pub trigger_event: Option<&'a GameEvent>, + pub subject_match_count: Option, + pub die_result: Option, +} + +/// The decision-and-binding half of [`bind_resolution_scope`], with no stack +/// entry in sight. Returns `false` exactly when the CR 603.4 intervening-if +/// recheck fails, in which case **nothing** has been bound — the caller must +/// abandon the resolution without applying any effect. +/// +/// `triggered` is `None` for a non-triggered resolution (a spell, an activated +/// ability, a keyword action); such a scope has no condition, no subject count, +/// and no die result, and reaches only the batch branch below. +pub(crate) fn bind_triggered_resolution_scope( + state: &mut GameState, + triggered: Option>, + trigger_event_batch: Option>, +) -> bool { + // CR 603.4: Intervening-if condition rechecked at resolution time. + if let Some(scope) = &triggered { + if let Some(condition) = scope.condition { + if !super::triggers::check_trigger_condition_with_source( + state, + condition, + scope.controller, + scope.trigger_source, + scope.trigger_event, + ) { + return false; + } } } // CR 608.2k: Set trigger event context for event-context target resolution. // TriggeringSpellController, TriggeringSource, etc. read this during resolution. - if let StackEntryKind::TriggeredAbility { - trigger_event: Some(ref te), - .. - } = entry.kind - { - state.current_trigger_event = Some(te.clone()); - state.current_trigger_events = trigger_event_batch.unwrap_or_else(|| vec![te.clone()]); - } else if let Some(trigger_events) = trigger_event_batch { - state.current_trigger_event = trigger_events.first().cloned(); - state.current_trigger_events = trigger_events; + match ( + triggered.as_ref().and_then(|scope| scope.trigger_event), + trigger_event_batch, + ) { + (Some(te), batch) => { + state.current_trigger_event = Some(te.clone()); + state.current_trigger_events = batch.unwrap_or_else(|| vec![te.clone()]); + } + (None, Some(trigger_events)) => { + state.current_trigger_event = trigger_events.first().cloned(); + state.current_trigger_events = trigger_events; + } + (None, None) => {} } // CR 603.2c: Lift the filtered subject count of a batched trigger into // resolution scope so `QuantityRef::EventContextAmount` resolves "that // many" against the count, not against zero. Set in lockstep with // `current_trigger_event` and cleared at every reset site below. - if let StackEntryKind::TriggeredAbility { - subject_match_count, - die_result, - .. - } = entry.kind - { - state.current_trigger_match_count = subject_match_count; + if let Some(scope) = &triggered { + state.current_trigger_match_count = scope.subject_match_count; // CR 706.2 + CR 706.4 + CR 603.12: re-stamp the carried die-roll result // into resolution scope so a reflexive "When you do … the result" // sub-ability resolving on its own stack entry (a later apply(), after // the original roll's resolution scope cleared) reads the rolled value // via the `QuantityRef::EventContextAmount` cascade. - state.die_result_this_resolution = die_result; + state.die_result_this_resolution = scope.die_result; } true @@ -3098,6 +3143,7 @@ fn self_counter_ability_is_batch_candidate(ability: &ResolvedAbility) -> bool { forward_result, unless_pay, distribution, + distribute, player_scope, starting_with, chosen_x, @@ -3160,6 +3206,7 @@ fn self_counter_ability_is_batch_candidate(ability: &ResolvedAbility) -> bool { && !*forward_result && unless_pay.is_none() && distribution.is_none() + && distribute.is_none() && player_scope.is_none() && starting_with.is_none() && chosen_x.is_none() @@ -3311,6 +3358,7 @@ fn fixed_controller_gain_life_ability_is_batch_candidate(ability: &ResolvedAbili forward_result, unless_pay, distribution, + distribute, player_scope, starting_with, chosen_x, @@ -3365,6 +3413,7 @@ fn fixed_controller_gain_life_ability_is_batch_candidate(ability: &ResolvedAbili && !*forward_result && unless_pay.is_none() && distribution.is_none() + && distribute.is_none() && player_scope.is_none() && starting_with.is_none() && chosen_x.is_none() @@ -3504,6 +3553,7 @@ fn fixed_opponent_lose_life_ability_is_batch_candidate(ability: &ResolvedAbility forward_result, unless_pay, distribution, + distribute, player_scope, starting_with, chosen_x, @@ -3558,6 +3608,7 @@ fn fixed_opponent_lose_life_ability_is_batch_candidate(ability: &ResolvedAbility && !*forward_result && unless_pay.is_none() && distribution.is_none() + && distribute.is_none() && *player_scope == Some(PlayerFilter::Opponent) && starting_with.is_none() && chosen_x.is_none() @@ -4144,6 +4195,7 @@ fn inert_trigger_abilities_eq_ignoring_provenance( forward_result: a_forward_result, unless_pay: a_unless_pay, distribution: a_distribution, + distribute: a_distribute, player_scope: a_player_scope, starting_with: a_starting_with, chosen_x: a_chosen_x, @@ -4200,6 +4252,7 @@ fn inert_trigger_abilities_eq_ignoring_provenance( forward_result: b_forward_result, unless_pay: b_unless_pay, distribution: b_distribution, + distribute: b_distribute, player_scope: b_player_scope, starting_with: b_starting_with, chosen_x: b_chosen_x, @@ -4267,6 +4320,7 @@ fn inert_trigger_abilities_eq_ignoring_provenance( && a_forward_result == b_forward_result && a_unless_pay == b_unless_pay && a_distribution == b_distribution + && a_distribute == b_distribute && a_player_scope == b_player_scope && a_starting_with == b_starting_with && a_chosen_x == b_chosen_x @@ -4698,6 +4752,83 @@ mod tests { GameState::new_two_player(42) } + #[test] + fn unassigned_distribution_rejects_all_inert_batch_candidates() { + let self_counter = ResolvedAbility::new( + Effect::PutCounter { + counter_type: crate::types::counter::CounterType::Plus1Plus1, + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::SelfRef, + }, + Vec::new(), + ObjectId(1), + PlayerId(0), + ); + assert!(self_counter_ability_is_batch_candidate(&self_counter)); + let mut divided_counter = self_counter.clone(); + divided_counter.distribute = Some(crate::types::game_state::DistributionUnit::Counters( + "+1/+1".to_string(), + )); + assert!(!self_counter_ability_is_batch_candidate(÷d_counter)); + + let gain_life = ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 2 }, + player: TargetFilter::Controller, + }, + Vec::new(), + ObjectId(2), + PlayerId(0), + ); + assert!(fixed_controller_gain_life_ability_is_batch_candidate( + &gain_life + )); + let mut divided_gain = gain_life.clone(); + divided_gain.distribute = Some(crate::types::game_state::DistributionUnit::Life); + assert!(!fixed_controller_gain_life_ability_is_batch_candidate( + ÷d_gain + )); + + let mut lose_life = ResolvedAbility::new( + Effect::LoseLife { + amount: QuantityExpr::Fixed { value: 2 }, + target: None, + }, + Vec::new(), + ObjectId(3), + PlayerId(0), + ); + lose_life.player_scope = Some(crate::types::ability::PlayerFilter::Opponent); + assert!(fixed_opponent_lose_life_ability_is_batch_candidate( + &lose_life + )); + let mut divided_loss = lose_life.clone(); + divided_loss.distribute = Some(crate::types::game_state::DistributionUnit::Life); + assert!(!fixed_opponent_lose_life_ability_is_batch_candidate( + ÷d_loss + )); + } + + #[test] + fn inert_trigger_identity_compares_unassigned_distribution_unit() { + let mut a = ResolvedAbility::new(Effect::NoOp, Vec::new(), ObjectId(10), PlayerId(0)); + a.distribute = Some(crate::types::game_state::DistributionUnit::Damage); + let mut same_shape_different_provenance = a.clone(); + same_shape_different_provenance.source_id = ObjectId(11); + same_shape_different_provenance.ability_index = Some(7); + + assert!(inert_trigger_abilities_eq_ignoring_provenance( + &a, + &same_shape_different_provenance + )); + + same_shape_different_provenance.distribute = None; + assert!(!inert_trigger_abilities_eq_ignoring_provenance( + &a, + &same_shape_different_provenance + )); + } + fn pending_spell_entry(id: ObjectId) -> StackEntry { StackEntry { id, @@ -13570,6 +13701,274 @@ mod tests { } } + /// **The shared CR 603.4 / CR 608.2k / CR 603.2c / CR 706.2 binder is + /// entry-shaped only at its adapter.** + /// + /// `bind_triggered_resolution_scope` is the authority a stackless CR 605.4a + /// triggered mana resolution will call — it owns no `StackEntry` and must + /// therefore reach every binding shape the entry-shaped wrapper reaches. + /// The delicate part is that baseline's three `if let` blocks are **not** + /// three independent branches: the event/batch block is an `if / else if` + /// chain, so a triggered ability carrying `trigger_event: None` and a batch + /// falls THROUGH to the non-triggered batch arm. A rewrite that keys the + /// batch arm on "not a triggered ability" silently loses that case. + /// + /// Rows, each keyed to one thing the extraction could have broken: + /// + /// * **(a)** triggered + `Some(event)` + no batch ⇒ the batch is + /// synthesized as a one-element vector from that event; + /// * **(b)** triggered + `Some(event)` + a batch ⇒ the batch wins and the + /// singleton event stays the authoritative one; + /// * **(c)** triggered + `None` event + a batch ⇒ the fall-through arm, so + /// the authoritative event is the batch head; + /// * **(d)** NOT triggered + a batch ⇒ same arm, but CR 603.2c/CR 706.2 + /// must NOT be re-stamped, because a spell carries neither; + /// * **(e)** a false CR 603.4 intervening-if returns `false` having bound + /// **nothing** — the caller must be able to abandon without unwinding; + /// * **(f)** the entry-shaped adapter and a hand-built scope produce + /// byte-identical bindings from the same facts. + /// + /// REACH-GUARD: every row asserts the pre-call scope is the sentinel value, + /// so "unchanged" can never be confused with "bound to the same thing". + /// + /// REVERT-PROBES: making the batch arm `else if triggered.is_none()` fails + /// (c); dropping the `(Some(te), batch)` arm's `unwrap_or_else` fails (a); + /// stamping the count/die outside the `triggered` guard fails (d); binding + /// before the condition check fails (e). + #[test] + fn the_shared_resolution_scope_binder_reaches_every_baseline_binding_shape() { + let event_a = GameEvent::StackResolved { + object_id: ObjectId(9001), + }; + let event_b = GameEvent::StackResolved { + object_id: ObjectId(9002), + }; + let sentinel = GameEvent::StackResolved { + object_id: ObjectId(9999), + }; + + // Every row starts from a distinguishable sentinel scope, so a row that + // asserts a bound value cannot pass because nothing ran. + let armed = || { + let mut state = setup(); + state.current_trigger_event = Some(sentinel.clone()); + state.current_trigger_events = vec![sentinel.clone()]; + state.current_trigger_match_count = Some(77); + state.die_result_this_resolution = Some(77); + state + }; + // ── (a) triggered + event, no batch ⇒ synthesized singleton batch ── + { + let mut state = armed(); + assert!(bind_triggered_resolution_scope( + &mut state, + Some(TriggeredResolutionScope { + condition: None, + controller: PlayerId(0), + trigger_source: None, + trigger_event: Some(&event_a), + subject_match_count: Some(4), + die_result: Some(6), + }), + None, + )); + assert_eq!(state.current_trigger_event.as_ref(), Some(&event_a)); + assert_eq!( + state.current_trigger_events, + vec![event_a.clone()], + "CR 608.2k: with no batch the authoritative event IS the batch" + ); + assert_eq!(state.current_trigger_match_count, Some(4)); + assert_eq!(state.die_result_this_resolution, Some(6)); + } + + // ── (b) triggered + event + batch ⇒ the batch wins ── + { + let mut state = armed(); + assert!(bind_triggered_resolution_scope( + &mut state, + Some(TriggeredResolutionScope { + condition: None, + controller: PlayerId(0), + trigger_source: None, + trigger_event: Some(&event_a), + subject_match_count: None, + die_result: None, + }), + Some(vec![event_a.clone(), event_b.clone()]), + )); + assert_eq!(state.current_trigger_event.as_ref(), Some(&event_a)); + assert_eq!( + state.current_trigger_events, + vec![event_a.clone(), event_b.clone()] + ); + assert_eq!( + state.current_trigger_match_count, None, + "CR 603.2c: a triggered scope stamps its own None over the sentinel" + ); + } + + // ── (c) triggered + NO event + batch ⇒ the fall-through arm ── + { + let mut state = armed(); + assert!(bind_triggered_resolution_scope( + &mut state, + Some(TriggeredResolutionScope { + condition: None, + controller: PlayerId(0), + trigger_source: None, + trigger_event: None, + subject_match_count: Some(2), + die_result: None, + }), + Some(vec![event_b.clone(), event_a.clone()]), + )); + assert_eq!( + state.current_trigger_event.as_ref(), + Some(&event_b), + "the batch HEAD becomes authoritative — this is the `else if` \ + fall-through a triggered ability with no singleton event reaches" + ); + assert_eq!( + state.current_trigger_events, + vec![event_b.clone(), event_a.clone()] + ); + assert_eq!(state.current_trigger_match_count, Some(2)); + } + + // ── (d) not triggered + batch ⇒ count/die are NOT re-stamped ── + { + let mut state = armed(); + assert!(bind_triggered_resolution_scope( + &mut state, + None, + Some(vec![event_a.clone()]), + )); + assert_eq!(state.current_trigger_event.as_ref(), Some(&event_a)); + assert_eq!( + ( + state.current_trigger_match_count, + state.die_result_this_resolution + ), + (Some(77), Some(77)), + "CR 603.2c + CR 706.2 belong to a TRIGGERED entry only; a spell must \ + leave the ambient values exactly as it found them" + ); + } + + // ── (e) a false CR 603.4 recheck binds nothing at all ── + { + let mut state = armed(); + assert!(!bind_triggered_resolution_scope( + &mut state, + Some(TriggeredResolutionScope { + // `setup()` is a 20-life board, so this is FALSE. + condition: Some(&TriggerCondition::LifeTotalGE { minimum: 99 }), + controller: PlayerId(0), + trigger_source: None, + trigger_event: Some(&event_a), + subject_match_count: Some(4), + die_result: Some(6), + }), + Some(vec![event_a.clone(), event_b.clone()]), + )); + assert_eq!( + ( + state.current_trigger_event.as_ref(), + state.current_trigger_events.as_slice(), + state.current_trigger_match_count, + state.die_result_this_resolution, + ), + ( + Some(&sentinel), + [sentinel.clone()].as_slice(), + Some(77), + Some(77) + ), + "CR 603.4: the recheck is the FIRST thing the binder does, so a \ + refused resolution leaves the caller's scope untouched" + ); + // The TRUE twin proves the row is not passing on a broken condition. + let mut state = armed(); + assert!(bind_triggered_resolution_scope( + &mut state, + Some(TriggeredResolutionScope { + condition: Some(&TriggerCondition::LifeTotalGE { minimum: 5 }), + controller: PlayerId(0), + trigger_source: None, + trigger_event: Some(&event_a), + subject_match_count: Some(4), + die_result: Some(6), + }), + None, + )); + assert_eq!(state.current_trigger_match_count, Some(4)); + } + + // ── (f) the entry-shaped adapter agrees with the hand-built scope ── + { + let entry = StackEntry { + id: ObjectId(9100), + source_id: ObjectId(9101), + controller: PlayerId(0), + kind: StackEntryKind::TriggeredAbility { + source_id: ObjectId(9101), + ability: Box::new(ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + ObjectId(9101), + PlayerId(0), + )), + condition: Some(TriggerCondition::LifeTotalGE { minimum: 5 }), + trigger_event: Some(event_a.clone()), + description: None, + source_name: String::new(), + subject_match_count: Some(3), + die_result: Some(20), + provenance: None, + }, + }; + let mut via_entry = armed(); + assert!(bind_resolution_scope( + &mut via_entry, + &entry, + Some(vec![event_a.clone(), event_b.clone()]), + )); + let mut via_scope = armed(); + assert!(bind_triggered_resolution_scope( + &mut via_scope, + Some(TriggeredResolutionScope { + condition: Some(&TriggerCondition::LifeTotalGE { minimum: 5 }), + controller: PlayerId(0), + trigger_source: None, + trigger_event: Some(&event_a), + subject_match_count: Some(3), + die_result: Some(20), + }), + Some(vec![event_a.clone(), event_b.clone()]), + )); + assert_eq!( + ( + via_entry.current_trigger_event, + via_entry.current_trigger_events, + via_entry.current_trigger_match_count, + via_entry.die_result_this_resolution, + ), + ( + via_scope.current_trigger_event, + via_scope.current_trigger_events, + via_scope.current_trigger_match_count, + via_scope.die_result_this_resolution, + ), + "the adapter is a projection of the entry onto the shared scope, \ + not a second binding policy" + ); + } + } + // ----------------------------------------------------------------------- // C2: resolution-default moves route through the zone pipeline so Moved // graveyard→exile redirects (Rest in Peace / Leyline of the Void class) diff --git a/crates/engine/src/game/triggers.rs b/crates/engine/src/game/triggers.rs index 6021b20a6d..2979b4df21 100644 --- a/crates/engine/src/game/triggers.rs +++ b/crates/engine/src/game/triggers.rs @@ -17,10 +17,11 @@ use crate::types::ability::{EffectScope, TapStateChange}; use crate::types::card_type::CoreType; use crate::types::events::{GameEvent, ManaTapState}; use crate::types::game_state::{ - AutoMayChoice, DamageRecord, DelayedTrigger, DistributionUnit, GameState, - LatchedBatchedTrigger, LatchedSuppressTrigger, LogicalZoneChangeGroup, - LogicalZoneChangeTerminalOutcome, MayTriggerAutoChoiceKey, MayTriggerOrigin, StackEntry, - StackEntryKind, SyntheticTriggerProvenance, TargetSelectionConstraint, TargetSelectionSlot, + AutoMayChoice, CollectedTriggerContextBatch, DamageRecord, DelayedTrigger, DistributionUnit, + GameState, LatchedBatchedTrigger, LatchedSuppressTrigger, LogicalZoneChangeGroup, + LogicalZoneChangeTerminalOutcome, MayTriggerAutoChoiceKey, MayTriggerOrigin, + ProductionOverride, StackEntry, StackEntryKind, SyntheticTriggerProvenance, + TargetSelectionConstraint, TargetSelectionSlot, TriggerObservationTime, TriggerSourceContext, WaitingFor, }; use crate::types::identifiers::{ @@ -6884,22 +6885,28 @@ pub(crate) fn park_observer_triggers_if_paused( events: &[GameEvent], slice_start: usize, ) { + if triggered_mana_sidecar_owns_local_collection(state) { + return; + } if matches!(state.waiting_for, WaitingFor::Priority { .. }) { return; } - let trigger_events: Vec = events[slice_start..] - .iter() - .filter(|ev| { - // CR 707.10: `copy_spell` collects `SpellCopied` observers at - // announcement and drains them after CopyRetarget finalization. - // Re-parking the same event duplicates Magecraft (issue #2866). - !matches!( - ev, - GameEvent::PhaseChanged { .. } | GameEvent::SpellCopied { .. } - ) - }) - .cloned() - .collect(); + let trigger_events: Vec = filter_consumed_trigger_events_from( + events, + slice_start, + &state.consumed_before_priority_trigger_events, + ) + .into_iter() + .filter(|ev| { + // CR 707.10: `copy_spell` collects `SpellCopied` observers at + // announcement and drains them after CopyRetarget finalization. + // Re-parking the same event duplicates Magecraft (issue #2866). + !matches!( + ev, + GameEvent::PhaseChanged { .. } | GameEvent::SpellCopied { .. } + ) + }) + .collect(); if !trigger_events.is_empty() { collect_triggers_into_deferred(state, &trigger_events); } @@ -6914,20 +6921,26 @@ pub(crate) fn collect_and_drain_observer_triggers_if_settled( events: &mut Vec, slice_start: usize, ) { + if triggered_mana_sidecar_owns_local_collection(state) { + return; + } if !matches!(state.waiting_for, WaitingFor::Priority { .. }) { park_observer_triggers_if_paused(state, events, slice_start); return; } - let trigger_events: Vec = events[slice_start..] - .iter() - .filter(|ev| { - !matches!( - ev, - GameEvent::PhaseChanged { .. } | GameEvent::SpellCopied { .. } - ) - }) - .cloned() - .collect(); + let trigger_events: Vec = filter_consumed_trigger_events_from( + events, + slice_start, + &state.consumed_before_priority_trigger_events, + ) + .into_iter() + .filter(|ev| { + !matches!( + ev, + GameEvent::PhaseChanged { .. } | GameEvent::SpellCopied { .. } + ) + }) + .collect(); if !trigger_events.is_empty() { collect_triggers_into_deferred(state, &trigger_events); } @@ -6936,8 +6949,46 @@ pub(crate) fn collect_and_drain_observer_triggers_if_settled( } } -/// CR 106.6 + CR 603.3b: Queue a synthetic cost-payment trigger for the same -/// post-announcement stack-placement path as event-collected cost triggers. +/// CR 605.4a: Does the classifier-accepted triggered-mana occurrence own local +/// event collection right now? +/// +/// The two authorities are deliberately independent and either alone is +/// sufficient. A scoped sidecar action may temporarily move the durable carrier +/// out of `GameState` while it delegates to an existing handler, and there the +/// live accepted-node marker is the only authority; at a resting pause the +/// durable carrier is present while the lexical marker has already been +/// restored by `with_rules_execution_node`. +/// +/// Both observer helpers fail closed on this predicate *before* collecting or +/// draining: they are collection and partial-release authorities, not readiness +/// probes, and the fixed point — not a helper — owns every emitted event of an +/// accepted occurrence. +/// +/// It stores no boolean and recognizes no `WaitingFor`, card name, trigger +/// mode, or event value. +fn triggered_mana_sidecar_owns_local_collection(state: &GameState) -> bool { + let sidecar_node = state + .pending_triggered_mana_resume + .as_ref() + .map(|resume| resume.rules_execution_node); + let marker = state.active_accepted_triggered_mana_node; + if let (Some(sidecar_node), Some(marker)) = (sidecar_node, marker) { + debug_assert_eq!( + sidecar_node, marker, + "the live accepted-occurrence marker must name the sidecar's current node" + ); + debug_assert_eq!( + state.active_rules_execution_node, + Some(marker), + "a live accepted-occurrence marker must equal the ambient rules-execution node" + ); + } + sidecar_node.is_some() || marker.is_some() +} + +/// CR 603.12 + CR 603.3b: Queue a synthetic reflexive or cost-payment trigger +/// for the same post-announcement stack-placement path as event-collected +/// triggers. This collector never drains or mutates trigger-construction state. pub(crate) fn defer_pending_trigger(state: &mut GameState, trigger: PendingTrigger) { state .deferred_triggers @@ -7604,6 +7655,191 @@ fn dispatch_pending_trigger_context_with_origin( } } +/// Where a dispatched trigger is placed once the common core has finished +/// modal legality, target preparation, event binding, and the CR 603.4 recheck. +/// +/// Private control flow, never a rules/data/wire enum: the two backends own +/// exactly one decision between them — "push and journal a stack entry" versus +/// "resolve without creating one". +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TriggerPlacement { + /// The baseline CR 603.3 placement. Every existing caller uses this and its + /// behaviour is unchanged in every branch. + OrdinaryStack, + /// CR 605.4a stackless placement, reachable **only** for a context that + /// [`classify_pending_triggered_mana`] accepted. It never pushes an entry, + /// never writes `pending_trigger_entry` or a stack side table, and never + /// emits `StackPushed`. + TriggeredManaImmediate, +} + +/// The typed accepted view produced by [`classify_pending_triggered_mana`]. +/// +/// Deliberately not a bare boolean: acceptance carries the exact source +/// incarnation the one `RulesExecutionNodeKind::TriggeredMana` node must be +/// begun from, which the classifier already had to prove exists. +#[derive(Debug, Clone)] +pub(crate) struct AcceptedTriggeredMana { + pub context: PendingTriggerContext, + /// Baseline's exact source derivation: the trigger-source incarnation + /// stamped at construction, else the still-live object incarnation. + pub source: ObjectIncarnationRef, +} + +/// CR 605.1b partition of one complete `PendingTriggerContext`. +#[derive(Debug, Clone)] +pub(crate) enum PendingTriggeredManaClass { + /// Immediate stackless placement is proven safe for this exact context. + Accepted(Box), + /// Everything else. The complete context is preserved unchanged and goes to + /// the ordinary deferred batch. + Ordinary(Box), +} + +/// True iff any reachable `sub_ability` / `else_ability` link satisfies `pred`. +fn any_reachable_link(ability: &ResolvedAbility, pred: &dyn Fn(&ResolvedAbility) -> bool) -> bool { + if pred(ability) { + return true; + } + if let Some(sub) = ability.sub_ability.as_deref() { + if any_reachable_link(sub, pred) { + return true; + } + } + if let Some(else_branch) = ability.else_ability.as_deref() { + if any_reachable_link(else_branch, pred) { + return true; + } + } + false +} + +/// True iff any reachable link carries an `unless_pay` modifier +/// (CR 118.12 / CR 608.2c). +/// +/// Rejecting this structurally — rather than by card name or by trusting the +/// current Oracle census — is what makes `UnlessPayment` and every nested cost +/// prompt unreachable while the immediate path owns accepted work. +fn chain_has_any_unless_payment(ability: &ResolvedAbility) -> bool { + any_reachable_link(ability, &|link| link.unless_pay.is_some()) +} + +/// True iff any reachable link can raise a repeat interaction from inside +/// `effects::resolve_ability_chain` — either repeat form (`RepeatDecision` and +/// its iteration frames). +/// +/// `optional` (`OptionalEffectChoice`) and `optional_for` (`OpponentMayChoice`) +/// were rejected here until their owning handler family — `engine_payment_choices` +/// — gained the readiness hook; they are accepted now, and that widening owns a +/// real behaviour delta of its own. It does **not** ride on the modal rejection's +/// "baseline never inlines this anyway" argument: baseline `is_triggered_mana_ability` +/// returns true for an all-mana `optional` body, so baseline resolves it inline +/// today and leaves `waiting_for` set with no continuation. The accepted path now +/// carries that pause instead. +/// +/// The repeat forms stay rejected only because `engine_resolution_choices` has +/// no readiness hook yet: deleting them here without one installs a carrier +/// nothing can resume, which is the exact state this predicate exists to +/// prevent. +fn chain_can_pause_on_player_interaction(ability: &ResolvedAbility) -> bool { + any_reachable_link(ability, &|link| { + link.repeat_for.is_some() || link.repeat_until.is_some() + }) +} + +/// CR 605.1b + CR 605.4a: the single complete-context authority that decides +/// whether one collected `PendingTriggerContext` may take stackless immediate +/// placement. +/// +/// `mana_abilities::is_triggered_mana_ability` remains the exact and +/// authoritative acceptance-time gate and is called here, unmodified, on the +/// unmodified carried graph and firing event. This function never reimplements +/// that predicate, never clears a target to force a match, and never accepts a +/// context the predicate rejects — it only *narrows* further, on the retained +/// complete-context safety axes below. +/// +/// Modal contexts are rejected in this revision. That is not a behaviour +/// regression: baseline's modal branch in `dispatch_pending_trigger_context` +/// pushes the entry to the stack *before* mode selection, so it never reaches +/// baseline's inline classifier check at all — no modal trigger resolves inline +/// today either. A stackless modal path is genuinely new machinery and is +/// tracked separately. +pub(crate) fn classify_pending_triggered_mana( + state: &mut GameState, + context: PendingTriggerContext, +) -> PendingTriggeredManaClass { + let ordinary = + |context: PendingTriggerContext| PendingTriggeredManaClass::Ordinary(Box::new(context)); + let trigger = &context.pending; + + // Modal: rejected to ordinary stack announcement (see the doc comment). + if trigger.modal.is_some() { + return ordinary(context); + } + // CR 601.2d + CR 603.3d: announcement-time division and declared constraints + // belong to the stack announcement authority. + if !trigger.target_constraints.is_empty() + || trigger.distribute.is_some() + || trigger.ability.distribution.is_some() + { + return ordinary(context); + } + // Round-11 finding 2: reject `unless_pay` on every reachable link, so a live + // sidecar can never coincide with `UnlessPayment` or any nested cost prompt. + if chain_has_any_unless_payment(&trigger.ability) { + return ordinary(context); + } + // Pause-narrowing for this revision (see the predicate's doc comment). + if chain_can_pause_on_player_interaction(&trigger.ability) { + return ordinary(context); + } + // The exact baseline acceptance gate, on the unmodified carried graph and + // the carried authoritative firing event. + if !super::mana_abilities::is_triggered_mana_ability( + &trigger.ability, + trigger.trigger_event.as_ref(), + ) { + return ordinary(context); + } + // Definition-owned slot proof, under the pushed event/count context. This is + // deliberately stronger than inspecting `target_constraints`, `multi_target`, + // or `ability.targets` separately: it catches `Effect::Mana { target: Some(..) }` + // and every downstream definition slot even when all three are empty. + let snapshot = push_trigger_event_context( + state, + trigger.trigger_event.as_ref(), + &context.trigger_events, + trigger.subject_match_count, + ); + let slots_are_empty = matches!( + super::ability_utils::build_target_slots(state, &trigger.ability), + Ok(slots) if slots.is_empty() + ); + restore_trigger_event_context(state, snapshot); + if !slots_are_empty { + return ordinary(context); + } + // CR 605.4a: exactly one `TriggeredMana` node per accepted occurrence, begun + // from baseline's exact source derivation. A malformed synthetic/legacy + // context for which neither authority exists is rejected rather than given + // an ambient node. + let Some(source) = trigger + .ability + .trigger_source + .as_ref() + .map(|trigger_source| trigger_source.identity.reference) + .or_else(|| { + state + .objects + .get(&trigger.ability.source_id) + .map(ObjectIncarnationRef::from_object) + }) + else { + return ordinary(context); + }; + PendingTriggeredManaClass::Accepted(Box::new(AcceptedTriggeredMana { context, source })) +} + /// CR 113.2c + CR 603.2 + CR 603.3b: Drive a single collected trigger through /// its disposition. Returns a [`TriggerDispatchDisposition`] classifying the /// terminal outcome: [`Paused`](TriggerDispatchDisposition::Paused) when the @@ -7627,6 +7863,21 @@ fn dispatch_pending_trigger_context( trigger_context: PendingTriggerContext, events_out: &mut Vec, ) -> TriggerDispatchDisposition { + dispatch_pending_trigger_context_core( + state, + trigger_context, + TriggerPlacement::OrdinaryStack, + events_out, + ) +} + +fn dispatch_pending_trigger_context_core( + state: &mut GameState, + trigger_context: PendingTriggerContext, + placement: TriggerPlacement, + events_out: &mut Vec, +) -> TriggerDispatchDisposition { + let immediate = matches!(placement, TriggerPlacement::TriggeredManaImmediate); let PendingTriggerContext { pending: trigger, trigger_events, @@ -7649,6 +7900,16 @@ fn dispatch_pending_trigger_context( // exists on the stack. if let Some(modal_ref) = trigger.modal.as_ref() { if !trigger.mode_abilities.is_empty() { + // The complete classifier rejects every modal context, so immediate + // placement can never reach the stack-pushing modal branch below. + if immediate { + debug_assert!( + false, + "classify_pending_triggered_mana must reject a modal context before \ + immediate placement; reaching the modal branch would push a stack entry" + ); + return TriggerDispatchDisposition::DroppedNoLegalMode; + } // CR 603.3c + CR 603.3d: a triggered modal's mode choice is announced as // the ability is put on the stack, by the same process as casting a spell // (CR 601.2c-d). The triggering event must be live for the ENTIRE choice, @@ -7767,7 +8028,35 @@ fn dispatch_pending_trigger_context( subject_match_count, ); - match prepare_trigger_targets(state, &trigger) { + let prepared = prepare_trigger_targets(state, &trigger); + + if immediate { + // CR 605.4a: the stackless backend. The classifier already proved an + // empty canonical slot set, so `NoTargets` is the only reachable + // preparation outcome; every other outcome is an invariant failure and + // must not fall through to a push or to the delayed re-push fallback. + let PreparedTriggerTargets::NoTargets { + trigger, + rng, + events, + } = prepared + else { + debug_assert!( + false, + "classify_pending_triggered_mana proved an empty canonical target-slot set, so \ + an accepted context cannot reach a target pause, drop, or fallback push" + ); + restore_trigger_event_context(state, context_snapshot); + return TriggerDispatchDisposition::DroppedTargetUnresolved; + }; + commit_prepared_trigger_targets(state, rng, events, events_out); + let disposition = + resolve_accepted_triggered_mana_body(state, &trigger, &trigger_events, events_out); + restore_trigger_event_context(state, context_snapshot); + return disposition; + } + + match prepared { PreparedTriggerTargets::NoTargets { trigger, rng, @@ -7901,6 +8190,487 @@ fn dispatch_pending_trigger_context( } } +/// The exhaustive whitelist of existing interactions an accepted triggered-mana +/// body may pause on (CR 605.4a). +/// +/// A triggered-body colour choice and every unless/nested-cost prompt are +/// deliberately absent: the classifier proved the graph choice-free at +/// acceptance and rejected every `unless_pay` link, so encountering one is an +/// invariant failure rather than a wait to serialize generically. +fn is_accepted_triggered_mana_pause(waiting_for: &WaitingFor) -> bool { + matches!( + waiting_for, + WaitingFor::OptionalEffectChoice { .. } + | WaitingFor::OpponentMayChoice { .. } + | WaitingFor::RepeatDecision { .. } + | WaitingFor::ReplacementChoice { .. } + ) +} + +/// CR 605.4a: rebind one accepted occurrence's rules-execution node together +/// with the equal accepted-occurrence marker and its occurrence-local +/// production override, for exactly one lexical extent. +/// +/// Both transient scopes are restored on every return, including a return +/// caused by a prompt — which is precisely why a pause must copy the node into +/// `TriggeredManaResume` rather than relying on either scope surviving it. +pub(crate) fn with_triggered_mana_resolution_scope( + state: &mut GameState, + node: crate::types::RulesExecutionNodeRef, + production_override: Option, + operation: impl FnOnce(&mut GameState) -> T, +) -> T { + state.with_rules_execution_node(node, |state| { + let previous_marker = state.active_accepted_triggered_mana_node.replace(node); + let previous_override = state.current_triggered_mana_override.take(); + state.current_triggered_mana_override = production_override; + let result = operation(state); + state.current_triggered_mana_override = previous_override; + state.active_accepted_triggered_mana_node = previous_marker; + result + }) +} + +/// The stackless placement backend: bind the CR 603.4 scope through the shared +/// binder, then run the body. No stack entry, stack event, stack side table, or +/// stack journal is created on any path. +fn resolve_accepted_triggered_mana_body( + state: &mut GameState, + trigger: &PendingTrigger, + trigger_events: &[GameEvent], + events_out: &mut Vec, +) -> TriggerDispatchDisposition { + let waiting_before = state.waiting_for.clone(); + // CR 603.4 + CR 608.2k + CR 603.2c + CR 706.2, through the same binder stack + // resolution uses — immediate dispatch must not re-implement the recheck. + let bound = super::stack::bind_triggered_resolution_scope( + state, + Some(super::stack::TriggeredResolutionScope { + condition: trigger.condition.as_ref(), + controller: trigger.controller, + trigger_source: trigger.ability.trigger_source.as_ref(), + trigger_event: trigger.trigger_event.as_ref(), + subject_match_count: trigger.subject_match_count, + die_result: trigger.die_result, + }), + Some(trigger_events.to_vec()), + ); + if !bound { + // CR 603.4: a false intervening-if is terminal with no effects at all. + return TriggerDispatchDisposition::ResolvedInline; + } + let _ = super::effects::resolve_ability_chain(state, &trigger.ability, events_out, 0); + if state.waiting_for != waiting_before && is_accepted_triggered_mana_pause(&state.waiting_for) { + return TriggerDispatchDisposition::Paused; + } + debug_assert!( + state.waiting_for == waiting_before, + "an accepted triggered-mana body may only pause on a whitelisted interaction; \ + encountering a new pause is a stop condition, not a wait to serialize generically" + ); + TriggerDispatchDisposition::ResolvedInline +} + +/// The trigger-side immediate wrapper: mint the one `TriggeredMana` node for +/// this accepted occurrence, then run the shared dispatcher core under that node +/// and its equal accepted-occurrence marker. +fn dispatch_accepted_triggered_mana( + state: &mut GameState, + accepted: AcceptedTriggeredMana, + production_override: Option, + events_out: &mut Vec, +) -> ( + TriggerDispatchDisposition, + crate::types::RulesExecutionNodeRef, +) { + let AcceptedTriggeredMana { context, source } = accepted; + // Baseline's exact cause derivation for the node. + let caused_by = match context.pending.trigger_event.as_ref() { + Some( + GameEvent::ManaAdded { source_id, .. } | GameEvent::TappedForMana { source_id, .. }, + ) => state + .resolved_rules_journal + .latest_mana_producer_for_source(*source_id), + _ => None, + }; + let node = state.begin_triggered_mana_journal_node( + source, + context.pending.ability.trigger_definition_ref.clone(), + caused_by, + ); + let disposition = + with_triggered_mana_resolution_scope(state, node, production_override, |state| { + dispatch_pending_trigger_context_core( + state, + context, + TriggerPlacement::TriggeredManaImmediate, + events_out, + ) + }); + (disposition, node) +} + +/// Partition one already-collected context list into the members that may take +/// stackless immediate placement and the members that stay on the ordinary +/// authority, both in collection order. +fn partition_collected_mana_frame_contexts( + state: &mut GameState, + contexts: Vec, +) -> (Vec, Vec) { + let mut accepted = Vec::new(); + let mut ordinary = Vec::new(); + for context in contexts { + match classify_pending_triggered_mana(state, context) { + PendingTriggeredManaClass::Accepted(view) => accepted.push(*view), + PendingTriggeredManaClass::Ordinary(context) => ordinary.push(*context), + } + } + (accepted, ordinary) +} + +/// CR 603.3b: queue every rejected context on `state.deferred_triggers`, in the +/// combined APNAP order the shared collector already established, through the +/// one journaled `ResolvedTriggerCollection::DeferPending` authority. +/// +/// A completed mana frame is **not** a release boundary: it is a cost-payment +/// micro-frame inside somebody else's action. Ordering and dispatching its +/// ordinary observers here would form one release group per micro-frame and put +/// CR 603.3b ordering in front of a player mid-payment. Deferring instead makes +/// the owner's own boundary — the reducer epilogue, a cast/resolution finalizer, +/// or the settled-Priority convergence wrapper — form exactly one release group +/// over every micro-frame the action produced. +/// +/// This is the plan's one-release-group behaviour and it is a deliberate +/// corpus-visible change from baseline, which dispatched a completed mana +/// frame's observers immediately from inside the payment. +fn defer_rejected_mana_frame_contexts(state: &mut GameState, ordinary: Vec) { + resolve_and_apply_trigger_collection( + state, + crate::types::resolved_commands::ResolvedTriggerCollection::DeferPending { + contexts: ordinary, + }, + ) + .expect("completed mana-frame deferred trigger collection cause must be live"); +} + +/// CR 603.3b + CR 605.4a: the one ordered driver for a completed mana frame. +/// +/// It performs exactly one normal observation pass and one delayed match pass +/// over `raw_batch`, durably owns the combined collection, partitions it through +/// the one complete classifier, queues every ordinary context, and only then +/// dispatches accepted work immediately. It deliberately never terminalizes an +/// unmatched reflexive: a frame-local child batch is not the complete creating +/// boundary of every live reflexive. It never calls `begin_trigger_ordering`, +/// `dispatch_collected_triggers`, or either deferred drain. +/// +/// Returns `Some(wait)` when an accepted body paused on a whitelisted +/// interaction, after storing the complete `pending_triggered_mana_resume`. +pub(crate) fn collect_mana_action_trigger_batch( + state: &mut GameState, + raw_batch: &[GameEvent], + outer_resume: crate::types::game_state::ManaTriggerFixedPointResume, +) -> Option { + // The dispatch buffer is frame-local, exactly as baseline `process_triggers` + // keeps its own: events emitted while announcing or inline-resolving this + // batch are represented by the contexts and journal entries they produced, + // and are deliberately not given a live occurrence identity in the reducer's + // public event vector. + let events_out = &mut Vec::new(); + let seed = collect_triggers_for_batch(state, raw_batch); + let collected = collect_pending_and_delayed_triggers_for_batch( + state, + seed, + raw_batch, + DelayedTriggerEventScope::Any, + ); + let (accepted, ordinary) = partition_collected_mana_frame_contexts(state, collected.contexts); + let pause = run_accepted_triggered_mana_fixed_point(state, accepted, outer_resume, events_out); + defer_rejected_mana_frame_contexts(state, ordinary); + pause +} + +/// CR 603.3b + CR 603.7 + CR 605.4a: materialize one **undispatched** combined +/// normal-plus-delayed collection for the range an accepted occurrence emitted, +/// for storage on the sidecar across a pause. +/// +/// This is the durable form the plan requires: the raw emitted range dies with +/// the frame that produced it, so a pause must persist *contexts*, not events. +/// Nothing here is claimed, ordered, queued, or given a node — an accepted child +/// discovered by this pass mints its node only when readiness makes it current, +/// and a rejected sibling enters `state.deferred_triggers` only then too. +/// +/// Returns `None` for an empty range or an empty collection so the sidecar never +/// accumulates vacuous batches across repeated pauses. +fn collect_undispatched_emission_batch( + state: &mut GameState, + emitted: &[GameEvent], +) -> Option { + if emitted.is_empty() { + return None; + } + let seed = collect_triggers_for_batch(state, emitted); + let batch = collect_pending_and_delayed_triggers_for_batch( + state, + seed, + emitted, + DelayedTriggerEventScope::Any, + ); + (!batch.contexts.is_empty()).then_some(batch) +} + +/// CR 605.4a fixed point: resolve each accepted occurrence under its own node, +/// then collect the events it emitted and prepend any accepted children ahead of +/// the prior simultaneous tail, until no accepted work remains. +fn run_accepted_triggered_mana_fixed_point( + state: &mut GameState, + mut accepted: Vec, + outer_resume: crate::types::game_state::ManaTriggerFixedPointResume, + events_out: &mut Vec, +) -> Option { + while !accepted.is_empty() { + let current = accepted.remove(0); + let context_for_resume = current.context.clone(); + let emitted_start = events_out.len(); + let (disposition, node) = + dispatch_accepted_triggered_mana(state, current, None, events_out); + if matches!(disposition, TriggerDispatchDisposition::Paused) { + let waiting_for = state.waiting_for.clone(); + // CR 605.4a: the occurrence's own scopes have already restored (they + // restore on a prompt return exactly as on a synchronous one), so + // the range it emitted before pausing may be combined-collected + // here — and it MUST be, because `events_out` is frame-local and + // dies with this call. The batch is stored undispatched: no child + // node is minted and no ordinary context is queued while the current + // occurrence is unresolved. + debug_assert!( + state.active_accepted_triggered_mana_node.is_none(), + "a paused occurrence's marker must restore before its emission batch is collected" + ); + let emitted: Vec = events_out[emitted_start..].to_vec(); + let collected_batches = collect_undispatched_emission_batch(state, &emitted) + .into_iter() + .collect(); + state.pending_triggered_mana_resume = + Some(Box::new(crate::types::game_state::TriggeredManaResume { + current: Box::new(context_for_resume), + current_override: None, + rules_execution_node: node, + accepted_tail: accepted.into_iter().map(|view| view.context).collect(), + collected_batches, + outer_resume, + stage: crate::types::game_state::TriggeredManaStage::ResolvingBody, + })); + return Some(waiting_for); + } + // CR 605.4a: the completed occurrence's own scopes have restored, so its + // emitted events may now be collected and any accepted child minted. + debug_assert!( + state.active_accepted_triggered_mana_node.is_none(), + "a completed occurrence's marker must restore before child discovery" + ); + if events_out.len() > emitted_start { + let emitted: Vec = events_out[emitted_start..].to_vec(); + let seed = collect_triggers_for_batch(state, &emitted); + let child = collect_pending_and_delayed_triggers_for_batch( + state, + seed, + &emitted, + DelayedTriggerEventScope::Any, + ); + let (mut children, child_ordinary) = + partition_collected_mana_frame_contexts(state, child.contexts); + defer_rejected_mana_frame_contexts(state, child_ordinary); + children.extend(accepted); + accepted = children; + } + } + None +} + +/// CR 605.4a: the response-side counterpart to +/// [`with_triggered_mana_resolution_scope`]. +/// +/// Every handler family that can re-enter a paused accepted occurrence's body +/// runs its unmodified baseline work inside this scope, so the occurrence's own +/// rules-execution node and the equal accepted-occurrence marker are ambient +/// again. That is what makes both payment-choice observer helpers fail closed on +/// [`triggered_mana_sidecar_owns_local_collection`] while the resumed body runs: +/// the fixed point, never a helper, owns every event an accepted occurrence +/// emits. +/// +/// The durable carrier deliberately stays in `GameState` for the whole scope — +/// it is the serde authority if the body pauses again inside it — so the +/// predicate's two authorities agree rather than one covering for the other. +/// +/// With no live carrier the operation runs bare and the hook is a no-op. +pub(crate) fn with_accepted_triggered_mana_action_scope( + state: &mut GameState, + operation: impl FnOnce(&mut GameState) -> T, +) -> T { + let Some(carrier) = state.pending_triggered_mana_resume.as_ref() else { + return operation(state); + }; + let node = carrier.rules_execution_node; + let production_override = carrier.current_override.clone(); + with_triggered_mana_resolution_scope(state, node, production_override, operation) +} + +/// CR 603.3b: claim every live occurrence in one resumed action's own emitted +/// range, by full-action index, through the same journal authority a completed +/// mana frame uses. +/// +/// The observer helpers were suppressed for this range by the sidecar guard, so +/// the combined collection above is its only collection; the journal is what +/// stops the ordinary post-action pipeline rediscovering the same occurrences. +fn claim_resumed_triggered_mana_events( + state: &mut GameState, + events: &[GameEvent], + event_start: usize, +) { + if event_start >= events.len() { + return; + } + let occurrences = (event_start..events.len()) + .map(|index| ConsumedTriggerEventOccurrence { + event: events[index].clone(), + occurrence: trigger_event_occurrence(events, index), + }) + .collect(); + resolve_and_apply_trigger_collection( + state, + crate::types::resolved_commands::ResolvedTriggerCollection::ConsumeBeforePriority { + occurrences, + }, + ) + .expect("resumed accepted triggered-mana consumed-before-priority journal cause must be live"); +} + +/// CR 605.4a: the one readiness authority for a resumed accepted triggered-mana +/// occurrence. Called by every sidecar-aware handler family **after** the action +/// scope has restored, and never from inside it. +/// +/// In order: combine-collect the exact unclaimed suffix this action emitted and +/// store it undispatched, claim that suffix's live identities, and then either +/// keep the carrier (the current occurrence re-paused on a whitelisted +/// interaction) or terminate it — partition every stored batch once, mint +/// accepted children ahead of the prior simultaneous tail, run that tail, and +/// resume the suspended mana frame exactly once. +/// +/// [`TriggeredManaReadiness::Pending`] means "no carrier, or the carrier is +/// still live": the handler returns its own unmodified result. +/// [`TriggeredManaReadiness::Resumed`] carries the resumed mana frame's +/// authoritative wait, which the handler must install and return. +pub(crate) fn finish_accepted_triggered_mana_action( + state: &mut GameState, + events: &mut Vec, + event_start: usize, +) -> Result { + if state.pending_triggered_mana_resume.is_none() { + return Ok(TriggeredManaReadiness::Pending); + } + debug_assert!( + state.active_accepted_triggered_mana_node.is_none(), + "readiness must run outside the resumed occurrence's own scope" + ); + let emitted: Vec = events[event_start.min(events.len())..].to_vec(); + let batch = collect_undispatched_emission_batch(state, &emitted); + claim_resumed_triggered_mana_events(state, events, event_start); + let carrier = state + .pending_triggered_mana_resume + .as_mut() + .expect("the carrier was proven live above and collection never clears it"); + carrier.collected_batches.extend(batch); + if is_accepted_triggered_mana_pause(&state.waiting_for) { + // A repeated pause of the same occurrence: the carrier keeps its node, + // its tail, and now one more undispatched batch. + return Ok(TriggeredManaReadiness::Pending); + } + let carrier = *state + .pending_triggered_mana_resume + .take() + .expect("the carrier was proven live above"); + let crate::types::game_state::TriggeredManaResume { + accepted_tail, + collected_batches, + outer_resume, + .. + } = carrier; + let stored: Vec = collected_batches + .into_iter() + .flat_map(|batch| batch.contexts) + .collect(); + let (mut accepted, ordinary) = partition_collected_mana_frame_contexts(state, stored); + defer_rejected_mana_frame_contexts(state, ordinary); + // CR 605.4a: children go ahead of the prior simultaneous tail. The tail is + // reclassified rather than trusted: acceptance is a property of live state, + // and a tail member whose source left the battlefield during the pause + // belongs to the ordinary authority now. + let (tail_accepted, tail_ordinary) = + partition_collected_mana_frame_contexts(state, accepted_tail); + defer_rejected_mana_frame_contexts(state, tail_ordinary); + accepted.extend(tail_accepted); + // The tail's dispatch buffer is frame-local, exactly as the synchronous + // frame's is: an accepted body's own emissions are represented by the + // contexts and journal entries they produce, not by a live identity in this + // action's public event vector. + let tail_events = &mut Vec::new(); + if run_accepted_triggered_mana_fixed_point(state, accepted, outer_resume.clone(), tail_events) + .is_some() + { + // A tail member paused and installed its own carrier naming the same + // outer continuation; that carrier is the authority now. + return Ok(TriggeredManaReadiness::Resumed { + wait: state.waiting_for.clone(), + settled_direct_priority_root: false, + }); + } + // CR 117.3c + CR 117.5: recognize the ONE consumed shape that may run full + // settled-Priority convergence, exhaustively and before the frame resumes: + // a direct `ManaAbilityResume::Priority` root, i.e. an activation that began + // at `WaitingFor::Priority` with nothing else owning the action. Every cast, + // payment, colour, unless and special-action owner is excluded here, not by + // the convergence wrapper. + let direct_priority_root = matches!( + outer_resume, + crate::types::game_state::ManaTriggerFixedPointResume::Root { ref resume, .. } + if matches!(**resume, crate::types::game_state::ManaAbilityResume::Priority) + ); + let Some(wait) = super::mana_abilities::resume_settled_mana_frame(state, outer_resume, events)? + else { + return Ok(TriggeredManaReadiness::Pending); + }; + let settled_direct_priority_root = direct_priority_root + && matches!(wait, WaitingFor::Priority { .. }) + && state.pending_triggered_mana_resume.is_none() + && state.pending_cast.is_none() + && !super::casting::mana_ability_cost_payment_is_paused(state) + && resolution_completion_can_settle(state); + Ok(TriggeredManaReadiness::Resumed { + wait, + settled_direct_priority_root, + }) +} + +/// What one sidecar-aware handler's readiness hook concluded. +pub(crate) enum TriggeredManaReadiness { + /// No carrier, or the carrier is still live after this action. The handler + /// returns its own unmodified result. + Pending, + /// The fixed point terminated and the suspended mana frame resumed exactly + /// once to `wait`. + /// + /// `settled_direct_priority_root` is true only for the single consumed shape + /// that may run full settled-Priority convergence — see the gate above. A + /// handler whose reducer arm returns its `ActionResult` directly (and so + /// never reaches the ordinary epilogue) uses it to decide whether to call + /// `run_post_action_pipeline_from_settled_priority`; every other owner + /// returns `wait` unchanged. + Resumed { + wait: WaitingFor, + settled_direct_priority_root: bool, + }, +} + /// CR 608.2e + issue #1793: True end-of-resolution boundary for draining /// `deferred_triggers`. Mid-resolution `Priority` from player-scope iteration, /// `repeat_for`, or replacement continuations must not drain (or offer CR @@ -7913,6 +8683,37 @@ pub(crate) fn resolution_completion_can_settle(state: &GameState) -> bool { if is_pending_trigger_construction_active(state) { return false; } + // CR 601.2h + CR 602.2b: A parked cost-move continuation still owns the + // announcement or activation whose events created these observers. The + // complete typed enum is a release barrier until its exact resumer takes + // and completes that continuation. + if state.pending_cost_move_resume.is_some() { + return false; + } + // CR 118.3b + CR 119.4: Deferred life payment likewise retains its cast, + // mana-root, or pay-amount owner across replacement interaction. + if state.pending_deferred_life_cost_resume.is_some() { + return false; + } + // CR 605.4a: A classifier-accepted triggered mana ability paused + // mid-resolution owns its own emitted-event fixed point. Its carrier — and + // only that carrier — joins this owned-prompt guard. + // + // `pending_trigger_construction_priority_recipient` is deliberately absent. + // `can_drain_deferred_triggers` consults this predicate first, so a + // construction recipient here would reject the settled wrapper's own drain, + // the construction finisher's drain, and every deferred-sibling drain in the + // same batch — deadlocking the batch with its own recipient. What protects + // an in-flight construction batch instead is unchanged baseline ownership: + // `is_pending_trigger_construction_active` above, the empty deferred queue + // while `OrderTriggers` is open, and the reducer dispatching on the + // `(waiting_for, action)` pair. The only waiting-state-agnostic entries are + // concession/elimination and debug actions, which is why `elimination.rs` + // clears that recipient alongside the construction cursors it already + // clears. + if state.pending_triggered_mana_resume.is_some() { + return false; + } if !state.resolution_stack.is_empty() { return false; } @@ -7932,14 +8733,49 @@ pub(crate) fn resolution_completion_can_settle(state: &GameState) -> bool { true } -fn can_drain_deferred_triggers(state: &GameState, allow_spell_on_stack: bool) -> bool { +/// Which deferred-trigger drain, if any, a post-action seam owns. +/// +/// Every variant still goes through `resolution_completion_can_settle`, so the +/// complete carrier census is shared; the policy decides only whether this seam +/// drains at all and whether a `Spell` entry may remain on the stack while it +/// does. Replacing the two historic booleans — `engine_priority`'s +/// `skip_deferred_trigger_drain` and this module's `allow_spell_on_stack` — +/// with one enum keeps those two independent axes from being recombined +/// accidentally at a new seam. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum DeferredTriggerDrainPolicy { + /// This seam owns no drain. Its caller has already drained the batch, or it + /// is a mid-continuation owner that must not. + Skip, + /// CR 603.3b + issue #1793: drain only once no `Spell` entry remains on the + /// stack, so parked observers cannot offer ordering mid-resolution. + ResolutionSafe, + /// CR 601.2h + CR 602.2b + CR 603.3: a passive announced stack object may + /// still be on the stack; its cost/cast observers, and the settled batch a + /// direct mana root converges into, drain above it. + SettledPriority, +} + +impl DeferredTriggerDrainPolicy { + fn allows_spell_on_stack(self) -> bool { + match self { + DeferredTriggerDrainPolicy::Skip | DeferredTriggerDrainPolicy::ResolutionSafe => false, + DeferredTriggerDrainPolicy::SettledPriority => true, + } + } +} + +fn can_drain_deferred_triggers(state: &GameState, policy: DeferredTriggerDrainPolicy) -> bool { + if matches!(policy, DeferredTriggerDrainPolicy::Skip) { + return false; + } if state.deferred_triggers.is_empty() || !resolution_completion_can_settle(state) { return false; } // CR 603.3b + issue #1793: observer triggers parked during a spell's // resolution must wait until that spell leaves the stack — draining while // a `Spell` entry remains would offer ordering mid player_scope iteration. - if !allow_spell_on_stack + if !policy.allows_spell_on_stack() && state .stack .iter() @@ -7951,7 +8787,22 @@ fn can_drain_deferred_triggers(state: &GameState, allow_spell_on_stack: bool) -> } pub(crate) fn should_drain_deferred_triggers_now(state: &GameState) -> bool { - can_drain_deferred_triggers(state, false) + can_drain_deferred_triggers(state, DeferredTriggerDrainPolicy::ResolutionSafe) +} + +/// Policy-parameterized form of [`drain_deferred_trigger_queue`] for the shared +/// post-action pipeline core, whose drain slot is owned by its caller's policy +/// rather than by a fixed boundary. +pub(super) fn drain_deferred_trigger_queue_with_policy( + state: &mut GameState, + events_out: &mut Vec, + policy: DeferredTriggerDrainPolicy, +) -> Option { + if !can_drain_deferred_triggers(state, policy) { + return None; + } + + drain_deferred_trigger_queue_unchecked(state, events_out) } /// CR 113.2c + CR 603.2 + CR 603.3b: Drain the deferred-trigger queue after @@ -7987,6 +8838,43 @@ pub(crate) fn drain_deferred_triggers_after_stack_object_announcement( state: &mut GameState, events_out: &mut Vec, ) -> Option { + // CR 601.2c + CR 601.2d + CR 601.2h: the EXTERNAL pending-cast carrier has + // not completed announcement yet. Cost observers wait until the finalizer + // consumes that exact cast root. + // + // The companion INLINE `WaitingFor` carrier check is deliberately NOT here, + // and that placement is load-bearing rather than an omission. Most callers + // of this helper run INSIDE a reducer action, before `apply_action` assigns + // the handler's returned wait — so `state.waiting_for` is the wait the + // action STARTED from, not the one the announcement is installing. Reading + // it here blocks the exact drain this helper exists to perform: the + // additional-sacrifice cast that pauses on `TargetSelection` (whose + // `CostResume::Spell` carries the cast inline) still holds that stale wait + // when `casting_targets` finalizes the announcement and calls in, so the + // cost-sacrifice observers stay parked behind the announced spell forever. + // + // Every post-announcement boundary already excludes an inline carrier + // STRUCTURALLY, which is why dropping the read loses no protection: + // + // * the `casting_costs`/`casting_targets` wrapper returns early unless the + // wait it is ABOUT to install is `Priority`, and a `Priority` wait + // carries no cast by construction; + // * `engine_priority`'s post-action call sits behind + // `settle_pending_resolution_completion`, which itself requires + // `state.waiting_for` to be `Priority`; + // * the copy-announcement callers (`engine.rs` copy-target choice, + // `effects/copy_spell.rs`) drain for a COPY, which is put on the stack + // without being cast, so no announcement is in flight; + // * `drain_deferred_triggers_after_trigger_construction` runs after a + // TRIGGER finished construction; a cast paused around it lives in the + // external `pending_cast` carrier this function does read. + // + // The live discriminator for this placement is + // `casting_costs::tests::cost_paid_multi_sacrifice_kicker_paused_under_observes`: + // restoring the `state.waiting_for.has_pending_cast()` read fails it. + if state.pending_cast.is_some() { + return None; + } // CR 603.3b + CR 608.2g: terminal Ripple settlement owns this exact // post-announcement boundary. It must first collect the current final // cast's SpellCast event with earlier accepted casts, then drain one batch. @@ -7995,7 +8883,7 @@ pub(crate) fn drain_deferred_triggers_after_stack_object_announcement( if state.pending_resolution_completion.is_some() { return None; } - if !can_drain_deferred_triggers(state, true) { + if !can_drain_deferred_triggers(state, DeferredTriggerDrainPolicy::SettledPriority) { return None; } @@ -8026,6 +8914,144 @@ pub(crate) fn drain_deferred_triggers_after_trigger_construction( } } +/// CR 117.3c + CR 117.5 + CR 605.4a: record the player who must receive +/// priority once a settled trigger batch has finished announcing, when that +/// player is not the active player. +/// +/// The recipient is installed only from the settled-Priority convergence core's +/// own exhaustively validated `WaitingFor::Priority { player }`, before any +/// ordered or singleton construction step can pause. Reinstalling the same +/// player is idempotent; observing a *different* player while the field is live +/// is an internal invariant error. The one carved-out rewrite is +/// `elimination.rs`'s CR 800.4a re-point past a departed recipient, which does +/// not come through here. +pub(crate) fn preserve_trigger_construction_priority_recipient( + state: &mut GameState, + player: PlayerId, +) { + debug_assert!( + state + .pending_trigger_construction_priority_recipient + .is_none_or(|live| live == player), + "a second settled batch installed a different construction priority recipient", + ); + state.pending_trigger_construction_priority_recipient = Some(player); +} + +/// The closed census of trigger-construction prompts that may be returned to a +/// player while a construction priority recipient stays installed. +/// +/// Any other wait shape at a finisher seam means a seam was added that the +/// Round-20 closure map does not cover, which is an invariant error rather than +/// something to coerce. +pub(super) fn is_trigger_construction_prompt( + waiting_for: &crate::types::game_state::WaitingFor, +) -> bool { + matches!( + waiting_for, + WaitingFor::OrderTriggers { .. } + | WaitingFor::NamedChoice { .. } + | WaitingFor::OptionalEffectChoice { .. } + | WaitingFor::AbilityModeChoice { .. } + | WaitingFor::TriggerTargetSelection { .. } + | WaitingFor::DistributeAmong { .. } + ) +} + +/// Close out one reducer action that may have been a trigger-construction step. +/// +/// This is the single authority that decides whether a construction wait is +/// really terminal, and it is deliberately event-aware: a `Priority` produced by +/// a successful announcement, a dangling-cursor recovery, or an optional decline +/// is only terminal once the *whole* deferred tail behind it has announced. +/// Baseline's `abandon_ceased_pending_trigger` deliberately leaves deferred +/// siblings installed, and both dangling recoveries return `Priority` without +/// draining, so the drain has to live here rather than at each return. +/// +/// Contract: +/// +/// 1. With no recipient installed, return `produced` byte-for-byte and perform +/// no drain. This is the entire ordinary-caller contract — every existing +/// controller/active-player fallback at the seams stays exactly baseline. +/// 2. With a recipient and one of the six approved construction prompts, return +/// the real prompt and leave the recipient installed for the next step. +/// 3. With a recipient and a `Priority` of any player, run the bounded +/// construction drain, return an approved prompt it surfaces with the +/// recipient retained, and otherwise consume the recipient and return +/// `Priority { player }` — never leaving a durable recipient installed across +/// an action that returns ordinary priority. +/// 4. With a recipient and any other wait shape, this is an invariant error: do +/// not coerce, do not consume, do not invent a recipient. +/// +/// Applied exactly once per reducer action, at the outermost handler return of +/// each enumerated seam — never inside trigger dispatch, where a `Priority` +/// result is discarded by the dispatcher and the recipient would be lost +/// mid-batch. +pub(crate) fn finish_trigger_construction_action( + state: &mut GameState, + events: &mut Vec, + produced: crate::types::game_state::WaitingFor, +) -> crate::types::game_state::WaitingFor { + debug_assert!( + !state.trigger_construction_finisher_ran_this_action, + "the trigger-construction finisher ran twice in one reducer action", + ); + state.trigger_construction_finisher_ran_this_action = true; + + let Some(player) = state.pending_trigger_construction_priority_recipient else { + return produced; + }; + + if is_trigger_construction_prompt(&produced) { + return produced; + } + + if !matches!(produced, WaitingFor::Priority { .. }) { + debug_assert!( + false, + "trigger-construction finisher reached a wait outside the closed \ + construction/priority census: {produced:?}", + ); + return produced; + } + + // CR 113.2c + CR 603.2 + CR 603.3b: announce the rest of the batch before + // priority is really handed over. The existing selector picks the + // post-announcement boundary whenever no ability continuation or spell + // resolution is active, which is exactly the settled-batch case and the only + // drain that may place newly announced triggers above a passive spell. + let bound = state.deferred_triggers.len(); + let mut iterations = 0usize; + while !state.deferred_triggers.is_empty() + && state.pending_trigger.is_none() + && state.pending_trigger_entry.is_none() + && state.pending_trigger_order.is_none() + { + let queued_before = state.deferred_triggers.len(); + if let Some(waiting_for) = drain_deferred_triggers_after_trigger_construction(state, events) + { + debug_assert!( + is_trigger_construction_prompt(&waiting_for), + "the construction drain surfaced a wait outside the closed census: {waiting_for:?}", + ); + return waiting_for; + } + if state.deferred_triggers.len() >= queued_before { + // Undrainable queue: an ineligible drain state the settled-root gate + // proves absent upstream. Fall through and consume rather than leak. + break; + } + iterations += 1; + debug_assert!( + iterations <= bound, + "construction drain loop exceeded its queued-context bound", + ); + } + + state.pending_trigger_construction_priority_recipient = None; + WaitingFor::Priority { player } +} + fn drain_deferred_trigger_queue_unchecked( state: &mut GameState, events_out: &mut Vec, @@ -8530,8 +9556,13 @@ pub fn check_state_triggers(state: &mut GameState) { /// One-shot triggers are removed after firing; multi-fire (WheneverEvent) triggers /// persist until end-of-turn cleanup (CR 603.7c). pub fn check_delayed_triggers(state: &mut GameState, events: &[GameEvent]) -> Vec { + // CR 603.7 + CR 603.12: this is a closing `Any` boundary. Its contract is + // "match, then terminalize, then dispatch": the unmatched-reflexive pass must + // precede the empty-batch early return, so a complete boundary with zero + // firing contexts still expires a reflexive that this batch did not satisfy. let (pending, _) = collect_matching_delayed_triggers(state, events, DelayedTriggerEventScope::Any); + terminalize_unmatched_reflexives_for_closed_batch(state, events, DelayedTriggerEventScope::Any); if pending.is_empty() { return vec![]; } @@ -8893,6 +9924,19 @@ fn delayed_trigger_to_context( ) } +/// CR 603.7 + CR 603.7b: Match-only delayed-trigger collection. It fires +/// matching instances, removes fired one-shots, keeps duration-bearing +/// multi-fire sources installed, synthesizes Epic upkeep copies, records +/// delayed-due provenance and consumed raw identities, and returns the batch in +/// APNAP order. +/// +/// It deliberately performs no unmatched-reflexive lifetime closure. CR 603.12 +/// expires a reflexive against its *creating* resolution/batch, so a partial +/// frame-local batch — for example the micro-batch of a nested mana payment +/// inside an outer resolution — must be able to run this collector without +/// terminating an outer reflexive that its own boundary has not yet closed. +/// Closing boundaries call [`terminalize_unmatched_reflexives_for_closed_batch`] +/// immediately afterwards. fn collect_matching_delayed_triggers( state: &mut GameState, events: &[GameEvent], @@ -8910,17 +9954,6 @@ fn collect_matching_delayed_triggers( let mut to_fire: Vec<(DelayedTrigger, usize, GameEvent, bool)> = Vec::new(); let mut to_remove: Vec<(usize, usize, GameEvent)> = Vec::new(); - // CR 603.12: A reflexive delayed trigger ("when you [do X] this way, ...", - // including "when you win/lose the flip") is checked immediately after - // creation and triggers only on the event(s) that occurred earlier during the - // creating resolution. It gets exactly one shot on that creation batch: if it - // did not match on this first check, it must be discarded rather than left to - // fire on a later same-turn matching event (which a bare CR 603.7b - // `WhenNextEvent` would do). The phase-only path keeps the historical - // narrower coin-flip discard gate because it filters non-phase matches out of - // the candidate batch. - let mut to_discard: Vec = Vec::new(); - for (idx, delayed) in state.delayed_triggers.iter().enumerate() { if let Some((event_index, trigger_event)) = delayed_trigger_event_with_index( &delayed.condition, @@ -8961,18 +9994,6 @@ fn collect_matching_delayed_triggers( to_fire.push((delayed.clone(), occ_index, occurrence, false)); } } - } else if match scope { - DelayedTriggerEventScope::Any => is_reflexive_lifetime(&delayed.condition), - DelayedTriggerEventScope::PhaseChangedOnly => { - reflexive_coin_flip_resolved_without_match( - &delayed.condition, - events, - state, - delayed.ability.trigger_source.as_ref(), - ) - } - } { - to_discard.push(idx); } } @@ -8996,31 +10017,22 @@ fn collect_matching_delayed_triggers( } } - // Remove fired one-shot triggers AND discarded non-matching reflexive triggers - // from `delayed_triggers` in a single descending-index pass so every index - // stays valid. Fired one-shots are collected into `to_fire`; discarded - // reflexives (CR 603.12) are dropped without firing. The two index sets are - // disjoint — a trigger either matched (fire) or resolved-without-match - // (discard), never both. + // Remove fired one-shot triggers from `delayed_triggers` in a descending-index + // pass so every index stays valid. Unmatched-reflexive lifetime closure is + // deliberately NOT performed here: it belongs to the batch's creating + // resolution boundary and is owned by + // `terminalize_unmatched_reflexives_for_closed_batch`, so a partial + // frame-local batch (a nested mana payment) cannot expire an outer reflexive. let mut fired_events: std::collections::HashMap = to_remove .iter() .map(|(idx, event_index, event)| (*idx, (*event_index, event.clone()))) .collect(); - let mut combined: Vec = to_remove - .iter() - .map(|(idx, _, _)| *idx) - .chain(to_discard.iter().copied()) - .collect(); + let mut combined: Vec = to_remove.iter().map(|(idx, _, _)| *idx).collect(); combined.sort_unstable(); for idx in combined.into_iter().rev() { let trigger = state.delayed_triggers.remove(idx); if let Some((event_index, trigger_event)) = fired_events.remove(&idx) { to_fire.push((trigger, event_index, trigger_event, true)); - } else { - super::lifecycle::record_delayed_terminal( - trigger.provenance.firing(), - super::lifecycle::DelayedTerminalDisposition::ReflexiveUnmatched, - ); } } @@ -9071,6 +10083,71 @@ fn collect_matching_delayed_triggers( (pending, consumed_events) } +/// CR 603.12: Close the lifetime of every reflexive delayed trigger that did not +/// match this *complete* event batch. +/// +/// A reflexive delayed trigger ("when you [do X] this way, ...", including "when +/// you win/lose the flip") is checked immediately after creation and triggers +/// only on the event(s) that occurred earlier during the creating resolution. It +/// gets exactly one shot on that creation batch: if it did not match, it is +/// discarded rather than left to fire on a later same-turn matching event (which +/// a bare CR 603.7b `WhenNextEvent` would do). The `PhaseChangedOnly` path keeps +/// the historical narrower coin-flip discard gate because it filters non-phase +/// matches out of the candidate batch. +/// +/// This runs only at a boundary that owns the complete batch. It constructs no +/// firing context, never touches Epic effects, and cannot remove a trigger that +/// [`collect_matching_delayed_triggers`] already fired and removed, because that +/// collector has already taken those instances out of `state.delayed_triggers`. +fn terminalize_unmatched_reflexives_for_closed_batch( + state: &mut GameState, + events: &[GameEvent], + scope: DelayedTriggerEventScope, +) { + if state.delayed_triggers.is_empty() { + return; + } + let mut to_discard: Vec = Vec::new(); + for (idx, delayed) in state.delayed_triggers.iter().enumerate() { + if delayed_trigger_event_with_index( + &delayed.condition, + events, + state, + delayed.source_id, + delayed.controller, + delayed.ability.trigger_source.as_ref(), + ) + .is_some() + { + // A matched instance is owned by the match collector: it either fired + // and was removed, was retained as duration-bearing, or was filtered + // out of this scope. None of those is an unmatched reflexive. + continue; + } + let expires = match scope { + DelayedTriggerEventScope::Any => is_reflexive_lifetime(&delayed.condition), + DelayedTriggerEventScope::PhaseChangedOnly => { + reflexive_coin_flip_resolved_without_match( + &delayed.condition, + events, + state, + delayed.ability.trigger_source.as_ref(), + ) + } + }; + if expires { + to_discard.push(idx); + } + } + for idx in to_discard.into_iter().rev() { + let trigger = state.delayed_triggers.remove(idx); + super::lifecycle::record_delayed_terminal( + trigger.provenance.firing(), + super::lifecycle::DelayedTerminalDisposition::ReflexiveUnmatched, + ); + } +} + pub(crate) fn collect_triggers_for_batch( state: &mut GameState, events: &[GameEvent], @@ -9078,6 +10155,42 @@ pub(crate) fn collect_triggers_for_batch( collect_pending_triggers(state, events) } +/// CR 603.3b + CR 603.7: Combine an already-observed normal seed with every +/// delayed trigger matching the same raw batch under an explicit scope, then +/// stable-sort the combined vector once by the existing APNAP rank/timestamp +/// key. +/// +/// `normal_pending` is a seed, never raw normal events: callers that own raw +/// normal events call [`collect_triggers_for_batch`] exactly once before entry, +/// so there is only ever one normal observation pass. `delayed_events` remains an +/// independent raw slice and `delayed_scope` is passed to the delayed matcher +/// unchanged. This authority is collection-only — it never terminalizes an +/// unmatched reflexive, so a partial frame-local batch can use it safely. +fn collect_pending_and_delayed_triggers_for_batch( + state: &mut GameState, + normal_pending: Vec, + delayed_events: &[GameEvent], + delayed_scope: DelayedTriggerEventScope, +) -> CollectedTriggerContextBatch { + let normal_was_non_empty = !normal_pending.is_empty(); + let (delayed_pending, delayed_consumed) = + collect_matching_delayed_triggers(state, delayed_events, delayed_scope); + let mut contexts = normal_pending; + contexts.extend(delayed_pending); + contexts.sort_by_key(|ctx| { + ( + trigger_apnap_rank(state, ctx.pending.controller), + ctx.pending.timestamp, + ) + }); + CollectedTriggerContextBatch { + contexts, + delayed_events: delayed_events.to_vec(), + delayed_consumed, + normal_was_non_empty, + } +} + fn current_trigger_prompt(state: &GameState, waiting_before: &WaitingFor) -> Option { let order_triggers_prompt = build_next_order_triggers_prompt_public(state); let active_trigger_prompt = (order_triggers_prompt.is_none() @@ -9137,11 +10250,21 @@ fn process_collected_triggers_with_delayed_events_scoped( ) -> TriggerBatchOutcome { let stack_before = state.stack.len(); let waiting_before = state.waiting_for.clone(); - let normal_was_non_empty = !normal_pending.is_empty(); - let (delayed_pending, consumed_events) = - collect_matching_delayed_triggers(state, delayed_events, delayed_scope); - let mut pending = normal_pending; - pending.extend(delayed_pending); + // CR 603.7 + CR 603.12: this is a closing boundary — match first, then expire + // every reflexive that this complete batch did not satisfy, before the + // empty-batch prompt recovery below can return. + let CollectedTriggerContextBatch { + contexts: pending, + delayed_events: _, + delayed_consumed: consumed_events, + normal_was_non_empty, + } = collect_pending_and_delayed_triggers_for_batch( + state, + normal_pending, + delayed_events, + delayed_scope, + ); + terminalize_unmatched_reflexives_for_closed_batch(state, delayed_events, delayed_scope); // CR 603.3b (issue #770 cluster): a truly-empty batch for *this* event // must still surface an orphaned `pending_trigger`/`deferred_triggers` @@ -9163,13 +10286,8 @@ fn process_collected_triggers_with_delayed_events_scoped( }; } - pending.sort_by_key(|ctx| { - ( - trigger_apnap_rank(state, ctx.pending.controller), - ctx.pending.timestamp, - ) - }); - + // The combined batch was stable-sorted exactly once by the shared collection + // authority above; do not re-sort it here. match begin_trigger_ordering(state, pending) { TriggerOrderingDisposition::PromptForChoice(wf) => { state.waiting_for = *wf; @@ -12130,8 +13248,9 @@ pub mod tests { use crate::types::counter::CounterType; use crate::types::events::{GameEvent, ManaTapState}; use crate::types::game_state::{ - DamageRecord, DelayedTrigger, DistributionUnit, GameState, LayersDirty, LoopDetectionMode, - NamedChoiceSourceBinding, SpellCastRecord, StackEntry, StackEntryKind, + DamageRecord, DeferredLifeCostResume, DelayedTrigger, DistributionUnit, GameState, + LayersDirty, LoopDetectionMode, NamedChoiceSourceBinding, PendingCast, + PendingCostMoveResume, SpellCastRecord, StackEntry, StackEntryKind, TransientContinuousEffect, WaitingFor, ZoneChangeRecord, }; use crate::types::identifiers::{ @@ -29552,6 +30671,503 @@ pub mod tests { assert_eq!(state.deferred_triggers.len(), 2); } + fn reflexive_delayed_trigger( + state: &mut GameState, + name: &str, + mode: crate::types::triggers::TriggerMode, + token: u64, + ) -> (ObjectId, DelayedTriggerOrigin) { + use crate::types::ability::{DelayedTriggerCondition, DelayedTriggerLifetime}; + let card_id = CardId(state.next_object_id); + let source_id = create_object( + state, + card_id, + PlayerId(0), + name.to_string(), + Zone::Battlefield, + ); + let origin = DelayedTriggerOrigin { + token: DelayedTriggerToken(token), + instance: DelayedTriggerInstanceId(token), + source_id, + }; + let mut ability = ResolvedAbility::new( + Effect::Draw { + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Controller, + }, + vec![], + source_id, + PlayerId(0), + ); + ability.description = Some(name.to_string()); + let is_phase = matches!(mode, crate::types::triggers::TriggerMode::Phase); + let mut definition = crate::types::ability::TriggerDefinition::new(mode); + if is_phase { + definition.phase = Some(crate::types::phase::Phase::Upkeep); + } + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenNextEvent { + trigger: Box::new(definition), + or_trigger: None, + lifetime: DelayedTriggerLifetime::Reflexive, + }, + ability: Box::new(ability), + controller: PlayerId(0), + source_id, + one_shot: true, + provenance: DelayedInstallIdentity::ReceiptEligible(origin), + }); + (source_id, origin) + } + + /// CR 603.12: `check_delayed_triggers` is a *closing* `Any` boundary. Its + /// contract is match, then terminalize, then dispatch — so a complete batch + /// with zero firing contexts must still expire an unmatched reflexive before + /// the empty early return. Moving terminalization below `pending.is_empty()` + /// leaves the instance installed and fails this row. + #[test] + fn direct_delayed_check_closes_unmatched_reflexive_before_empty_return() { + let mut state = setup(); + let (_, origin) = reflexive_delayed_trigger( + &mut state, + "Unmatched reflexive", + crate::types::triggers::TriggerMode::SpellCast, + 60_312, + ); + assert_eq!(state.delayed_triggers.len(), 1); + + // A complete `Any` batch that this reflexive's condition does not match. + let batch = vec![GameEvent::PhaseChanged { + phase: crate::types::phase::Phase::Upkeep, + }]; + + let guard = super::super::lifecycle::enter_action_frame(); + let fired = check_delayed_triggers(&mut state, &batch); + let facts = guard + .take_outer_facts() + .expect("outer action owns lifecycle facts"); + + assert!( + fired.is_empty(), + "no delayed context fires on a non-matching batch" + ); + assert!( + state.delayed_triggers.is_empty(), + "CR 603.12: the unmatched reflexive must be removed at its closing boundary" + ); + assert!( + facts.receipt_terminalized(origin), + "the closing boundary must record exactly one ReflexiveUnmatched terminal" + ); + assert!( + state.stack.is_empty(), + "an unmatched reflexive never reaches the stack" + ); + } + + /// **CR 605.1b + CR 605.4a — the complete-context partition at the mana + /// settlement seam, driven through the real `collect_mana_action_trigger_batch` + /// authority rather than by calling the classifier alone.** + /// + /// One completed mana frame emits one `ManaAdded` event that every candidate + /// below is coupled to. Exactly one of them may take stackless immediate + /// placement; every other one must be preserved unchanged and appended once + /// to the ordinary deferred batch. Rows: + /// + /// * **(a) targetless nonmodal all-mana** — ACCEPTED. Positive reach guard: + /// its mana is in the pool when the driver returns, with no stack entry, + /// no `pending_trigger*` cursor, and no deferred-queue member for it. + /// * **(b) `unless_pay` on the root** — rejected structurally, so a live + /// sidecar can never coincide with `UnlessPayment`. + /// * **(c) modal** — rejected. Baseline pushes a modal trigger to the stack + /// *before* mode selection, so it never reached the inline classifier + /// either; this cut keeps it on the ordinary announcement authority. + /// * **(d) `optional`** — rejected by this revision's pause-narrowing. This + /// one is an honest behaviour delta, not a no-op: baseline's + /// `is_triggered_mana_ability` returns **true** here. + /// * **(e) a target already present at classification time** — rejected by + /// the exact baseline predicate, with its referent preserved. + /// * **(f) `Effect::Mana { target: Some(..) }` with empty + /// `target_constraints` and empty runtime `targets`** — rejected only by + /// the canonical definition-owned slot proof. This is the row a predicate + /// that inspects `target_constraints`/`multi_target`/`ability.targets` + /// cannot fail on. + /// * **(g) an all-mana body with a non-CR-605.1b firing event** (the + /// Firebending shape) — rejected on the event axis. + /// + /// The deferral assertions are the reach guard that rejection means + /// "ordinary deferred dispatch", not "dropped": every rejected context is in + /// `deferred_triggers` exactly once and none of them reached the stack. + /// + /// REVERT-PROBES. Delete the modal guard ⇒ (c) is accepted and its stackless + /// dispatch trips the core's modal invariant. Delete the `unless_pay` walk ⇒ + /// (b) is accepted. Delete the slot proof ⇒ (f) is accepted. Replace the + /// baseline-predicate call with a hand-rolled all-mana check ⇒ (e) and (g) + /// are accepted. Any of those flips the exact-one-accepted count below. + #[test] + fn the_completed_mana_frame_classifier_accepts_only_the_stackless_safe_context() { + use crate::types::ability::{ManaProduction, UnlessPayModifier}; + use crate::types::game_state::ManaTriggerFixedPointResume; + use crate::types::mana::ManaType; + + let mut state = setup(); + let producer_card = CardId(state.next_object_id); + let producer = create_object( + &mut state, + producer_card, + PlayerId(0), + "Mana producer".to_string(), + Zone::Battlefield, + ); + let firing = GameEvent::ManaAdded { + player_id: PlayerId(0), + mana_type: ManaType::Green, + source_id: producer, + tap_state: ManaTapState::default(), + }; + + let mana_effect = |target: Option| Effect::Mana { + produced: ManaProduction::Colorless { + count: QuantityExpr::Fixed { value: 1 }, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target, + }; + + let mut contexts = Vec::new(); + let candidate = + |state: &mut GameState, label: &str, configure: &dyn Fn(&mut PendingTrigger)| { + let card_id = CardId(state.next_object_id); + let source_id = create_object( + state, + card_id, + PlayerId(0), + label.to_string(), + Zone::Battlefield, + ); + let mut ability = + ResolvedAbility::new(mana_effect(None), vec![], source_id, PlayerId(0)); + ability.description = Some(label.to_string()); + let mut pending = PendingTrigger::ordinary( + source_id, + PlayerId(0), + None, + Box::new(ability), + state.next_timestamp() as u32, + ); + pending.trigger_event = Some(firing.clone()); + configure(&mut pending); + PendingTriggerContext::single(pending) + }; + + contexts.push(candidate(&mut state, "(a) accepted", &|_| {})); + contexts.push(candidate(&mut state, "(b) unless_pay", &|pending| { + pending.ability.unless_pay = Some(UnlessPayModifier { + cost: AbilityCost::Mana { + cost: crate::types::mana::ManaCost::zero(), + }, + payer: TargetFilter::Controller, + }); + })); + contexts.push(candidate(&mut state, "(c) modal", &|pending| { + pending.modal = Some(ModalChoice::default()); + pending.mode_abilities = vec![AbilityDefinition::new( + AbilityKind::Spell, + Effect::Mana { + produced: ManaProduction::Colorless { + count: QuantityExpr::Fixed { value: 1 }, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }, + )]; + })); + // The surviving pause narrowing is the repeat family only: `optional` + // and `optional_for` are accepted now that `engine_payment_choices` + // carries their readiness hook, and `engine_resolution_choices` does not + // yet carry `RepeatDecision`'s. + contexts.push(candidate(&mut state, "(d) repeat", &|pending| { + pending.ability.repeat_until = + Some(crate::types::ability::RepeatContinuation::ControllerChoice); + })); + contexts.push(candidate(&mut state, "(e) inherited target", &|pending| { + pending.ability.targets = vec![TargetRef::Player(PlayerId(1))]; + })); + let slot_effect = mana_effect(Some(crate::types::ability::ManaTargetRole::Recipient { + recipient: TargetFilter::Player, + })); + contexts.push(candidate(&mut state, "(f) definition slot", &|pending| { + pending.ability.effect = slot_effect.clone(); + })); + contexts.push(candidate( + &mut state, + "(g) wrong firing event", + &|pending| { + pending.trigger_event = Some(GameEvent::AttackersDeclared { + attacker_ids: Vec::new(), + defending_player: PlayerId(1), + attacks: Vec::new(), + }); + }, + )); + + // Positive reach guard: every candidate really is coupled to the frame + // and really would qualify structurally but for its one distinguishing + // axis — (a) proves the accepted shape exists at all. + let accepted_labels: Vec<_> = contexts + .iter() + .filter(|context| { + matches!( + classify_pending_triggered_mana(&mut state.clone(), (*context).clone()), + PendingTriggeredManaClass::Accepted(_) + ) + }) + .filter_map(|context| context.pending.ability.description.clone()) + .collect(); + let accepted_count = accepted_labels.len(); + assert_eq!( + accepted_count, 1, + "exactly one of the seven complete contexts may take stackless placement; \ + accepted = {accepted_labels:?}" + ); + + let pool_before = state.players[0].mana_pool.total(); + let mut events = Vec::new(); + let (accepted, ordinary) = + partition_collected_mana_frame_contexts(&mut state, contexts.clone()); + assert_eq!(accepted.len(), 1); + assert_eq!( + ordinary.len(), + 6, + "every rejected context is preserved for the ordinary authority exactly once" + ); + assert_eq!( + accepted[0].context.pending.ability.description.as_deref(), + Some("(a) accepted"), + "the accepted view must carry the complete context unchanged" + ); + let pause = run_accepted_triggered_mana_fixed_point( + &mut state, + accepted, + ManaTriggerFixedPointResume::Parent, + &mut events, + ); + + assert!(pause.is_none(), "a pause-free accepted body settles inline"); + assert!( + state.players[0].mana_pool.total() > pool_before, + "(a) CR 605.4a: the accepted body's mana is in the pool before the frame returns" + ); + assert!( + state.stack.is_empty(), + "CR 605.4a: immediate placement creates no stack object for any context" + ); + assert!( + state.pending_trigger.is_none() && state.pending_trigger_entry.is_none(), + "immediate placement never writes a construction cursor" + ); + assert!( + state.pending_triggered_mana_resume.is_none() + && state.active_accepted_triggered_mana_node.is_none(), + "a synchronous occurrence leaves no sidecar and restores its marker" + ); + assert!( + state.deferred_triggers.is_empty(), + "the driver itself neither queues nor drains: rejected contexts are returned to \ + the caller's ordinary authority" + ); + let deferred_labels: Vec<_> = ordinary + .iter() + .filter_map(|context| context.pending.ability.description.clone()) + .collect(); + for label in [ + "(b) unless_pay", + "(c) modal", + "(d) repeat", + "(e) inherited target", + "(f) definition slot", + "(g) wrong firing event", + ] { + assert!( + deferred_labels.iter().any(|found| found == label), + "{label} must be preserved for ordinary dispatch rather than dropped" + ); + } + let inherited = ordinary + .iter() + .find(|context| { + context.pending.ability.description.as_deref() == Some("(e) inherited target") + }) + .expect("(e) is preserved for ordinary dispatch"); + assert_eq!( + inherited.pending.ability.targets, + vec![TargetRef::Player(PlayerId(1))], + "a rejected context's pre-existing referent is preserved, never purged" + ); + } + + /// Positive control for the rows above: the extraction must not have broken + /// ordinary delayed matching. A matching one-shot still fires, is removed + /// exactly once by the match collector, and is never recorded as a terminal + /// by the separated unmatched-reflexive pass. + #[test] + fn closing_boundary_still_fires_and_removes_a_matching_one_shot_once() { + use crate::types::ability::DelayedTriggerCondition; + + let mut state = setup(); + let card_id = CardId(state.next_object_id); + let source_id = create_object( + &mut state, + card_id, + PlayerId(0), + "Matching delayed source".to_string(), + Zone::Battlefield, + ); + let victim = make_creature(&mut state, PlayerId(0), "Victim", 1, 1); + let origin = DelayedTriggerOrigin { + token: DelayedTriggerToken(60_313), + instance: DelayedTriggerInstanceId(60_313), + source_id, + }; + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ResolvedAbility::new( + Effect::Draw { + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Controller, + }, + vec![], + source_id, + PlayerId(0), + )), + controller: PlayerId(0), + source_id, + one_shot: true, + provenance: DelayedInstallIdentity::ReceiptEligible(origin), + }); + + let batch = vec![zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + vec![], + )]; + + let guard = super::super::lifecycle::enter_action_frame(); + check_delayed_triggers(&mut state, &batch); + let facts = guard + .take_outer_facts() + .expect("outer action owns lifecycle facts"); + + assert!( + state.delayed_triggers.is_empty(), + "a matched one-shot is removed exactly once by the match collector" + ); + assert!( + !facts.receipt_terminalized(origin), + "a matched instance must never reach the unmatched-reflexive terminalizer" + ); + assert_eq!( + state.stack.len(), + 1, + "the matched delayed trigger reaches the stack through the ordinary dispatcher" + ); + } + + /// CR 603.12: a *partial* frame-local batch must be able to run the shared + /// match-only collector without ending an outer reflexive's lifetime. Only a + /// closing boundary terminalizes. + #[test] + fn match_only_collection_leaves_an_unmatched_reflexive_installed() { + let mut state = setup(); + let (_, origin) = reflexive_delayed_trigger( + &mut state, + "Outer reflexive", + crate::types::triggers::TriggerMode::SpellCast, + 60_314, + ); + + let child_batch = vec![GameEvent::PhaseChanged { + phase: crate::types::phase::Phase::Upkeep, + }]; + + let guard = super::super::lifecycle::enter_action_frame(); + let batch = collect_pending_and_delayed_triggers_for_batch( + &mut state, + Vec::new(), + &child_batch, + DelayedTriggerEventScope::Any, + ); + let facts = guard + .take_outer_facts() + .expect("outer action owns lifecycle facts"); + + assert!(batch.contexts.is_empty()); + assert!(!batch.normal_was_non_empty); + assert_eq!( + state.delayed_triggers.len(), + 1, + "a frame-local micro-batch must not expire an outer reflexive" + ); + assert!( + !facts.receipt_terminalized(origin), + "collection-only must record no terminal disposition" + ); + } + + /// The phase closing processor must keep its narrow `PhaseChangedOnly` scope + /// for both matching and unmatched-reflexive cleanup: an unrelated non-phase + /// reflexive stays installed even though the batch also carries its event. + #[test] + fn phase_scope_stays_narrow_for_matching_and_terminalization() { + let mut state = setup(); + let (_, unrelated) = reflexive_delayed_trigger( + &mut state, + "Unrelated non-phase reflexive", + crate::types::triggers::TriggerMode::SpellCast, + 60_315, + ); + let seed = vec![make_draw_pending_trigger(&mut state, "Seed", PlayerId(0))]; + + let batch = vec![GameEvent::PhaseChanged { + phase: crate::types::phase::Phase::Upkeep, + }]; + + let guard = super::super::lifecycle::enter_action_frame(); + let mut events_out = Vec::new(); + let outcome = process_collected_triggers_with_delayed_phase_events( + &mut state, + seed, + &batch, + &mut events_out, + ); + let facts = guard + .take_outer_facts() + .expect("outer action owns lifecycle facts"); + + assert!( + outcome.fired, + "the pre-collected normal seed still dispatches" + ); + assert_eq!( + state.delayed_triggers.len(), + 1, + "PhaseChangedOnly must not expire an unrelated non-phase reflexive" + ); + assert!( + !facts.receipt_terminalized(unrelated), + "widening the phase scope to Any would terminalize this row" + ); + } + #[test] fn resolution_completion_requires_an_empty_resolution_stack() { let mut state = setup(); @@ -29567,6 +31183,769 @@ pub mod tests { ); } + #[test] + fn pending_cost_move_and_deferred_life_owners_block_until_released() { + let mut state = setup(); + state.deferred_triggers = vec![ + make_draw_pending_trigger(&mut state, "Watcher A", PlayerId(0)), + make_draw_pending_trigger(&mut state, "Watcher B", PlayerId(0)), + ]; + state.pending_cost_move_resume = Some(PendingCostMoveResume::Foretell { + player: PlayerId(0), + object_id: ObjectId(100), + cost: ManaCost::NoCost, + turn_foretold: 1, + }); + assert!(!resolution_completion_can_settle(&state)); + + state.pending_cost_move_resume = None; + state.pending_deferred_life_cost_resume = Some(DeferredLifeCostResume::PayAmount { + player: PlayerId(0), + total: 3, + resume_at_resolution_depth: 0, + }); + assert!(!resolution_completion_can_settle(&state)); + + state.pending_deferred_life_cost_resume = None; + assert!(resolution_completion_can_settle(&state)); + let mut events = Vec::new(); + assert!(matches!( + drain_deferred_trigger_queue(&mut state, &mut events), + Some(WaitingFor::OrderTriggers { .. }) + )); + } + + /// Build a `TriggeredManaResume` naming `node`, with an otherwise inert + /// accepted work item. The row-level assertions never read its body; what + /// matters is that the durable carrier exists and names a node. + fn triggered_mana_sidecar( + state: &mut GameState, + node: crate::types::resolved_commands::RulesExecutionNodeRef, + ) -> Box { + let source = create_object( + state, + CardId(state.next_object_id), + PlayerId(0), + "Accepted triggered mana source".to_string(), + Zone::Battlefield, + ); + let pending = PendingTrigger::ordinary( + source, + PlayerId(0), + None, + Box::new(ResolvedAbility::new( + Effect::Draw { + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Controller, + }, + Vec::new(), + source, + PlayerId(0), + )), + state.turn_number, + ); + Box::new(crate::types::game_state::TriggeredManaResume { + current: Box::new(PendingTriggerContext::single(pending)), + current_override: None, + rules_execution_node: node, + accepted_tail: Vec::new(), + collected_batches: Vec::new(), + outer_resume: crate::types::game_state::ManaTriggerFixedPointResume::Parent, + stage: crate::types::game_state::TriggeredManaStage::ResolvingBody, + }) + } + + /// Round-20 closure map, "Why the carrier is not part of + /// `resolution_completion_can_settle`": the settlement predicate takes the + /// triggered-mana sidecar **and only that carrier**. + /// + /// The construction priority recipient is deliberately excluded. + /// `can_drain_deferred_triggers` consults this predicate first, so putting + /// the recipient in it would reject the settled wrapper's own drain, the + /// construction finisher's drain, and every deferred-sibling drain in the + /// same batch — the batch would deadlock on its own recipient. The third + /// assertion below is that revert discriminator: a regression that adds the + /// recipient to the predicate turns the `OrderTriggers` drain into `None`. + #[test] + fn settlement_predicate_takes_the_mana_sidecar_and_never_the_construction_recipient() { + let mut state = setup(); + state.deferred_triggers = vec![ + make_draw_pending_trigger(&mut state, "Watcher A", PlayerId(0)), + make_draw_pending_trigger(&mut state, "Watcher B", PlayerId(0)), + ]; + let baseline = resolution_completion_can_settle(&state); + assert!( + baseline, + "positive reach guard: the batch settles at baseline" + ); + + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + assert_eq!( + resolution_completion_can_settle(&state), + baseline, + "a live construction recipient must not change the settlement result" + ); + + let mut events = Vec::new(); + assert!( + matches!( + drain_deferred_trigger_queue(&mut state, &mut events), + Some(WaitingFor::OrderTriggers { .. }) + ), + "a settled batch must still drain with the construction recipient installed" + ); + assert_eq!( + state.pending_trigger_construction_priority_recipient, + Some(PlayerId(1)), + "the predicate must not consume or rewrite the recipient" + ); + + // Rebuild the batch the drain just consumed, then prove the other side. + let mut state = setup(); + state.deferred_triggers = vec![ + make_draw_pending_trigger(&mut state, "Watcher A", PlayerId(0)), + make_draw_pending_trigger(&mut state, "Watcher B", PlayerId(0)), + ]; + let node = crate::types::resolved_commands::RulesExecutionNodeRef::TriggeredMana( + crate::types::resolved_commands::SettlementNodeOrdinal(7), + ); + state.pending_triggered_mana_resume = Some(triggered_mana_sidecar(&mut state, node)); + assert!( + !resolution_completion_can_settle(&state), + "an accepted triggered-mana occurrence owns its own emitted-event fixed point" + ); + let mut events = Vec::new(); + assert!( + drain_deferred_trigger_queue(&mut state, &mut events).is_none(), + "no ordinary drain may run while the sidecar is live" + ); + + state.pending_triggered_mana_resume = None; + assert!( + resolution_completion_can_settle(&state), + "removing the sidecar restores the baseline result exactly" + ); + } + + /// One instance of each of the six approved trigger-construction prompts, + /// in the census order of `is_trigger_construction_prompt`. + fn approved_construction_prompts() -> Vec { + vec![ + WaitingFor::OrderTriggers { + player: PlayerId(0), + triggers: Vec::new(), + }, + WaitingFor::NamedChoice { + player: PlayerId(0), + choice_type: crate::types::ability::ChoiceType::Player { + distinctness: Default::default(), + }, + options: vec!["P2".to_string()], + source: None, + persist_player: None, + }, + WaitingFor::OptionalEffectChoice { + player: PlayerId(0), + source_id: ObjectId(1), + description: None, + may_trigger_key: None, + }, + WaitingFor::AbilityModeChoice { + player: PlayerId(0), + modal: crate::types::ability::ModalChoice { + min_choices: 1, + max_choices: 1, + mode_count: 2, + ..Default::default() + }, + source_id: ObjectId(1), + mode_abilities: Vec::new(), + is_activated: false, + ability_index: None, + ability_cost: None, + unavailable_modes: Vec::new(), + }, + WaitingFor::TriggerTargetSelection { + player: PlayerId(0), + trigger_controller: Some(PlayerId(0)), + trigger_event: None, + trigger_events: Vec::new(), + target_slots: Vec::new(), + mode_labels: Vec::new(), + target_constraints: Vec::new(), + selection: Default::default(), + source_id: None, + description: None, + }, + WaitingFor::DistributeAmong { + player: PlayerId(0), + total: 2, + targets: Vec::new(), + unit: crate::types::game_state::DistributionUnit::Damage, + }, + ] + } + + /// Round-20 closure map, "Exact preserve/finish contract" clause 1: with no + /// recipient installed, the finisher returns `produced` byte-for-byte and + /// performs no drain. + /// + /// This is the entire ordinary-caller contract. Every existing + /// controller/active-player fallback at the seven seams depends on it, so + /// the row includes a terminal `Priority` with a *drainable* queue behind it + /// — exactly the shape clause 3 would drain — and requires the queue to be + /// left untouched. + #[test] + fn construction_finisher_is_a_no_op_without_a_construction_recipient() { + for produced in approved_construction_prompts() + .into_iter() + .chain([WaitingFor::Priority { + player: PlayerId(0), + }]) + { + let mut state = drain_policy_fixture(false); + state.trigger_construction_finisher_ran_this_action = false; + let mut events = Vec::new(); + let returned = + finish_trigger_construction_action(&mut state, &mut events, produced.clone()); + assert_eq!(returned, produced, "the ordinary contract is byte-for-byte"); + assert_eq!( + state.deferred_triggers.len(), + 2, + "an ordinary caller performs no drain: {produced:?}" + ); + assert!(events.is_empty()); + assert!(state + .pending_trigger_construction_priority_recipient + .is_none()); + } + } + + /// Clause 2: with a recipient installed and one of the six approved + /// construction prompts, the finisher returns the real prompt and leaves the + /// recipient installed for the next construction step. + #[test] + fn construction_finisher_retains_the_recipient_across_every_approved_prompt() { + for produced in approved_construction_prompts() { + let mut state = drain_policy_fixture(false); + state.trigger_construction_finisher_ran_this_action = false; + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + let mut events = Vec::new(); + let returned = + finish_trigger_construction_action(&mut state, &mut events, produced.clone()); + assert_eq!(returned, produced, "the real prompt is surfaced unchanged"); + assert_eq!( + state.pending_trigger_construction_priority_recipient, + Some(PlayerId(1)), + "the recipient is retained across {produced:?}" + ); + assert_eq!( + state.deferred_triggers.len(), + 2, + "an intermediate prompt does not drain the tail" + ); + } + } + + /// Clause 3: a `Priority` produced by a successful announcement, a dangling + /// recovery, or an optional decline is only terminal once the whole deferred + /// tail behind it has announced. + /// + /// Baseline's `abandon_ceased_pending_trigger` deliberately leaves deferred + /// siblings installed and both dangling recoveries return `Priority` without + /// draining, which is exactly why this drain lives in the finisher rather + /// than at each return. The row proves both halves: a queue that still owes + /// an ordering choice surfaces `OrderTriggers` with the recipient retained, + /// and only an exhausted tail consumes the recipient and returns + /// `Priority { player }` — the carried player, not the produced one. + #[test] + fn construction_finisher_drains_the_tail_before_consuming_the_recipient() { + let mut state = drain_policy_fixture(false); + state.trigger_construction_finisher_ran_this_action = false; + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + let mut events = Vec::new(); + let returned = finish_trigger_construction_action( + &mut state, + &mut events, + WaitingFor::Priority { + player: PlayerId(0), + }, + ); + assert!( + matches!(returned, WaitingFor::OrderTriggers { .. }), + "a queued same-controller batch is not terminal: {returned:?}" + ); + assert_eq!( + state.pending_trigger_construction_priority_recipient, + Some(PlayerId(1)), + "the drain's own prompt retains the recipient" + ); + + // Exhausted tail: the recipient is consumed and the returned wait names + // the carried player rather than the produced one. + let mut state = setup(); + state.trigger_construction_finisher_ran_this_action = false; + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + let mut events = Vec::new(); + let returned = finish_trigger_construction_action( + &mut state, + &mut events, + WaitingFor::Priority { + player: PlayerId(0), + }, + ); + assert_eq!( + returned, + WaitingFor::Priority { + player: PlayerId(1) + }, + "an exhausted tail hands priority to the carried recipient" + ); + assert!( + state + .pending_trigger_construction_priority_recipient + .is_none(), + "no durable recipient may survive an action returning ordinary priority" + ); + } + + /// Round-20 audit class (b), "Finisher branch: carrier live, queue + /// non-empty, drain ineligible." + /// + /// The settled-root gate proves this state absent upstream, so this is a + /// defensive unit proof and never action coverage. What it must show is that + /// the finisher consumes the recipient rather than leaking it: an + /// undrainable queue is not a reason to leave a durable recipient installed + /// across an action that returns ordinary priority. + #[test] + fn construction_finisher_consumes_rather_than_leaks_an_undrainable_queue() { + let mut state = drain_policy_fixture(false); + state.trigger_construction_finisher_ran_this_action = false; + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + // A live terminal-resolution marker is one of the three states the plan + // names for this branch: the post-announcement drain returns early on + // `pending_resolution_completion` before it ever reaches the policy. + state.pending_resolution_completion = + Some(crate::types::game_state::PendingResolutionCompletion { + player: PlayerId(0), + source_id: ObjectId(1), + final_cast: None, + }); + let mut probe = Vec::new(); + assert!( + drain_deferred_triggers_after_trigger_construction(&mut state, &mut probe).is_none(), + "positive reach guard: the construction drain really is ineligible" + ); + assert_eq!(state.deferred_triggers.len(), 2); + let mut events = Vec::new(); + let returned = finish_trigger_construction_action( + &mut state, + &mut events, + WaitingFor::Priority { + player: PlayerId(0), + }, + ); + assert_eq!( + returned, + WaitingFor::Priority { + player: PlayerId(1) + } + ); + assert!(state + .pending_trigger_construction_priority_recipient + .is_none()); + assert_eq!( + state.deferred_triggers.len(), + 2, + "the undrainable queue is left exactly as found" + ); + } + + /// Round-20 audit class (b), "Finisher branch: carrier live, non-`Priority`, + /// non-approved wait." The six approved prompts are the closed census of + /// what the seven seams can produce, so anything else means a seam was added + /// below dispatch. Do not coerce, do not consume, do not invent a recipient. + #[test] + #[should_panic(expected = "outside the closed")] + fn construction_finisher_rejects_a_wait_outside_the_closed_census() { + let mut state = setup(); + state.trigger_construction_finisher_ran_this_action = false; + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + let mut events = Vec::new(); + let _ = finish_trigger_construction_action( + &mut state, + &mut events, + WaitingFor::GameOver { + winner: Some(PlayerId(0)), + }, + ); + } + + /// The install rule: reinstalling the same player is idempotent, and + /// observing a different player while the field is live is an internal + /// invariant error rather than a silent overwrite. (`elimination.rs`'s + /// CR 800.4a re-point past a departed recipient deliberately does not come + /// through this authority.) + #[test] + fn preserving_the_same_construction_recipient_twice_is_idempotent() { + let mut state = setup(); + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + assert_eq!( + state.pending_trigger_construction_priority_recipient, + Some(PlayerId(1)) + ); + } + + #[test] + #[should_panic(expected = "different construction priority recipient")] + fn preserving_a_different_construction_recipient_while_live_is_an_invariant_error() { + let mut state = setup(); + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(1)); + preserve_trigger_construction_priority_recipient(&mut state, PlayerId(0)); + } + + /// Build a two-observer batch that raises `OrderTriggers` when it drains, + /// optionally under a passive announced spell. + fn drain_policy_fixture(spell_on_stack: bool) -> GameState { + let mut state = setup(); + state.deferred_triggers = vec![ + make_draw_pending_trigger(&mut state, "Watcher A", PlayerId(0)), + make_draw_pending_trigger(&mut state, "Watcher B", PlayerId(0)), + ]; + if spell_on_stack { + let card_id = CardId(state.next_object_id); + let spell = create_object( + &mut state, + card_id, + PlayerId(0), + "Passive announced spell".to_string(), + Zone::Stack, + ); + state.stack.push_back(StackEntry { + id: spell, + source_id: spell, + controller: PlayerId(0), + kind: StackEntryKind::Spell { + card_id: CardId(2), + ability: None, + casting_variant: Default::default(), + actual_mana_spent: 0, + }, + }); + } + state + } + + /// Plan v23 Step 5, "replace the existing two drain booleans inside the + /// shared core with private `DeferredTriggerDrainPolicy`". + /// + /// `engine_priority`'s `skip_deferred_trigger_drain` and this module's + /// `allow_spell_on_stack` were two independent axes carried as two + /// unrelated booleans. The enum has to keep them independent — collapsing + /// them (making `Skip` drain, or letting `ResolutionSafe` tolerate a + /// `Spell` entry) flips exactly one assertion below — while leaving the + /// shared carrier census in `resolution_completion_can_settle` in front of + /// every variant. + #[test] + fn deferred_trigger_drain_policy_separates_the_skip_and_spell_on_stack_axes() { + // Positive reach guard: with no spell on the stack, the resolution-safe + // policy really drains this batch. + let mut state = drain_policy_fixture(false); + let mut events = Vec::new(); + assert!( + matches!( + drain_deferred_trigger_queue_with_policy( + &mut state, + &mut events, + DeferredTriggerDrainPolicy::ResolutionSafe, + ), + Some(WaitingFor::OrderTriggers { .. }) + ), + "positive reach guard: ResolutionSafe drains an unobstructed batch" + ); + assert!(state.deferred_triggers.is_empty()); + + // `Skip` owns no drain at all, and leaves the queue exactly as it found it. + let mut state = drain_policy_fixture(false); + let mut events = Vec::new(); + assert!( + drain_deferred_trigger_queue_with_policy( + &mut state, + &mut events, + DeferredTriggerDrainPolicy::Skip, + ) + .is_none(), + "Skip must not drain even when every other gate is open" + ); + assert_eq!( + state.deferred_triggers.len(), + 2, + "Skip must leave the queue intact for its owning caller" + ); + assert!(events.is_empty()); + + // CR 603.3b + issue #1793: a `Spell` entry blocks the resolution-safe + // policy and only the resolution-safe policy. + let mut state = drain_policy_fixture(true); + let mut events = Vec::new(); + assert!( + drain_deferred_trigger_queue_with_policy( + &mut state, + &mut events, + DeferredTriggerDrainPolicy::ResolutionSafe, + ) + .is_none(), + "ResolutionSafe must refuse to order observers above a spell on the stack" + ); + assert_eq!(state.deferred_triggers.len(), 2); + + // CR 601.2h + CR 602.2b: the post-announcement policy drains the same + // batch above the same spell. + let mut state = drain_policy_fixture(true); + let mut events = Vec::new(); + assert!( + matches!( + drain_deferred_trigger_queue_with_policy( + &mut state, + &mut events, + DeferredTriggerDrainPolicy::SettledPriority, + ), + Some(WaitingFor::OrderTriggers { .. }) + ), + "SettledPriority drains above the passive announced spell" + ); + + // Every policy still runs behind the shared carrier census: a live + // triggered-mana sidecar rejects both draining variants. + for policy in [ + DeferredTriggerDrainPolicy::ResolutionSafe, + DeferredTriggerDrainPolicy::SettledPriority, + ] { + let mut state = drain_policy_fixture(false); + let node = crate::types::resolved_commands::RulesExecutionNodeRef::TriggeredMana( + crate::types::resolved_commands::SettlementNodeOrdinal(3), + ); + state.pending_triggered_mana_resume = Some(triggered_mana_sidecar(&mut state, node)); + let mut events = Vec::new(); + assert!( + drain_deferred_trigger_queue_with_policy(&mut state, &mut events, policy).is_none(), + "{policy:?} must still consult resolution_completion_can_settle" + ); + assert_eq!(state.deferred_triggers.len(), 2); + } + } + + /// Round-18 closure, "Baseline bypass and exact ownership guard": both + /// observer helpers are collection and partial-release authorities, so they + /// must fail closed on sidecar ownership *before* collecting or draining. + /// + /// Either authority alone suffices, and the row proves both independently: + /// a scoped sidecar action can move the durable carrier out of `GameState` + /// (leaving only the live accepted-node marker), and at a resting pause the + /// durable carrier is present while the lexical marker has been restored. + #[test] + fn both_observer_helpers_are_no_ops_under_either_triggered_mana_authority() { + let node = crate::types::resolved_commands::RulesExecutionNodeRef::TriggeredMana( + crate::types::resolved_commands::SettlementNodeOrdinal(11), + ); + + // Positive reach guard: with neither authority, each helper really + // collects this event. + for settled in [false, true] { + let (mut state, events) = observer_helper_fixture(settled); + let mut events = events; + if settled { + collect_and_drain_observer_triggers_if_settled(&mut state, &mut events, 0); + } else { + park_observer_triggers_if_paused(&mut state, &events, 0); + } + assert!( + !state.deferred_triggers.is_empty() || !state.stack.is_empty(), + "positive reach guard (settled={settled}): the ordinary helper collects" + ); + } + + // Durable carrier only (resting pause). + for settled in [false, true] { + let (mut state, mut events) = observer_helper_fixture(settled); + state.pending_triggered_mana_resume = Some(triggered_mana_sidecar(&mut state, node)); + if settled { + collect_and_drain_observer_triggers_if_settled(&mut state, &mut events, 0); + } else { + park_observer_triggers_if_paused(&mut state, &events, 0); + } + assert!( + state.deferred_triggers.is_empty() && state.stack.is_empty(), + "durable sidecar (settled={settled}): the helper must collect nothing" + ); + } + + // Live marker only (scoped sidecar action). + for settled in [false, true] { + let (mut state, mut events) = observer_helper_fixture(settled); + state.active_accepted_triggered_mana_node = Some(node); + state.active_rules_execution_node = Some(node); + if settled { + collect_and_drain_observer_triggers_if_settled(&mut state, &mut events, 0); + } else { + park_observer_triggers_if_paused(&mut state, &events, 0); + } + assert!( + state.deferred_triggers.is_empty() && state.stack.is_empty(), + "live accepted marker (settled={settled}): the helper must collect nothing" + ); + } + } + + /// Round-18 closure: the ordinary (no-sidecar) branch of both helpers now + /// derives its suffix through `filter_consumed_trigger_events_from`, so an + /// exact occurrence already claimed by `ConsumeBeforePriority` cannot be + /// recollected — while an equal-looking *unclaimed* occurrence still is. + /// + /// The journal is borrowed, never taken: unrelated callers with an empty + /// journal stay behaviour-identical, which the positive reach guard in the + /// row above asserts. + #[test] + fn ordinary_observer_helpers_filter_one_preclaimed_occurrence_out_of_two_equal_events() { + for settled in [false, true] { + let (mut state, mut events) = observer_helper_fixture(settled); + // Two equal-looking full-buffer occurrences of the same event. + let repeated = events[0].clone(); + events.push(repeated.clone()); + state.consumed_before_priority_trigger_events = vec![ConsumedTriggerEventOccurrence { + event: repeated.clone(), + occurrence: trigger_event_occurrence(&events, 0), + }]; + + if settled { + collect_and_drain_observer_triggers_if_settled(&mut state, &mut events, 0); + } else { + park_observer_triggers_if_paused(&mut state, &events, 0); + } + + let collected = state.deferred_triggers.len() + state.stack.len(); + assert_eq!( + collected, 1, + "settled={settled}: exactly the unclaimed second occurrence is collected" + ); + assert_eq!( + state.consumed_before_priority_trigger_events.len(), + 1, + "settled={settled}: the helper borrows the journal, it never takes it" + ); + } + } + + /// One battlefield observer whose `Taps` trigger fires on the single event + /// in the returned buffer. `settled` selects the wait each helper requires + /// to reach its collecting branch. + fn observer_helper_fixture(settled: bool) -> (GameState, Vec) { + let mut state = setup(); + state.active_player = PlayerId(0); + state.priority_player = PlayerId(0); + let observer = make_creature(&mut state, PlayerId(0), "Tap observer", 2, 2); + let observer_trigger = TriggerDefinition::new(TriggerMode::Taps) + .valid_card(TargetFilter::Typed(TypedFilter::creature())) + .execute(AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + )); + let object = state.objects.get_mut(&observer).expect("observer exists"); + object.trigger_definitions.push(observer_trigger.clone()); + std::sync::Arc::make_mut(&mut object.base_trigger_definitions).push(observer_trigger); + object.materialize_base_trigger_definitions(); + + let tapper = make_creature(&mut state, PlayerId(0), "Tapper", 2, 2); + let victim = make_creature(&mut state, PlayerId(1), "Tapped victim", 2, 2); + let events = vec![GameEvent::PermanentTapped { + object_id: victim, + caused_by: Some(tapper), + }]; + + state.waiting_for = if settled { + WaitingFor::Priority { + player: PlayerId(0), + } + } else { + WaitingFor::OptionalEffectChoice { + player: PlayerId(0), + source_id: observer, + description: Some("paused".to_string()), + may_trigger_key: None, + } + }; + (state, events) + } + + /// CR 601.2h + CR 602.2b: the post-announcement drain's cast guard reads the + /// EXTERNAL `pending_cast` carrier, and deliberately does NOT read + /// `state.waiting_for`. + /// + /// The second leg is the load-bearing one. `state.waiting_for` is the wait + /// the current reducer action STARTED from — `apply_action` assigns the + /// handler's returned wait only after the handler returns — so an inline + /// carrier read here is a read of the past. The concrete casualty is an + /// additional-sacrifice cast that paused on `TargetSelection`: its + /// `CostResume::Spell` still holds that stale wait while `casting_targets` + /// finalizes the announcement and calls in, so the cost-sacrifice observers + /// would stay parked behind the announced spell forever + /// (`casting_costs::tests::cost_paid_multi_sacrifice_kicker_paused_under_observes` + /// is that row, and it is what fails if the read is restored). Every real + /// boundary excludes an inline carrier structurally instead — see the + /// contract comment on the function. + #[test] + fn the_external_pending_cast_carrier_blocks_the_post_announcement_drain() { + let mut state = setup(); + state.deferred_triggers = vec![ + make_draw_pending_trigger(&mut state, "Watcher A", PlayerId(0)), + make_draw_pending_trigger(&mut state, "Watcher B", PlayerId(0)), + ]; + let ability = ResolvedAbility::new(Effect::NoOp, Vec::new(), ObjectId(101), PlayerId(0)); + let pending = PendingCast::new(ObjectId(101), CardId(101), ability, ManaCost::NoCost); + state.pending_cast = Some(Box::new(pending.clone())); + let mut events = Vec::new(); + assert!( + drain_deferred_triggers_after_stack_object_announcement(&mut state, &mut events) + .is_none(), + "an unconsumed external cast root blocks the drain" + ); + + // A STALE inline carrier must not block: this is the exact state the + // mid-action callers are in when they finalize an announcement. + state.pending_cast = None; + state.waiting_for = WaitingFor::ModeChoice { + player: PlayerId(0), + modal: ModalChoice::default(), + pending_cast: Box::new(pending), + unavailable_modes: Vec::new(), + }; + assert!( + matches!( + drain_deferred_triggers_after_stack_object_announcement(&mut state, &mut events), + Some(WaitingFor::OrderTriggers { .. }) + ), + "the drain reads the external carrier, never the action's incoming wait" + ); + + // Positive reach guard for the first leg: the same queue drains once the + // external carrier is gone. + state.deferred_triggers = vec![ + make_draw_pending_trigger(&mut state, "Watcher C", PlayerId(0)), + make_draw_pending_trigger(&mut state, "Watcher D", PlayerId(0)), + ]; + state.waiting_for = WaitingFor::Priority { + player: PlayerId(0), + }; + assert!(matches!( + drain_deferred_triggers_after_stack_object_announcement(&mut state, &mut events), + Some(WaitingFor::OrderTriggers { .. }) + )); + } + /// Issue #1793: at a true resolution boundary, 2+ same-controller deferred /// triggers must surface CR 603.3b ordering before dispatch. #[test] @@ -32133,6 +34512,45 @@ pub mod tests { panic!("stack did not settle: waiting_for={:?}", state.waiting_for); } + /// CR 603.3b + CR 603.12: drive to the next REAL player decision, answering + /// any CR 603.3b ordering prompt along the way in the order the engine + /// offered. + /// + /// A materialized reflexive trigger is an ordinary member of the deferred + /// APNAP batch, so a same-controller batch that previously held only the + /// co-triggered observer now also holds the reflexive and surfaces an + /// ordering prompt that the inline-resolution flow never showed. + /// [`resolve_stack_until_paused`] treats that prompt as a terminal pause and + /// returns without resolving anything, which is correct for its other ~30 + /// callers and wrong for the reflexive rows — hence a separate driver rather + /// than a change to the shared one. + /// + /// Returns as soon as a non-ordering wait is reached (a genuine choice) or + /// the stack empties at `Priority`. + fn settle_answering_trigger_order(state: &mut GameState) { + for _ in 0..30 { + match &state.waiting_for { + WaitingFor::OrderTriggers { triggers, .. } => { + let order: Vec = (0..triggers.len()).collect(); + crate::game::engine::apply_as_current( + state, + GameAction::OrderTriggers { order }, + ) + .expect("submit the CR 603.3b ordering prompt"); + } + WaitingFor::Priority { .. } if !state.stack.is_empty() => { + crate::game::engine::apply_as_current(state, GameAction::PassPriority) + .expect("pass priority"); + } + _ => return, + } + } + panic!( + "did not settle while answering trigger order: waiting_for={:?}", + state.waiting_for + ); + } + /// CR 702.93a + issue #423 (4a, B1 baseline): an Undying creature with zero /// +1/+1 counters sacrificed inside the `EffectZoneChoice` resolution /// handler must still fire its dies-trigger and return to the battlefield @@ -32202,7 +34620,14 @@ pub mod tests { // The Undying trigger must have been collected and dispatched; resolve // whatever reached the stack. - resolve_stack_until_paused(&mut state); + // + // CR 603.12 + CR 603.3b: the reflexive `Destroy SelfRef` is now a + // CREATED reflexive trigger rather than an inline continuation, so it is + // an ordinary member of the same same-controller deferred batch as the + // Undying dies-trigger and the batch opens a CR 603.3b ordering prompt. + // Answer it and keep going; the row's claim (Undying fires and returns + // the creature) is unchanged. + settle_answering_trigger_order(&mut state); let obj = state.objects.get(&young_wolf).expect("object tracked"); assert_eq!( @@ -32546,59 +34971,45 @@ pub mod tests { ) .expect("select Young Wolf to sacrifice"); - // (ii) The reflexive `WhenYouDo` continuation was NOT dropped — it - // resolved into a second `EffectZoneChoice` (the opponent sacrifice). - assert!( - matches!(state.waiting_for, WaitingFor::EffectZoneChoice { .. }), - "the reflexive opponent Sacrifice must raise an EffectZoneChoice, got {:?}", - state.waiting_for - ); - - // The Undying dies-trigger and the co-triggered observer trigger were - // batched into the deferred queue — they would be lost pre-fix. - assert_eq!( - state.deferred_triggers.len(), - 2, - "the Undying dies-trigger and the targeted observer trigger must \ - both be batched into deferred_triggers (issue #423)" - ); - - // Resolve the reflexive opponent sacrifice → the handler settles to - // `Priority` and drains the deferred queue. - crate::game::engine::apply_as_current( - &mut state, - GameAction::SelectCards { cards: vec![opp_a] }, - ) - .expect("opponent sacrifices a creature"); - - // #1793: the deferred flush now routes through `begin_trigger_ordering`, - // so the two same-controller deferred triggers (Undying dies-trigger + - // targeted observer) surface a CR 603.3b ordering prompt before - // dispatch. Order the no-input Undying trigger first so it reaches the - // stack, leaving the targeted observer to pause on its own target - // selection. + // (ii) CR 603.12 + CR 603.3b: the reflexive `WhenYouDo` opponent + // Sacrifice was NOT dropped — but it is no longer an inline continuation + // that pauses the parent resolution on a second `EffectZoneChoice`. It + // is a CREATED reflexive trigger, so the parent resolution RUNS TO + // COMPLETION and the reflexive joins the very same same-controller + // deferred batch as the two dies-triggers. That is what structurally + // removes this row's original blocker: a reflexive can no longer strand + // a co-triggered batch by pausing mid-resolution, because it never + // pauses mid-resolution. let WaitingFor::OrderTriggers { triggers: order_choices, .. } = &state.waiting_for else { panic!( - "the two same-controller deferred triggers must surface a CR 603.3b \ - ordering prompt after the deferred flush, got {:?}", + "the co-triggered batch (Undying dies-trigger + targeted observer + \ + the materialized reflexive) must surface one CR 603.3b ordering \ + prompt, got {:?}", state.waiting_for ); }; - // The reflexive opponent sacrifice itself kills a creature, so the - // dies-observer fires again for that death: the co-triggered group is - // the Undying dies-trigger plus one targeted observer per creature that - // died (Young Wolf + the sacrificed opponent). All are P0-controlled and - // ordered together (CR 603.3b). - assert!( - order_choices.len() >= 2, - "the co-triggered group (Undying dies-trigger + targeted dies-observers) \ - must be ordered together (issue #423), got {}", - order_choices.len() + let names: Vec<&str> = order_choices + .iter() + .map(|t| t.source_name.as_str()) + .collect(); + assert_eq!( + order_choices.len(), + 3, + "exactly three P0-controlled triggers are co-ordered, got {names:?}" ); + for expected in ["Young Wolf", "Grim Observer", "Grist Stand-In"] { + assert!( + names.contains(&expected), + "{expected} must be in the co-triggered group, got {names:?}" + ); + } + + // Order the no-input Undying trigger first so it reaches the stack, and + // leave the targeted observer and the reflexive behind it. let undying_idx = order_choices .iter() .position(|t| t.source_name == "Young Wolf") @@ -32614,7 +35025,8 @@ pub mod tests { ) .expect("submit deferred-trigger order"); - // (iii) The drained targeted observer reached its own target selection. + // (iii) The drained targeted observer reached its own target selection + // as it was put on the stack (CR 603.3d). assert!( matches!(state.waiting_for, WaitingFor::TriggerTargetSelection { .. }), "the targeted dies-observer must reach TriggerTargetSelection after the \ @@ -32630,15 +35042,20 @@ pub mod tests { "the Undying dies-trigger must have reached the stack via the deferred flush" ); - // Drive the flush to completion: each targeted dies-observer picks a - // legal Tap target (opp_b is always a legal creature target), and the - // stack resolves. The #423 invariant: nothing is dropped and Undying - // returns its creature to the battlefield. + // Drive the whole batch to completion. Each targeted dies-observer picks + // a legal Tap target (`opp_b` is always a legal creature target), each + // CR 603.3b ordering prompt is answered in engine order, and the + // reflexive opponent Sacrifice raises its `EffectZoneChoice` when IT + // resolves from the stack — answered with `opp_a`. The #423 invariant is + // unchanged: nothing is dropped, and Undying returns its creature. + let mut saw_reflexive_sacrifice_choice = false; let mut guard = 0; - while state.objects.get(&young_wolf).map(|o| o.zone) != Some(Zone::Battlefield) { + while state.objects.get(&young_wolf).map(|o| o.zone) != Some(Zone::Battlefield) + || !state.stack.is_empty() + { guard += 1; assert!( - guard < 16, + guard < 32, "issue #423 deferred flush failed to settle (state: {:?})", state.waiting_for ); @@ -32652,12 +35069,28 @@ pub mod tests { ) .expect("choose observer Tap target"); } - _ => { - resolve_stack_until_paused(&mut state); + WaitingFor::EffectZoneChoice { .. } => { + saw_reflexive_sacrifice_choice = true; + crate::game::engine::apply_as_current( + &mut state, + GameAction::SelectCards { cards: vec![opp_a] }, + ) + .expect("the reflexive opponent Sacrifice picks its victim"); } + _ => settle_answering_trigger_order(&mut state), } } + assert!( + saw_reflexive_sacrifice_choice, + "the materialized reflexive must resolve from the stack and raise its own \ + opponent-Sacrifice EffectZoneChoice" + ); + assert_eq!( + state.objects.get(&opp_a).map(|o| o.zone), + Some(Zone::Graveyard), + "the reflexive opponent Sacrifice must actually sacrifice its victim" + ); let wolf = state.objects.get(&young_wolf).expect("wolf tracked"); assert_eq!( wolf.zone, @@ -32828,7 +35261,10 @@ pub mod tests { }, ) .expect("select Plain Bear to sacrifice"); - resolve_stack_until_paused(&mut state); + // CR 603.12 + CR 603.3b: the reflexive `Destroy SelfRef` joins the same + // same-controller deferred batch as the Blood Artist-class observer, so + // the flush opens an ordering prompt before either can dispatch. + settle_answering_trigger_order(&mut state); assert_eq!( state.players[0].life, 21, diff --git a/crates/engine/src/game/visibility.rs b/crates/engine/src/game/visibility.rs index c4b9090aea..f93c9a99ef 100644 --- a/crates/engine/src/game/visibility.rs +++ b/crates/engine/src/game/visibility.rs @@ -138,6 +138,19 @@ pub fn filter_state_for_viewer(state: &GameState, viewer: PlayerId) -> GameState // hidden card and target context. The projected WaitingFor is the only // viewer-facing interaction surface. filtered.pending_deferred_life_cost_resume = None; + // CR 605.4a: The triggered-mana continuation is trusted persistence + // authority. Its pending context can carry hidden object identities, + // last-known source snapshots, controller-only event batches, chosen + // players, legal-mode sets, the current rules-execution node, and a + // suspended parent/payment cursor. The projected WaitingFor is the complete + // public interaction surface, so this is cleared for every viewer — + // including the controller who owns the prompt — before the later + // per-controller pending-trigger redaction. + filtered.pending_triggered_mana_resume = None; + // CR 117.3c: The construction priority recipient is engine scheduling + // authority for who receives priority after the batch finishes announcing. + // No viewer projection carries it. + filtered.pending_trigger_construction_priority_recipient = None; // Resolution frames are server-authoritative continuations. They can carry // private object identities, trigger source contexts, and resolved ability // payloads; the separately projected `WaitingFor` prompt is the complete @@ -5679,6 +5692,154 @@ mod tests { ); } + /// CR 605.4a + CR 117.3c (plan Step 6): the triggered-mana continuation and + /// the trigger-construction priority recipient are trusted persistence + /// authority. They must survive an authoritative round trip exactly, and + /// must be absent from **every** viewer projection — including the + /// projection of the player who owns the live prompt. + /// + /// Each carrier gets a distinct sentinel so the two redaction lines are + /// independently revert-sensitive: deleting either clearing statement leaves + /// its own sentinel (the private description string, or the exact nonactive + /// `PlayerId`) reachable in a viewer snapshot while the other row still + /// passes. + #[test] + fn triggered_mana_sidecar_and_construction_recipient_are_erased_from_every_viewer() { + let (state, marker) = triggered_mana_projection_fixture(); + + // Trusted persistence retains both authorities exactly, and the live + // public prompt is unchanged. + let trusted = serde_json::to_value(&state).expect("authoritative state serializes"); + assert!( + trusted["pending_triggered_mana_resume"].is_object(), + "trusted persistence must retain the triggered-mana continuation" + ); + assert_eq!( + trusted["pending_trigger_construction_priority_recipient"], 1, + "trusted persistence must retain the exact carried recipient" + ); + let trusted_text = serde_json::to_string(&state).expect("authoritative state serializes"); + assert!( + trusted_text.contains(marker), + "test precondition: the private sidecar payload is really present" + ); + let restored: GameState = + serde_json::from_value(trusted).expect("the authoritative state round-trips"); + assert_eq!( + restored.pending_triggered_mana_resume, state.pending_triggered_mana_resume, + "the sidecar and its rules-execution node must survive serde exactly" + ); + assert_eq!( + restored.pending_trigger_construction_priority_recipient, + Some(PlayerId(1)), + "the carried recipient must survive serde exactly" + ); + + // The prompt owner is P0; P1 is the carried recipient; P2 is an + // unrelated opponent. None of them may receive either carrier. + for viewer in [PlayerId(0), PlayerId(1), PlayerId(2)] { + let filtered = filter_state_for_viewer(&state, viewer); + assert!( + filtered.pending_triggered_mana_resume.is_none(), + "viewer {viewer:?} must not receive the triggered-mana continuation" + ); + assert!( + filtered + .pending_trigger_construction_priority_recipient + .is_none(), + "viewer {viewer:?} must not receive the construction priority recipient" + ); + let wire = serde_json::to_string(&filtered).expect("the filtered snapshot serializes"); + assert!( + !wire.contains(marker), + "viewer {viewer:?} snapshot leaked the private sidecar payload" + ); + assert!( + !wire.contains("pending_triggered_mana_resume") + && !wire.contains("pendingTriggeredManaResume") + && !wire.contains("pending_trigger_construction_priority_recipient") + && !wire.contains("pendingTriggerConstructionPriorityRecipient"), + "viewer {viewer:?} snapshot leaked a carrier field name" + ); + assert!( + matches!( + filtered.waiting_for, + WaitingFor::OptionalEffectChoice { .. } + ), + "the public prompt remains the complete viewer-facing surface" + ); + } + + assert!( + state.pending_triggered_mana_resume.is_some() + && state.pending_trigger_construction_priority_recipient == Some(PlayerId(1)), + "filtering must not alter the authoritative carriers" + ); + } + + /// A three-player authoritative state carrying both Step-6 authorities: + /// a live `TriggeredManaResume` whose pending context holds a private + /// description sentinel and a real `TriggeredMana` rules-execution node plus + /// an accepted tail, and a construction recipient naming nonactive P1 while + /// P0 owns the live prompt. Returns the private sentinel. + fn triggered_mana_projection_fixture() -> (GameState, &'static str) { + use crate::game::triggers::{PendingTrigger, PendingTriggerContext}; + use crate::types::ability::QuantityExpr; + use crate::types::game_state::{ + ManaTriggerFixedPointResume, TriggeredManaResume, TriggeredManaStage, + }; + use crate::types::resolved_commands::{RulesExecutionNodeRef, SettlementNodeOrdinal}; + + const MARKER: &str = "SIDECAR-PRIVATE-ORACLE-SENTINEL"; + + let mut state = GameState::new(FormatConfig::standard(), 3, 42); + state.next_object_id = 70_501; + let hidden = create_object( + &mut state, + CardId(70_501), + PlayerId(0), + "Hidden Sidecar Source".to_string(), + Zone::Battlefield, + ); + let work = |description: &str| { + let mut pending = PendingTrigger::ordinary( + hidden, + PlayerId(0), + None, + Box::new(ResolvedAbility::new( + Effect::Draw { + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Controller, + }, + Vec::new(), + hidden, + PlayerId(0), + )), + 1, + ); + pending.description = Some(description.to_string()); + PendingTriggerContext::single(pending) + }; + + state.pending_triggered_mana_resume = Some(Box::new(TriggeredManaResume { + current: Box::new(work(MARKER)), + current_override: None, + rules_execution_node: RulesExecutionNodeRef::TriggeredMana(SettlementNodeOrdinal(3)), + accepted_tail: vec![work("accepted tail")], + collected_batches: Vec::new(), + outer_resume: ManaTriggerFixedPointResume::Parent, + stage: TriggeredManaStage::ResolvingBody, + })); + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + state.waiting_for = WaitingFor::OptionalEffectChoice { + player: PlayerId(0), + source_id: hidden, + description: Some("Accepted triggered mana may".to_string()), + may_trigger_key: None, + }; + (state, MARKER) + } + #[test] fn active_search_grants_only_exact_incarnation_and_filters_event_by_latched_audience() { let mut state = GameState::new_two_player(7); diff --git a/crates/engine/src/parser/oracle_effect/sequence.rs b/crates/engine/src/parser/oracle_effect/sequence.rs index 9a41a81a71..8aff9aa207 100644 --- a/crates/engine/src/parser/oracle_effect/sequence.rs +++ b/crates/engine/src/parser/oracle_effect/sequence.rs @@ -2,7 +2,7 @@ use crate::parser::oracle_nom::error::{OracleError, OracleResult}; use nom::branch::alt; use nom::bytes::complete::{tag, tag_no_case, take_until}; use nom::character::complete::multispace1; -use nom::combinator::{all_consuming, eof, map, opt, rest, value}; +use nom::combinator::{all_consuming, eof, map, map_opt, opt, rest, value}; use nom::sequence::{preceded, terminated}; use nom::Parser; @@ -3562,8 +3562,8 @@ fn recognize_counter_spell_zone_redirect(lower: &str) -> Option Option OracleResult<'_, ()> { + map_opt( + crate::parser::oracle_nom::condition::parse_affirmative_reflexive_connector, + |condition| condition.is_optional_effect_performed().then_some(()), + ) + .parse(input) +} + pub(super) fn parse_copy_retarget_clause(input: &str) -> OracleResult<'_, bool> { map( ( - opt(crate::parser::oracle_nom::condition::parse_affirmative_reflexive_connector), + opt(parse_inline_copy_retarget_connector), opt(alt((tag(", and "), tag("and ")))), opt(tag("you ")), tag("may choose "), @@ -9744,15 +9752,17 @@ mod tests { "may choose a new target for the creature" )); - // CR 707.10c + CR 603.12: the grant printed as the CONSEQUENT of an - // AFFIRMATIVE reflexive gate rather than as its own sentence (Spider-Verse's + // CR 707.10c + CR 608.2c: the grant printed as the consequent of an + // affirmative `If` continuation rather than as its own sentence (Spider-Verse's // "you may copy it. If you do, you may choose new targets for the copy."). // The gate is redundant on an already-optional copy — no copy, nothing to // retarget — so it folds into the same continuation. assert!(recognize_copy_retarget_clause( "if you do, you may choose new targets for the copy." )); - assert!(recognize_copy_retarget_clause( + // CR 603.12: literal `When` creates a reflexive trigger and cannot be + // folded into an inline copy-retarget permission. + assert!(!recognize_copy_retarget_clause( "when you do, you may choose new targets for the copies" )); assert_eq!( diff --git a/crates/engine/src/parser/oracle_nom/condition.rs b/crates/engine/src/parser/oracle_nom/condition.rs index 1265fc71a9..fe05ae1fc8 100644 --- a/crates/engine/src/parser/oracle_nom/condition.rs +++ b/crates/engine/src/parser/oracle_nom/condition.rs @@ -9508,19 +9508,16 @@ pub fn parse_you_draw_this_way_condition(input: &str) -> OracleResult<'_, Abilit )) } -/// CR 603.12: the AFFIRMATIVE half of the reflexive-conditional connector set — -/// "the preceding optional effect WAS performed" ("if you do, ", "when you do, ", -/// "if they do, ", …). +/// CR 603.12 + CR 608.2c: the affirmative connector set. Literal "when you do" +/// creates a reflexive triggered ability; literal "if ... do" continues the +/// resolving effect when its preceding instruction was performed. /// /// Split out from [`parse_reflexive_conditional_connector`] because the two halves -/// are NOT interchangeable to a consumer that wants to fold the gate away. A gate -/// that is redundant in the affirmative — a permission attached to an effect that -/// only exists when the antecedent happened, e.g. CR 707.10c's "If you do, you may -/// choose new targets for the copy" riding an already-optional `CopySpell` — is the -/// exact OPPOSITE of redundant in the negative ("if they don't, …" gates a branch -/// that runs precisely when the antecedent did NOT happen). A consumer must -/// therefore be able to ask for the affirmative set ALONE; matching the whole set -/// and discarding the condition would silently invert a negated clause. +/// are NOT interchangeable to a consumer that wants to fold the gate away. Such a +/// consumer must inspect the typed result: folding an already-proven +/// `EffectOutcome::OptionalEffectPerformed` can be sound, while folding +/// `WhenYouDo` would erase a CR 603.12 trigger. The negative set remains separate +/// because discarding it would invert the branch. pub(crate) fn parse_affirmative_reflexive_connector( input: &str, ) -> OracleResult<'_, AbilityCondition> { @@ -9582,7 +9579,7 @@ fn parse_discard_this_way_affirmative_connector(input: &str) -> OracleResult<'_, .parse(input) } -/// CR 603.12: the NEGATED half — "the preceding optional effect was NOT performed". +/// CR 608.2c: the negated half — the preceding optional effect was not performed. /// /// Kept disjoint from the affirmative half by construction, not by luck: each tag /// here ends in `n't, `, so no affirmative tag (which requires `, ` immediately @@ -9642,15 +9639,14 @@ fn parse_discard_this_way_negated_connector(input: &str) -> OracleResult<'_, Abi .parse(input) } -/// CR 603.12 + CR 608.2c: Recognize a leading reflexive-conditional connector +/// CR 603.12 + CR 608.2c: Recognize a leading conditional connector /// and return the corresponding AbilityCondition with the connector consumed. /// Single authority for this set; consumed by both /// `oracle_effect::conditions::strip_if_you_do_conditional` and the /// `oracle_effect::sequence` chunk-splitter sticky-detection so they never drift. /// -/// Composed from the affirmative + negated halves so a consumer that needs only one -/// polarity (CR 707.10c copy-retarget) shares this exact tag set rather than -/// re-spelling it. +/// Composed from the affirmative + negated halves so consumers share this exact +/// grammar while retaining the typed `When`/`If` distinction. pub(crate) fn parse_reflexive_conditional_connector( input: &str, ) -> OracleResult<'_, AbilityCondition> { diff --git a/crates/engine/src/parser/oracle_replacement.rs b/crates/engine/src/parser/oracle_replacement.rs index 6a733bdc2d..711c802058 100644 --- a/crates/engine/src/parser/oracle_replacement.rs +++ b/crates/engine/src/parser/oracle_replacement.rs @@ -3170,8 +3170,9 @@ fn parse_clone_replacement( // BecomeCopy replacement still registers — dropping the entire replacement // for an unparsed suffix would lose the clone behaviour entirely. // - // The suffix may also carry a trailing "When you do, ..." reflexive trigger - // clause past the sentence boundary — parsed separately into a sub_ability. + // The suffix may also carry a trailing post-replacement rider past the + // sentence boundary — literal `When` remains a reflexive trigger, while an + // accepted-branch `If` remains an inline continuation. let (mana_value_limit, duration, additional_modifications, post_period) = parse_clone_suffix(suffix.trim(), card_name); @@ -3192,13 +3193,11 @@ fn parse_clone_replacement( ) .description(original_text.to_string()); - // CR 603.12: "When you do, ..." — reflexive trigger that fires when the - // clone replacement's choose-and-copy action was performed. Parsed as a - // sub_ability with condition `WhenYouDo`; the parent's targets (the copied - // source card) are forwarded so "that card" (`TargetFilter::TriggeringSource`) - // resolves to the chosen card for e.g. "exile that card". - if let Some(reflexive) = parse_when_you_do_reflexive(post_period) { - copy_effect = copy_effect.sub_ability(reflexive); + // CR 603.12 + CR 608.2c: Preserve literal `When` as a reflexive trigger and + // consume literal `If` only at this accepted replacement branch. The parent's + // copied-card referent is forwarded to either rider. + if let Some(rider) = parse_post_replacement_rider(post_period) { + copy_effect = copy_effect.sub_ability(rider); } // CR 614.1c: When the verb phrase includes "tapped" ("enter tapped as a copy @@ -3378,20 +3377,20 @@ fn attach_zone_to_filter(filter: TargetFilter, zone: Zone) -> TargetFilter { } } -/// Parse a trailing "When you do, ..." / "If you do, ..." reflexive trigger clause. +/// Parse a trailing "When you do, ..." / "If you do, ..." post-replacement rider. /// /// Delegates to the existing effect-chain parser. The "when you do" connector /// maps to `AbilityCondition::WhenYouDo`; the "if you do" connector maps to /// `AbilityCondition::EffectOutcome { OptionalEffectPerformed }`. On the -/// clone-replacement path the parent "do" is the optional copy, applied via the -/// copy-target-choice completion (a non-cost `BecomeCopy` parent), so the -/// condition is normalized to `WhenYouDo` (CR 603.12) — see the normalization -/// note below. Returns None when the text doesn't start with a "when you do" / -/// "if you do" phrase or the chain parser produces an unimplemented effect (so +/// At this owning accepted-branch seam, literal `When` keeps its CR 603.12 +/// `WhenYouDo` creation gate, while literal `If` has its CR 608.2c performed gate +/// consumed because reaching the replacement's execute branch proves acceptance. +/// Any other condition fails closed. Returns None when the text doesn't start +/// with a connector or the chain parser produces an unimplemented effect (so /// the caller can fall back to the plain BecomeCopy replacement without a /// reflexive trigger). -fn parse_when_you_do_reflexive(post_period: &str) -> Option { - use crate::types::ability::{AbilityCondition, EffectOutcomeSignal}; +fn parse_post_replacement_rider(post_period: &str) -> Option { + use crate::types::ability::AbilityCondition; // Strip the sentence terminator / separator space preceding the reflexive // clause. These are structural punctuation, not parsing dispatch. @@ -3404,8 +3403,8 @@ fn parse_when_you_do_reflexive(post_period: &str) -> Option { // seam for future reflexive-clause variants ("when that happens", etc.) // without reshaping the guard. let lower = trimmed.to_lowercase(); - // CR 603.12: both reflexive connectors — "when you do" (Superior Spider-Man) - // and "if you do" (The Fourteenth Doctor). + // CR 603.12 + CR 608.2c: admit the two typed connector classes; + // classification remains owned by the shared effect-chain parser below. nom_on_lower(trimmed, &lower, |i| { value( (), @@ -3423,24 +3422,12 @@ fn parse_when_you_do_reflexive(post_period: &str) -> Option { if matches!(*def.effect, Effect::Unimplemented { .. }) { return None; } - // CR 603.12: The reflexive parent here is the optional enter-as-a-copy - // replacement, resolved via the copy-target-choice completion — a non-cost - // `BecomeCopy` parent, NOT an `Effect::OptionalEffect` resolution. The engine - // gates `BecomeCopy` / copy-replacement reflexives on `WhenYouDo`, which is - // unconditionally true when the sub-ability is reached (the copy having been - // performed is guaranteed by the CopyTargetChoice completion path; a declined - // copy never reaches the sub-ability). The generic "if you do" mapping to - // `EffectOutcome { OptionalEffectPerformed }` reads a resolution-context flag - // that this replacement path never sets, so it would silently never fire. - // Normalize it to the `WhenYouDo` contract (Superior Spider-Man's "when you - // do" already lands there). - if matches!( - def.condition, - Some(AbilityCondition::EffectOutcome { - signal: EffectOutcomeSignal::OptionalEffectPerformed, - }) - ) { - def.condition = Some(AbilityCondition::WhenYouDo); + match def.condition.take() { + Some(AbilityCondition::WhenYouDo) => { + def.condition = Some(AbilityCondition::WhenYouDo); + } + Some(condition) if condition.is_optional_effect_performed() => {} + Some(_) | None => return None, } Some(def) } @@ -7629,7 +7616,7 @@ fn strip_optional_draw_skip<'a>(lower_body: &str, original_body: &'a str) -> Opt Some(rest.trim_start()) } -/// CR 603.12 + issue #5655: Attach an optional `"if you do, …"` rider to an +/// CR 608.2c + issue #5655: Attach an optional `"if you do, …"` rider to an /// optional draw-skip replacement. Returns `None` when non-empty rider text is /// present but cannot be lowered to a typed effect — fail closed rather than /// report the card as supported with a silently discarded rider (Island @@ -7642,7 +7629,7 @@ fn attach_optional_draw_skip_rider( if trimmed.is_empty() { return Some(def); } - let rider = parse_when_you_do_reflexive(remainder)?; + let rider = parse_post_replacement_rider(remainder)?; Some(def.execute(rider)) } @@ -19628,7 +19615,7 @@ mod tests { /// graveyard zone + ZoneChangedThisTurn predicate + "if you do" connector. #[test] fn fourteenth_doctor_graveyard_copy_with_zone_change_predicate_and_haste() { - use crate::types::ability::{AbilityCondition, Effect, FilterProp, TypeFilter}; + use crate::types::ability::{Effect, FilterProp, TypeFilter}; let def = parse_replacement_line( "You may have The Fourteenth Doctor enter as a copy of a Doctor card in your graveyard that was put there from your library this turn. If you do, it gains haste until end of turn.", @@ -19691,19 +19678,18 @@ mod tests { other => panic!("expected Typed filter, got {other:?}"), } - // Reflexive "If you do, it gains haste until end of turn." attaches as a - // sub-ability. CR 603.12: normalized to `WhenYouDo` because the parent is - // a non-cost BecomeCopy replacement (the copy-completion path the engine - // gates on `WhenYouDo`), not an `Effect::OptionalEffect` resolution. + // CR 608.2c: literal "If you do" is an inline continuation. Reaching the + // accepted replacement execute branch proves the copy occurred, so the + // owning parser seam consumes the performed gate instead of fabricating + // a CR 603.12 `WhenYouDo` trigger. let sub = execute .sub_ability .as_ref() .expect("reflexive haste sub_ability"); assert_eq!( + sub.condition, None, + "accepted-branch If rider must be conditionless and inline, got {:?}", sub.condition, - Some(AbilityCondition::WhenYouDo), - "reflexive haste must gate on WhenYouDo (the BecomeCopy reflexive contract), got {:?}", - sub.condition ); assert!( !matches!(*sub.effect, Effect::Unimplemented { .. }), diff --git a/crates/engine/src/types/ability.rs b/crates/engine/src/types/ability.rs index f1c1c5b351..fe027763ad 100644 --- a/crates/engine/src/types/ability.rs +++ b/crates/engine/src/types/ability.rs @@ -19881,13 +19881,10 @@ pub enum AbilityCondition { /// state sources. Feeds `RepeatContinuation::WhileCondition` ("repeat this /// process") and any cross-sentence flip-result gate. CoinFlipOutcome { result: CoinFlipResult }, - /// CR 603.12: "When you do" — reflexive trigger that fires based on whether the - /// parent's trigger event actually occurred. For a non-cost parent (e.g. a - /// `BecomeCopy` reflexive or a copy/exile replacement sub-ability) the "do" - /// always occurred, so this is unconditionally true. For a cost-payment parent - /// (`Effect::PayCost`), an unpayable or declined cost is not an occurrence, so - /// the reflexive sub-ability is skipped — `evaluate_condition` gates on - /// `cost_payment_failed_flag` for that case (mirrors `IfYouDo`). + /// CR 603.12: Literal "When you do" creation gate for a reflexive triggered + /// ability. Once the antecedent occurred, the runtime materializer consumes + /// this root condition on the trigger clone so it cannot recreate itself. + /// Unpayable or declined cost antecedents do not create the trigger. WhenYouDo, /// CR 601.2a + CR 707.10: "if [this spell] was cast from [zone]" — sub_ability /// executes only if the spell was cast. `zone: None` = cast from any origin; @@ -24406,6 +24403,11 @@ pub struct ResolvedAbility { /// Each entry maps a target to its assigned portion. Read at resolution. #[serde(default, skip_serializing_if = "Option::is_none")] pub distribution: Option>, + /// CR 601.2d + CR 603.3d: Unassigned division metadata carried from the + /// definition until this stack object's targets and portions are announced. + /// Distinct from `distribution`, which stores the completed assignment. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub distribute: Option, /// Player scope for "each player/opponent [effect]" patterns. /// When set, the effect iterates over matching players (each becomes the acting player). #[serde(default, skip_serializing_if = "Option::is_none")] @@ -24541,7 +24543,7 @@ pub struct ResolvedAbility { pub sibling_condition: SiblingCondition, /// CR 700.2b + CR 603.3c: Modal choice for a reflexive modal trigger whose modes /// are gated behind an optional cost (Caesar). Carried from the def so - /// try_begin_reflexive_target_selection can hand it to the PendingTrigger and + /// `try_materialize_reflexive_trigger` can hand it to the PendingTrigger and /// route to AbilityModeChoice. None for non-modal abilities. #[serde(default, skip_serializing_if = "Option::is_none")] pub modal: Option, @@ -24598,6 +24600,7 @@ impl ResolvedAbility { forward_result: false, unless_pay: None, distribution: None, + distribute: None, player_scope: None, starting_with: None, chosen_x: None, diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 1312281cbf..479fb5eb16 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -6032,6 +6032,18 @@ pub struct ManaAbilityCostParent { pub cursor: Box, #[serde(default)] pub lifecycle: ManaAbilityCostParentLifecycle, + /// CR 603.2 + CR 603.3b: Index into the live reducer action's `events` + /// vector marking where this parent frame's own unscanned cost events + /// begin, while the lifecycle is `Synchronous`. Each nested child entry + /// prepares a fresh local snapshot whose ledger is extended with + /// `events[current_action_event_start..]`, so a later pausing child + /// inherits the parent prefix plus every earlier synchronously completed + /// sibling. It is never consulted once the parent becomes `Suspended`, + /// because the pause has already made that prefix durable, so it is not + /// serialized. Distinct from `ManaAbilityCostCursor::current_action_deferred_start`, + /// which indexes the local ledger rather than the event vector. + #[serde(skip)] + pub current_action_event_start: usize, } /// CR 601.2h + CR 602.2b + CR 605.3b + CR 616.1: The unpaid suffix of an @@ -6079,16 +6091,18 @@ pub struct ManaAbilityCostCursor { /// before the cost cursor advances to the next component. #[serde(default, skip_serializing_if = "Option::is_none")] pub selected_sacrifice_remaining: Option>, - /// CR 603.2 + CR 603.3b: Cost events produced before a replacement-choice - /// pause cannot reach the ordinary post-action pipeline. Keep them with - /// their typed payment root so observers are collected exactly once when - /// that root completes. + /// CR 603.2 + CR 603.3b: Frame-local cost events produced before a + /// replacement-choice pause cannot reach the ordinary post-action + /// pipeline. Each cursor owns only its own unscanned events; an ancestor's + /// ledger remains opaque in `parent` until a suspended child moves its + /// local ledger upward. Only the ultimate parentless cursor may settle the + /// accumulated batch. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub deferred_cost_events: Vec, - /// Ephemeral split point for the active reducer action. It lets a newly - /// nested typed root prepend its parent's unscanned batch without copying - /// the local events this root already captured on pause. The split is - /// consumed before state is returned to a player, so it is not serialized. + /// Ephemeral split point into this cursor's frame-local ledger for the + /// active reducer action. It lets an immediately re-paused ultimate root + /// retain its already captured local suffix while inherited root events + /// move in front of that suffix without duplication. It is not serialized. #[serde(skip)] pub current_action_deferred_start: usize, /// A nested costed mana source owns this parent until it completes. This @@ -7347,6 +7361,132 @@ pub struct ReplacementCandidateSummary { pub description: String, } +/// CR 603.3b + CR 603.7: One completed normal-plus-delayed trigger collection +/// for a single raw event batch, produced before any live occurrence is claimed. +/// +/// Generic and phase processing consume this immediately; the triggered-mana +/// continuation may instead persist it undispatched across a pause, so a later +/// action partitions already-materialized contexts rather than rematching raw +/// events. It deliberately carries no live action ordinal: occurrence identities +/// are only meaningful against the reducer event vector of the action that +/// claimed them. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct CollectedTriggerContextBatch { + /// The combined normal and delayed contexts in APNAP rank/timestamp order. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub contexts: Vec, + /// The raw logical delayed batch, stored exactly once for durable replay and + /// accounting. Immediate consumers drop it. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub delayed_events: Vec, + /// Raw identities consumed by the delayed matcher for this batch. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub delayed_consumed: Vec, + /// Whether the caller's already-observed normal seed was non-empty. The + /// closing processors clear post-collection transients only in that case. + #[serde(default)] + pub normal_was_non_empty: bool, +} + +/// CR 605.3b + CR 608.2d: The activated mana ability's own colour prompt, +/// latched exactly as the mana frame had already built it before the +/// triggered-mana fixed point suspended that frame. +/// +/// This wait belongs to the outer activated mana ability, never to an accepted +/// triggered-mana body: the fixed point reconstructs it verbatim rather than +/// re-deriving an option set against a board the accepted triggers may have +/// changed. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ManaColorChoiceResume { + pub player: PlayerId, + pub choice: ManaChoicePrompt, + pub context: ManaChoiceContext, +} + +/// CR 605.4a: What resumes once every accepted triggered-mana occurrence in one +/// completed mana frame has reached a terminal disposition. +/// +/// Exactly one of the three shapes a completed frame can own. `Parent` is a +/// nested child returning to its suspended cursor; `Root` is the completed root +/// frame's own resume root; `ColorChoice` is the already-built prompt above. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", content = "data")] +pub(crate) enum ManaTriggerFixedPointResume { + /// A nested child frame: its suspended parent cursor is the authority and + /// no wait is reconstructed here. + Parent, + /// The completed root mana frame's own activation resume root, together + /// with the mana source's controller — `resume_mana_ability_root`'s own + /// first argument, which its catch-all arm needs to rebuild the waiting + /// state. It travels with the root because a resumed accepted occurrence + /// no longer has the payment cursor that supplied it. + Root { + player: PlayerId, + resume: Box, + }, + /// The root frame's already-built `ChooseManaColor` wait. + ColorChoice(Box), +} + +/// CR 605.4a: Which existing interaction the current accepted triggered-mana +/// occurrence is paused on. Private control state, never a wire discriminator: +/// the public interaction surface is the ordinary `WaitingFor` value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub(crate) enum TriggeredManaStage { + /// CR 700.2e: the controller is choosing which other player picks modes. + ChoosingModalChooser, + /// The chosen player is selecting modes. + ChoosingModes, + /// The selected body is executing and paused on one whitelisted + /// optional / optional-for / repeat / replacement interaction. + ResolvingBody, +} + +/// CR 605.4a + CR 603.12a: The authoritative continuation for a classifier- +/// accepted triggered mana ability that paused mid-resolution. +/// +/// It owns only "after this immediate trigger finishes under this exact +/// rules-execution node, partition these already-collected emission batches, +/// then run this accepted tail, then resume this exact mana frame". Every inner +/// operation (resolution frames, optional/repeat frames, pending replacement or +/// cost-move state, and `waiting_for` itself) keeps its existing owner. +/// +/// This carrier is trusted persistence authority: it can hold hidden object +/// identities, last-known source snapshots, controller-only event batches, and +/// the current rules-execution node. It is redacted from every client +/// projection by `visibility.rs` and `derived_views.rs`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct TriggeredManaResume { + /// The complete accepted work item currently executing. + pub current: Box, + /// The occurrence-local production override captured by the auto-tap + /// planner for this exact `TapsForMana` occurrence, if any. The transient + /// override map is cleared before an interactive trigger can resume, so it + /// must travel with the occurrence. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_override: Option, + /// CR 605.4a: The one `RulesExecutionNodeKind::TriggeredMana` node for this + /// occurrence. Required, not optional: every classifier-accepted context + /// yields exactly one source incarnation, and every pause of this + /// occurrence rebinds this exact ref. + pub rules_execution_node: RulesExecutionNodeRef, + /// Accepted work that has not been made current yet, in collection order. + /// Tail members deliberately have no node until they become current. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub accepted_tail: Vec, + /// Already-collected, still-undispatched emission batches produced by the + /// current occurrence across its pauses. Stored before the live occurrences + /// were claimed, so a later action partitions materialized contexts instead + /// of rematching raw events. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub collected_batches: Vec, + /// The suspended mana frame this fixed point resumes exactly once. + pub outer_resume: ManaTriggerFixedPointResume, + /// Which existing interaction owns the current pause. + pub stage: TriggeredManaStage, +} + /// CR 603.3b: One controller's group within an in-flight trigger ordering /// pass. `ordered = true` once the controller has submitted their permutation /// (or once the group is single-trigger and trivially in final order, or once @@ -15909,6 +16049,55 @@ declare_game_state! { #[serde(default, skip_serializing_if = "Option::is_none")] pub pending_deferred_life_cost_resume: Option, + /// CR 605.4a + CR 603.12a: Typed continuation for a classifier-accepted + /// triggered mana ability that paused mid-resolution. It stays serialized + /// with the matching whitelisted prompt so a host checkpoint resumes the + /// same stackless occurrence under the same rules-execution node. + /// Redacted from every client projection (`visibility.rs`, + /// `derived_views.rs`); the projected `WaitingFor` is the complete public + /// interaction surface. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) pending_triggered_mana_resume: Option>, + + /// CR 117.3c + CR 117.5 + CR 605.4a: The player who must receive priority + /// once a settled-Priority trigger batch finishes announcing, when that + /// player is not the active player. Installed only by the settled-Priority + /// convergence wrapper from its own exhaustively validated + /// `WaitingFor::Priority { player }`, retained across every + /// trigger-construction prompt, and consumed by the construction finisher + /// once the whole construction/deferred tail is exhausted. + /// + /// Deliberately **not** part of `resolution_completion_can_settle`: that + /// predicate gates `can_drain_deferred_triggers`, so a carrier inside it + /// would reject the very drains this batch depends on. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) pending_trigger_construction_priority_recipient: Option, + + /// CR 605.1b + CR 605.4a: The exact rules-execution node of the accepted + /// triggered-mana occurrence whose stackless body is currently executing or + /// resuming. Presence means the complete classifier accepted the original + /// graph, so a resolver-time context-ref injection into `targets` cannot + /// reopen classification (CR 605.4a). + /// + /// Transient by construction: it is a lexical scope marker equal to + /// `active_rules_execution_node` while live, and every sidecar-owned action + /// reinstalls it from `TriggeredManaResume::rules_execution_node`. Serde + /// persists the sidecar and its node, never this marker. + #[serde(skip)] + pub(crate) active_accepted_triggered_mana_node: Option, + + /// Debug-only witness that the trigger-construction finisher ran at most + /// once per reducer action. The finisher is applied at the outermost handler + /// return of each of its enumerated action seams; a second call in the same + /// action would mean a seam was added below dispatch, where a `Priority` + /// result is discarded and the recipient would be lost mid-batch. + /// + /// Transient by construction — `apply_action_boundary_core` clears it at + /// action entry beside the other per-action transients — so it is never + /// serialized and never part of state equality. + #[serde(skip)] + pub(crate) trigger_construction_finisher_ran_this_action: bool, + /// CR 601.2h + CR 616.1: Resume a sequential discard cost after a /// replacement choice. Cost moves use `pending_cost_move_resume` above. #[serde(skip)] @@ -19844,6 +20033,10 @@ impl GameState { current_triggered_mana_override: None, pending_cost_move_resume: None, pending_deferred_life_cost_resume: None, + pending_triggered_mana_resume: None, + pending_trigger_construction_priority_recipient: None, + active_accepted_triggered_mana_node: None, + trigger_construction_finisher_ran_this_action: false, pending_discard_for_cost: None, pending_cast: None, ring_level: HashMap::new(), @@ -21543,6 +21736,10 @@ fn _gamestate_partition_is_total(s: &GameState) { current_triggered_mana_override: _, pending_cost_move_resume: _, pending_deferred_life_cost_resume: _, + pending_triggered_mana_resume: _, + pending_trigger_construction_priority_recipient: _, + active_accepted_triggered_mana_node: _, + trigger_construction_finisher_ran_this_action: _, pending_discard_for_cost: _, pending_cast: _, ring_level: _, @@ -21792,6 +21989,9 @@ impl PartialEq for GameState { && self.pending_library_search_delivery == other.pending_library_search_delivery && self.pending_search_found_batch == other.pending_search_found_batch && self.pending_cost_move_resume == other.pending_cost_move_resume + && self.pending_triggered_mana_resume == other.pending_triggered_mana_resume + && self.pending_trigger_construction_priority_recipient + == other.pending_trigger_construction_priority_recipient && self.may_trigger_auto_choices == other.may_trigger_auto_choices && self.decision_templates == other.decision_templates && self.priority_yields == other.priority_yields diff --git a/crates/engine/tests/fixtures/integration_cards.json.gz b/crates/engine/tests/fixtures/integration_cards.json.gz index b447bdcf504645143e7a7e2f88586bb851395dc5..133c77c57e04195d12249a0712014e374e905359 100644 GIT binary patch delta 193635 zcmV(@K-Ry$hic7-Y9Sws2nYZG00092X>N37XL4b5X>V>{V_|Y+b1rIgZ*BmA3WW** zg$e?N3Il}-1ceF(g$f3R3I~M>2!#p>g$fFV3JZk_4222}g$fRZ3J-+}5QPd6g$fdd z3KNA26om>EwF(xDrhmXmlNHguTq33%|NO`A^RdJ>FxlM&S{`NcX)hMaXdA~KFGz=+ z82k7=^|mr(Sfopf7^(k4u55xj;yr^qqYw`R|R*}$@rhhkk!ianht%CwO3k%MmYI&w%6B8d9UKd@)bqqH;%%2&`!pSjk4&aah zQNis3_U)rzU@NHg*_rYw44l=GaWEFNGFG`hpqU4{rKrN;SQc{FPG^gRr!?D@h?77} ziFK*99KVy9!F)RaD%*pMUO{z`8~o)>ah~xBiWR+tN`JqqS8HV4fducIj*PjrNH)RI z?J^6THC0GNNo4vK4NP)lGWWW?hU98t*^!-*A#iaC^*hZRo|9*R-{Cg?!y)~(@(YA*!iSIab zTdrv`&3`T?Fdy}DFkW(lrDB3))GwAqz3(*FZ4E8q-Yui^%1Ti3nI?)|t9OSz%QgG; zT2q)PowjM!x|TU?wEM$W%c{4#&b^1Qg0P!|wrc99WK-8byLx>Lcy1@~Pp%=>M&N*U z401j4t&3$QDI>mklQ6kPG^Xu{HrS1>6Tky+41f71v2F z1eu8i%p|e^BU13^U@=`!fkBQ^H2_h8P-l=*8FMs{cGut&@InNuA34M;Ms-24{=qa+_u#K_)A)`Qd=3tcRf8Zn+DT1D91XmS+|v>Hxzzmizl zwAATuQ4C6dyG2YL{6=CO#F!z+niEzufV0>f6Gf;HX^w2^)5^}t{7B3|0-;RsTdIk{ zlvo-@(NC!%B5_X3zzw7CAmkN-({T_@vVXlHXT3Fx2mKUm)Owc?2O|rt{;KA4A+K$DBZ?xtVw}cu@U)8>a zcVR2yUlogtFo(v9%Lwl(J4o?)7vcde1{dwhYjvmV$1?z3?jP-UI&0!>Vz3i0Dc%U4Y^zBRIy}AoybNmYfBU@hirM z{h<qE0VDbqeI7Q5C&61M5v--kc^L$=Sujq13xg)G zjF1>e^nlw>El>j@L1Bpm#+w-#R4iKHHj9>J*GU#M4Ev6=YGnI2F@H^^L%y3umtT38 zMKI!pfGN#IUc?>HDlQ`D)qHX>3nJfX(tU;XV@N79yNJKRSovkn8yauv;91)JI$^-xUwd2 z#?{N^N6D=n$-Km7$A6?q(164kUI{arskX#HcVW|~P=e;8>pUGcPlwIZVe@p@JRLTV z_poW~?67I14x7eK4x1k&XLoCyd0D_RrMpe^(>o==dy?iqyijWHOei%|LaBK-l?KZc zCvNsSN|Ra#kG;%WTk!EUgr3dUXD?%J%dHk_)33yq1jDI$fq%1;I+|9i;|`i;v*+~9 z*09wz`-5)XY;^3tYjxdW9icQI1235{!-6^N2z6)x2#44+7h*fOeto2~ z;D6p9<>z!X%v!^&_w+(_^${ez2>%QV($`Qj5@7FF>v0fGP&222JoynG~mO=ra zDt%7`SW@GHb%By>#VQ&kNUUPUaLJStPu&WCF?=i}#kfS(kP(W!IG(JG&Hrw!W>)&` zwUTeekrc&^6HStNA=pc&|%Dy8IH18ptIy^)iMFZUr5rIl?3pbuB{gTe<=)t zFR}Q4>wj^_|Bcg#Ct%3C4wC{XpsoXlMU(=eE-C{ZqD$p?0x}IIBVl+x<7G@=9I}+t zqWC9}2SDlHxgWu8K;jwFJ*j{+TNFN|sQyxJm*c5Iz>AXtY^9v5S`4nNO+dMKZp@_e zVEB_X9KxrV=7Wf7z9RXvreEfvq9i;m;Kv;&(SOhsR*WNT>vUNKmO_Vq*zf>a`?H_d zjS~Gp@sg(y#Z!plDMWFvLKMnS%?werTCUTwYqn|C>J78i>$gn{pm6{gr(J8en}dP% zNJ12`@WVm957=r)N8*299pnF=9zZ1G?de%=D5BmoYb~?U)k6_+uf>f5d6Yg#baCm^ zjemHNbIe<}$Qq|In~~_K10ExX_@fy|O@A~L7A#bCvJ)9RY5z61MwG1%&Nynzvyh)p zc-t^H|H>2QZ>&A>#&7`++zV!QvU?FyX4M#o2M^JSAZ{ewP1$P;iUShfqm0oD>2wZS?*A|6O6@641e_T5nw5jABM(r9k(~j@$rI6bwmYS z(^gEP8!d0KjAKh*ivv)A0y`jA{Xx=`NF~Q}En0`e%$Phk;3F0zBk7e>jX< z&iKG^%1S?zDk(99jwck&DJ;3Pe={Dez%3J@P2osD6`BW_xUs?7Gv34ZV7bKyF@}As z9Pdl^aG8#dWyN~B{i^9%aT;%$+JCvSPBNN+Sa}Xr0^X>ipea;;Pi67G!u01qao}zE z#fQ>9xo@MDfwj5X$EvalU40`n>Qg)>ugF6G8X&$^Fgqp8hJ;gG_99{WTIn|J{@+m&!gM1SP;sinm@ z&}tF6a1vMcB@#IV5(rQ(VXWlyFszx423@Yz1(HUJ~RVi*#uDsY@?b6YHd8rBEFhz!^`2FMe>%u}uxKUd*LyO@Bw8s)*+EcWix$ zux|K?@-s!2@jaR<&ZoG7V;Q1`faO4C6GSYr&1vG0lAIw9GG$GW7DOD2$qfkJ*cyo& zY|l!@5~EvA)Geh$igPfn-@8sMu2@?e@=^D^>G8!kbI~60rFX#q^P)d`e}Mt3MI4uc z0!MMkWw)&oo&0vT_Xz; z#(i{D&zRYK2nGPYACeUS&taxM0xOQvD+pmm)9Ul)T5_NdWn7vHp=G8^>^-3>YL=cu zuw-r*`;kHAgq8}t-cG!x>DYGGwYqDY{b7A*wwjHBIdHAM>2z97uhFobet#X#d5j8NXt4k)858GO>bD;Z z#&YH2h+hxiEycL(i2u`r5Mq~#-U4!1UM<9()jlA&pL%fnRS+4YF$=!9zH&y}5fxXI zRBYuHCP0g`v;1X1fO4C|-xM6ySAGqh7o zJ*W6bKY!#m{2INP(VH=Umt#IS9>fWV5;%KcnUG8uv%^HLIp*CK&Oo9LWy#{)Sq)0| zH}84VclDP(yY%66H8cgyc3#aWb2bEX7$SsJB~&li3{g-cNK6jO_Uc6(5wpPzr}A z$8hW0K(vXmP{jjp#QZ#4%53ENPSVfqJAbQH%2YEdW>!SZJVajSWVOt8>trY>hAWmi z0vZuV0BNtoY|<@=Ws3Xu&V2@#Twa*6XPmm$)V@mdWveQcDn>#`zma&B5@|Y$Zc-qF z`FJ^Q0TV@3Tq!nSmys`_5K;$&4X@_I;fMn~KBmN|n2ai+B#VO15{x@jt_27>>UEY>@b&k2YJdH2=a`aO_A$MPdR^Mp#+gPf6hQvE62&RET8*S zEVOsx|M$bV+c>M-=)>eJK#}(6lDJmA)ot~fomO|TNFlmO$7T|dt|YZk4z*@Jo7Qf@ zgmSkGW8K~3RY;PovL*V`5(Pp+R)1E?(e&V`I=>3h3F{vYoA*i&$7Vb+u0A2rJd>u>2nQ$TDGEi`Yp~c~)gF&?S&n8C--~l|+&{@bgwf zIf`4p2ro7juX$S-uW9R@TaheH>OiM6GLR(Gdo`vg$%9zalvbly+uWpCiGSW~)e4ke zvu)S=ZOiQ0&9>R9bvtIS-*L>I)97~_wq-ZEwI7`M?BpX-pM6}$eAYB;eX~|iF`q?Z zj$N!Qr>-rTTN`tvqTQCn@hS zq%;(lAW+m)<|1(Z0aoiMERcOheSr!eA5)!Km_dpcfte&_8x@zpf`1(Gy-0eF^%Ux^ zsxb}bSPugJx81^kycPY#EZ|j<39=23#|%r5Nb8X+>5(SkX!{_(?#tC6=VA%0nKt;e z-X8gS%NMj8F1Qoy@M;K0?o3zQ3p$!IQ(k#rg%0|jWHt8Hpt{# z!X4f2x^Au8teH*A78ca7-Zd?|*Dwc8%j$HxO}jA|JaI>_&X;(*7I^_mm-4$7BY&+@ zF}9srv0E|nlKoV;4;U5_)?8GsbFdKqmtU$QnVU7*&vX9(NnLSiVPd*WCl|IGsL<>V zPI8&UGXBsQi*%uw`YfEe zm7Uavt-jdqz9Otzte3yM7A=2Yz-5$I6GyFdlUwJ9h2#33IDdAI#I>Zz#;!Syf#dYe zy4&fQEvIRjz2>lIxbELSl`}<^Gj9(Lm&Hixb+ca6YDF?c37o5-yi%^H zj~V?p&x900B@0tYuczK@CRD`p%721N_W2j%KVRc+{k(TxF-}2O=F@*P0YQ`{x(Mt* zmW<#xWn!~8g$!&DkA7%zI>eFekFR|Kk1!4P1(MAv@=ZrHA97fNp_v8{XBCCf=nGp2_9GwV-M^kShsW$O^T%VA&FShpOn<^z1kWEdJ5SC9A&-rR04&Q%5>`K>WNaF z@-xORz~4E7$=Tgh&OvQ>8`yKKD|zjYAO+v`;p7z!84A12x7mM8qm*n|StvR%MH;FN zxY7tRQ*z!U9uZPLn5oDT0_ zEFnn&oKf5n9hlH&v1SR9iGrIDKOJ$SF^b8+T7)`Ag(8q0Pu~;AAc9ztM5Kfo$ZMZ+ z{(Iad;CP_J59WU}4Yq8>zRL0{-2Ifk?;D@E(2c^V&}*$?BpM2EWLF~*aAid(Fj}Jg zW-S6M0rUNz^Uk9JABg0hR}K)>1_kNg|N3unDg?x(0^m$9w3bDZU$qZTUs8P?P1s;a z8sF)7LtI@brc00N7;vsUp+ZSmH7Fm0AiEnOyio-ryN7=O7mxOQ2nU)3Wz($Pd^=ZG z7|qheI4Q%Idgw&Q0PJS!DWIllAE9beeh>xkw;HBIh>3Bdyh zltS*wG=pGcdhHi}(bR0J9c|{e1P}UjCg${7*x*?i2gO8Kc+d z^5ZHYM#$V`w(J zUAsGUx?RV$o-%1vec>5ulwg9TT3FcnSGSguQs~ z$G=XlC91&H@c4ZCG`54?hNB>P2ekAS%l&i0*(?VL+-HvLe?$q^9degU5L^Qy449LL z^d%$DhxLobXee)+hVJwi<9msT**M}|>La%pT)lsju=)Dje2%uUNA4yuA*%BHz)2rk z#&?D+`3|GCLQX=?Y9JYuS_8{PQ{^d}`tI26mM)6ILuIY%fQ?nSRVz&g!%m~o=+{lB z19{R`&o&25f4gb+>Q1*g=rn7cVK=GWCDoq~@7_du4`!W)Bk!x5bYc98mrm(YNxi3x ziLz5#0B&vxy{KDGFY2zS7adrk4Waz!NKv5*Qf8`vxyrsVEd)g6mlKuzFA)KRP2-I; z7J5r&k!itl%5K0C=g8T|2&FQMcVgXm%=)70K=+7we>@oyl0NHTHiK;e?%S#OU@&HW zqZj8s4KZ~ljCx3^AJqE|ydgY(m8DgOMQ{L67WPqe{&#vYo&xee)faL~`g6gxPidLK zQ9_GsQI2Z702zSI-!;qc6T)}3s`^qhxhM(JEcZI?@~RHXTxGfNIbYrtJ(pcH(so-; zYtS(3e**_h-A3QE8gTZ+43x&}m*;t?ok@=SSArEsHspZ+>U z(MJfL9At^oqd0#98>hY`rb0#!nZSwPCZi@)f7uemIlkZzlD#^gQ2a^8Q=#wSTewX! z?i0yq7VDfUk@LC)y-{lDlS7pRw4+nuhGx7DGqw`LGjeB|6p#*k z77~P`fJYAhqGi2B!Xg$Os$;);I<7Si;j>W35r`wEV%G*Y$;0Xpv2rQA10or8(ytT= z76$ZW4ZMMV!4R>Bxc?z%9N+wEqkNrUngO$E2Y_ zBza0LiY$bG-%uPHGll4zhN_lu(>h`dC7^&a@5;g~Way1rM`>~d{VP!-9$sPqYb;)d zO^2!d99`C#8y&WWMcLVPtz!CRW=8^ zg;|4tQoV5kRAGoA_X<$D*_i=)59~QiG2AU8(V74;?{Tuuf=Ln5o=O9#x3-06yN@?4 zgS6FA^-|??XJSo%ah+3l_AB}=2sFgO3Zt^f#JZn@w`D0BvBm9;K?Kgc*$BJ~W|i9+ z7~UuslORmuLB7Nt4y%dw^9Al_nYQwTkNd-alKg2EpR31Y2#)j4qcIQqNZn~bXrn6X zmst6BAdc`%8MwCm%5q?<*B<(exXu;C(MhC{1i&pM$`KeGF@?ms7h5PUw}bc#HbsR0 zjnq^sV67|-4&V{)`SV~7iIJ;8`bOD+jw3@0RV}jc1{9sjVvGL$m0kK+wR5G~5sA4y z$_LQDUi;4RkY`2T(5sv@={McqQ>PNsiof4O4fxJ52ae@)Fl095uc)Q48Un$+AL4D` zA^hjcajiAtS(h)#79f9FCE{19U{!W(swfbJktW@5vCL<&s=q>xY|2T|xF=4E9i?2# zjW!B`FTlRYeQ}Nby}iBv8!CE@<5F6$=|#zg#p+sSq+5UN<~)U6~6|7q8oiKt5w1(B;?=}DNcXPQ|r>Ax;^>T?O{R{ zr3&Dn&0Dc~_Gf3oo(2ya3GW{x8Z=g z)iitUL8~JMz#2OC!%(t5zByis6ICd_@7P_kf^wYD122Am98tjw9Mz04uV7*&}fx~RKqEte5@H!w$uI&R2e`1>PL7_QTSXR zdniO#pC*6fEawYRW=Q-SdUO^DMU3a*3shzCQ2hhJalcBqD#WoYew|pJqtU9rC>*GF z9GX5yrT@GFSrFn67-aDWen_8^aF0OJf|28@ z%zWY?4#6>sEKc+VR%oteha7gcnBlF3-TL6;21e~1Y-ZFMf30&JS;+1Pu&nfGML;u`UdxVFIud!M3h9vbzmZPOsrAK{v zJMw>Q$U@?@gQC|(z(}DbPHCYfXeT13eyFrd&LfEd&D<<|{EO>n0=4Pn=$KKZ-*hCD ziIHm!jrV|&5-}?4c?k1M4>Mr0EqZx%1@5%CAy-qybnx zBuvDrr%z+CSloR$FGJ#%4(_e(QaPk!p|YhAaT!0$DIGuC`{d`vt;aQ0!$9hYhQC6` z@uN#RZQz=YTSXU=&^?4$h*ek z2w80qgRhSl?Vxiu!O!HeQ-j<(NX=ft?HE;`WEXR)cBlM}2>CV4Fw#QyQ zZdFM-9HAKND6f+2?ir%}pvUkYI$E{b$t*dKi!ZB0N+mItf2x&Y+nP$YB-%8LoH`(~ z&|ZHCXXjZERWv0@t%G623X0rxQKMtz*vT5N2%^?Xto;{qMUte&GF03t$>P%);^0mu zqnqfmR)}h(>$DqW6?if5St>S?dMhMAVUP`S753Lld%0;UR<5(6X4+`vARA7v!r!2!v zJgd24F9Xp3#oULqLY#y%_BXnI6kT5tzmAo+9cEZj6CpLu`wQdjTdM(UuIP*1k6^xa zz~antG|*%DEF^_YshQPWJ^`^xe;3a|!7;rIH<2v+VPx0>k2x8o%uR7rfTL7L>(JN= zNUF{x#4(~~^X7#3Kn5~E!kwJ%BY-D`SP&X9Xkhs`l|}K|mWmk%9Ee^b%Kc>U=+iIW z^owya4Mg+7TV0jkdkY0SH|oz5P>`k`7Sfj=&=x3v^zXXV>=h2qtCc;+kivfk<90mE z^4Wmx3g+^)zoZC_{F)bJj_TGr5rU~dH*y8uH`lfLQ0R_a6xkPv^yc?C@d)yB#~^11 zxPg%PaRm#l3vwkdLo7YMlQ^FD*BrFX)Aq^s>ODa z3wJDkWS3LPXq)z}_fi*hotHNUiHf2_Ur0f%xuj>P%*{8JPpv{ObrHYvv8}gB+$iHQN|^!QGpRC(fI)mUsSB|$6CC&vF5ZwTS68S^ zEfovWn!nYrZ94gf;em3}fy4gu;`X8|IU3)8_s5{!ZJ0`#FaR>Wx1CO>;WTTG*|Dv* z*|M7r)2iEbv(+5BEyryP`kldQ-dlvsy%5K}ue_)V#2(H^fZ`rd!(ensZ>NXYy)G>$ z@n6EW3ccY?WC7mAK?WXXVYw@+PneYgaL(UV<;*40_z6|?OVX~x3m@S^wn6tUK$C8N zHLUU1J|qLq#(aRqU^T5vYs-`&&-X(n^APWr5Ym{F1m02FdJcGEg*`H1--_6-Q$;8M zSd@pDyL_fzLkdq*({*@V1d~ZXWKQA>=&Xpm2f!{PnaIT}>7DFwo?T5{*Ej=D4&IA| zHh34^_8}RHN#NZ`Hy|YTE^NRaKIV z;$+m>-(h6f3B`1l-YkRB(s&-xW*PIzi(MN{Oy?jD(^c!AUx`^JlMD2=aS^#m`8bnl{Eo@Q7>m9NWPUw-AOR8fQW&Wp;$(QX)IbR_RxiYO`j6K zEJ+Gle_5j^AOawlLZ?#b5UZ7Qh~aH1il}PzA*mnmR*3KD)!Yfqb^R7k8|%Jptj5kZ zRwK2s8avroP_p)`SPN`Mme0Ba)~BJK4ONYjluCjBR3+@-1;vOiG-Zt^NA`|@s`As3 zwuP@$0Inph=Cng9k)MaV`-01VNK|iJnsLe8tqBai5|<)t2!Y$^tG%j>h3F=x!yV0LozNtbydTZ6h|^)1V^nl;;OwFW(Z)2i28bJ%z6 z=Fsid2A$!(=R7RO_b{?+g;i;i1 zEcVmX%lT>Qx}QenO;zP*bT3VUJ!eCwP5sGf^W+V^r{2(~J@&MJ$DSOcPkZcXk3Bgm zZp~4#jXkzby?-oQzC@5AxgS1E&H}XA{)`b@fDQa#Y^L((9|*{BTH z8;^r2lv{^$KM_hfl$TEDe#t0`9zN-;U2m)M`{H4g?FLXzn7xuPC>4mL7;EvyYilH? zJbY&G8x1Av_Rjr%Dn}o<;vm7nmAJH8yaoT$@?+$vh{@rfCK&6n%>7$^!4V9qcNg~^ zs9=R&PyzW4;6K%W{;9s}0K*9$bE}t1))qPua<2W+h&Oj+aF;euPqh7C(wCLi7D0cx z1ELah@GY+eUey&-c9is@(i1$GbtxaKO;sngcE?=~a^dN^N(o}7UfXUonhncxOt)oQ zW~<$*xCoSpZFpP&R&{BNGxTkn~*x>@h<7y{`Q zaE_+#7!kqdJ~0wFWl@d18N(SVZ5)3Z6Hu9^;X2mc@O22VzQ?QLVxbR_NP#53CT(^K$*ly z6Oxtq3KwTqF;U1OSs|@xsARtK#+WB`J1Txnj%u`0Rh3Avqq22H0)_ z-!MIj#qekZt3lv9%cu<*ksZundc+dOi$9XcJrOTmf(&PHV>n(2|4Afk_+t@<_Xb)l z4NPUXpoUmg3+2{l!2}EYU=I?SU7ikx!>7a7h%T{uu!3Un4Lpb7F$A=)MY+_Z&2m`^ z>dKQP(<|13168V=rJ!O>^kPa>2%Fny=~d8j%8W(-jfPf*8lqQ?Mv{# z!avIr4!UNelVVyWcczLwwf%;GJN2b4tU!pev?41tw&UAkhHbR6U!vy(QJ!aJ_ zeEq2htkCcUfH)+~axjaM{^1nDyuv;I`(OXh49N+QK>{~i^68Mat+=^rTnT*<0r&6` z-TD&uRSk*6|HT8JihP;}UXfeCr^Yh9C*v(@;Nc^P=3Y1uf5_=f46AW{MKEg-Nv*I6 ziOGWo%VE5EhG>5z9u(PO9Gz>+i+Qt078sWv-A`nSXBi0fDFAx$GJ>GT$?`6Jp}0FS zzqn|Ujs93~8USj1Dp@7E;}GsRvi$Ov#@x*?G+LYIuT*nwjyLl$Hn6Neyj{-PXs>8( z{5qnj$Q2-`XJ@wX(HAhsQ-WcAK8vy0CfI(ikZ?V(m==FwO2yN!|7-3|;B2XqXXA=7 z{&J>u0~><6i_p8Gzm=V3!el0jOT6FJ0B&+Zw|a`y+Sti&$k@2osp#kiy(+d)ZbkQ3 z--dCdNp=9h^P_dMlC2jpVyd-Y(&&4%e0=;up$&Mp5Knk$V8BisM{|5$`tnqWJr*TUgMV4>bO?BZ+7gqW3~paWA=shVGi2uer?#d z>I3J=YrH$Ju@Kz$)VfTnaPsHXbB!DKJJB>rDh!Q_DvrDMLUNnC0A^ zL&A|#2Ja>}aw{Sw$?_fX%S5VtQ7Xy(TXt%i_th7)Y5h%DO0!dJ!anx_4+b&V(tg-c z#_qC25-GM{-c%MVX7M%)vv`|#x>A>&NR@v@a%S@8q|AYmZ-#CsxjKE&@Da9x64YF* zb1KJQTCasVg<)SY;-l@m&6>4iG3#Tic;p1uq-ZG+7NR8KR#(o7rVhdwo59eN!(fdJ}-Yk zaT_v1z3hz@>gp;Vxw@+Qd9e}@$$BCA-8;KBG>E@HBrEt1!t$9eu%6xP53F9_><5f&0e=@o1J0X>UZkhW)ti7e&kU4$;V^BMS}m$4W&0tF>tlIR>Y)vXP{&s`L8*? z3V%Qdo3#>mq!VI)ac?AK?N_vt8wbdv?vD42lQ}u*K~YjXhQVwmZc8TrAdUm2 zuNlGA0r=%azE2`HPw95q$vx-(YZ6M0#kUWO+x4v7SM@l(&Kqkf$%&DDs5*c0mh)m? zd6&hRut|4n7Jl4ozyvxV{>yzT312KSLv&wUcXGk6tW_)X$;B*)I-Lx)GNc+z$3@1= zVjyst;p@Am?TG;OB*dO%v{?mP5A$l*$R)nd8301c?#~z-@gxX64hEOw0xFSWJdy8= z?s|zryt3mI>y@UQ7q||Gh--hzS)QCtx_g4B3yEQ2S7Y2(3$Z^XQ2!7RdQW-j_d740 zbS&373znp&Kk3BxG~sQxq1kU)HPh}l+HJdM4V;eigWKH4 zXa5fA>+sLA^!1imZ|pREognn!J+#&eR$nRyJJmqdEJ>36_rL!4vUh)0|NhtiiDja5 zTeWO!6nR5Lkc|Kmgl7N>I(g+Y(!u?*0#NG!g>3OvZbCHyQ7*{}fAjW;|EtXmVd_8I|Jy zzkU4q)S>4PEs8!fhBMqC*z87hD5ot=K2YMv8x|LAUB(qoOev zayN&Z@M&j4R}mVL953MKvSzYEOe`XAYT$T_#SRgjgqEQ5WqE&7?KOIxnf8_qQt)Z~ zOQzn7?M5&dE5grMH@w;tdq7-xD@HgAjxMpR(0Mdj;_kRVc`I8DIjIzn-_9r(q#$Jk zR}*=asBBtp*gFlYBw?Y~Fa00e>Y|-ATm3WJx(noQKtZuSJsFx^Q z*`>9O!=YrH0o8w|w9Zu9?Sb)D=ouxCuKK2DlbDXYPi49EPp0lyPn=F9XOvU_2gV4y zBauvQo{J=C-7)K`6lhr*{5p7VbZd>hdaZ8#Vz8zR*oaCKUZ(z5^K{?J!xHs7fdx{uia{N zYOO)P*Q`AidzEIg&!f60Bj`r%e_fTN= zA+7My_6L7>ci8emB^1KKM~Gu`Ia(ot`xA#a%D)g^u>HENT)pF07`&|{mZ%di1H)Xb zi+?7FM7Nu_vX;)3U#}*Pmt)_>s`oW^O?G0f{XCCGx5vxYKhMe z1N*|7@KL6|#*J|sW^GB;&QI{vY~A2vrxh zV$jGw;_#>upTMII;-RW=Lkz_1N;KQP+Jk?x3||AF9ga_;Yj*5EeP(9fWvAYy2(Q`fPgl(MwrgfM4n1w^WYZ>QN;IqA;_Vm z<5V5e&qCFmXWE2m!Coq0fj3TrFY}~Lr7pgQi!1Ygl!wennO3in+hj&r{_)^0a7uq} zIrIPden=}G`6?-`6va28mnR9ykklncX$kSxLv)m2mZZvV0;P zsC2g}EW{n0vzbhO;%yDZ>EsqE0TP!J?*vDv@{} zdQ`?ITZ5>VLzZ>f|gntPu7qXShA#$fN6ix-xWM^IUYC3+(+s>7ZUZ7=>f<65%05V~0T zM^0??;Bg%;Rp=7cO?r5rK?G)sMUOPXs;VtHceoda$)dZE=1NnUI~tMAAdL*X%N5#_R$r_+)ni|kQGXZsni$|wAnZNm`p)R59!v>rC`r5woMa${ zF8mfacSaJE%bFfIWy<4P<;vq)YZ1aEYSl2(V)3M7{0fe*`pv+bDtn~zP(@kV!YDdkDHkp&-h1%OgG{)CvJj+H5NDFbAQ^M zip7qM2UZ4+0{nnM9Typn+{xyQGHXF8LATemXEF;5)MIq6fv8CPP-qD;w+ zT|N$P-W_A9?6{6%*9Y2$#cXR6FMr^+a9_Eo2A!XWz z>Ly1W@%whw!dtF++5)weD~wC4TW<;L(y`3epk|xBTBB~-!=~GF?EawMcYhyKwbL5e zp8LPQ2Wrj+WWByQ{viJM==cEs_XG^`6Z+}o?I{!@oW6aP>shULforwiig|M~+@a{X zi1|XJ~pja7CzJ>@s-u8Kpe~c>L1I&F;+xGh%ZP16O0oIoswiX?h@F_C+LHeVpI~;h9 z0;e3G-M#v-zE5fkX!zJ1vA<5~Y`Et+sIXu_5;A)^Z$70@i2v3wKvPiALQk7|gE^*; zPu-D~xG7Rg#I%W9byz5|aL%9-Evvxh6o7yni8j4Rmf635jyB>_#ct_0G^hpZ@^UI9 z=!_yze?t8TXPb0kv{g_;~@>V_-h)!9=g6#TxHVK0#7?%9Ma{WC{{q*YGtMy zrbm8#WwMf#6!)uG|3HW(3}4=u5u zhR}=%-l3|9N{s8PAVM?p0}ZSeS}gp~nSk@V#2$}Lg&xp%y-&(W1}UJ(ll z{(xtJu663hza-@8DS(;;BN%$}3todO5%;6(L}Hn}e=r#Xf6T-cM~2B1|p zyAJTLkcY?MVG*+|7B`aTavpc|4es||GzZejN{RH>fr+8YSfdL2gTZ^N@EF5p->5`p zTnn0*e}Vq6sNpO-m=BAoH#m#kMkA5$!{JezMT@wNEun$@i7!rbZXBtLXZeg9W#Vip zKb$Zc{d7*S^b+oXU=?@P1il(xTcazRznl>rmO=p>Q|s7f&nrhg`ws^`P(fW`!Sss`MH)rHzVGq-IrI zmQpJhwyx=ADVmp5eMm1%`J7bFR*Pgbe!$|B2d5FO>^WY=3fPvnHB$XiV~3;TB!+M} zoMQL_y_Miy7<*K=2&dl#?@3UaRAnJhe<{5aX0tWo{FF%$fba8!0=^U#rm|%U6-*I$ z5ZID)@VA-dJPEwLMSN_1A*=?Jy?tx;`yS>smi2*WySSZJ%V~87X2W$YvtYg}l?x#KOY^wii(_1_^ovz#$z}+{99H)3)2#eIVhY(z%>vzFRI2TRbE>o z3`0r-h@pSP@z$zhG6uXGYoEm<$aRJ&Z_>`wZ9rsUn?WW4R$`0c$G?)MVEnd^>b7zg zp>F=Q*Eau@$1#y4Z~~?SHD_0LU{3dyCZ{E2{Qa;0KKK9r*Z)VrN8sC%OeyeVk`a%T zzV^>%JJWLfcDDEhsa3umEX4o8%yr}BcCG>eb>EVz&p6*j;xFziD z!!eP-%9;;b9pvPT)AG%uqtlvH7Hg!`Z)FlIcTRt>ZQH*D+2QB((5_3bcZ>QMB56WL z$DV(GLD^d&mQIeXKjVLk5xn44-rzsA7Shobmm&~tSw(~(Kp!|KFDQQz{n=p@gh>|? z#UbgiO{>w_mP7YzJT5%Ea;~o8NSxU&7mB0%o_t6NbiXI(600P4V~=DS9d@(daR#-f zIT+UKX3K5VpctWMwuXJn>J6Q4&#@k((eZ!s{V|vp@K3I7(ZC>6E2Dyeb_?O?fa)b7 zu^||Qe`7*#FdldSwxbZ>32$9bD+3gF%xDK47T)&753V`YA@z6`ruR&uFuP$gnK{+` zSc*Gjc0(?+7&#|~?~d%25)CN=vbMNd;Jl&|F(A2I*fR18)?)BMSkw|qc|MTq#`sKX zO+5<$GN?f^&jS~-Ax&take)$!*hjk1(G4K~D*)^$QR3t$jLqlSfQ zf7&48UBvkeiTiRf%h$8T&mYJsI8nt0qLs;4C*Dzw)pHae&8LPZ3u!(j47h|MoR8&3 zh#tw{HVF5OeVqs1g4ROet6sI2@flcs%lQs61!9%GH=*7U(d1fT=Ve_7Zvlaq+;?<%J&@60-s&23HYs*&p7>n@YTXmi*S9!E{ezf3Eh0 zJ}v%v{a9 zT-|;8aRJ`c_Cowe)xe+MSX20n5g)8Qgy4H#erre@={QWy>C4 z>DY)6?Rg)`G^snv30#w)b4$Y8V>W4V~iDXB5|iIOB3>FwTLK+E)@{=C*; zXYI1@_kiX>0>IxcpjVDd?nG|%$_9X+`#uU01{!(vRJECqN_WuXCq`NhbU-?!R_(m#BaO3Xt zT-526>2e;_yKT4E=$N&>+cR53%QgGMhHDP`wZ^dD9yEFl=f~q%jd`b!PTy2?BO?5w zvX~QAguJpEU!4BW_Ot)1xv;J8z|B3jrlsIo>$UQHOsm)-iJ^|5v_{+ljuc9S0LS^oc=0bZR|lA4jH0g!>jP|4(b4w*IiW( z#H_t^UsBgAne|UvC~k)}P%D={eNWF^R^?Je{7o40uU5{5IPylLjF@>=A;D|-6a>n} z-0f4!>Y^x?o}ZHWJ@`_#%QA;&9oToaDbr-+q?S-yF3OS$luSJ5m-DnmFF#Q*Ci!r; zSiBNmfnLE%lX#9g&x(P}qh`>L#Y&d~_!cGsotG*27GZy|B6j_6Q`hyk)PgI)uQ zoA}nRbN_N4X6i!!fPgp=*07pW`M)YT`cU4zD=9@&tZ6;>zU^{hFVQnS|0B9W;{XppJW<(#GDwpX+T#*z z{|1||L58oKxJtQN&^5el3ET=^v%4l!m0KfDrpo0LD?wLescGQtBEJ{L`Cx@`hTJSj z6vdRL42pb)#d&-gXvC3o7XWO?=blg&m?vmWtnzIlEQI4B6=;x_63Kax zOVN+&tcJ~2%AQ&1VXLTDXX<2l$UVCy;m6?daoKmfn%PQ_F!`|PO1aK<40rm2!Jy}u z-C=*vHd}3{WA=MZ%j~xMgZ9vNJMO@FvLVabkZ%tTv6NE$vkXS3Zq{qD4Cj;zYehr? zRgPR#HGFh8^88jAj8Lqd2uzdls%1tcE;FD&lb8iO4kOZ9 z7N6GWAPc^FBz-#?@Vm9ErM)NCwiqVW14@1rlFw07IuM5dhduGNYM$u3IQ_A=r#T!} zF}3wAMWxvq5*JLpf}%W~StF1lLPvjv|NJ5wNGh`S80DR1Rd{EzD8~k`aJmraU*>cV zDHA+KLIFvT=at;Chw@d|@?}V9X0EHak3!d`*XTf&wyrvrDZ$RPZD7Mb zum(_sAAeE2Z{hCxbf$|T3?qPg=7vTof7x{~{Xz-5Vx^$Uhq>KQkO0y*{*Hg_^jG7# zg$oGQniO1k(n^1?Ccs|E6H#BFbRH%@H4B9`67#461E>eo2H^z8DCJmH*H7vCJpdWl zZl3DC^A*&5R52iX6jSA1N2Mj^7A4I{M4t--O_KBAjS&n?p_kD77D<1~ zN|LRzVIr-NRzN#?JUqx5i8FD|d~uyq7mh}7dDIq#so<&fEZU?+2pd~L_}HpT^EXg9 z81)6`35!iwv=ud0N&l>2)xN%S2G-~p`d&uWzrDS$xmoue=XW~8D(W=DiUrZ8%Hl2~ zHYgXfPxbHRPV&XWM4fr)EUizRx%YpVGk43e!0g)-O#*T@09SyB8BE zmV>=YYR{rv{@1!G@XLG}XY_v^vGDUL6<5fWUx#Y3oN@N6m_@F0OxY0oew6GLGh6b~ zz1~W?mLkNqYOWy+n~YD03pyb#G&vCA6>1bWkqYvy@@p{N58sNlx6icSX@UL0pNgaT z()a$NKF}6RuT!QyD6Khf{o_w+_&vxPD+P;3iXqY^tw0kONrt!s6Q$_LvuRPb zkRlI4{1Kch_Xp8@Nli$n)vMQPwqrW2mSwhjLWAmc-Ii(9op!6!9X4A%yYdhdcgkhs zh-ns*UHpQOvlq_LCT1EjI->1=;!z$`8GnCs1Q|R04`ZMM__N1tzQBRtXq>g4r?4@8$Aw2jGPnTC(g{ z?iNW#bdHg;WWgk;sdSARUMUs!%J=bZEAW*Agq+O|MOa|R(D)u?d81V3^18ctTu{no zuzd1X`1jS8^?g94*(H8@>6I}TJ%2^owHnWlR)}QhizVz=Yee6d@w)BcfO;yMlA(_e zsN&t5%BeLp&hNKU>$ItZy=hs!rt37!VXHSVTejOVtxmmVHtcrGafa<$yV*-Y04=fl z29{4XlleBDx+UAlTcg0@&GCV_>+RWlwAREwOUf*29TUmB*sy`DE12jdf`7@j<{{?D zQY;8O0-VjsxuL`yEOV+Y-|$A*PKi(D0U9d*feHSYa+>izu{nqaS!}dm^pz8=A>|yv zQu%oZwDAYn7m|99%w*@+V3f*d%mH|nL=(7zdR++O`Sxh;pcThg=00Nfx(X+YzD`)# zh=UvwGPvww$O*BkU2b->z<-N^Oj{NLDg)Fve}UX8&;EN-q z9IOkzk|V61+cTOkgdG{ZM?DZt1I!~6h^yiC(zFAqVR#qNicVk@i+@+yLXj0s!RB(| zk8C@9g4Y|M_ez{}Mp&nX#?2x(9t;Z24(=QYS?_ou+8JDJG>qy9_KX8n3kM5=Q>(FV z8lR@BBw0MOboRtVT!Et_=|E(l{~H~nh62wD3L~1WqH#RQ7Z&)kc{mrl_C}%Ia3e?# zq*sk-1kuq!0=c|dO@F(#h>)zo3%O)P$IzL&fEOFe1jr$HK4+qfR>(~2R5v)lw71y> zrE#DvzqH2gg^k9Q^7#P)-UawYh3VjX+Sx0*Keag6C4U+U6UY+{6hCA9SSCS{qwKFh ztj(*WpNxN9-Az`%-)^!Ze%Czwcv=}(mLnAitUCbBnersiA%Cr1(FQTcPnMUgp*@L` zF!?>1p8=#Kc=V_naHZ>y2Uy(^XW7~{gZf(>z_?Yk3VxXAl~mDTiKSv6Du9}&t4nQq z9+@N3x|rjtMbZq;Py_+SFs!8};eqZM-`<{D(ZJ3<*#tPHBE{vtPAk5-em^yIjy4fDKn`W!lADV-j)iK+xW^*v8HQQaM@fbPg4u3yJ2LZSVPUsH^ z=T?f{D0nIT-(h)ehq6lLtEI5H%BU;I3A34;mwk*L>rC73*?@}#EUm$`4XA!k&G#B zdd6c60g~`8F-aZP>Mt4Jv~MYmDZ!U0K}P>h;De6wVS#eT!g}MNriFU(~IkFNg zU)dBQ`dGqUcP#}z_SHy3bjV@9>0d1diF2IF*CA<;M5y?BWubHO>`_x zD5#eAH3V*Qna${CgboNZ5R7DtNqx;Kb)OSaS=K|TZYl(UD*B-k)@o|u$gM~}7p}oS z;(z-U(L~VS!c1gEc5db_mia*FK@c(!0_vBf3ux|+6M6L&T`iep{vOp>_+Q-v(%d=* zDz|ETiRSGU6U~3cL+ac=5_2yOujk5;O4gTy;vZHS)h2cr;~JvrQFx2dlh-8w!w?8# zp^HFCrM`lWajscOobVbsU$#+K)tNYR3xAP6B>Bd*Iv7YnRIC&mJvnTAt4^PYQxaO?Kg@3h$qHEUb?EQUc%muikU0+U_hn7 zW$KF`Dqr{mPRf4rPGxwf?wPnNkvuJZMG@F!B7fqfeo|kW`LRz&$7&L8Sq3G3aeux1 z$u0P&B1eM47S@VePG+&HKyV|#uZ&*GNhy_s`E*JYF|p#mw`V1BKojpvCAk17=f*Yu ztPud}N6w*d@wsj?*b@#2Htazpcx6kCI~5cM?~QJ)u~%=ltX~Xm0}Jr-b9t z+RG(|*S#sSU}Zehx7HdAEvwh*nt!g-ADFFf*D?FuUe~M*hrM2FIOx|pgAFiKmsPcj z3*i`gY79jCFOx`NC+hM`y1Ww8t;SJB(6MBfq}ds^Ls_*`Jh7tmE(o<-qnsil{g8{Y z73DM9ftd}udNQAB^mEFnjQOGfs*dQ+6(M*w6F4H+8A1zn;2KeX?>6$fMt?ZyJ&p`& z1I1QC3Q78Sx>_0NO7Wd)F^PaSgQZ^ixtyu<#huJ9mL>VZ7n;LJ@kRg zkNc6Wngl7ACk`J&iqf(O+&u5O%+tFMd0tlAF|B&5X}0?9nrU_G1Jmg?J8oytbNlUn z<*6Dn7B^$<16&;mjpIGd!+&1Ba+IvaOs{Ec7lz7Kb0f;7WlJ ze>Igpf6}U+wZzb>PPx$pGSYdHdV}N+;!{Jtv(TxWd#Dz?61^n!i>#`OgON3!9C@&P z71OGeM8jc74zmQ#iZ31Xb&vODoA>*!Htgo(oG1DxKr*=pD= zb1-Pu&2HE2c53~;TYpDUSh}ibV^syx2oiS|iH35x)_I1645+F z6_4r#X4Xw6LcNzExbzS|B>ijQYXIK!0Y4?B#=-AmC#*)@xm(K4H}f$z=%e=+?8g#e z>&9-_%d?IPB7D-xIU;HWs(*1ES#Yl?4;09urO~my^k6Z>$k@nZ$|+rhJebX>xp!VU z_rEn4-(D`N41bp$<5%XUgxr!nLn2A=LdN2>K%A4)o4wmbY=1>9TQjthSyZlF9b=ps zPcQ&|)V)e#kz18Q2~w#g7#7q{>WAsRZ(TIyveuVweh|Ew#mASl}3Vj!~(9`)?fc15LBHPXwrb|E z<3cTg?U?;ut7kUtuGMQdtd8BV9ur688f6Esjy`}z0{`R|88pmVU;JM@Sz0OHsjOF( zxNGHYIe+D*P(*$jfMKk5Un_{u-;rp1prWBZ4XC$OG-DJUAAT7hjNTMX6W95BRJWY? zAbuuB-zH5XDE*I&A;~w4*aKNFiZO{P0|!i>sbQlDG)-DLbn$>Mu8^Ip>w%4}BS63Y zuemp&nD|yb+W20fcSIr;aV02o17s;Ih#8UDvwv*O$;d5$>P!Z*lyHyWI?u!pVZ=6# zh6}dO5DM|_njV--b+wI(!W@?3lyyM6nSoy_3iwo&0W zCy9oZQF-Hqr`k=4;tbPiDpLA*u5+eN^mbH_19V_}95^eRT+a-@(DR zR@A4MY3huQ@w?@k+C<^_z7r(_$745Xpv|t;u$pzpY}eh6*|KY0bI`II=AcpQSWc_v z4qA<^gkG}kmSU3x!XpHjcGNtG;H_gB9hX-O7ZZPy3UfMPw7FWkhg3jzneCzaH*R90xkD)xpBMsX^)Riu+?_9&ETY?znI+S7&9 z)gFIZ^nX)^S|u}|q{f{Ah3~-ssZOk%9E)aWShEJE(`+`)mR0MSmQx>??LpVAJIe}}#V{{D{=31bt`?N?C2PwlqLotwVxZ}ad9UE|j5Of0G>`C4~aO5MziO@NDD-nqv zJw_CK;2A`yXz3h+Rr2#%ibebb?7b!wC8`hH4?~2UUNp zmc1M@UVmPRVX3NAd%#Em;>hbO4_w@;c#YZ)=`|TID;(HUSbhxQ3X3B3T?w2)6CBVS zt1{ZDIHSC&M5G}0N_4SHhze?1CSHsc=AZ^=pbyWIz%Xhh)9LWxu-EHrROiAQ^+a}Ac&!%F~@cHDTz)K?mF{-Z9b7@98;yEHbQ7W{;Z^Nm^W#0z6dW=FJ^F4l)6Y;SZ%5K3K zp;aYHT-v)qW-J6eaATDUBixsth=4>of8*dJFg`8uVE+$s(0B>2^|}A8vH11@Y9f9y zUK8e60t}Z(E0MN2QX`m321Fa>2{Uf2CEh%_kuu8t({?8Stc+J$Kess1m|CJ6WzpIY z`vqWsr!J%xBA-)0tVEDi@+`#K&CH}s>@0XGWj^LDm(-A*#;T0e=3~lCT&b7%e}_8- zE<5z|bSvDiIH(sKWr(O|D9ALm{7{Ijv6dAS4?pQYF3IRwz4j+(*EbfclinVP<%o$_ z$HPiF{K&P7KUS|Yu^dvdg^8M*&L*0cNM~zl=_ulAMmk&Kjss90D;f1N+HLO|Fma*|EMmv<;?@19rq7Dz$Y3-^Dk zoTis$sYK%qVRKJy4C&kVv1dOJgZC|qkmBOZ9}&=NSNiRC!M3WKJLO51zj9#}I8l~) zmbYzx2JG8XZt7Ks3;o4KUwkQ~hMlQso>+iP%M4G8jAiw}wmHzpg4zlif87NtFAqJn zd|99%5fz9V7rNPz5)IMC`w%VIs=ars=(Q0L9-1smq4MM%nf_Y4+jiUSVZ$6c9oua6 z>K(K1^gCvw?z)3sv(vYR!ymlFv+)UlX7+!FEUAMd`0rQmDZ?5{KaWljpN9Te#x+&% znzgQ3ZzWQ1V#RiFh5+gdf8)5s<0sOMms)+4#X&4Ikm&@@Qc9ez=_JH6kE(_+>4>g)b`2Oph3bVygRC?ZOgoESRIH&Cbo#954WI}S4!8r( z;DVRE6Bk2&6#=sq)adSbi-le#4gq^=4QF@gACCN_G*BwU9=g`lzKSOc!=|+|@|DrO zps;f#@2S|uo=@c11dN9v8s7-7=ELC#oV2Gi)GCbUXVyf;zp^quf59ARs9Czyc%IZ= zfQ3OY^a1CYMPs{X$giZac8o)GYWNX;rv{2|bbkU7lA-QT#{V=VztJ&q*-@5tL6?ve z7bJhnR5p}ychbr$E?X#rkeKCbkH#ae3HO!#qHH3YX@^h_n&PU zIbvyP-WiE%Eh<%fq-hZ>(s+tdC$eVqFs6T4JepbET*F#nEtK3$B?FS_cxt;ap4xsa z^9Gz#jrv|=@QbE}>#2oVmWr z*iPKbK`&L2q%-F(GrH-;j%pP9)8hfP&=NvG2u{cjhm3PZGFC)5Lk7kt@azrj-)L|u zLa2U>YqDaP$myiXOp=Qc(7wyEV9?FyPqz3g4!6BblC!}(#Q z$gCRQ-pvqy6q_Q_$E1FztEPdZ3h9pvcrji!7+(!W?z0dWN-bPu}tiY~50mAQZ0jPNt4 z{eMdWgd4d%rK7V(008%Ldgvl%*r@&<0+q7#3(0PbSMYX6quIEe1D`LHF}P&RNy@ut zA)t9#2DTJzgG)?VnM&SRJsc%%h6EG}!5Q4}Hyq=78u*tHIkgSIxEMWz;MvTWST{0o zgkOe5fG^Ri)!`9~Yw0m|aBWmUjko_51T(-q3tda)NghDUQAlM34^ZHpLo#O7P-i}) z*}@~g7_Y=OkZ`%@l@~j%?_G;Cn<#>Q6{k5XI;=(1Ri-yPz2UD5PH5Jh#;e2(tZ8YN zgBKSn0VJ2M7Z)CXm8iO5xJB$fZLJk1lJUmAppAW@Np?CQ6(a&lcW3H-r76S&gZB`j zp5q*C*6PDZINT1StES-3a&r1!V69TH9MFd>N)moX1m`x<%>E(lWZczTS_5f(I0rOBfdse_6~sT!LY^)pYBF zTDxgB>_Ojb4eE~Bcbh%4UaQ$dtJ~;w2c1pqx!c2#{rKj185mr>X4adD;&fSB4L}YM zY@wBesa;8IF4(`?BBJZc=t)C1r9X!?<*+vx{7XC!h)bds-##s5{-augRQgTZK&*94 z;I9>a=AG-|emX~cf9$+s_yPO@O%#6s3`VR&;huqzU%|*>LT|COpRNJupxpcYc#+Gn^we=O85mS(Ol#bg2}eSRI0 z_6Le0Oj*V8MKrXpgoU6!$r8^Hr^Xcmp`hTCW$ExfoPR5v5wIQ2OX1jm@JHSk9+1T9 zfw4TYEZ%&cmcjCA$0$FRz3cofXgNY&)HD+bGgv_~;Nr zqCu29xHv%KSs-%~)k3sV#oY(e={kStWx+%Sklh~hp!moCthdz zOy*${7B1vp!+Vg*=XO~rv3VzF6=f32(EM(&$0FMD#!hG}lf~*Q@>Nz4PW!d*91oe| zELA_e!Mxsa{jV06+b4T{ZRV83e##(F(IBeW;kwid6QKa?E7myCy}57m$OD0#T1_|F zI+Zp>e}zl=#CW{~m6&YVEJyHtX<7!aCXFk&yXDBnNH*2+uo7|1w#YRfLlxf+qsWgj zZuy{&-4*R+%z(6XyvlDU!##p#Bwpt3Z(K>9c_xNCry zhD*%RoQ7?!6~#t1qAQF@R5lr}mQAn#XpT1yK%eP_xJq^R!o1?qIG`&yd(JB_7;O-W zjOlqYV6a`Kh+Y_%E~ZMWS#2&&)%CX zw~=INqpuQ%jU+1sg}{vb7uP6~l2|2*Y>`T3b}@|>6UZn55hw&8CQC+HF8}ql+LT%M z<~~|J$vuO+hsOXQFmaG*U9FT5laI&G_;!c%B0L#+vO?7iU%#tgO`vt z!^V2{MKA!MGZ#in>3^SIk|cumwB{TlDj|3T_6TXASd(N6(mj#Yifd|ibG8j2PspC$ ze&8Q74$KmU_sMi3a&?pP23cHjVx3r(QubcXnWMC~C)Ur+n^`l=>Lu*1k}LY#V=Jwj zLDH!n`xuU4Y%jy$Uu?Os94RjqDtB1P^KAn{tfXm>1PBD?_bqqJ9P)qvBh8i+-j2)7112$Je zNL5ADaNK?pKpKfZX}965Dm-d@249s^hjQQj0CP&ZBfT!@^KM3+Z0z&ukSZLLL}|ba z9Ja!n7(H6SK7Uguuw~(&fBZ*0xnrCLaG=@a!A+Hb!DSQa?QkC`t(PHm+{-J@Mgm`zEhi9YVtpxQW+xvuKqb~>{U|K^oa6&An zW;0NbuaWA>zk}K`izB%H@-KD85&Q+FdtigMpzAq)?te*`OiC55H3gqIee}rRCJxSF|&Zc*$12Q+&YhiAZQUDYr;1)j}xR z8kmXoxqniE{yvMrWhBapl#N(A`JRb?(c7HE?9HdT&&NHW%zQhXz)~V%LOR2D6I5f- z?R#g@_mYEp7bop1rW=TUYKQ2ViX>IkE?U#J+azk0RCHe5EJp6g07BF=NcVX_h|C>D z#^%pbhsv6pYel4%wiB1s_qK}m;CJkr)9BR9o`2Qw%%ndX^AN>}$m!T=w4>GA@iT{qhX{0VNMw0$ z5PvmQu_jA~K+Lnuas-MjUJA%-X{l{=bWq%s5Tp{HNYZKdsN-E6XL3KFjHZTz~k49SE3(mrOk#-gW`?jV45Qjct%VU(u_1Q{Dg<+U7~GT~;d zJOsi}&Y*h>x4$FsUR01O4~juF;~uXkRDTN>$$6aC;&7SkHHfJ?!*>S-iDbEAW0dMY zU{wzoh$9p;Ryl1)1~`&jmQ2V4g&8jAD;UG82ztk9H6U0nT>cbQ;Lc_ZSbd}M3Bjdx zmAlolW>Hx#p;1{2H7e`TXjJnK0&3a3gkF6GFE7|;rUpM%0&ZMp)VC%x>N%NF-+zkC zc#X=%wh87^;5qq<`we?fA0uaY5uVczyssKt+gFX;zG`fMC;SL2`!<&~A)5UoWlfZ2 zQ!iTZ=`}DRSK*g`;i}}%%uQ$csqE*6TX75+)cIUjZ9cRJ)dv@oV9B;;U zI7#T#M8mbJi52K4=Y()H3H99tR(=e^LAjy66>Xw5Sp)fvhwMExS`|w0*DHUMBkB z*kMRAZA0OQcL#5f$?O0X2O0E79Ga^ zV1mLx+h}kCjO`1`P*gwlL4S+}FplsuITFRXkq7j6Y{&nc=8t3f`%1_Hn?)Fym4KcU zua-zXua(R_udR@Lu11SuZ1DdRCNo6|46C|!tA{8MzsHWLZbeVJO20=z6Ctn%hYqq7 z(m5y(J>vV|U<^0rqF9s@QS6=g9`ewC03favSNmg1O@E8hp|a2io_~W>7utkGQ7@xS z)NXHYXJx>DvSR{eyz@l%C8Xna3tIbLyIr@Mrr+v-l3;gyv)gL*%%qJH74? z&cwR=8aY@`#DCtMp3TmMw~*tsu0b!6yeDwFn6RIuXpyC;oZ@E__i8+$h&VMuZhQ%h zrH;gBF&FPCU@TZE!+&Wk1V%PTKGz<^OjDx6#?tEq6c3@i=&M@xy-KQz1>(7aNSA_# zQxmRjCPG`8tAN!E8ZzP$H2E46Xn&w%@kCUe(jup){+}`} zq{)N#jYgnhVaY3ll(_?2<-%d^2^ISa@>*vQ@sIXc-3qPw$hHKvLLM3cG2Zm$QjDkL zFCf9e2(qb7cvG52YoC_)ZIkAyfbLGUoc^`phIuI(vD2mX6fY-)|X+hiqpy|^htjIl&QlK&e zUP&+`Z+{V5X=yug=_C2ux|X6;xQau*?QXx`cO0|p_B&>?;n&QrukQ zs(sA%FGR0vqKpmc`aA3weXZs7dBQhrY{4tE`w9f1hg8e9^vsRNd~@y>6@Jnys4K1w9B6AbXB&+TCW~?>1YFp4)m# zeM?f`uE;`hV2fXlgW)i%WANoyaQO{gUKA?Mu4^2vtuZlU#bcS+dZ%1yy|b4CffURPQ)kagX1{1Obn<}T0>{I*pp(_Z|_Ua}@pLeH#ZS^O>w)^s+h?j*Y z7bAbG-S%32%WT<>XEtjs2cy3Y)2p{TzU%m%y6rqs0k3rKMMH<^fZT!ouW0hG?71f{ z@5;+}mfT(01MPd$h4LW#!x#x1h1U@yu7Mpsi;cg+md1GZi}R3(Ja#?L`1#8(k$L%**6j&naS_hBBV{_UmXm@EAgpU9tfalliu{^Wtvty+oc2NI(S7 z_X>``gy({3!;3Jxh{k0F46f8vSMHEr5=0X1Bh^}xtJQL2wutq0qihz3q4 z_Q^3fn^9KQ;-x8NXBN(uo7BaM7S=d(>e<2BE1`15d7Vq{j4~x(>#^Nx4WhOjrHs(| z8EH~|P?8a=tyv-n^r%Oq^W6oN?QFW(JXI< z*yLw~hCIqCmg?#mUM-z@C1$ZCQ2r9gOn04R<->NL3~my-QNbwT_>*g7Nn#j_7Kh8h zw&kQZbU|a*Vp;$oZm~>wOejJF_e#E05y4WHV#vV9%U&Rv@B!<86!L&2 zkOx|@X3#erx)WYjm3{F~)aFjfsqmHr1jqkJoF*WZ!*!I9;^x9>$&03>D~j*@Y=iCUP{FkjtYSk|3ImWQ_86vwLC-2{_I5_pwG3E7**Iv4q)90+78L@o zqu*~#k6x&{ekEd-m~G%lT*mPlkFnphK<`wAz4NXfhVDi?`+fE1VN8GBRt>D%CyVv| zEY_!l@cT#zf3gB*S^?iSoF{+hPKB=DYxs8EGFyJ5Z#J7<-?Xhx%j~t?ZnM#K8}+V# zkFd(0-X70ozO0#6x+1T@ZpV7SFM^vaTeYAB0Yz(+imVEi6 zqxvX^p;_dY;30qDx~~HMapiMjpUV^a^&>{V-e=jk*cH^+t}Ey$7z{Shh%76U7VCaQ zp;npheG!Ak;}`F|$JgE`Wa~$OY|X@Pm@u+SH^@oRcG3dovu#3RFGyu!we(^tj4w=z zSvTk+N24;I{f+dx1zv2hEMSI3O>6=e$MY`3vs(SDxWa!-W2qnG3r6ZlcIq85E0`xAKkoq@M&F4ASar@c-LA9%v; zdO*c{B}>NjRJd$8-V&He8`(vLLC^P-D_m}8AaO)Sc_^}9?<4Lx2Z6`&d-ZywT{C^Z z*EfHgZr?GTzEv~5X4i6Cp5JmE>#10;Y?Suo?A{2GHWXVdlH&vJW1f3@U(&D4Wf<-t>u}62U{VDDu^rv zM2h%668Os|+JNfZ9vldaQ- jwa!zj)hVyiF-0&e$w@el3~ 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 z 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 delta 193687 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 AJpcdz diff --git a/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs b/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs index 5959b35ed4..e5b2628f38 100644 --- a/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs +++ b/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs @@ -214,8 +214,15 @@ fn ancient_brass_dragon_zero_targets_is_clean_no_op() { .act(GameAction::SelectTargets { targets: vec![] }) .expect("selecting zero targets must be a clean no-op"); + // CR 603.12 + CR 603.3b: the reflexive is its own stack object, so let it + // RESOLVE and then STOP. Passing priority into an already-empty stack + // advances phases instead, and by turn 3 P1 is decked — elimination exiles + // every card that player owned, including the graveyard cards this row + // measures. The observation point for "clean no-op" is the first settled + // empty stack, not an unbounded pass loop. for _ in 0..30 { match runner.state().waiting_for.clone() { + WaitingFor::Priority { .. } if runner.state().stack.is_empty() => break, WaitingFor::Priority { .. } => { if runner.act(GameAction::PassPriority).is_err() { break; diff --git a/crates/engine/tests/integration/cost_zone_pipeline.rs b/crates/engine/tests/integration/cost_zone_pipeline.rs index 2da2c23299..e8e4b766b7 100644 --- a/crates/engine/tests/integration/cost_zone_pipeline.rs +++ b/crates/engine/tests/integration/cost_zone_pipeline.rs @@ -1,3 +1,4 @@ +use engine::ai_support::legal_actions_full; use engine::database::synthesis::synthesize_plot; use engine::game::effects::resolve_ability_chain; use engine::game::game_object::AttachTarget; @@ -7,11 +8,13 @@ use engine::parser::oracle_cost::parse_oracle_cost; use engine::types::ability::{ AbilityCost, AbilityDefinition, AbilityKind, BounceSelection, CardPlayMode, CardSelectionMode, CastFromZoneDriver, CastingPermission, CategoryChooserScope, ChoiceType, Chooser, - ContinuousModification, DigRestOrder, DigSource, DiscardSelfScope, Effect, EffectKind, - FilterProp, ForEachCategoryAction, IterationCategory, ManaContribution, ManaProduction, - ModalChoice, QuantityExpr, QuantityRef, ReplacementDefinition, ReplacementMode, + ContinuousModification, DelayedTriggerCondition, DelayedTriggerLifetime, DigRestOrder, + DigSource, DiscardSelfScope, Effect, EffectKind, FilterProp, ForEachCategoryAction, + IterationCategory, ManaContribution, ManaProduction, ManaSpendRestriction, ModalChoice, + OpponentMayScope, QuantityExpr, QuantityRef, ReplacementDefinition, ReplacementMode, ResolvedAbility, SacrificeCost, SpellCastingOption, TargetFilter, TargetRef, - TargetSelectionMode, TriggerDefinition, TypeFilter, TypedFilter, + TargetSelectionMode, TriggerConstraint, TriggerDefinition, TypeFilter, TypedFilter, + UnlessPayModifier, WheneverEventExpiry, }; use engine::types::actions::GameAction; use engine::types::card::CardFace; @@ -20,11 +23,13 @@ use engine::types::counter::CounterType; use engine::types::events::{GameEvent, PlayerActionKind}; use engine::types::game_state::{ BatchCompletion, CastPaymentMode, CollectEvidenceResume, GameState, - ManaAbilityCostParentLifecycle, ManaAbilityCostResolutionMode, ManaAbilityResume, PayCostKind, - PendingCast, PendingCostMoveResume, PendingReplacement, StackEntryKind, WaitingFor, + ManaAbilityCostParentLifecycle, ManaAbilityCostResolutionMode, ManaAbilityResume, ManaChoice, + PayCostKind, PendingCast, PendingCostMoveResume, PendingReplacement, StackEntryKind, + WaitingFor, }; +use engine::types::identifiers::ObjectId; use engine::types::keywords::Keyword; -use engine::types::mana::{ManaColor, ManaCost, ManaCostShard}; +use engine::types::mana::{ManaColor, ManaCost, ManaCostShard, ManaType}; use engine::types::phase::Phase; use engine::types::proposed_event::{ProposedEvent, ReplacementId}; use engine::types::replacements::ReplacementEvent; @@ -3721,6 +3726,139 @@ fn effect_pay_cost_auto_tap_redirect_serializes_exact_cost_and_trailing_effect_o assert!(runner.state().pending_cost_move_resume.is_none()); } +/// The plan's §"Exact no-ledger split" ordering, measured at a DURABLE-LEDGER +/// root: *"For a completed root, invoke the wrapper immediately after mana +/// production/activation completion and **before** `resume_mana_ability_root`."* +/// +/// The witness is an `ManaAbilityResume::EffectPayCost` root — the one root +/// family whose resume is not inert: `pay_ability_cost_for_resolution` spends +/// the pool and then `resolve_effect_pay_cost_rider` runs the trailing effect, +/// all inside `resume_mana_ability_root`. Two distinguishable observers pin the +/// boundary: OT watches the frame's own tap, OL watches the RIDER's life gain. +/// +/// With settlement before the resume, the frame's batch is exactly its own tap +/// events, so OT is a one-member batch that needs no CR 603.3b ordering prompt, +/// and the rider's life-gain event is not part of the frame at all. Restoring +/// baseline's resume-then-settle order sweeps the rider's event into the same +/// completed-frame batch, producing a two-member group and an `OrderTriggers` +/// prompt — the exact durable-state difference this slice owns. +#[test] +fn durable_ledger_effect_pay_cost_root_settles_before_its_resume_runs_the_rider() { + let (mut scenario, source) = mana_self_exile_cost_redirect_witness(); + let observer_t = scenario + .add_creature(P0, "OT Source-Tap Observer", 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::Taps) + .execute(AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 2 }, + player: TargetFilter::Controller, + }, + )) + .valid_card(TargetFilter::SpecificObject { id: source }) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id(); + let observer_l = scenario + .add_creature(P0, "OL Life-Gain Observer", 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::LifeGained) + .execute(AbilityDefinition::new( + AbilityKind::Database, + Effect::Draw { + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Controller, + }, + )) + .valid_card(TargetFilter::Any) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id(); + let mut runner = scenario.build(); + + let cost = ManaCost::Cost { + shards: vec![ManaCostShard::Green], + generic: 0, + }; + let mut ability = ResolvedAbility::new( + Effect::PayCost { + cost: AbilityCost::Mana { cost: cost.clone() }, + scale: None, + payer: TargetFilter::Controller, + }, + vec![], + source, + P0, + ); + ability.sub_ability = Some(Box::new(ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + P0, + ))); + + let mut events = Vec::new(); + resolve_ability_chain(runner.state_mut(), &ability, &mut events, 0) + .expect("effect payment pauses only for the replacement choice"); + // Positive reach guard: the DURABLE ledger really is live, and the root + // really is the effect-payment family whose resume is not inert. + assert!(matches!( + runner.state().pending_cost_move_resume.as_ref(), + Some(PendingCostMoveResume::ManaAbilityPayment { pending, cursor }) + if matches!(&pending.resume, ManaAbilityResume::EffectPayCost { payer: P0, .. }) + && !cursor.deferred_cost_events.is_empty() + )); + assert!( + runner.state().deferred_triggers.is_empty(), + "no context is materialized while the replacement choice is live" + ); + let life_before = runner.state().players[P0.0 as usize].life; + + let resumed = runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("the redirected source cost resumes the exact outer effect cost"); + + // The frame settled BEFORE the rider ran, so its batch is exactly OT. + assert!( + !matches!(resumed.waiting_for, WaitingFor::OrderTriggers { .. }), + "a one-member completed-frame batch needs no CR 603.3b ordering prompt, got {:?}", + resumed.waiting_for + ); + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before + 1, + "the rider ran exactly once, after the frame settled" + ); + let stacked: Vec = runner + .state() + .stack + .iter() + .filter_map(|entry| match entry.kind { + StackEntryKind::TriggeredAbility { .. } => Some(entry.source_id), + _ => None, + }) + .collect(); + assert!( + stacked.contains(&observer_t), + "OT was collected by the completed mana frame: {stacked:?}" + ); + assert!( + stacked.contains(&observer_l), + "and OL was collected for the rider's own event: {stacked:?}" + ); + assert_eq!( + stacked.len(), + 2, + "each observer is placed exactly once: {stacked:?}" + ); +} + #[test] fn effect_pay_cost_rider_waits_for_scry_post_effect_before_typed_root_settles() { let mut scenario = GameScenario::new(); @@ -4491,23 +4629,54 @@ fn paused_mana_cost_events_create_observer_triggers_once_and_preserve_order_resu .act(GameAction::ChooseReplacement { index: 0 }) .expect("resume the typed cost event settlement"); - assert!(matches!( - resumed.waiting_for, - WaitingFor::OrderTriggers { ref triggers, .. } if triggers.len() == 2 - )); + // A completed mana frame is a cost-payment micro-frame inside somebody + // else's action, not a CR 603.3b release boundary. Its ordinary observers + // are collected exactly once and deferred; ordering and announcement belong + // to the owner's own boundary, so the resumed action returns straight to the + // outer payment with the queue intact and nothing on the stack. + assert!( + matches!( + resumed.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + ), + "a completed mana micro-frame returns to its owner's payment, not to CR 603.3b ordering: \ + {:?}", + resumed.waiting_for + ); assert!(runner.state().pending_cost_move_resume.is_none()); - let ordered = runner - .act(GameAction::OrderTriggers { order: vec![0, 1] }) - .expect("both observer triggers remain orderable after the cost settles"); - assert!(matches!( - ordered.waiting_for, - WaitingFor::ManaPayment { player: P0, .. } - )); + assert!( + runner.state().stack.is_empty(), + "no observer may be announced from inside the payment that produced its events" + ); + let queued_amounts = |state: &GameState| -> Vec { + state + .deferred_triggers + .iter() + .map(|context| match &context.pending.ability.effect { + Effect::GainLife { + amount: QuantityExpr::Fixed { value }, + .. + } => *value, + other => panic!("unexpected deferred observer effect {other:?}"), + }) + .collect() + }; assert_eq!( - runner.state().stack.len(), - 2, - "each actual observer trigger is collected exactly once, not once per pause and resume" + queued_amounts(runner.state()), + vec![1, 2], + "each actual observer trigger is collected exactly once, in the collector's APNAP order, \ + not once per pause and resume" ); + + // The queue is durable across the returned prompt: it is engine state, not + // action-local coordination. + let json = serde_json::to_string(runner.state()) + .expect("the deferred observer release group serializes at the outer payment prompt"); + let across: GameState = serde_json::from_str(&json) + .expect("the deferred observer release group deserializes at the outer payment prompt"); + assert_eq!(queued_amounts(&across), vec![1, 2]); + assert!(across.stack.is_empty()); + assert_eq!( initial_events .iter() @@ -4636,76 +4805,2898 @@ fn nested_costed_mana_source_serializes_parent_cursor_and_finishes_outer_payment }) )); - let json = - serde_json::to_string(runner.state()).expect("the suspended parent mana cursor serializes"); + let json = + serde_json::to_string(runner.state()).expect("the suspended parent mana cursor serializes"); + assert!( + json.contains("Suspended"), + "the serialized parent frame must retain its typed re-entry ownership" + ); + let restored: GameState = + serde_json::from_str(&json).expect("the suspended parent mana cursor deserializes"); + let mut runner = GameRunner::from_state(restored); + let resumed = runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("redirect inner self-exile and resume the exact parent cursor"); + + assert_eq!(runner.state().objects[&inner].zone, Zone::Graveyard); + assert!(runner.state().objects[&outer].tapped); + assert!(matches!( + resumed.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + )); + assert_eq!( + initial_events + .iter() + .chain(resumed.events.iter()) + .filter(|event| matches!(event, GameEvent::PermanentTapped { object_id, .. } if *object_id == outer)) + .count(), + 1, + "the outer tap prefix is retained by the parent cursor rather than replayed" + ); + assert_eq!( + initial_events + .iter() + .chain(resumed.events.iter()) + .filter(|event| matches!(event, GameEvent::PermanentTapped { object_id, .. } if *object_id == inner)) + .count(), + 1, + "the inner source's tap cost is paid once across the replacement pause" + ); + assert_eq!( + initial_events + .iter() + .chain(resumed.events.iter()) + .filter(|event| matches!( + event, + GameEvent::ZoneChanged { + object_id, + from: Some(Zone::Battlefield), + to: Zone::Graveyard, + .. + } if *object_id == inner + )) + .count(), + 1, + "the redirected inner self-exile cost is delivered once" + ); + for source_id in [inner, outer] { + assert_eq!( + initial_events + .iter() + .chain(resumed.events.iter()) + .filter(|event| matches!(event, GameEvent::ManaAdded { source_id: id, .. } if *id == source_id)) + .count(), + 1, + "each nested mana ability produces exactly once" + ); + } + + runner + .act(GameAction::PassPriority) + .expect("the outer spell payment consumes the outer mana once"); + assert_eq!(runner.state().objects[&spell].zone, Zone::Stack); +} + +/// Which permanent the ordinary observer of the A/B/C/D topology watches. +/// +/// This is the ONLY axis the sibling row changes, which is what makes it the +/// discriminator for the ancestor-prefix finding: observer E fires on A's tap, +/// which lives in the *suspended parent's* prefix, not in the paused child's +/// local ledger. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HostileObserverAxis { + /// Observer D: source-filtered `Taps` on the paused child B. + TapsB, + /// Observer E: source-filtered `Taps` on the suspended parent A. + TapsA, +} + +struct HostileAbcdFixture { + scenario: GameScenario, + source_a: ObjectId, + source_b: ObjectId, + source_c: ObjectId, + observer: ObjectId, + observer_gain: i32, +} + +/// The plan's four-permanent hostile topology, shared byte-for-byte by the +/// direct-Priority row, its observer-axis sibling, and both masked-root +/// controls. Only `axis` differs between them. +fn hostile_abcd_fixture(axis: HostileObserverAxis) -> HostileAbcdFixture { + fn targetless_reflexive_gain(amount: i32) -> AbilityDefinition { + let mut reflexive = AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: amount }, + player: TargetFilter::Controller, + }, + ); + reflexive.condition = Some(engine::types::ability::AbilityCondition::WhenYouDo); + reflexive + } + + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + // A — noncreature ARTIFACT so no summoning sickness and no creature-tier + // ambiguity in source selection. `{T}, {2}: Add {G}`, plus a targetless true + // `WhenYouDo` rider gaining 5. + let source_a = scenario + .add_creature(P0, "A Root Green Source", 1, 1) + .as_artifact() + .with_ability_definition( + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced: ManaProduction::Fixed { + colors: vec![ManaColor::Green], + contribution: ManaContribution::Base, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(AbilityCost::Composite { + costs: vec![ + AbilityCost::Tap, + AbilityCost::Mana { + cost: ManaCost::generic(2), + }, + ], + }) + .sub_ability(targetless_reflexive_gain(5)), + ) + .id(); + + // B — noncreature LAND. Its land card tier deterministically precedes C's + // artifact tier, and its reflexive continuation classifies it + // `HasIrreversibleContinuation`; both penalties stay in tier zero. No + // auto-tap ordering code is touched — the fixture must EARN B-before-C by + // reaching the pause, and the assertions in each row are what prove it did. + let source_b = scenario + .add_creature(P0, "B Paused Reflexive Land", 1, 1) + .as_land() + .with_ability_definition( + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced: ManaProduction::Colorless { + count: QuantityExpr::Fixed { value: 1 }, + }, + restrictions: vec![ManaSpendRestriction::ActivateOnly], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(AbilityCost::Composite { + costs: vec![ + AbilityCost::Tap, + AbilityCost::Exile { + count: 1, + zone: None, + filter: Some(TargetFilter::SelfRef), + }, + ], + }) + .sub_ability(targetless_reflexive_gain(3)), + ) + .id(); + + // C — noncreature artifact, `{T}: Add {C}`, no continuation. + let source_c = scenario + .add_creature(P0, "C Synchronous Source", 1, 1) + .as_artifact() + .with_ability_definition( + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced: ManaProduction::Colorless { + count: QuantityExpr::Fixed { value: 1 }, + }, + restrictions: vec![ManaSpendRestriction::ActivateOnly], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(AbilityCost::Tap), + ) + .id(); + + // Two COMPETING redirects for B's self-exile, so the exile raises a real + // `ReplacementChoice` rather than applying silently. + for name in ["B Redirect One", "B Redirect Two"] { + scenario + .add_creature(P0, name, 0, 0) + .as_enchantment() + .with_replacement_definition(redirect_moved_to(Zone::Exile, Zone::Graveyard)); + } + + let (observer_name, watched, observer_gain) = match axis { + HostileObserverAxis::TapsB => ("D B-Tap Observer", source_b, 2), + HostileObserverAxis::TapsA => ("E A-Tap Observer", source_a, 4), + }; + let observer = scenario + .add_creature(P0, observer_name, 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::Taps) + .execute(AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { + value: observer_gain, + }, + player: TargetFilter::Controller, + }, + )) + .valid_card(TargetFilter::SpecificObject { id: watched }) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id(); + + HostileAbcdFixture { + scenario, + source_a, + source_b, + source_c, + observer, + observer_gain, + } +} + +/// CR 605.3a-c + CR 603.12 + CR 601.2h — the plan's **direct-priority A/B/C/D +/// hostile regression**, replacing the blocked `EndContinuousEffect` row. +/// +/// `EndContinuousEffect` cannot host this proof and was not weakened to match +/// the trace it actually produces: `pay_non_cast_mana_cost`'s automatic planner +/// pre-funds a costed source LEAF-FIRST, so B is parentless and C is already +/// committed before A starts, and no live suspended parent with a LATER +/// synchronous child is ever formed. `game/casting_costs.rs` and the +/// special-action protocol are out of scope, so the root moves instead. +/// +/// The reachable root is a real player action: `ActivateAbility` at +/// `WaitingFor::Priority`. A becomes a genuine parentless cursor with no +/// `PendingCast`, no resolution stack entry, and no unless-payment owner. Its +/// `{2}` Mana component asks automatic payment, which selects the LAND B before +/// the artifact C; B's `{T}, Exile this` hits two competing exile replacements +/// and serializes the whole nested cursor tree with A recursively `Suspended`. +/// +/// What this row discriminates that a ledger-shape unit test cannot: the +/// **prepared parent snapshot's wiring**. At the pause, A's suspended parent +/// ledger must hold exactly A's own tap and no B tap, and B's frame-local +/// ledger exactly B's tap and no A tap. Replacing +/// `parent_snapshot_with_current_cost_events(..)` at the +/// `resolve_mana_ability_excluding` call site with a plain `parent.cloned()` +/// leaves A's parent prefix EMPTY and fails assertion (7); restoring the +/// pre-fix clone-and-scan child ledger puts A's tap into B's ledger and fails +/// assertion (6). +/// +/// SCOPE: this row lands the plan's assertions 1-16 for the DIRECT-Priority +/// root. Its observer-E sibling +/// (`direct_priority_mana_root_orders_the_ancestor_prefix_observer_with_both_reflexives`) +/// and the masked cast-root control +/// (`masked_cast_root_mana_batch_stays_queued_until_the_spell_is_announced`) +/// carry the rest of this topology's plan requirements. The second masked +/// control — an `UnlessPayment` resolution owner over the same A/B/C/D fixture — +/// is `masked_unless_payment_root_mana_batch_stays_queued_until_the_owner_settles`. +#[test] +fn direct_priority_mana_root_suspends_its_parent_and_keeps_ledgers_disjoint() { + let HostileAbcdFixture { + scenario, + source_a, + source_b, + source_c, + observer, + observer_gain: _, + } = hostile_abcd_fixture(HostileObserverAxis::TapsB); + let observer_d = observer; + let mut runner = scenario.build(); + + // (1) Reach guards for the ROOT itself: a real empty-stack Priority with no + // cast, resolution, or payment owner. Without these the row could pass from + // some other production chronology. + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { player } if player == P0 + )); + assert!(runner.state().stack.is_empty()); + assert!(runner.state().pending_cast.is_none()); + assert!(runner.state().pending_cost_move_resume.is_none()); + let life_before = runner.state().players[P0.0 as usize].life; + assert_eq!(runner.state().players[P0.0 as usize].mana_pool.total(), 0); + + // (2) The production root action. + let paused = runner + .act(GameAction::ActivateAbility { + source_id: source_a, + ability_index: 0, + }) + .expect("A's {2} Mana component pays through B and pauses on its exile replacement"); + + // (3) A real replacement pause, not a synthesized one. + assert!( + matches!(paused.waiting_for, WaitingFor::ReplacementChoice { .. }), + "expected B's exile ReplacementChoice, got {:?}", + paused.waiting_for + ); + + // (4)-(7) The nested cursor tree, its owner, and the two DISJOINT ledgers. + assert_nested_hostile_pause(runner.state(), source_a, source_b); + + // (5) Tapped-state reach guards for the intended chronology: A and B are + // paid, C has not been reached yet, and B is still on the battlefield + // pending its replacement choice. + assert!(runner.state().objects[&source_a].tapped, "A paid its tap"); + assert!(runner.state().objects[&source_b].tapped, "B paid its tap"); + assert!( + !runner.state().objects[&source_c].tapped, + "C must be untapped at the pause — a tapped C means the leaf-first \ + planner chronology, not the nested-parent one this row exists to prove" + ); + assert_eq!(runner.state().objects[&source_b].zone, Zone::Battlefield); + + // (11) Nothing has resolved yet. + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + + // (6) The mandatory durable branch. `current_action_event_start` is + // `#[serde(skip)]`, so A's one-event parent prefix must survive on the + // serialized ledger itself, not on the marker. + let json = serde_json::to_string(runner.state()).expect("paused cursor tree serializes"); + let restored: GameState = serde_json::from_str(&json).expect("paused cursor tree restores"); + let mut runner = GameRunner::from_state(restored); + assert!(matches!( + runner.state().waiting_for, + WaitingFor::ReplacementChoice { .. } + )); + assert_nested_hostile_pause(runner.state(), source_a, source_b); + assert!(runner.state().objects[&source_a].tapped); + assert!(runner.state().objects[&source_b].tapped); + assert!(!runner.state().objects[&source_c].tapped); + + // (7) Resume through the normal action. + let resumed = runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("the replacement choice resumes B, then A's later synchronous C"); + + // (8) B moved exactly once, to the redirected zone. + assert_eq!(runner.state().objects[&source_b].zone, Zone::Graveyard); + assert_eq!( + resumed + .events + .iter() + .filter(|event| matches!( + event, + GameEvent::ZoneChanged { object_id, .. } if *object_id == source_b + )) + .count(), + 1, + "no duplicate exile or move delivery across the pause/resume boundary" + ); + + // (9) C then taps exactly once, and A produces its green mana. B's and C's + // colorless mana was spent on A's {2}; only A's green remains. + assert!( + runner.state().objects[&source_c].tapped, + "A's remaining generic mana must come from the LATER synchronous child C" + ); + let pool = &runner.state().players[P0.0 as usize].mana_pool; + assert_eq!( + pool.total(), + 1, + "only A's own production remains in the pool" + ); + assert_eq!(pool.count_color(ManaType::Green), 1); + + // (10) Every paid cost and every production happened exactly once across + // the pause/resume boundary. + let all_events: Vec<&GameEvent> = paused.events.iter().chain(resumed.events.iter()).collect(); + for (label, id) in [("A", source_a), ("B", source_b), ("C", source_c)] { + assert_eq!( + all_events + .iter() + .filter(|event| matches!( + event, + GameEvent::PermanentTapped { object_id, .. } if *object_id == id + )) + .count(), + 1, + "{label} paid its tap cost exactly once" + ); + assert_eq!( + all_events + .iter() + .filter(|event| matches!( + event, + GameEvent::ManaAdded { source_id, .. } if *source_id == id + )) + .count(), + 1, + "{label} produced mana exactly once" + ); + } + + // (11, after resume) Still nothing resolved: neither reflexive nor D. + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before, + "no reflexive rider and no observer may resolve before the batch is ordered" + ); + // (12) ONE empty-stack `OrderTriggers` group holding exactly three members: + // B's reflexive rider, D's B-tap observer, and A's reflexive rider — the + // last of which only exists after C completed and A produced its mana. + // + // (13) This exact three-member, empty-stack shape is the joint revert + // discriminator. Under the old inherited-ledger behaviour C scans A's cloned + // ancestor batch before A finishes, exposing an early incomplete or + // duplicated group; under a split collection seam D is already a stack entry + // and only the two reflexives remain to order. Neither revert can produce + // one empty-stack order prompt containing all three. + let WaitingFor::OrderTriggers { + player: order_player, + triggers: ref group, + } = resumed.waiting_for + else { + panic!( + "the completed root must expose one CR 603.3b ordering group, got {:?}", + resumed.waiting_for + ); + }; + assert_eq!(order_player, P0); + assert!( + runner.state().stack.is_empty(), + "no member of the batch may be dispatched separately: the stack must still \ + be empty when the single ordering group is offered" + ); + let members: Vec = group.iter().map(|summary| summary.source_id).collect(); + assert_eq!( + members.len(), + 3, + "expected exactly B's reflexive, D's observer, and A's reflexive; got {members:?}" + ); + for (label, id) in [("A", source_a), ("B", source_b), ("D", observer_d)] { + assert_eq!( + members.iter().filter(|member| **member == id).count(), + 1, + "{label} must appear in the single ordering group exactly once: {members:?}" + ); + } + + // (14) Order the group through the real action and identify the resulting + // stack entries by source. + let ordered = runner + .act(GameAction::OrderTriggers { + order: vec![0, 1, 2], + }) + .expect("the three-member group orders through the real CR 603.3b action"); + assert!( + matches!(ordered.waiting_for, WaitingFor::Priority { player } if player == P0), + "ordering the whole group returns priority, got {:?}", + ordered.waiting_for + ); + let announced: Vec = runner + .state() + .stack + .iter() + .filter_map(|entry| match entry.kind { + StackEntryKind::TriggeredAbility { source_id, .. } => Some(source_id), + _ => None, + }) + .collect(); + assert_eq!( + announced.len(), + 3, + "exactly three triggered entries, no duplicates and no preexisting D entry: {announced:?}" + ); + for (label, id) in [("A", source_a), ("B", source_b), ("D", observer_d)] { + assert_eq!( + announced.iter().filter(|entry| **entry == id).count(), + 1, + "{label} announced exactly once: {announced:?}" + ); + } + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before, + "announcement alone resolves nothing" + ); + assert!(runner.state().deferred_triggers.is_empty()); + + // (15) Clone at the post-order point for two normal-action branches. + let post_order = runner.state().clone(); + + // (15a) Resolve all three: 5 + 3 + 2, each exactly once. + let mut resolve_all = GameRunner::from_state(post_order.clone()); + for _ in 0..3 { + resolve_all + .act(GameAction::PassPriority) + .expect("P0 passes priority to resolve the next triggered ability"); + resolve_all + .act(GameAction::PassPriority) + .expect("P1 passes priority to resolve the next triggered ability"); + } + assert!( + resolve_all.state().stack.is_empty(), + "all three triggered abilities resolve through the normal priority path" + ); + assert_eq!( + resolve_all.state().players[P0.0 as usize].life, + life_before + 10, + "the 5, 3 and 2 life effects each occur exactly once" + ); + + // (15b) Counter one identified reflexive through the normal stack path and + // resolve the other two: the countered effect must not occur while the other + // two occur exactly once. + let mut countered = GameRunner::from_state(post_order); + let top = countered + .state() + .stack + .last() + .expect("the ordered group left three entries on the stack"); + let countered_source = match top.kind { + StackEntryKind::TriggeredAbility { source_id, .. } => source_id, + ref other => panic!("unexpected top-of-stack entry {other:?}"), + }; + let countered_gain = if countered_source == source_a { + 5 + } else if countered_source == source_b { + 3 + } else { + 2 + }; + let top_id = top.id; + let counter_ability = ResolvedAbility::new( + Effect::Counter { + target: TargetFilter::StackAbility { + controller: None, + tag: None, + kind: None, + }, + source_rider: None, + countered_spell_zone: None, + }, + vec![TargetRef::Object(top_id)], + observer_d, + P0, + ); + engine::game::effects::counter::resolve( + countered.state_mut(), + &counter_ability, + &mut Vec::new(), + ) + .expect("the identified entry is countered through the production counter resolver"); + assert_eq!( + countered + .state() + .stack + .iter() + .filter(|entry| matches!( + entry.kind, + StackEntryKind::TriggeredAbility { source_id, .. } if source_id == countered_source + )) + .count(), + 0, + "the countered entry leaves the stack" + ); + for _ in 0..2 { + countered + .act(GameAction::PassPriority) + .expect("P0 passes priority to resolve a surviving triggered ability"); + countered + .act(GameAction::PassPriority) + .expect("P1 passes priority to resolve a surviving triggered ability"); + } + assert!(countered.state().stack.is_empty()); + assert_eq!( + countered.state().players[P0.0 as usize].life, + life_before + 10 - countered_gain, + "the countered reflexive's effect does not occur while the other two occur exactly once" + ); +} + +/// CR 603.3b + CR 605.3a — the plan's **observer-axis sibling** of the hostile +/// direct-Priority row, and the positive-and-revert discriminator for the +/// ancestor-prefix finding. +/// +/// It changes exactly one thing: the ordinary observer watches A's tap instead +/// of B's. A's tap lives in the SUSPENDED PARENT's retained prefix, not in the +/// paused child B's frame-local ledger, so the completed root can only collect +/// observer E if the parent-snapshot suffix augmentation actually preserved +/// A's first-action tap across the pause. Reverting that augmentation leaves no +/// collector holding A's tap: E never fires, the single ordering group has only +/// two members, and E's distinguishable +4 never occurs. A ledger-shape-only +/// unit test cannot close this — the shapes look identical until a real +/// observer has to match out of them. +#[test] +fn direct_priority_mana_root_orders_the_ancestor_prefix_observer_with_both_reflexives() { + let HostileAbcdFixture { + scenario, + source_a, + source_b, + source_c, + observer: observer_e, + observer_gain, + } = hostile_abcd_fixture(HostileObserverAxis::TapsA); + let mut runner = scenario.build(); + + assert!(matches!( + runner.state().waiting_for, + WaitingFor::Priority { player } if player == P0 + )); + assert!(runner.state().stack.is_empty()); + assert!(runner.state().pending_cast.is_none()); + let life_before = runner.state().players[P0.0 as usize].life; + + let paused = runner + .act(GameAction::ActivateAbility { + source_id: source_a, + ability_index: 0, + }) + .expect("A's {2} Mana component pays through B and pauses on its exile replacement"); + assert!( + matches!(paused.waiting_for, WaitingFor::ReplacementChoice { .. }), + "expected B's exile ReplacementChoice, got {:?}", + paused.waiting_for + ); + // The identical chronology reach guards: same nested cursor tree, same two + // DISJOINT ledgers, same untapped C. + assert_nested_hostile_pause(runner.state(), source_a, source_b); + assert!(runner.state().objects[&source_a].tapped); + assert!(runner.state().objects[&source_b].tapped); + assert!(!runner.state().objects[&source_c].tapped); + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + + let json = serde_json::to_string(runner.state()).expect("paused cursor tree serializes"); + let restored: GameState = serde_json::from_str(&json).expect("paused cursor tree restores"); + let mut runner = GameRunner::from_state(restored); + assert_nested_hostile_pause(runner.state(), source_a, source_b); + assert!(!runner.state().objects[&source_c].tapped); + + let resumed = runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("the replacement choice resumes B, then A's later synchronous C"); + assert!(runner.state().objects[&source_c].tapped); + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + + // ONE empty-stack ordering group holding exactly B's reflexive, E's A-tap + // observer, and A's reflexive. + let WaitingFor::OrderTriggers { + player: order_player, + triggers: ref group, + } = resumed.waiting_for + else { + panic!( + "the completed root must expose one CR 603.3b ordering group, got {:?}", + resumed.waiting_for + ); + }; + assert_eq!(order_player, P0); + assert!( + runner.state().stack.is_empty(), + "no member of the batch may be dispatched separately" + ); + let members: Vec = group.iter().map(|summary| summary.source_id).collect(); + assert_eq!( + members.len(), + 3, + "expected exactly B's reflexive, E's A-tap observer, and A's reflexive; got {members:?}" + ); + for (label, id) in [("A", source_a), ("B", source_b), ("E", observer_e)] { + assert_eq!( + members.iter().filter(|member| **member == id).count(), + 1, + "{label} must appear in the single ordering group exactly once: {members:?}" + ); + } + + runner + .act(GameAction::OrderTriggers { + order: vec![0, 1, 2], + }) + .expect("the three-member group orders through the real CR 603.3b action"); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })) + .count(), + 3 + ); + for _ in 0..3 { + runner.act(GameAction::PassPriority).expect("P0 passes"); + runner.act(GameAction::PassPriority).expect("P1 passes"); + } + assert!(runner.state().stack.is_empty()); + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before + 5 + 3 + observer_gain, + "E's distinct effect occurs exactly once together with the two reflexive effects" + ); +} + +/// CR 601.2h + CR 603.3b — the plan's **masked cast-root control** for the same +/// A/B/C/D topology. +/// +/// A completed mana frame is a cost-payment micro-frame inside somebody else's +/// action, so it may not release its own batch. With a real `PendingCast` and a +/// live `WaitingFor::ManaPayment` owner, the whole three-member batch must stay +/// in `state.deferred_triggers` — no ordering prompt, no triggered stack entry, +/// and specifically no observer-D entry — until the spell is actually announced. +/// Only the cast finalizer may expose the single ordering group, and it must +/// expose exactly those three above the already-announced spell. +/// +/// The `deferred_triggers`-size assertion alone does not close the unified-seam +/// finding; the explicit "no D on the stack" assertion is the one that fails +/// under a separate fresh dispatch of the ordinary half. +#[test] +fn masked_cast_root_mana_batch_stays_queued_until_the_spell_is_announced() { + let HostileAbcdFixture { + mut scenario, + source_a, + source_b, + source_c, + observer: observer_d, + observer_gain, + } = hostile_abcd_fixture(HostileObserverAxis::TapsB); + let spell = scenario + .add_spell_to_hand(P0, "Masked Cast Root Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::Green], + generic: 0, + }) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + let cast = runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Manual, + }) + .expect("the masked owner announces a real pending cast in manual payment mode"); + assert!( + matches!(cast.waiting_for, WaitingFor::ManaPayment { player: P0, .. }), + "expected a live ManaPayment owner, got {:?}", + cast.waiting_for + ); + assert!(runner.state().pending_cast.is_some()); + + let paused = runner + .act(GameAction::ActivateAbility { + source_id: source_a, + ability_index: 0, + }) + .expect("A is manually activated during the cast's mana payment"); + assert!( + matches!(paused.waiting_for, WaitingFor::ReplacementChoice { .. }), + "expected the same nested B pause under the masked owner, got {:?}", + paused.waiting_for + ); + assert_nested_hostile_pause_cursor_tree(runner.state(), source_a, source_b); + assert!( + !runner.state().objects[&source_c].tapped, + "C must be untapped at the pause under the masked owner too" + ); + + let resumed = runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("the replacement choice resumes B, then A's later synchronous C"); + assert!(runner.state().objects[&source_c].tapped); + + // The masked contract: the owner is still live, and the WHOLE batch is + // queued rather than released. + assert!( + matches!( + resumed.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + ), + "the cast owner must retain the action, got {:?}", + resumed.waiting_for + ); + assert!( + !matches!(resumed.waiting_for, WaitingFor::OrderTriggers { .. }), + "a masked mana frame may not open CR 603.3b ordering" + ); + assert!( + !runner + .state() + .stack + .iter() + .any(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })), + "no member of the batch — and specifically not observer D — may reach the stack \ + while the cast owner is live" + ); + let queued: Vec = runner + .state() + .deferred_triggers + .iter() + .map(|context| context.pending.source_id) + .collect(); + assert_eq!( + queued.len(), + 3, + "one deferred queue holding exactly B's reflexive, D's observer, and A's reflexive: \ + {queued:?}" + ); + for (label, id) in [("A", source_a), ("B", source_b), ("D", observer_d)] { + assert_eq!( + queued.iter().filter(|member| **member == id).count(), + 1, + "{label} is queued exactly once: {queued:?}" + ); + } + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + + // Complete the payment through the real action protocol. Only the cast + // finalizer may release the group. + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1, + "A's green mana is available to pay the spell's single green pip" + ); + let finalized = runner.act(GameAction::PassPriority).expect( + "the manual payment finalizes from the available green mana and announces the spell", + ); + + assert!( + runner + .state() + .stack + .iter() + .any(|entry| matches!(entry.kind, StackEntryKind::Spell { .. })), + "the spell is announced exactly once before the batch is released" + ); + let WaitingFor::OrderTriggers { + triggers: ref group, + .. + } = finalized.waiting_for + else { + panic!( + "the cast finalizer must expose the single ordering group, got {:?}", + finalized.waiting_for + ); + }; + let members: Vec = group.iter().map(|summary| summary.source_id).collect(); + assert_eq!(members.len(), 3, "{members:?}"); + for (label, id) in [("A", source_a), ("B", source_b), ("D", observer_d)] { + assert_eq!( + members.iter().filter(|member| **member == id).count(), + 1, + "{label} appears in the released group exactly once: {members:?}" + ); + } + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })) + .count(), + 0, + "no trigger entry may be on the stack before the group is ordered" + ); + + runner + .act(GameAction::OrderTriggers { + order: vec![0, 1, 2], + }) + .expect("the released group orders above the announced spell"); + let stack_kinds: Vec = runner + .state() + .stack + .iter() + .map(|entry| matches!(entry.kind, StackEntryKind::Spell { .. })) + .collect(); + assert_eq!(stack_kinds.iter().filter(|is_spell| **is_spell).count(), 1); + assert!( + stack_kinds[0], + "CR 603.3 places the newly ordered triggers ABOVE the spell that was already announced" + ); + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + for _ in 0..3 { + runner.act(GameAction::PassPriority).expect("P0 passes"); + runner.act(GameAction::PassPriority).expect("P1 passes"); + } + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before + 5 + 3 + observer_gain, + "each masked batch member resolves exactly once after the owner released it" + ); +} + +/// CR 118.12 + CR 603.3b + CR 608.2 — the plan's **masked resolution-root +/// control** for the same A/B/C/D topology, and the sibling that session 7's +/// report recorded as the one remaining owed member of this family. +/// +/// The masked cast-root control proves a live `PendingCast` masks the batch. +/// This row proves the *other* masking owner the plan names: a real resolution +/// that has stopped at `WaitingFor::UnlessPayment`. There is no `PendingCast` +/// here at all — the owner is a live `resolution_stack` frame — so the two +/// controls cannot share a bug: a completed mana frame may not release its own +/// batch under EITHER owner, and only the owner's own settlement may. +/// +/// The `deferred_triggers`-size assertion alone does not close the unified-seam +/// finding; the explicit "no D on the stack" assertion is the one that fails +/// under a separate fresh dispatch of the ordinary half. The unpaid punishment +/// is -100 life, so a resolution that leaked past its unless-payment would be +/// impossible to confuse with the batch's own +1/+5/+3/+2. +/// +/// FIXTURE NOTE, deliberately recorded rather than absorbed: the punisher +/// carries an "if you do" rider, so the paid branch runs an ability chain and +/// the owner reaches its own post-action settlement, which is what releases the +/// group. A rider-FREE unless-payment currently has no settlement convergence +/// at all — `finish_successful_unless_payment` runs the pipeline only when it +/// resolved a sub-ability, so a bare paid cost lands on `Priority` with the +/// three contexts still queued. That gap is exactly what the plan's settled- +/// Priority convergence wrapper (`run_post_action_pipeline_from_settled_priority` +/// plus the `engine_payment_choices` readiness hook) closes, and it is still +/// owed; this row deliberately does not encode the gap as a contract. +#[test] +fn masked_unless_payment_root_mana_batch_stays_queued_until_the_owner_settles() { + let HostileAbcdFixture { + mut scenario, + source_a, + source_b, + source_c, + observer: observer_d, + observer_gain, + } = hostile_abcd_fixture(HostileObserverAxis::TapsB); + + // "You lose 100 life unless you pay {1}. If you do, you gain 1 life." The + // punishment is unmissable if the owner ever leaked past its payment, and + // the "if you do" rider is what carries the resolution to its own + // completion — this row's release boundary. + let mut paid_rider = AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + ); + paid_rider.condition = Some(engine::types::ability::AbilityCondition::EffectOutcome { + signal: engine::types::ability::EffectOutcomeSignal::OptionalEffectPerformed, + }); + let mut punisher = AbilityDefinition::new( + AbilityKind::Spell, + Effect::LoseLife { + amount: QuantityExpr::Fixed { value: 100 }, + target: Some(TargetFilter::Controller), + }, + ) + .sub_ability(paid_rider); + punisher.unless_pay = Some(UnlessPayModifier { + cost: AbilityCost::Mana { + cost: ManaCost::generic(1), + }, + payer: TargetFilter::Controller, + }); + let spell = scenario + .add_spell_to_hand(P0, "Masked Resolution Root Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![], + generic: 0, + }) + .with_ability_definition(punisher) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Auto, + }) + .expect("the free witness spell is announced"); + runner.act(GameAction::PassPriority).expect("P0 passes"); + let owner = runner + .act(GameAction::PassPriority) + .expect("P1 passes and the witness resolves into its unless-payment"); + assert!( + matches!( + owner.waiting_for, + WaitingFor::UnlessPayment { player: P0, .. } + ), + "expected a live UnlessPayment resolution owner, got {:?}", + owner.waiting_for + ); + // Positive reach guard for the axis that distinguishes this control from the + // cast-root one: the masking owner here is a paused resolution, not a cast. + assert!( + runner.state().pending_cast.is_none(), + "no PendingCast may mask this batch — the owner is the resolution itself" + ); + let WaitingFor::UnlessPayment { + pending_effect: ref parked, + ref cost, + .. + } = owner.waiting_for + else { + unreachable!() + }; + assert_eq!( + parked.source_id, spell, + "the parked punisher is the witness spell's own resolution" + ); + assert_eq!( + cost, + &AbilityCost::Mana { + cost: ManaCost::generic(1) + } + ); + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + + let paused = runner + .act(GameAction::ActivateAbility { + source_id: source_a, + ability_index: 0, + }) + .expect("A is manually activated during the unless payment (CR 118.12)"); + assert!( + matches!(paused.waiting_for, WaitingFor::ReplacementChoice { .. }), + "expected the same nested B pause under the resolution owner, got {:?}", + paused.waiting_for + ); + assert_nested_hostile_pause_cursor_tree(runner.state(), source_a, source_b); + assert!( + !runner.state().objects[&source_c].tapped, + "C must be untapped at the pause under the resolution owner too" + ); + + let resumed = runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("the replacement choice resumes B, then A's later synchronous C"); + assert!(runner.state().objects[&source_c].tapped); + + // The masked contract: the resolution owner is still live, and the WHOLE + // batch is queued rather than released. + assert!( + matches!( + resumed.waiting_for, + WaitingFor::UnlessPayment { player: P0, .. } + ), + "the resolution owner must retain the action, got {:?}", + resumed.waiting_for + ); + assert!( + !matches!(resumed.waiting_for, WaitingFor::OrderTriggers { .. }), + "a masked mana frame may not open CR 603.3b ordering" + ); + assert!( + !runner + .state() + .stack + .iter() + .any(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })), + "no member of the batch — and specifically not observer D — may reach the stack \ + while the resolution owner is live" + ); + let queued: Vec = runner + .state() + .deferred_triggers + .iter() + .map(|context| context.pending.source_id) + .collect(); + assert_eq!( + queued.len(), + 3, + "one deferred queue holding exactly B's reflexive, D's observer, and A's reflexive: \ + {queued:?}" + ); + for (label, id) in [("A", source_a), ("B", source_b), ("D", observer_d)] { + assert_eq!( + queued.iter().filter(|member| **member == id).count(), + 1, + "{label} is queued exactly once: {queued:?}" + ); + } + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1, + "A's green mana is available to pay the unless cost" + ); + + // Only the resolution owner's own settlement may release the group. + let settled = runner + .act(GameAction::PayUnlessCost { pay: true }) + .expect("the unless cost is paid from A's green mana and the punisher is prevented"); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 0, + "the green pip really paid the unless cost" + ); + let WaitingFor::OrderTriggers { + triggers: ref group, + .. + } = settled.waiting_for + else { + panic!( + "the settled resolution owner must expose the single ordering group, got {:?}", + settled.waiting_for + ); + }; + let members: Vec = group.iter().map(|summary| summary.source_id).collect(); + assert_eq!(members.len(), 3, "{members:?}"); + for (label, id) in [("A", source_a), ("B", source_b), ("D", observer_d)] { + assert_eq!( + members.iter().filter(|member| **member == id).count(), + 1, + "{label} appears in the released group exactly once: {members:?}" + ); + } + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })) + .count(), + 0, + "no trigger entry may be on the stack before the group is ordered" + ); + // The paid branch already ran to completion: the +1 "if you do" rider is the + // owner's own last instruction and it happened BEFORE the release, while the + // -100 punishment never did. + assert_eq!(runner.state().players[P0.0 as usize].life, life_before + 1); + + runner + .act(GameAction::OrderTriggers { + order: vec![0, 1, 2], + }) + .expect("the released group orders once the resolution owner has settled"); + for _ in 0..3 { + runner.act(GameAction::PassPriority).expect("P0 passes"); + runner.act(GameAction::PassPriority).expect("P1 passes"); + } + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before + 1 + 5 + 3 + observer_gain, + "each masked batch member resolves exactly once, and the paid-for punisher never does" + ); +} + +/// The plan's assertions (4)-(7) at the hostile pause, factored so the +/// in-memory and the restored-from-JSON branches assert byte-identical +/// properties. The two ledger assertions are the discriminating pair: they fail +/// in OPPOSITE directions for the two reverts this row exists to catch. +fn assert_nested_hostile_pause_cursor_tree( + state: &GameState, + source_a: ObjectId, + source_b: ObjectId, +) { + let Some(PendingCostMoveResume::ManaAbilityPayment { pending, cursor }) = + state.pending_cost_move_resume.as_ref() + else { + panic!( + "expected a nested ManaAbilityPayment owner, got {:?}", + state.pending_cost_move_resume + ); + }; + assert_eq!(pending.source_id, source_b, "the paused child is B"); + + let parent = cursor + .parent + .as_ref() + .expect("B's cursor must carry the nested A parent"); + assert_eq!( + parent.lifecycle, + ManaAbilityCostParentLifecycle::Suspended, + "pausing B recursively suspends A" + ); + assert_eq!(parent.pending.source_id, source_a, "the parent is A"); + assert!( + parent.cursor.remaining.iter().any(|cost| matches!( + cost, + AbilityCost::Mana { + cost: ManaCost::Cost { generic: 2, .. } + } + )), + "A still owes its {{2}} Mana component, so a LATER synchronous child follows: {:?}", + parent.cursor.remaining + ); + + let tap_of = |events: &[GameEvent], id: ObjectId| { + events + .iter() + .filter(|event| { + matches!( + event, + GameEvent::PermanentTapped { object_id, .. } if *object_id == id + ) + }) + .count() + }; + // (6) B's FRAME-LOCAL ledger: exactly B's own tap, and no ancestor data. + // Restoring the pre-fix clone-and-scan child ledger puts A's tap here too. + assert_eq!( + tap_of(&cursor.deferred_cost_events, source_b), + 1, + "B's local ledger owns exactly its own tap: {:?}", + cursor.deferred_cost_events + ); + assert_eq!( + tap_of(&cursor.deferred_cost_events, source_a), + 0, + "a child may never scan its ancestor's events: {:?}", + cursor.deferred_cost_events + ); + // (7) A's PREPARED parent snapshot: exactly A's pre-child suffix. Reverting + // the snapshot augmentation at the `resolve_mana_ability_excluding` call + // site leaves this empty. + assert_eq!( + tap_of(&parent.cursor.deferred_cost_events, source_a), + 1, + "the prepared synchronous parent captured A's pre-child suffix: {:?}", + parent.cursor.deferred_cost_events + ); + assert_eq!( + tap_of(&parent.cursor.deferred_cost_events, source_b), + 0, + "and never the child's own events: {:?}", + parent.cursor.deferred_cost_events + ); +} + +/// The direct-Priority root additionally owns the plan's "no competing owner" +/// half of assertion (4). The masked-root controls deliberately do NOT assert +/// it: a live `PendingCast` or resolution owner is exactly what they exist to +/// mask the batch behind. +fn assert_nested_hostile_pause(state: &GameState, source_a: ObjectId, source_b: ObjectId) { + assert_nested_hostile_pause_cursor_tree(state, source_a, source_b); + assert!(state.pending_cast.is_none()); + assert!(state.stack.is_empty()); + assert!(state.resolution_stack.is_empty()); + assert!(!matches!( + state.waiting_for, + WaitingFor::OrderTriggers { .. } + )); +} + +// --------------------------------------------------------------------------- +// Round-9 finding 2: the required NO-PAUSE matrix. One synchronous fixture, all +// three real roots, both colour halves — six action rows through the single +// typed completed-mana-frame seam, plus the `TapLandForMana` regression that +// exercises the one changed match arm none of the six reach. +// --------------------------------------------------------------------------- + +#[derive(Clone, Copy, PartialEq, Eq)] +enum NoPauseColorAxis { + /// N produces `{G}` outright: the activation completes in one action. + Fixed, + /// N produces one mana of any colour: the activation returns + /// `WaitingFor::ChooseManaColor` first, and the choice action completes it. + AnyOneColor, +} + +struct NoPauseFixture { + scenario: GameScenario, + source_n: ObjectId, + observer_o: ObjectId, +} + +/// The plan's synchronous no-pause fixture: source N `{T}: Add {G}` with a +/// targetless true `WhenYouDo` rider gaining 5, and a source-filtered ordinary +/// `Taps` observer O of N gaining 2. No replacement, no deferred life cost, no +/// other pause is reachable, so N's durable cost ledger is provably empty and +/// every row exercises the EMPTY-LEDGER half of the completed-frame seam. +fn no_pause_mana_fixture(axis: NoPauseColorAxis) -> NoPauseFixture { + let mut reflexive = AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 5 }, + player: TargetFilter::Controller, + }, + ); + reflexive.condition = Some(engine::types::ability::AbilityCondition::WhenYouDo); + + let produced = match axis { + NoPauseColorAxis::Fixed => ManaProduction::Fixed { + colors: vec![ManaColor::Green], + contribution: ManaContribution::Base, + }, + NoPauseColorAxis::AnyOneColor => ManaProduction::AnyOneColor { + count: QuantityExpr::Fixed { value: 1 }, + color_options: vec![ManaColor::Green, ManaColor::Blue], + contribution: ManaContribution::Base, + }, + }; + + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let source_n = scenario + .add_creature(P0, "N Synchronous Green Source", 1, 1) + .as_artifact() + .with_ability_definition( + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(AbilityCost::Tap) + .sub_ability(reflexive), + ) + .id(); + let observer_o = scenario + .add_creature(P0, "O N-Tap Observer", 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::Taps) + .execute(AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 2 }, + player: TargetFilter::Controller, + }, + )) + .valid_card(TargetFilter::SpecificObject { id: source_n }) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id(); + + NoPauseFixture { + scenario, + source_n, + observer_o, + } +} + +/// The plan's shared per-boundary assertions for the no-pause matrix: neither +/// member is separately pushed, the deferred queue holds exactly N's reflexive +/// and O once each, and no life has been gained yet. +fn assert_exactly_two_contexts_queued_and_unstacked( + state: &GameState, + source_n: ObjectId, + observer_o: ObjectId, + life_before: i32, +) { + assert!( + !state + .stack + .iter() + .any(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })), + "no member of the pair may be separately pushed before its owner releases it" + ); + let queued: Vec = state + .deferred_triggers + .iter() + .map(|context| context.pending.source_id) + .collect(); + assert_eq!( + queued.len(), + 2, + "exactly N's reflexive and O, once each: {queued:?}" + ); + for (label, id) in [("N", source_n), ("O", observer_o)] { + assert_eq!( + queued.iter().filter(|member| **member == id).count(), + 1, + "{label} is queued exactly once: {queued:?}" + ); + } + assert_eq!( + state.players[P0.0 as usize].life, life_before, + "no trigger may resolve before the release group is ordered" + ); +} + +/// The plan's terminal assertions: exactly one group of exactly the two +/// contexts, nothing on the stack before ordering, then the two distinguishable +/// effects exactly once each for +7. +fn assert_single_group_of_two_then_resolve_for_seven( + runner: &mut GameRunner, + group_wait: &WaitingFor, + source_n: ObjectId, + observer_o: ObjectId, + life_before: i32, +) { + let WaitingFor::OrderTriggers { + triggers: ref group, + .. + } = group_wait + else { + panic!("expected the single release group, got {group_wait:?}"); + }; + let members: Vec = group.iter().map(|summary| summary.source_id).collect(); + assert_eq!(members.len(), 2, "{members:?}"); + for (label, id) in [("N", source_n), ("O", observer_o)] { + assert_eq!( + members.iter().filter(|member| **member == id).count(), + 1, + "{label} appears in the released group exactly once: {members:?}" + ); + } + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })) + .count(), + 0, + "no trigger entry may be on the stack before the group is ordered" + ); + + runner + .act(GameAction::OrderTriggers { order: vec![0, 1] }) + .expect("the released group orders"); + for _ in 0..3 { + runner.act(GameAction::PassPriority).expect("P0 passes"); + runner.act(GameAction::PassPriority).expect("P1 passes"); + } + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before + 5 + 2, + "the distinguishable 5- and 2-life effects each happen exactly once" + ); +} + +/// Root 1 of the plan's no-pause matrix: a direct `ActivateAbility` from +/// `WaitingFor::Priority` with an EMPTY durable ledger. +/// +/// Baseline had no seam here at all: a `Priority` resume with no deferred cost +/// events fell straight through `finish_mana_ability_cost_payment` to the +/// generic post-action scan, which dispatched observer O on its own while N's +/// synthetic reflexive was materialized by the mana frame. Routing the empty +/// ledger through `collect_completed_mana_frame_events` makes the two one batch. +/// Restoring the `has_deferred_cost_events` gate splits them and the +/// exactly-two group assertion fails. +#[test] +fn direct_priority_no_pause_root_releases_its_reflexive_and_observer_as_one_group() { + let NoPauseFixture { + scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + assert!(runner.state().stack.is_empty()); + assert!(runner.state().pending_cast.is_none()); + + let acted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); + + // The durable ledger really was empty: nothing paused. + assert!( + runner.state().pending_cost_move_resume.is_none(), + "positive reach guard: the fixture is synchronous, so no cursor survives" + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1, + "N's mana is available immediately (CR 605.3b)" + ); + assert_eq!(runner.state().players[P0.0 as usize].life, life_before); + + // The direct root exposes ONE empty-stack ordering group after the action. + assert!( + runner + .state() + .stack + .iter() + .all(|entry| !matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })), + "CR 603.3b ordering happens before any entry is placed" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &acted.waiting_for, + source_n, + observer_o, + life_before, + ); +} + +/// Root 2 of the plan's no-pause matrix: manual activation during a real +/// spell's `WaitingFor::ManaPayment`. The owner retains the action and one +/// two-context queue until the spell is announced, and only the cast finalizer +/// exposes the group — above the announced spell. +#[test] +fn masked_cast_no_pause_root_queues_both_contexts_until_the_spell_is_announced() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let spell = scenario + .add_spell_to_hand(P0, "No-Pause Masked Cast Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::Green], + generic: 0, + }) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + let cast = runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Manual, + }) + .expect("the masked owner announces a real pending cast in manual payment mode"); + assert!(matches!( + cast.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + )); + + let activated = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is manually activated during the cast's mana payment"); + assert!( + matches!( + activated.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + ), + "the cast owner must retain the action, got {:?}", + activated.waiting_for + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1, + "N's mana is available immediately, before any trigger resolves" + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + source_n, + observer_o, + life_before, + ); + + let finalized = runner + .act(GameAction::PassPriority) + .expect("the manual payment finalizes and announces the spell"); + assert!( + runner + .state() + .stack + .iter() + .any(|entry| matches!(entry.kind, StackEntryKind::Spell { .. })), + "the spell is announced exactly once before the batch is released" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &finalized.waiting_for, + source_n, + observer_o, + life_before, + ); +} + +/// Root 3 of the plan's no-pause matrix: manual activation during a real +/// `UnlessPayment` resolution owner. Same fixture, same queue, and the group is +/// exposed only once that owner has settled. +#[test] +fn masked_resolution_no_pause_root_queues_both_contexts_until_the_owner_settles() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let spell = scenario + .add_spell_to_hand(P0, "No-Pause Masked Resolution Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![], + generic: 0, + }) + .with_ability_definition(unless_pay_one_punisher()) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Auto, + }) + .expect("the free witness spell is announced"); + runner.act(GameAction::PassPriority).expect("P0 passes"); + let owner = runner + .act(GameAction::PassPriority) + .expect("P1 passes and the witness resolves into its unless-payment"); + assert!( + matches!( + owner.waiting_for, + WaitingFor::UnlessPayment { player: P0, .. } + ), + "expected a live UnlessPayment resolution owner, got {:?}", + owner.waiting_for + ); + assert!(runner.state().pending_cast.is_none()); + + let activated = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is manually activated during the unless payment (CR 118.12)"); + assert!( + matches!( + activated.waiting_for, + WaitingFor::UnlessPayment { player: P0, .. } + ), + "the resolution owner must retain the action, got {:?}", + activated.waiting_for + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1 + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + source_n, + observer_o, + life_before, + ); + + let settled = runner + .act(GameAction::PayUnlessCost { pay: true }) + .expect("the unless cost is paid from N's green mana"); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 0, + "the green pip really paid the unless cost" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &settled.waiting_for, + source_n, + // The paid branch's +1 rider ran before the release, so the two batch + // members are measured against the post-rider baseline. + observer_o, + life_before + 1, + ); +} + +/// "You lose 100 life unless you pay {1}. If you do, you gain 1 life." — the +/// resolution-owner witness shared by the hostile masked control and the +/// no-pause resolution root. The rider is what carries the resolution to its +/// own completion, which is this family's release boundary; see +/// `masked_unless_payment_root_mana_batch_stays_queued_until_the_owner_settles` +/// for the recorded gap in the rider-free shape. +fn unless_pay_one_punisher() -> AbilityDefinition { + let mut paid_rider = AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + ); + paid_rider.condition = Some(engine::types::ability::AbilityCondition::EffectOutcome { + signal: engine::types::ability::EffectOutcomeSignal::OptionalEffectPerformed, + }); + let mut punisher = AbilityDefinition::new( + AbilityKind::Spell, + Effect::LoseLife { + amount: QuantityExpr::Fixed { value: 100 }, + target: Some(TargetFilter::Controller), + }, + ) + .sub_ability(paid_rider); + punisher.unless_pay = Some(UnlessPayModifier { + cost: AbilityCost::Mana { + cost: ManaCost::generic(1), + }, + payer: TargetFilter::Controller, + }); + punisher +} + +/// The plan's colour-half reach guard: the activation action returns +/// `ChooseManaColor` after collecting O but WITHOUT ordering or stacking it, and +/// the reflexive does not exist yet because no mana has been produced. +fn assert_color_prompt_holds_only_the_observer( + state: &GameState, + source_n: ObjectId, + observer_o: ObjectId, + life_before: i32, +) { + assert!( + !matches!(state.waiting_for, WaitingFor::OrderTriggers { .. }), + "the colour prompt may not be preceded by a CR 603.3b ordering pass" + ); + assert!( + !state + .stack + .iter() + .any(|entry| matches!(entry.kind, StackEntryKind::TriggeredAbility { .. })), + "O may not be separately pushed while the colour choice is open" + ); + let queued: Vec = state + .deferred_triggers + .iter() + .map(|context| context.pending.source_id) + .collect(); + assert_eq!( + queued, + vec![observer_o], + "the already-paid cost range was collected into exactly O; N's reflexive \ + cannot exist yet because no mana has been produced" + ); + assert!( + !queued.contains(&source_n), + "no reflexive before production: {queued:?}" + ); + assert_eq!(state.players[P0.0 as usize].life, life_before); +} + +/// Colour half of root 1: the direct `ActivateAbility` root whose `AnyOneColor` +/// production returns `ChooseManaColor` first. Both halves of the seam run — +/// the pre-prompt collection in `finish_mana_ability_cost_payment` and the +/// post-choice collection in `handle_choose_mana_color` — and the release group +/// is identical to the fixed-colour row's. +#[test] +fn direct_priority_color_choice_root_releases_the_same_two_context_group() { + let NoPauseFixture { + scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::AnyOneColor); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let prompted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N's AnyOneColor production opens the colour choice"); + assert!( + matches!( + prompted.waiting_for, + WaitingFor::ChooseManaColor { player: P0, .. } + ), + "expected ChooseManaColor, got {:?}", + prompted.waiting_for + ); + assert_color_prompt_holds_only_the_observer(runner.state(), source_n, observer_o, life_before); + + let chosen = runner + .act(GameAction::ChooseManaColor { + choice: ManaChoice::SingleColor(ManaType::Green), + count: 1, + }) + .expect("the choice action produces the chosen mana and materializes the reflexive"); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1 + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &chosen.waiting_for, + source_n, + observer_o, + life_before, + ); +} + +/// Colour half of root 2: the same two halves under a live `PendingCast`. The +/// cast owner retains the action across BOTH halves and only the finalizer +/// exposes the group. +#[test] +fn masked_cast_color_choice_root_releases_the_same_two_context_group() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::AnyOneColor); + let spell = scenario + .add_spell_to_hand(P0, "Colour-Half Masked Cast Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::Green], + generic: 0, + }) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Manual, + }) + .expect("the masked owner announces a real pending cast"); + + let prompted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is manually activated during the cast's mana payment"); + assert!( + matches!( + prompted.waiting_for, + WaitingFor::ChooseManaColor { player: P0, .. } + ), + "expected ChooseManaColor, got {:?}", + prompted.waiting_for + ); + assert_color_prompt_holds_only_the_observer(runner.state(), source_n, observer_o, life_before); + + let chosen = runner + .act(GameAction::ChooseManaColor { + choice: ManaChoice::SingleColor(ManaType::Green), + count: 1, + }) + .expect("the choice action completes N under the still-live cast owner"); + assert!( + matches!( + chosen.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + ), + "the cast owner must retain the action across the colour choice, got {:?}", + chosen.waiting_for + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + source_n, + observer_o, + life_before, + ); + + let finalized = runner + .act(GameAction::PassPriority) + .expect("the manual payment finalizes and announces the spell"); + assert!(runner + .state() + .stack + .iter() + .any(|entry| matches!(entry.kind, StackEntryKind::Spell { .. }))); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &finalized.waiting_for, + source_n, + observer_o, + life_before, + ); +} + +/// Colour half of root 3: the same two halves under a live `UnlessPayment` +/// resolution owner. +#[test] +fn masked_resolution_color_choice_root_releases_the_same_two_context_group() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::AnyOneColor); + let spell = scenario + .add_spell_to_hand(P0, "Colour-Half Masked Resolution Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![], + generic: 0, + }) + .with_ability_definition(unless_pay_one_punisher()) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Auto, + }) + .expect("the free witness spell is announced"); + runner.act(GameAction::PassPriority).expect("P0 passes"); + let owner = runner + .act(GameAction::PassPriority) + .expect("P1 passes and the witness resolves into its unless-payment"); + assert!(matches!( + owner.waiting_for, + WaitingFor::UnlessPayment { player: P0, .. } + )); + + let prompted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is manually activated during the unless payment"); + assert!( + matches!( + prompted.waiting_for, + WaitingFor::ChooseManaColor { player: P0, .. } + ), + "expected ChooseManaColor, got {:?}", + prompted.waiting_for + ); + assert_color_prompt_holds_only_the_observer(runner.state(), source_n, observer_o, life_before); + + let chosen = runner + .act(GameAction::ChooseManaColor { + choice: ManaChoice::SingleColor(ManaType::Green), + count: 1, + }) + .expect("the choice action completes N under the still-live resolution owner"); + assert!( + matches!( + chosen.waiting_for, + WaitingFor::UnlessPayment { player: P0, .. } + ), + "the resolution owner must retain the action across the colour choice, got {:?}", + chosen.waiting_for + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + source_n, + observer_o, + life_before, + ); + + let settled = runner + .act(GameAction::PayUnlessCost { pay: true }) + .expect("the unless cost is paid from N's green mana"); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &settled.waiting_for, + source_n, + observer_o, + life_before + 1, + ); +} + +/// The plan's required `ManaPayment` + `TapLandForMana` action regression. +/// +/// The six-row no-pause matrix's manual-cast `ActivateAbility` row does not +/// execute the ADJACENT changed match arm, so this row drives the real +/// engine-authored `GameAction::TapLandForMana` instead — never +/// `ActivateAbility`, never `handle_tap_land_for_mana`, never a production +/// helper directly. +/// +/// Explicit revert discriminator: restoring that one arm's baseline immediate +/// `process_triggers(state, &mana_events)` dispatches or stages O separately +/// while L's reflexive stays under the cast guard, so the immediate exact-two +/// deferred queue, the empty trigger stack, and the later exact-two single group +/// cannot all hold. This row must fail under that one-arm revert even when the +/// adjacent `ManaPayment` `ActivateAbility` routing remains correct. +#[test] +fn manual_cast_tap_land_for_mana_defers_its_reflexive_and_observer_as_one_group() { + let mut reflexive = AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 5 }, + player: TargetFilter::Controller, + }, + ); + reflexive.condition = Some(engine::types::ability::AbilityCondition::WhenYouDo); + + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let land_l = scenario + .add_creature(P0, "L Reflexive Green Land", 1, 1) + .as_land() + .with_ability_definition( + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced: ManaProduction::Fixed { + colors: vec![ManaColor::Green], + contribution: ManaContribution::Base, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(AbilityCost::Tap) + .sub_ability(reflexive), + ) + .id(); + let observer_o = scenario + .add_creature(P0, "O L-Tap Observer", 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::Taps) + .execute(AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 2 }, + player: TargetFilter::Controller, + }, + )) + .valid_card(TargetFilter::SpecificObject { id: land_l }) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id(); + let spell = scenario + .add_spell_to_hand(P0, "Tap-Land Payment Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::Green], + generic: 0, + }) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + let cast = runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Manual, + }) + .expect("a real one-green cast reaches manual mana payment"); + assert!( + matches!(cast.waiting_for, WaitingFor::ManaPayment { player: P0, .. }), + "expected the real ManaPayment owner, got {:?}", + cast.waiting_for + ); + + // The selection must be ENGINE-AUTHORED, not hand-built. + let (_, _, grouped) = legal_actions_full(runner.state()); + let selection = grouped + .get(&land_l) + .into_iter() + .flatten() + .find_map(|action| match action { + GameAction::TapLandForMana { selection } => Some(selection.clone()), + _ => None, + }) + .expect("the engine authors a ManaSourceSelection for L at the payment prompt"); + assert_eq!(selection.source.object_id, land_l); + assert_eq!( + selection.ability_index, + Some(0), + "the selection names L's own {{T}}: Add {{G}} ability" + ); + + let tapped = runner + .act(GameAction::TapLandForMana { selection }) + .expect("the engine-authored land tap is submitted as its own action"); + + assert!(runner.state().objects[&land_l].tapped, "L is tapped"); + assert_eq!( + tapped + .events + .iter() + .filter(|event| matches!( + event, + GameEvent::TappedForMana { source_id, .. } if *source_id == land_l + )) + .count(), + 1, + "exactly one source-identifiable TappedForMana(L): {:?}", + tapped.events + ); + assert_eq!( + tapped + .events + .iter() + .filter(|event| matches!(event, GameEvent::ManaAdded { .. })) + .count(), + 1, + "exactly one base ManaAdded occurrence: {:?}", + tapped.events + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1, + "L's green mana is present and usable immediately" + ); + assert!( + runner.state().pending_cast.is_some(), + "the pending cast is still live" + ); + assert!( + matches!( + tapped.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + ), + "the cast owner retains the action, got {:?}", + tapped.waiting_for + ); + assert!( + !matches!(tapped.waiting_for, WaitingFor::OrderTriggers { .. }), + "a masked land tap may not open CR 603.3b ordering" + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + land_l, + observer_o, + life_before, + ); + assert!( + runner.state().deferred_triggers.iter().all(|context| { + !engine::game::mana_abilities::is_triggered_mana_ability( + &context.pending.ability, + context.pending.trigger_event.as_ref(), + ) + }), + "neither queued context is an accepted triggered-mana context" + ); + + let finalized = runner + .act(GameAction::PassPriority) + .expect("the manual payment spends L's green pip and announces the spell"); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 0, + "the green pip is consumed by the real cast" + ); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(entry.kind, StackEntryKind::Spell { .. })) + .count(), + 1, + "the spell is announced exactly once" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &finalized.waiting_for, + land_l, + observer_o, + life_before, + ); +} + +// --------------------------------------------------------------------------- +// `ManaAdded` immediacy: the plan's "targetless nonmodal `ManaAdded`" classifier +// row, driven as real actions over the no-pause fixture. A third permanent M +// observes N's `ManaAdded` with a targetless nonmodal mana body, so the complete +// classifier accepts it and the immediate backend resolves it stacklessly inside +// the completed mana frame — its bonus mana is in the pool before the frame's +// owner is resumed, and it never becomes a queue or stack member. +// --------------------------------------------------------------------------- + +/// Attach permanent M to the no-pause fixture: a `TriggerMode::ManaAdded` +/// observer whose executed body is `body`. `OncePerTurn` is mandatory — M's own +/// mana body emits a further `ManaAdded`, and CR 603.2h is what stops the +/// fixed point from re-triggering M off its own production. +fn add_mana_added_observer(scenario: &mut GameScenario, label: &str, body: Effect) -> ObjectId { + scenario + .add_creature(P0, label, 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::ManaAdded) + .execute(AbilityDefinition::new(AbilityKind::Database, body)) + .constraint(TriggerConstraint::OncePerTurn) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id() +} + +/// M's accepted body: one colorless mana, targetless and nonmodal, so +/// `build_target_slots` is empty and `is_triggered_mana_ability` holds. +fn mana_added_bonus_mana_body() -> Effect { + Effect::Mana { + produced: ManaProduction::Colorless { + count: QuantityExpr::Fixed { value: 1 }, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + } +} + +fn count_mana_added_from(events: &[GameEvent], source: ObjectId) -> usize { + events + .iter() + .filter( + |event| matches!(event, GameEvent::ManaAdded { source_id, .. } if *source_id == source), + ) + .count() +} + +/// How many pips in P0's pool were produced by `source`. +/// +/// Provenance rather than the raw event vector is the right witness for an +/// accepted occurrence: `collect_mana_action_trigger_batch` resolves the +/// accepted body into its own frame-local dispatch buffer, exactly as baseline +/// `process_triggers` does, so an inline body's own `ManaAdded` deliberately +/// never receives a live occurrence identity in the reducer's public vector. +/// The produced pip and its `source_id` are the durable record. +fn pips_produced_by(state: &GameState, source: ObjectId) -> usize { + state.players[P0.0 as usize] + .mana_pool + .mana + .iter() + .filter(|unit| unit.source_id == source) + .count() +} + +/// The plan's `ManaAdded` immediacy row at the direct-`Priority` root. +/// +/// M is accepted by the complete classifier, so `TriggerPlacement::TriggeredManaImmediate` +/// resolves it inside N's completed mana frame: the colorless pip exists in the +/// same action, M is never appended to `state.deferred_triggers`, no +/// `TriggeredAbility` entry is ever pushed for it, and the single release group +/// is still exactly N's reflexive plus O. +#[test] +fn direct_priority_mana_added_bonus_resolves_inline_without_queue_or_stack() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let bonus_m = add_mana_added_observer( + &mut scenario, + "M Mana-Added Bonus", + mana_added_bonus_mana_body(), + ); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let acted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); + + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1, + "N's own base mana (CR 605.3b)" + ); + // The immediacy claim: M's accepted body already ran, stacklessly, inside + // the same completed mana frame. + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Colorless), + 1, + "M's accepted triggered mana is spendable in the SAME action (CR 605.4a)" + ); + assert_eq!( + pips_produced_by(runner.state(), bonus_m), + 1, + "M produces exactly once — CR 603.2h stops it observing its own ManaAdded" + ); + assert_eq!( + pips_produced_by(runner.state(), source_n), + 1, + "and N's base production happens exactly once" + ); + assert_eq!( + count_mana_added_from(&acted.events, source_n), + 1, + "N's own base ManaAdded is a live action event exactly once" + ); + assert!( + !runner + .state() + .deferred_triggers + .iter() + .any(|context| context.pending.source_id == bonus_m), + "an accepted triggered-mana context is never appended to the ordinary queue" + ); + if let WaitingFor::OrderTriggers { + triggers: ref group, + .. + } = acted.waiting_for + { + assert!( + !group.iter().any(|summary| summary.source_id == bonus_m), + "and it is never a member of the released ordinary group" + ); + } + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &acted.waiting_for, + source_n, + observer_o, + life_before, + ); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(&entry.kind, StackEntryKind::TriggeredAbility { .. })) + .count(), + 0, + "M never occupied the stack at any point" + ); +} + +/// The pure-axis positive reach guard for the row above: the ONLY difference is +/// M's executed body. A `GainLife` body makes `is_triggered_mana_ability` false, +/// so the same firing `ManaAdded` event, the same matcher and the same +/// `OncePerTurn` constraint now produce an ORDINARY deferred context — proving +/// the fixture really does couple M to N's production, and that immediacy is a +/// property of the accepted mana body rather than of the fixture's topology. +#[test] +fn direct_priority_mana_added_nonmana_body_is_deferred_as_an_ordinary_trigger() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let bonus_m = add_mana_added_observer( + &mut scenario, + "M Mana-Added Life", + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 3 }, + player: TargetFilter::Controller, + }, + ); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let acted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); + + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Colorless), + 0, + "a nonmana body produces no mana at all" + ); + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before, + "and nothing resolves before the release group is ordered" + ); + + let WaitingFor::OrderTriggers { + triggers: ref group, + .. + } = acted.waiting_for + else { + panic!("expected one release group, got {:?}", acted.waiting_for); + }; + let members: Vec = group.iter().map(|summary| summary.source_id).collect(); + assert_eq!( + members.len(), + 3, + "the rejected M joins N's reflexive and O in the SAME ordinary group: {members:?}" + ); + for (label, id) in [("N", source_n), ("O", observer_o), ("M", bonus_m)] { + assert_eq!( + members.iter().filter(|member| **member == id).count(), + 1, + "{label} appears exactly once: {members:?}" + ); + } + + runner + .act(GameAction::OrderTriggers { + order: vec![0, 1, 2], + }) + .expect("the released group orders"); + for _ in 0..4 { + runner.act(GameAction::PassPriority).expect("P0 passes"); + runner.act(GameAction::PassPriority).expect("P1 passes"); + } + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before + 5 + 2 + 3, + "all three distinguishable effects happen exactly once each" + ); +} + +/// The immediacy payoff at a masked root: M's bonus colorless pip must be +/// spendable by the very cast whose `WaitingFor::ManaPayment` masks the frame. +/// The witness spell costs `{1}{G}`, which is unpayable unless M resolved +/// stacklessly inside N's completed frame — reverting `settles_completed_frame` +/// to baseline's `is_ultimate_root && has_deferred_cost_events` leaves the +/// empty-ledger `ManaPayment` shape with no collection at all, so M never fires +/// and the generic pip cannot be paid. +#[test] +fn masked_cast_mana_added_bonus_is_spendable_before_the_spell_is_announced() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let bonus_m = add_mana_added_observer( + &mut scenario, + "M Mana-Added Bonus", + mana_added_bonus_mana_body(), + ); + let spell = scenario + .add_spell_to_hand(P0, "Mana-Added Immediacy Witness", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::Green], + generic: 1, + }) + .id(); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let card_id = runner.state().objects[&spell].card_id; + let cast = runner + .act(GameAction::CastSpell { + object_id: spell, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Manual, + }) + .expect("the masked owner announces a real pending cast in manual payment mode"); + assert!(matches!( + cast.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + )); + + let activated = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is manually activated during the cast's mana payment"); + assert!( + matches!( + activated.waiting_for, + WaitingFor::ManaPayment { player: P0, .. } + ), + "the cast owner must retain the action, got {:?}", + activated.waiting_for + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 1 + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Colorless), + 1, + "M's accepted bonus is in the pool BEFORE the payment owner resumes" + ); + assert_eq!( + pips_produced_by(runner.state(), bonus_m), + 1, + "the colorless pip really is M's, not a second pip of N's" + ); + assert!( + !runner + .state() + .deferred_triggers + .iter() + .any(|context| context.pending.source_id == bonus_m), + "an accepted triggered-mana context is never appended to the ordinary queue" + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + source_n, + observer_o, + life_before, + ); + + let finalized = runner + .act(GameAction::PassPriority) + .expect("the manual payment spends BOTH pips and announces the spell"); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Green), + 0 + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Colorless), + 0, + "the generic pip is paid by M's bonus mana" + ); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(entry.kind, StackEntryKind::Spell { .. })) + .count(), + 1, + "the spell is announced exactly once" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &finalized.waiting_for, + source_n, + observer_o, + life_before, + ); +} + +// --------------------------------------------------------------------------- +// Delayed-trigger families: the plan's `WhenNextEvent` one-shot control and the +// duration-bearing `WheneverEvent` persistence control. Both prove the mana +// frame's COMBINED collector materializes normal and delayed contexts together +// in one APNAP batch — `collect_pending_and_delayed_triggers_for_batch` — rather +// than letting the generic delayed pass discover them separately after the +// frame has already claimed its live occurrences. +// --------------------------------------------------------------------------- + +/// A free witness spell that installs one real delayed trigger through +/// `Effect::CreateDelayedTrigger`, and the second `{T}: Add {G}` source used to +/// prove one-shot removal versus duration-bearing persistence. +struct DelayedFrameFixture { + scenario: GameScenario, + source_n: ObjectId, + observer_o: ObjectId, + installer: ObjectId, + source_n2: ObjectId, +} + +/// The embedded matcher both delayed rows install. `valid_card` is mandatory: +/// without it `taps_for_mana_card_matches` requires the tapping permanent to BE +/// the delayed trigger's own source, which an installer in the graveyard never +/// is. `Any` (rather than a specific id) is what makes the one-shot row's second +/// tap a non-vacuous probe — N2 would match, and only removal stops it. +fn delayed_taps_for_mana_matcher() -> TriggerDefinition { + TriggerDefinition::new(TriggerMode::TapsForMana).valid_card(TargetFilter::Any) +} + +fn delayed_frame_fixture(condition: DelayedTriggerCondition) -> DelayedFrameFixture { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let installer = scenario + .add_spell_to_hand(P0, "Delayed Trigger Installer", true) + .with_mana_cost(ManaCost::Cost { + shards: vec![], + generic: 0, + }) + .with_ability_definition(AbilityDefinition::new( + AbilityKind::Spell, + Effect::CreateDelayedTrigger { + condition, + effect: Box::new(AbilityDefinition::new( + AbilityKind::Database, + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 4 }, + player: TargetFilter::Controller, + }, + )), + uses_tracked_set: false, + }, + )) + .id(); + // A second, otherwise identical mana source with NO rider and NO observer, + // so a later tap is a clean probe for whether the delayed source is still + // installed. + let source_n2 = scenario + .add_creature(P0, "N2 Plain Green Source", 1, 1) + .as_artifact() + .with_ability_definition( + AbilityDefinition::new( + AbilityKind::Activated, + Effect::Mana { + produced: ManaProduction::Fixed { + colors: vec![ManaColor::Green], + contribution: ManaContribution::Base, + }, + restrictions: vec![], + grants: vec![], + expiry: None, + target: None, + }, + ) + .cost(AbilityCost::Tap), + ) + .id(); + + DelayedFrameFixture { + scenario, + source_n, + observer_o, + installer, + source_n2, + } +} + +/// Cast and resolve the free installer, then assert exactly one delayed trigger +/// is installed and return the caller's life total at that point. +fn resolve_delayed_installer(runner: &mut GameRunner, installer: ObjectId) -> i32 { + let card_id = runner.state().objects[&installer].card_id; + runner + .act(GameAction::CastSpell { + object_id: installer, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Auto, + }) + .expect("the free installer is announced"); + runner.act(GameAction::PassPriority).expect("P0 passes"); + runner + .act(GameAction::PassPriority) + .expect("P1 passes and the installer resolves"); + assert_eq!( + runner.state().delayed_triggers.len(), + 1, + "exactly one real delayed trigger is installed by the resolved effect" + ); + runner.state().players[P0.0 as usize].life +} + +/// Order and resolve a released group of exactly `expected` members, then assert +/// the total life delta. +fn resolve_group_of( + runner: &mut GameRunner, + group_wait: &WaitingFor, + expected: &[ObjectId], + life_before: i32, + total_gain: i32, +) { + let WaitingFor::OrderTriggers { + triggers: ref group, + .. + } = group_wait + else { + panic!("expected one release group, got {group_wait:?}"); + }; + let members: Vec = group.iter().map(|summary| summary.source_id).collect(); + assert_eq!( + members.len(), + expected.len(), + "the frame releases exactly one combined APNAP batch: {members:?}" + ); + for id in expected { + assert_eq!( + members.iter().filter(|member| *member == id).count(), + 1, + "{id:?} is a member exactly once: {members:?}" + ); + } + runner + .act(GameAction::OrderTriggers { + order: (0..members.len()).collect(), + }) + .expect("the released group orders"); + for _ in 0..(members.len() + 1) { + runner.act(GameAction::PassPriority).expect("P0 passes"); + runner.act(GameAction::PassPriority).expect("P1 passes"); + } + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_before + total_gain, + "each distinguishable effect happens exactly once" + ); +} + +/// The plan's synchronous delayed one-shot `TappedForMana` row. +/// +/// A real `Effect::CreateDelayedTrigger` carrying `WhenNextEvent` with an +/// embedded `TriggerMode::TapsForMana` is installed by a resolved spell. N's +/// fixed-colour activation then emits the base tap plus one source-identifiable +/// `TappedForMana`, and the completed mana frame's COMBINED collector must +/// return the delayed context in the SAME APNAP batch as N's reflexive and the +/// ordinary observer O — one group of three, each effect once, for +11. +/// +/// The one-shot half is measured twice: the instance is removed from +/// `state.delayed_triggers` exactly once, and a second identical tap by N2 +/// afterwards produces no further firing at all. +#[test] +fn synchronous_delayed_one_shot_taps_for_mana_joins_the_frame_batch_once() { + let DelayedFrameFixture { + scenario, + source_n, + observer_o, + installer, + source_n2, + } = delayed_frame_fixture(DelayedTriggerCondition::WhenNextEvent { + trigger: Box::new(delayed_taps_for_mana_matcher()), + or_trigger: None, + lifetime: DelayedTriggerLifetime::default(), + }); + let mut runner = scenario.build(); + let life_before = resolve_delayed_installer(&mut runner, installer); + + // Positive reach guard on the INSTALLED shape, before anything fires. + let installed = &runner.state().delayed_triggers[0]; assert!( - json.contains("Suspended"), - "the serialized parent frame must retain its typed re-entry ownership" + matches!( + installed.condition, + DelayedTriggerCondition::WhenNextEvent { ref trigger, .. } + if trigger.mode == TriggerMode::TapsForMana + ), + "the installed condition is the real embedded TapsForMana one-shot: {:?}", + installed.condition + ); + assert!(installed.one_shot, "CR 603.7: it is a one-shot"); + assert_eq!( + installed.source_id, installer, + "install origin is preserved" ); - let restored: GameState = - serde_json::from_str(&json).expect("the suspended parent mana cursor deserializes"); - let mut runner = GameRunner::from_state(restored); - let resumed = runner - .act(GameAction::ChooseReplacement { index: 0 }) - .expect("redirect inner self-exile and resume the exact parent cursor"); - assert_eq!(runner.state().objects[&inner].zone, Zone::Graveyard); - assert!(runner.state().objects[&outer].tapped); - assert!(matches!( - resumed.waiting_for, - WaitingFor::ManaPayment { player: P0, .. } - )); + let acted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); assert_eq!( - initial_events + acted + .events .iter() - .chain(resumed.events.iter()) - .filter(|event| matches!(event, GameEvent::PermanentTapped { object_id, .. } if *object_id == outer)) + .filter(|event| matches!( + event, + GameEvent::TappedForMana { source_id, .. } if *source_id == source_n + )) .count(), 1, - "the outer tap prefix is retained by the parent cursor rather than replayed" + "exactly one source-identifiable TappedForMana reach event" + ); + assert!( + runner.state().delayed_triggers.is_empty(), + "the matched one-shot instance is removed exactly once: {:?}", + runner.state().delayed_triggers + ); + resolve_group_of( + &mut runner, + &acted.waiting_for, + &[source_n, observer_o, installer], + life_before, + 5 + 2 + 4, + ); + + // The generic delayed pass creates none: a second identical tap after the + // one-shot was consumed produces no further delayed firing. + let life_after_group = runner.state().players[P0.0 as usize].life; + let second = runner + .act(GameAction::ActivateAbility { + source_id: source_n2, + ability_index: 0, + }) + .expect("N2 taps for mana with no delayed source installed"); + assert!( + !matches!(second.waiting_for, WaitingFor::OrderTriggers { .. }), + "no trigger at all may be released by the second tap, got {:?}", + second.waiting_for ); assert_eq!( - initial_events - .iter() - .chain(resumed.events.iter()) - .filter(|event| matches!(event, GameEvent::PermanentTapped { object_id, .. } if *object_id == inner)) - .count(), + runner.state().players[P0.0 as usize].life, + life_after_group, + "the consumed one-shot cannot fire a second time" + ); +} + +/// The plan's duration-bearing `WheneverEvent` persistence row, deliberately +/// separate from the one-shot control above. +/// +/// The same combined collector must place the delayed context in the frame's one +/// APNAP batch, but the duration-bearing source stays INSTALLED afterwards, and +/// a later matching event proves persistence by firing exactly one more time. +#[test] +fn duration_bearing_delayed_whenever_event_stays_installed_and_fires_again() { + let DelayedFrameFixture { + scenario, + source_n, + observer_o, + installer, + source_n2, + } = delayed_frame_fixture(DelayedTriggerCondition::WheneverEvent { + trigger: Box::new(delayed_taps_for_mana_matcher()), + expiry: WheneverEventExpiry::default(), + }); + let mut runner = scenario.build(); + let life_before = resolve_delayed_installer(&mut runner, installer); + assert!( + !runner.state().delayed_triggers[0].one_shot, + "CR 603.7c: a duration-bearing WheneverEvent is not a one-shot" + ); + + let acted = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); + assert_eq!( + runner.state().delayed_triggers.len(), 1, - "the inner source's tap cost is paid once across the replacement pause" + "the duration-bearing source remains installed while its context fires" + ); + resolve_group_of( + &mut runner, + &acted.waiting_for, + &[source_n, observer_o, installer], + life_before, + 5 + 2 + 4, + ); + + // Persistence: a later matching event fires it exactly one additional time. + // N2 carries no rider and no observer, so the delayed context is the batch's + // only member and CR 603.3b needs no ordering prompt for it. + let life_after_group = runner.state().players[P0.0 as usize].life; + let second = runner + .act(GameAction::ActivateAbility { + source_id: source_n2, + ability_index: 0, + }) + .expect("N2 taps for mana while the duration-bearing source is still installed"); + assert!( + !matches!(second.waiting_for, WaitingFor::OrderTriggers { .. }), + "a one-member batch needs no ordering prompt, got {:?}", + second.waiting_for ); assert_eq!( - initial_events + runner + .state() + .stack .iter() - .chain(resumed.events.iter()) - .filter(|event| matches!( - event, - GameEvent::ZoneChanged { - object_id, - from: Some(Zone::Battlefield), - to: Zone::Graveyard, - .. - } if *object_id == inner - )) + .filter( + |entry| matches!(&entry.kind, StackEntryKind::TriggeredAbility { .. }) + && entry.controller == P0 + ) .count(), 1, - "the redirected inner self-exile cost is delivered once" + "the persistent delayed source fired exactly one additional time" ); - for source_id in [inner, outer] { - assert_eq!( - initial_events - .iter() - .chain(resumed.events.iter()) - .filter(|event| matches!(event, GameEvent::ManaAdded { source_id: id, .. } if *id == source_id)) - .count(), - 1, - "each nested mana ability produces exactly once" - ); - } - + runner.act(GameAction::PassPriority).expect("P0 passes"); runner .act(GameAction::PassPriority) - .expect("the outer spell payment consumes the outer mana once"); - assert_eq!(runner.state().objects[&spell].zone, Zone::Stack); + .expect("P1 passes and the second firing resolves"); + assert_eq!( + runner.state().players[P0.0 as usize].life, + life_after_group + 4, + "and its distinguishable effect happens exactly once more" + ); + assert_eq!( + runner.state().delayed_triggers.len(), + 1, + "and it is still installed after firing again" + ); } fn two_source_assist_replacement_witness( @@ -11195,3 +14186,296 @@ fn commander_hand_return_keeps_its_may_choice_inside_cr_616_ordering() { "the still-applicable Hand-to-Command redirect resolves after the decline" ); } + +// --------------------------------------------------------------------------- +// The accepted-pause families: `optional` (`OptionalEffectChoice`) and +// `optional_for` (`OpponentMayChoice`). +// +// These are the first shapes the complete classifier accepts that do NOT +// complete synchronously. The occurrence pauses mid-body inside the completed +// mana frame, its continuation carrier holds the frame's own resume root, and +// the answering action's readiness hook in `engine_payment_choices` partitions +// the stored emission batches, runs the accepted tail, and resumes that exact +// mana frame once. +// +// The widening owns a real behaviour delta: baseline `is_triggered_mana_ability` +// returns true for an all-mana `optional` body, so baseline resolved it inline +// and left `waiting_for` set with no continuation at all. +// --------------------------------------------------------------------------- + +/// M's accepted body, made optional: still one colorless mana, still targetless +/// and nonmodal, so `build_target_slots` stays empty and the baseline +/// acceptance gate still holds — the ONLY difference from +/// `mana_added_bonus_mana_body` is the "you may". +fn optional_mana_added_bonus_observer(scenario: &mut GameScenario, label: &str) -> ObjectId { + scenario + .add_creature(P0, label, 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::ManaAdded) + .execute( + AbilityDefinition::new(AbilityKind::Database, mana_added_bonus_mana_body()) + .optional(), + ) + .constraint(TriggerConstraint::OncePerTurn) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id() +} + +/// The pause itself, asserted before it is answered: N's activation returns +/// `OptionalEffectChoice`, not a wait belonging to the payment, and NOTHING of +/// the frame has been released — M owns no pip, no queue slot and no stack +/// entry, while the frame's two ordinary observers are already queued +/// undispatched behind it. +fn assert_optional_pause_is_open_and_nothing_released( + runner: &GameRunner, + bonus_m: ObjectId, + source_n: ObjectId, + observer_o: ObjectId, + waiting_for: &WaitingFor, + life_before: i32, +) { + assert!( + matches!(waiting_for, WaitingFor::OptionalEffectChoice { .. }), + "the accepted body's own pause is the action's wait, got {waiting_for:?}" + ); + assert_eq!( + pips_produced_by(runner.state(), bonus_m), + 0, + "an accepted occurrence produces nothing while its own decision is open" + ); + assert_eq!( + pips_produced_by(runner.state(), source_n), + 1, + "N's base production already happened — the frame really is mid-settlement" + ); + assert!( + !runner + .state() + .deferred_triggers + .iter() + .any(|context| context.pending.source_id == bonus_m), + "a paused accepted occurrence is never queued on the ordinary authority" + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + source_n, + observer_o, + life_before, + ); +} + +/// Accept: the resumed body's mana reaches the pool inside the answering +/// action, and readiness then resumes the suspended mana frame exactly once — +/// the frame's own release group is still exactly N's reflexive plus O. +#[test] +fn accepted_optional_mana_body_pauses_the_frame_and_resumes_it_on_accept() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let bonus_m = optional_mana_added_bonus_observer(&mut scenario, "M Optional Mana-Added Bonus"); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let paused = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); + assert_optional_pause_is_open_and_nothing_released( + &runner, + bonus_m, + source_n, + observer_o, + &paused.waiting_for, + life_before, + ); + + let resumed = runner + .act(GameAction::DecideOptionalEffect { accept: true }) + .expect("P0 accepts the accepted occurrence's own optional body"); + assert_eq!( + pips_produced_by(runner.state(), bonus_m), + 1, + "the resumed accepted body produces exactly once (CR 605.4a)" + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Colorless), + 1, + "and the bonus is spendable from the answering action onward" + ); + assert!( + !runner + .state() + .deferred_triggers + .iter() + .any(|context| context.pending.source_id == bonus_m), + "resumption never demotes the occurrence to the ordinary queue" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &resumed.waiting_for, + source_n, + observer_o, + life_before, + ); + assert_eq!( + runner + .state() + .stack + .iter() + .filter(|entry| matches!(&entry.kind, StackEntryKind::TriggeredAbility { .. })) + .count(), + 0, + "M never occupied the stack across the pause" + ); +} + +/// Decline: the same resumption runs, the same single group is released, and +/// the ONLY difference is that M produced nothing. This is the pure-axis +/// control for the accept row — it proves the frame's resumption is owned by +/// readiness rather than by the body having produced mana. +#[test] +fn accepted_optional_mana_body_declined_still_resumes_the_frame_once() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let bonus_m = optional_mana_added_bonus_observer(&mut scenario, "M Optional Mana-Added Bonus"); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let paused = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); + assert_optional_pause_is_open_and_nothing_released( + &runner, + bonus_m, + source_n, + observer_o, + &paused.waiting_for, + life_before, + ); + + let resumed = runner + .act(GameAction::DecideOptionalEffect { accept: false }) + .expect("P0 declines the accepted occurrence's own optional body"); + assert_eq!( + pips_produced_by(runner.state(), bonus_m), + 0, + "a declined body produces nothing" + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Colorless), + 0, + "and nothing colorless reaches the pool by any other route" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &resumed.waiting_for, + source_n, + observer_o, + life_before, + ); +} + +/// The same accepted body under `optional_for` (`OpponentMayChoice`): the "you +/// may" is routed to the opponent, so the pause belongs to a NON-ACTIVATOR while +/// the mana frame it suspends belongs to P0. +/// +/// This is the second accepted-pause family and the second readiness hook in +/// `engine_payment_choices`. Its reducer arm returns the `ActionResult` +/// directly, without the ordinary post-action pipeline — see the assertion on +/// the resumed wait, which records exactly where full settled-Priority +/// convergence is still owed. +fn opponent_may_mana_added_bonus_observer(scenario: &mut GameScenario, label: &str) -> ObjectId { + let mut body = + AbilityDefinition::new(AbilityKind::Database, mana_added_bonus_mana_body()).optional(); + body.optional_for = Some(OpponentMayScope::AnyOpponent); + scenario + .add_creature(P0, label, 0, 0) + .as_enchantment() + .with_trigger_definition( + TriggerDefinition::new(TriggerMode::ManaAdded) + .execute(body) + .constraint(TriggerConstraint::OncePerTurn) + .trigger_zones(vec![Zone::Battlefield]), + ) + .id() +} + +#[test] +fn accepted_opponent_may_mana_body_pauses_on_the_nonactivator_and_resumes_the_frame() { + let NoPauseFixture { + mut scenario, + source_n, + observer_o, + } = no_pause_mana_fixture(NoPauseColorAxis::Fixed); + let bonus_m = + opponent_may_mana_added_bonus_observer(&mut scenario, "M Opponent-May Mana-Added Bonus"); + let mut runner = scenario.build(); + let life_before = runner.state().players[P0.0 as usize].life; + + let paused = runner + .act(GameAction::ActivateAbility { + source_id: source_n, + ability_index: 0, + }) + .expect("N is activated directly from Priority"); + let WaitingFor::OpponentMayChoice { player, .. } = paused.waiting_for else { + panic!( + "the accepted body's opponent-may pause is the action's wait, got {:?}", + paused.waiting_for + ); + }; + assert_eq!( + player, P1, + "CR 608.2d: the decision belongs to the opponent, not to the frame's activator" + ); + assert_eq!( + pips_produced_by(runner.state(), bonus_m), + 0, + "nothing is produced while the opponent's decision is open" + ); + assert_exactly_two_contexts_queued_and_unstacked( + runner.state(), + source_n, + observer_o, + life_before, + ); + + let resumed = runner + .act(GameAction::DecideOptionalEffect { accept: true }) + .expect("P1 accepts"); + assert_eq!( + pips_produced_by(runner.state(), bonus_m), + 1, + "the resumed accepted body produces exactly once, into its own controller's pool" + ); + assert_eq!( + runner.state().players[P0.0 as usize] + .mana_pool + .count_color(ManaType::Colorless), + 1, + "CR 605.4a: the bonus belongs to M's controller, not to the accepting opponent" + ); + assert_single_group_of_two_then_resolve_for_seven( + &mut runner, + &resumed.waiting_for, + source_n, + observer_o, + life_before, + ); +} diff --git a/crates/engine/tests/integration/cr733_resolved_trigger_collection.rs b/crates/engine/tests/integration/cr733_resolved_trigger_collection.rs index 9d238b4861..d534525982 100644 --- a/crates/engine/tests/integration/cr733_resolved_trigger_collection.rs +++ b/crates/engine/tests/integration/cr733_resolved_trigger_collection.rs @@ -174,6 +174,51 @@ fn assert_current_action_consumed_occurrences_were_journaled( ); } +/// A completed mana frame claims exactly the live occurrences it settled, and +/// its ordinary observers are *deferred* rather than dispatched from inside the +/// payment. The owner's own boundary later drains that release group into the +/// same public event vector, so the action's final event list is strictly longer +/// than the claim. Comparing the journal against the whole final list — which +/// the pre-deferral contract did — is therefore no longer meaningful; the exact +/// contract is that the claim is the settlement's own full-buffer prefix. +/// +/// Returns the claimed prefix length so the caller can assert the release +/// actually happened after it. +fn assert_mana_frame_claimed_exactly_its_own_event_prefix( + state: &GameState, + events: &[GameEvent], +) -> usize { + let expected = current_action_occurrences(events); + let claims: Vec> = state + .resolved_rules_journal + .entries() + .iter() + .filter_map(|entry| match entry.command.as_ref() { + Some(ResolvedRulesCommand::TriggerCollection(ResolvedTriggerCollectionCommand { + collection: ResolvedTriggerCollection::ConsumeBeforePriority { occurrences }, + .. + })) => Some(occurrences.clone()), + _ => None, + }) + .collect(); + assert_eq!( + claims.len(), + 1, + "one completed mana frame claims its live occurrences exactly once" + ); + let claimed = &claims[0]; + assert!( + !claimed.is_empty() && claimed.len() <= expected.len(), + "the claim must be a non-empty prefix of the action's events" + ); + assert_eq!( + claimed.as_slice(), + &expected[..claimed.len()], + "the settlement must journal its own event prefix by exact full-buffer occurrence identity" + ); + claimed.len() +} + #[test] fn trigger_collection_command_variants_round_trip_inside_the_journal_envelope() { for command in [ @@ -381,7 +426,8 @@ fn paused_mana_cost_settlement_journals_consumed_occurrences_without_recollectin event, GameEvent::ZoneChanged { object_id, .. } if *object_id == source ))); - assert_current_action_consumed_occurrences_were_journaled(runner.state(), &resumed.events); + let claimed = + assert_mana_frame_claimed_exactly_its_own_event_prefix(runner.state(), &resumed.events); assert_eq!( runner .state() @@ -395,6 +441,19 @@ fn paused_mana_cost_settlement_journals_consumed_occurrences_without_recollectin 1, "the settled mana-cost zone change must not be collected again at priority" ); + // One release group, formed at the owner's boundary rather than inside the + // payment: the observer was deferred at the claim and announced afterwards, + // so the action's public event vector grew past the claimed prefix. + assert!( + claimed < resumed.events.len(), + "the deferred release group must append its announcement after the settlement's claim \ + (claimed {claimed} of {} events)", + resumed.events.len() + ); + assert!( + runner.state().deferred_triggers.is_empty(), + "the owner's boundary must release the whole deferred group exactly once" + ); } #[test] diff --git a/crates/engine/tests/integration/reflexive_body_token_referent.rs b/crates/engine/tests/integration/reflexive_body_token_referent.rs index 099d3216c9..95b77f1b4e 100644 --- a/crates/engine/tests/integration/reflexive_body_token_referent.rs +++ b/crates/engine/tests/integration/reflexive_body_token_referent.rs @@ -265,6 +265,17 @@ fn decide(runner: &mut GameRunner, accept: bool) -> bool { .is_ok() } +/// CR 603.12 + CR 603.3b: a `When you do` body is a CREATED reflexive TRIGGERED +/// ABILITY. Accepting the gate creates it; it does nothing until it is put on +/// the stack at the next priority point and resolves there. Every row that +/// measures the BODY's board effect must therefore drive that window, exactly as +/// an `If you do` row does not (an inline CR 608.2c continuation has already run +/// when the deciding action returns). Answering the ordering prompt with +/// identity is handled inside `advance_until_stack_empty`. +fn settle_reflexive_body(runner: &mut GameRunner) { + runner.advance_until_stack_empty(); +} + /// Answer a sub-clause's `TriggerTargetSelection` prompt and let the resulting /// stack object resolve. Used where the targeted clause is NOT the chain's head /// (North Pole's `SetTapState`, Ratonhnhaké꞉ton's `ChangeZone`), so its target @@ -333,6 +344,7 @@ fn iroh_reflexive_body_counter_binds_created_token() { vec![TargetRef::Object(bear), TargetRef::Player(P1)], ); assert!(decide(&mut runner, true), "the reflexive gate was accepted"); + settle_reflexive_body(&mut runner); let ally = token_named(&runner, decoy, "Ally"); assert_eq!( @@ -1419,6 +1431,7 @@ fn drive_clone(text: &str, types: &[&str], funded: bool) -> CloneRow { let def = parse_trigger(text, "Synth", types, 0); resolve_def(&mut runner, &def, source, vec![]); decide(&mut runner, true); + settle_reflexive_body(&mut runner); let live = live_tokens(&runner, decoy); CloneRow { diff --git a/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs b/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs index 5f118cea0d..82130a7da9 100644 --- a/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs +++ b/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs @@ -192,6 +192,7 @@ fn make_grant_ability(controller: PlayerId, source: ObjectId) -> ResolvedAbility forward_result: false, unless_pay: None, distribution: None, + distribute: None, target_selection_mode: TargetSelectionMode::Chosen, chosen_players: Vec::new(), repeat_until: None, From ae8c1fee68e5674d0c155fb55779e09fb3e0e013 Mon Sep 17 00:00:00 2001 From: Nishad Date: Thu, 13 Aug 2026 01:05:48 -0700 Subject: [PATCH 02/11] review round 1: modal QuantityCheck gate, turn-order re-point, loop-normalize sidecar - Restore the modal-router gate to accept QuantityCheck resolution gates (parity with pre-deferral behavior); relax the builder's debug_assert to match and document why the gate survives onto the stack object. - Re-point a departed construction-priority recipient with next_player_in_turn_order, not seat-forward next_player; add a TurnDirection::Reversed regression test. - Canonicalize pending_triggered_mana_resume in normalize_for_loop (trigger identities in current/accepted_tail/collected_batches, zeroed settlement ordinal). - Pin the propagated-parent-targets => slot-less invariant with a debug_assert; add positive reach guards to the two no-op tests. - Doc fixes: drop the wrong CR 605.1b citation, repair a broken sentence in oracle_replacement.rs; re-derive census pins (+25). Co-Authored-By: Claude Fable 5 --- crates/engine/src/game/effects/mod.rs | 49 ++++++++++++++++--- crates/engine/src/game/elimination.rs | 29 ++++++++++- crates/engine/src/game/engine.rs | 16 ++++-- .../engine/src/parser/oracle_replacement.rs | 2 +- crates/engine/src/types/game_state.rs | 20 ++++++++ .../ancient_brass_dragon_roll_d20.rs | 12 +++++ 6 files changed, 115 insertions(+), 13 deletions(-) diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 4fb0e7f3c0..53641f41c6 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -2220,7 +2220,17 @@ fn build_reflexive_pending_trigger( parent: Option<&ResolvedAbility>, ) -> crate::game::triggers::PendingTrigger { let mut ability = reflexive.clone(); - debug_assert_eq!(ability.condition, Some(AbilityCondition::WhenYouDo)); + // The modal router accepts a `QuantityCheck` resolution gate as well as the + // `WhenYouDo` creation gate (both dispatch here at the call sites); the + // consume below strips only `WhenYouDo`, so a `QuantityCheck` survives onto + // the stack object and is re-checked at resolution per CR 603.4. + debug_assert!( + matches!( + ability.condition, + Some(AbilityCondition::WhenYouDo) | Some(AbilityCondition::QuantityCheck { .. }) + ), + "build_reflexive_pending_trigger requires a WhenYouDo or QuantityCheck gate" + ); consume_reflexive_creation_gate(&mut ability); let source_id = parent.map_or(ability.source_id, |parent| parent.source_id); @@ -2300,6 +2310,7 @@ fn try_materialize_reflexive_trigger_inner( // the same resolution-scoped referents that the pending trigger will later // carry. Stamp one clone before `build_target_slots` so chosen-player and // amassed-Army filters enumerate legal targets from the actual parent event. + let mut propagated_parent_targets = false; let reflexive_context_owned; let reflexive = if let Some(parent) = parent { let mut owned = reflexive.clone(); @@ -2339,6 +2350,7 @@ fn try_materialize_reflexive_trigger_inner( && should_propagate_parent_targets(parent, &owned) { owned.targets = parent.targets.clone(); + propagated_parent_targets = true; } apply_parent_chain_context(&mut owned, parent, effect_context_object, state); reflexive_context_owned = owned; @@ -2355,10 +2367,12 @@ fn try_materialize_reflexive_trigger_inner( // pending trigger carrying the modal + per-mode abilities, then defer to the // shared modal-trigger router, which prompts `WaitingFor::AbilityModeChoice` // and only then collects each chosen mode's targets. - if creates_reflexive_trigger - && reflexive.modal.is_some() - && !reflexive.mode_abilities.is_empty() - { + // NOT gated on `creates_reflexive_trigger`: a target-less modal marker + // behind a `QuantityCheck` resolution gate must also route through the + // modal-trigger router (as it did before deferral), or the slot-less + // fallback below would return `Ok(false)` and the descent would resolve + // every mode without a `WaitingFor::AbilityModeChoice`. + if reflexive.modal.is_some() && !reflexive.mode_abilities.is_empty() { let pending = build_reflexive_pending_trigger(state, reflexive, parent); let trigger_events = crate::game::triggers::take_pending_trigger_event_batch(state, &pending); @@ -2396,6 +2410,17 @@ fn try_materialize_reflexive_trigger_inner( } Err(error) => return Err(EffectError::InvalidParam(error.to_string())), }; + // A parent-referent rider carries no declared slot of its own (that is the + // `ability_refs_parent_target` shape), so propagated targets and non-empty + // slots are mutually exclusive. If a future ability both reads a parent + // referent AND declares its own slot, the fresh stack-time selection below + // would clobber the bound referent — make that a counted event here rather + // than a silent exile-of-nothing at resolution. + debug_assert!( + !propagated_parent_targets || target_slots.is_empty(), + "a reflexive with propagated parent targets must be slot-less; \ + a declared slot would re-prompt and clobber the bound referent" + ); if target_slots.is_empty() { if creates_reflexive_trigger { let pending = build_reflexive_pending_trigger(state, reflexive, parent); @@ -13164,8 +13189,18 @@ mod tests { let reflexive = reflexive_counter_ability(ObjectId(100)); let mut events = Vec::new(); - try_materialize_reflexive_trigger(&mut state, &reflexive, None, None, &mut events, 0) - .unwrap(); + let materialized = + try_materialize_reflexive_trigger(&mut state, &reflexive, None, None, &mut events, 0) + .unwrap(); + + // Positive reach guard: the empty-state assertions below only prove the + // shared-dispatch DROP if the reflexive actually took the deferral path. + // An `Ok(false)` fall-through would leave the same empty state without + // exercising the dispatch at all. + assert!( + materialized, + "the reflexive must materialize into the deferral path before dispatch can drop it" + ); crate::game::triggers::drain_deferred_trigger_queue(&mut state, &mut events); assert!(state.deferred_triggers.is_empty()); diff --git a/crates/engine/src/game/elimination.rs b/crates/engine/src/game/elimination.rs index c28771a75d..39b70f8c35 100644 --- a/crates/engine/src/game/elimination.rs +++ b/crates/engine/src/game/elimination.rs @@ -254,7 +254,7 @@ pub fn eliminate_players_simultaneously( if let Some(recipient) = state.pending_trigger_construction_priority_recipient { if !players::is_alive(state, recipient) { state.pending_trigger_construction_priority_recipient = - Some(players::next_player(state, recipient)); + Some(players::next_player_in_turn_order(state, recipient)); } } } @@ -3693,4 +3693,31 @@ mod tests { "the re-pointed recipient must survive trusted serde" ); } + + /// CR 800.4a + CR 101.4: the re-point follows the CURRENT turn-order + /// direction, not fixed seating. Under `TurnDirection::Reversed` the next + /// player in turn order after a departed P1 is P0, not seat-forward P2. + /// + /// Revert discriminator: `players::next_player` (seat-forward) re-points to + /// P2 here; `players::next_player_in_turn_order` re-points to P0. + #[test] + fn game_continues_repoints_a_departed_recipient_in_reversed_turn_order() { + let mut state = setup_three_player(); + state.turn_direction = crate::types::phase::TurnDirection::Reversed; + let entry = open_live_trigger_construction_prompt(&mut state, PlayerId(0)); + state.pending_trigger_construction_priority_recipient = Some(PlayerId(1)); + + concede(&mut state, PlayerId(1)); + + assert!( + state.stack.iter().any(|e| e.id == entry), + "P0's tracked entry survives an opponent's departure" + ); + assert_eq!( + state.pending_trigger_construction_priority_recipient, + Some(PlayerId(0)), + "under reversed turn order the departed recipient re-points backward \ + through seating (the next player in TURN order), not seat-forward" + ); + } } diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 516cfca452..14e33cd2b6 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -9394,7 +9394,7 @@ fn apply_action( }, None, )?; - // CR 605.1b + CR 605.4a: no outer scan. `activate_mana_ability` + // CR 605.4a: no outer scan. `activate_mana_ability` // builds a real typed cursor here, and its completed frame has // already run `collect_completed_mana_frame_events` — collecting // TapsForMana multipliers and ordinary cost observers together @@ -16192,9 +16192,17 @@ mod stage2_injector_tests { // coordinate is byte-identical to `117b430c2:game/effects/mod.rs` at its old // one, and `scoped_library_search.rs:452` did not move at all — the // set-preservation evidence that no producer was gained or lost. - "game/effects/mod.rs:6429".to_string(), - "game/effects/mod.rs:6506".to_string(), - "game/effects/mod.rs:9701".to_string(), + // + // REVIEW ROUND 1 (QuantityCheck modal-gate restore + propagated-target + // invariant assert): `:6429/:6506/:9701 ⇒ :6454/:6531/:9726`, uniform +25 + // above all three, all from this round's comment/assert insertions in + // `try_materialize_reflexive_trigger` and `build_reflexive_pending_trigger`. + // LOCAL again; coordinates re-derived from this row's own failure output. + // The engine.rs entry did not move (that round's engine.rs edit was an + // in-place one-line comment fix), which is the set-preservation evidence. + "game/effects/mod.rs:6454".to_string(), + "game/effects/mod.rs:6531".to_string(), + "game/effects/mod.rs:9726".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. diff --git a/crates/engine/src/parser/oracle_replacement.rs b/crates/engine/src/parser/oracle_replacement.rs index 711c802058..ea41833a7c 100644 --- a/crates/engine/src/parser/oracle_replacement.rs +++ b/crates/engine/src/parser/oracle_replacement.rs @@ -3381,7 +3381,7 @@ fn attach_zone_to_filter(filter: TargetFilter, zone: Zone) -> TargetFilter { /// /// Delegates to the existing effect-chain parser. The "when you do" connector /// maps to `AbilityCondition::WhenYouDo`; the "if you do" connector maps to -/// `AbilityCondition::EffectOutcome { OptionalEffectPerformed }`. On the +/// `AbilityCondition::EffectOutcome { OptionalEffectPerformed }`. /// At this owning accepted-branch seam, literal `When` keeps its CR 603.12 /// `WhenYouDo` creation gate, while literal `If` has its CR 608.2c performed gate /// consumed because reaching the replacement's execute branch proves acceptance. diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 479fb5eb16..20943ef7ff 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -20766,6 +20766,26 @@ impl GameState { dt.ability.clear_trigger_identity_recursive(); dt.provenance = DelayedInstallIdentity::LegacyDelayed; } + // The triggered-mana sidecar is eq-compared like its sibling carriers + // above, and it holds both ability carriers and a globally advancing + // `SettlementNodeOrdinal`. The live loop-sample sites only fire at + // `WaitingFor::Priority`, where the sidecar is None today — this + // normalization keeps that a non-load-bearing coincidence rather than a + // hidden precondition of CR 104.4b detection. + if let Some(resume) = clone.pending_triggered_mana_resume.as_mut() { + resume.current.pending.ability.clear_trigger_identity_recursive(); + for ctx in resume.accepted_tail.iter_mut() { + ctx.pending.ability.clear_trigger_identity_recursive(); + } + for batch in resume.collected_batches.iter_mut() { + for ctx in batch.contexts.iter_mut() { + ctx.pending.ability.clear_trigger_identity_recursive(); + } + } + resume.rules_execution_node = crate::types::resolved_commands::RulesExecutionNodeRef::TriggeredMana( + crate::types::resolved_commands::SettlementNodeOrdinal(0), + ); + } for epic in clone.epic_effects.iter_mut() { epic.spell.clear_trigger_identity_recursive(); } diff --git a/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs b/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs index e5b2628f38..3f8a49b871 100644 --- a/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs +++ b/crates/engine/tests/integration/ancient_brass_dragon_roll_d20.rs @@ -232,6 +232,18 @@ fn ancient_brass_dragon_zero_targets_is_clean_no_op() { } } + // Positive reach guard: the loop's error/other-prompt breaks may exit + // before the observation point, and the negative assertions below would + // vacuously pass on an unresolved trigger. Prove the settled window first. + assert!( + matches!(runner.state().waiting_for, WaitingFor::Priority { .. }) + && runner.state().stack.is_empty(), + "the zero-target reflexive must resolve to a settled priority window with an \ + empty stack before the no-op is measured, got {:?} with {} stack entries", + runner.state().waiting_for, + runner.state().stack.len() + ); + // No graveyard card moved (still in a graveyard) and the battlefield count // did not grow from reanimation. for (id, _) in &grave { From 0cabc8ae1999c9183613ca27e6fbbb31ff3c44f9 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Thu, 13 Aug 2026 03:06:38 -0700 Subject: [PATCH 03/11] style(PR-7332): format maintainer port --- crates/engine/src/game/triggers.rs | 4 ++-- crates/engine/src/types/game_state.rs | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/game/triggers.rs b/crates/engine/src/game/triggers.rs index 2979b4df21..139a35c006 100644 --- a/crates/engine/src/game/triggers.rs +++ b/crates/engine/src/game/triggers.rs @@ -21,8 +21,8 @@ use crate::types::game_state::{ GameState, LatchedBatchedTrigger, LatchedSuppressTrigger, LogicalZoneChangeGroup, LogicalZoneChangeTerminalOutcome, MayTriggerAutoChoiceKey, MayTriggerOrigin, ProductionOverride, StackEntry, StackEntryKind, SyntheticTriggerProvenance, - TargetSelectionConstraint, TargetSelectionSlot, - TriggerObservationTime, TriggerSourceContext, WaitingFor, + TargetSelectionConstraint, TargetSelectionSlot, TriggerObservationTime, TriggerSourceContext, + WaitingFor, }; use crate::types::identifiers::{ DelayedInstallIdentity, DelayedTriggerInstanceId, DelayedTriggerOrigin, DelayedTriggerToken, diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 84681b5899..98db14a9b8 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -20983,7 +20983,11 @@ impl GameState { // normalization keeps that a non-load-bearing coincidence rather than a // hidden precondition of CR 104.4b detection. if let Some(resume) = clone.pending_triggered_mana_resume.as_mut() { - resume.current.pending.ability.clear_trigger_identity_recursive(); + resume + .current + .pending + .ability + .clear_trigger_identity_recursive(); for ctx in resume.accepted_tail.iter_mut() { ctx.pending.ability.clear_trigger_identity_recursive(); } @@ -20992,9 +20996,10 @@ impl GameState { ctx.pending.ability.clear_trigger_identity_recursive(); } } - resume.rules_execution_node = crate::types::resolved_commands::RulesExecutionNodeRef::TriggeredMana( - crate::types::resolved_commands::SettlementNodeOrdinal(0), - ); + resume.rules_execution_node = + crate::types::resolved_commands::RulesExecutionNodeRef::TriggeredMana( + crate::types::resolved_commands::SettlementNodeOrdinal(0), + ); } for epic in clone.epic_effects.iter_mut() { epic.spell.clear_trigger_identity_recursive(); From b367262a0fe3f616f3aa05e8aa52d00866146b09 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 20:44:47 -0700 Subject: [PATCH 04/11] fix(PR-7332): preserve current main during conflict resolution --- crates/engine/src/game/ability_scan.rs | 20 +- crates/engine/src/game/effects/vote.rs | 8 +- crates/engine/src/game/engine.rs | 3374 ++++++++++++++++++++++-- crates/engine/src/game/triggers.rs | 2953 ++++++++++++++++++++- crates/engine/src/types/ability.rs | 2180 ++++++++++++++- 5 files changed, 8161 insertions(+), 374 deletions(-) diff --git a/crates/engine/src/game/ability_scan.rs b/crates/engine/src/game/ability_scan.rs index 42fa20d5d3..89881d55aa 100644 --- a/crates/engine/src/game/ability_scan.rs +++ b/crates/engine/src/game/ability_scan.rs @@ -252,7 +252,6 @@ fn resolved_ability_axes(a: &ResolvedAbility, mode: ScanMode) -> Axes { copy_count_status: _, // status tag forward_result: _, // bool distribution: _, // concrete pre-assigned (TargetRef, u32) portions - distribute: _, // announcement unit tag/string, no resolution-time dynamic read chosen_x: _, // concrete cast-time X cost_paid_object: _, // concrete captured-object snapshot cost_paid_object_ids: _, // concrete captured-object ids (issue #4948) @@ -265,6 +264,7 @@ fn resolved_ability_axes(a: &ResolvedAbility, mode: ScanMode) -> Axes { replacement_applied: _, // replacement provenance set, no dynamic read sub_link: _, // SubAbilityLink kind tag sibling_condition: _, // SiblingCondition replication marker, no dynamic read + distribute: _, // announcement unit tag/string, no resolution-time dynamic read parent_target_missing_reason: _, // seam flag } = a; @@ -6389,6 +6389,24 @@ mod tests { ) } + #[test] + fn unassigned_distribution_unit_adds_no_dynamic_read_axis() { + let base = fixed_drain(); + let mut divided = base.clone(); + divided.distribute = Some(crate::types::game_state::DistributionUnit::Life); + + let base_axes = resolved_ability_axes(&base, ScanMode::Conservative); + let divided_axes = resolved_ability_axes(÷d, ScanMode::Conservative); + assert_eq!( + (base_axes.event, base_axes.sibling, base_axes.projected), + ( + divided_axes.event, + divided_axes.sibling, + divided_axes.projected + ) + ); + } + // ---- P0/P2: the ScanMode split + descending object-growth firewall ---- /// A read-free vanilla token (Presence of Gond's "1/1 green Elf Warrior"): diff --git a/crates/engine/src/game/effects/vote.rs b/crates/engine/src/game/effects/vote.rs index d8ce4b0cf7..c372bcda49 100644 --- a/crates/engine/src/game/effects/vote.rs +++ b/crates/engine/src/game/effects/vote.rs @@ -1874,7 +1874,9 @@ mod tests { Box::new(AbilityDefinition::new(AbilityKind::Spell, Effect::NoOp)), Box::new(AbilityDefinition::new( AbilityKind::Spell, - Effect::BecomeMonarch, + Effect::BecomeMonarch { + target: crate::types::ability::TargetFilter::Controller, + }, )), ]; let options = vec!["innocent".to_string(), "guilty".to_string()]; @@ -1920,7 +1922,9 @@ mod tests { Box::new(AbilityDefinition::new(AbilityKind::Spell, Effect::NoOp)), Box::new(AbilityDefinition::new( AbilityKind::Spell, - Effect::BecomeMonarch, + Effect::BecomeMonarch { + target: crate::types::ability::TargetFilter::Controller, + }, )), ]; let options = vec!["innocent".to_string(), "guilty".to_string()]; diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 2768fec04a..8cac679b8c 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -8,12 +8,13 @@ use crate::types::ability::{EffectScope, TapStateChange}; use crate::types::actions::{ DebugAction, GameAction, MayTriggerAutoChoiceOp, PriorityYieldOp, TriggerOrderTemplateOp, }; -use crate::types::events::{BendingType, ContestRound, GameEvent, ManaTapState, PlayerActionKind}; +use crate::types::events::{BendingType, ContestRound, GameEvent, ManaTapState}; use crate::types::game_state::{ ActionResult, AssistState, AutoMayChoice, AutoPassMode, AutoPassRequest, CastOfferKind, CastingVariant, ConvokeMode, CostResume, GameState, LandPlayRecord, LoopDetectionMode, - ManaAbilityResume, MayTriggerAutoChoiceKey, PayCostKind, PendingCostMoveResume, RetargetScope, - StackEntry, StackEntryKind, WaitingFor, + ManaAbilityResume, MayTriggerAutoChoiceKey, PayCostKind, PendingCostMoveResume, + PendingCounterPostAction, PendingEffectResolved, RetargetScope, StackEntry, StackEntryKind, + WaitingFor, }; use crate::types::identifiers::{CardId, DelayedTriggerOrigin, ObjectId, ObjectIncarnationRef}; use crate::types::match_config::MatchType; @@ -1451,6 +1452,10 @@ fn reconcile_terminal_result(state: &mut GameState, result: &mut ActionResult) { predicted_winner: None, certificate, schema, + // CR 732.2a: the object-growth path re-derives its pins at materialize time + // from the carried recast template, so this offer states no engine-side + // declaration of its own. + declaration: None, }; result.waiting_for = state.waiting_for.clone(); } @@ -1527,6 +1532,9 @@ fn interactive_loop_bridge(state: &mut GameState, result: &mut ActionResult) { predicted_winner: Some(winner), certificate, schema, + // CR 732.2a: Path A publishes no decision points at all (the pin list above is + // empty), so there is nothing for a declaration to pin. + declaration: None, }; result.waiting_for = state.waiting_for.clone(); } @@ -2071,13 +2079,10 @@ fn certified_bounded_cycle_offer<'a>( verdicts: &mut crate::analysis::resource::PeriodVerdicts<'a>, cert_out: &mut Option, ) -> Result { - use crate::analysis::decision_template::{ - DecisionPoint, DecisionPointKind, DecisionSlot, IterationCount, - }; + use crate::analysis::decision_template::{DecisionPoint, DecisionSlot, IterationCount}; use crate::analysis::resource::{ certified_period_touch, PeriodCertification, PeriodTouch, PeriodicDelta, ResourceVector, }; - use crate::types::ability::TargetRef; let cur = ResourceVector::snapshot(state); // Written as an explicit newest-first walk rather than `find_map` because the candidate @@ -2290,20 +2295,38 @@ fn certified_bounded_cycle_offer<'a>( return Err(BoundedOfferRefusal::UnspecifiedChoiceWindow); } - // (7) THE BOUND. `declarable_victims` is the union of the published slots' legal targets - // — EMPTY for the untargeted class, where the victims are already in `delta.life`. + // (7) THE BOUND, derived from the ANNOUNCEMENT authority — never from `points`. + // + // ⚠ THE PUBLISHED POINT SET IS THE WRONG INPUT HERE, and reading it from there was a + // measured fail-OPEN. Publication answers CR 732.2a ("a sequence of game choices"), so + // step (6)'s mint WITHHOLDS the point for a FORCED announcement — correctly, since the + // announcing player makes no choice. The bound answers CR 704.5a ("if a player has 0 or + // less life, that player loses the game"), and a forced victim loses that life exactly as + // a chosen one does. Deriving the bound from `points` therefore made the CR 732.2a + // withhold drop the forced victim out of `declarable_victims`, charging it bare + // `observed_life_loss` instead of `observed_life_loss.max(0) + declared_life_magnitude` — + // so `max_iterations` GREW, and the offer stated more legal repetitions than are legal. + // Measured on an ordinary forced 2p targeted drain: 9 charged vs 19 uncharged; on a + // victim whose measured period NETS A LIFE GAIN the uncharged form leaves + // `elimination_bounds`' `narrow` guard (`magnitude > 0`) unfired and DISARMS the life + // axis at `MAX_SHORTCUT_CYCLES` entirely. + // + // `bounded_cycle_charged_targets_for_window` reads the SAME acceptance authority the + // point mint does (`entry_announces`), so the charged SLOT set is a superset of the + // published `Targets` slots by construction: on a board where every announcement is the + // proposer's own choice — every tracked dump today — this derivation is value-identical to + // the one it replaces. ⚠ THE SUPERSET IS OVER SLOTS ONLY. A repeated slot's per-slot LEGAL + // set is what `declarable_victims` below reads, and the two mints keep different frames of + // a repeat, so that set is made a superset separately, by the charging mint's UNION dedup + // (see its doc for the monotonicity proof). Claiming the per-slot legal set is a superset + // "by construction" from the shared acceptance authority alone is FALSE. + let charged_targets = bounded_cycle_charged_targets_for_window(&touch, proposer); + // `declarable_victims` is the union of those announcements' legal PLAYER sets — EMPTY for + // the untargeted class, where the victims are already in `delta.life`. let declarable_victims: Vec = { - let mut v: Vec = points + let mut v: Vec = charged_targets .iter() - .filter_map(|p| match &p.kind { - DecisionPointKind::Targets { legal_targets, .. } => Some(legal_targets), - _ => None, - }) - .flatten() - .filter_map(|t| match t { - TargetRef::Player(p) => Some(*p), - _ => None, - }) + .flat_map(|(_, victims)| victims.iter().copied()) .collect(); v.sort_unstable(); v.dedup(); @@ -2311,20 +2334,19 @@ fn certified_bounded_cycle_offer<'a>( }; // CR 704.5a: what ONE repetition charges to whichever seat a slot's pin names. The // max-vs-sum reasoning, the gain clamp and the fail-closed direction live on the - // function; `elimination_bounds` then sums the published slots per declarable victim. + // function; `elimination_bounds` then sums the charged slots per declarable victim. // Extracted rather than inlined so the fork has a callable seam. ⚠ THE "`victim_slot` IS // EMPTY ON EVERY TRAJECTORY THAT OFFERS TODAY" NOTE THAT STOOD HERE IS FALSIFIED, and is // replaced rather than softened: the answer-beat sampling site in `apply_action` announces // the entries a FORCED pre-priority window puts on the stack, and a CR 608.2b `Targets` - // declaration is exactly the shape that resolves across one. On the F4 boards `points` now - // carries Torch's `Targets` point, so this value is NOT dropped — it reaches + // declaration is exactly the shape that resolves across one. On the F4 boards the + // announcement carries Torch's target slot, so this value is NOT dropped — it reaches // `elimination_bounds` in production and `r1_the_bounded_offer_fires_on_the_real_f4_dump` // re-derives the published bound with a non-zero declared term. let worst_seat_life_loss: i64 = periodic.delta.worst_seat_life_loss(); - periodic.victim_slot = points + periodic.victim_slot = charged_targets .iter() - .filter(|p| matches!(p.kind, DecisionPointKind::Targets { .. })) - .map(|p| (p.slot.clone(), worst_seat_life_loss)) + .map(|(slot, _)| (slot.clone(), worst_seat_life_loss)) .collect(); // `.cloned()`, not `.copied()`: `(DecisionSlot, i64)` is not `Copy`. let slot_magnitude: std::collections::BTreeMap = @@ -2365,14 +2387,161 @@ fn certified_bounded_cycle_offer<'a>( IterationCount::Fixed(max_iterations), max_iterations, ); + // (10) The DECLARATION the engine can already specify for this offer, read out of the + // answer journal the same window populated. Built AFTER the schema because `points` is + // moved into `build_shortcut_schema`, and taking `&schema` keeps one point list rather + // than two. + let declaration = build_bounded_declaration(state, proposer, &schema); Ok(WaitingFor::LoopShortcut { proposer, predicted_winner: None, certificate, schema, + declaration, }) } +/// CR 732.2a: the declaration THIS offer can already state, derived from what the proposer +/// actually answered at each published point — never from a constant and never from the +/// declaring client. +/// +/// CR 732.2a describes a shortcut proposal as "a sequence of game choices, for all players, +/// that may be legally taken based on the current game state and the predictable results of +/// the sequence of choices". Every published point of `schema` is one such choice; the +/// `(DecisionSlot, PlayerId)` journal holds the answer the proposer gave it during the +/// detection window (CR 601.2c announcements via `record_trigger_target_answer`, CR 603.5 +/// "may" answers via the `DecideOptionalEffect` arm). This function is the single authority +/// for turning that observation into a [`DecisionTemplate`], so the AI candidate generator and +/// the per-viewer projection read ONE value instead of each deriving their own. +/// +/// **Not a duplicate authority.** `game::interaction::materialize_loop_shortcut_response` +/// builds a conformant `DecisionTemplate` of the same shape (same `owner` / `decisions` / +/// `ReplayMode::Scheduled` / `DecisionGroupKey::from_sources` / `(!points.is_empty())` guard), +/// but from the CLIENT'S OWN submitted pins — a human's picks. This one is built from the +/// ENGINE'S OWN observed answers. Two inputs, one shape; a reviewer reading only the shape +/// would otherwise see duplication. +/// +/// # PUBLISHED IS VALIDATED — `is_some()` means "the declare handler will take this" +/// +/// `ai_support::candidates` gates its `DeclareShortcut` candidate on `declaration.is_some()` +/// and hands this very template to `handle_declare_shortcut`, which validates it. So the +/// publisher must not be able to emit anything that handler would refuse: step (5) runs the +/// SHARED [`crate::analysis::decision_template::declaration_conforms`] — the same coverage + +/// value-legality predicate that handler and the human ingress run — rather than a third +/// derivation of `required` alongside theirs. +/// +/// The range is `shortcut_validated_range(&schema.iteration_count, ..)`, i.e. this offer's own +/// ceiling, because it is the WIDEST count any declarer may name against this schema +/// (`is_bounded()` publishers set `iteration_count == Fixed(max_iterations)` and the handler +/// rejects anything above the cap). `validate_pins` re-checks `0..range`, so passing at the +/// ceiling implies passing at every shorter `Fixed(n)` the handler could be given. +/// +/// LATENT, NOT LIVE, and the distinction is not decoration: no tracked board reaches a +/// declaration this refuses — row D1 measures both gates passing at the full range on all +/// three dumps — because the publisher copies `legal_targets` from the same announcement the +/// journal answer came from, and `record_trigger_target_answer` bails above one announced +/// slot while this schema hard-codes `min/max: 1`. That agreement is an accident of two +/// functions with two predicates; step (5) is what makes it an invariant. +/// +/// FAIL-CLOSED on every uncertainty, because a wrong pin is worse than no offer: +/// +/// * an empty point set publishes no declaration at all — a declaration against an empty +/// schema would be the one shape `handle_declare_shortcut` validates neither +/// `predictability_gate` nor `validate_pins` against (both live inside its +/// `if !offer.schema.points.is_empty()` block); +/// * `None` (that seat never answered this slot) and [`LoopAnswer::Conflicted`] (it answered +/// two ways — see that type: an engine-capability refusal, NOT a CR 732.2a mandate) are the +/// SAME disposition here, because neither names a single answer to pin; +/// * the `(kind, value)` match is WILDCARD-FREE, so a future `DecisionPointKind` or +/// `LoopAnswerValue` variant gets a compile-time visit here instead of a silent pin. The +/// two kind/value MISMATCH groups return `None` rather than `unreachable!` because what +/// makes them unreachable is a key-shape agreement between publisher and writer, not a type +/// guarantee. +fn build_bounded_declaration( + state: &GameState, + proposer: PlayerId, + schema: &crate::analysis::decision_template::ShortcutDecisionSchema, +) -> Option { + use crate::analysis::decision_template::{ + DecisionGroupKey, DecisionKind, DecisionPointKind, DecisionTemplate, LoopAnswer, + LoopAnswerValue, PinnedDecision, ReplayMode, + }; + // (1) D4's grounds: an empty schema publishes no declaration. + if schema.points.is_empty() { + return None; + } + let mut decisions = Vec::with_capacity(schema.points.len()); + for point in &schema.points { + // (2) The journal read, under the PROPOSER's own key — the same key + // `record_trigger_target_answer` and the `DecideOptionalEffect` arm write under. + let LoopAnswer::Uniform(value) = state.loop_answer(&point.slot, proposer)? else { + return None; + }; + // (3) The wildcard-free (kind, value) match. + decisions.push(match (&point.kind, value) { + // CR 603.5: the "may" gate, answered Take or Decline. + (DecisionPointKind::MayChoice, LoopAnswerValue::May(take)) => { + PinnedDecision::MayChoice { + slot: point.slot.clone(), + take, + } + } + // CR 601.2c + CR 608.2b: the announced targets for this slot, in announcement + // order, re-checked for legality at every resolution. + (DecisionPointKind::Targets { .. }, LoopAnswerValue::Targets(targets)) => { + PinnedDecision::Targets { + slot: point.slot.clone(), + targets, + } + } + // Kind/value MISMATCH — the publisher and the journal writer disagree about what + // this slot is. Fail closed. + (DecisionPointKind::MayChoice, LoopAnswerValue::Targets(_)) + | (DecisionPointKind::Targets { .. }, LoopAnswerValue::May(_)) => return None, + // CR 700.2 modal / CR 732.6 "[A] unless [B]" / CR 601.2h + CR 702.51a convoke / + // CR 608.2d + CR 605.3b mana color: kinds this offer's publisher + // (`bounded_cycle_pin_slots_for_window`, which mints only `Targets` and + // `MayChoice`) cannot produce today. `LoopAnswerValue` carries no answer shape for + // any of them, so there is nothing to pin even when the slot IS journalled. + ( + DecisionPointKind::Mode { .. } + | DecisionPointKind::UnlessBreak + | DecisionPointKind::ConvokeTaps { .. } + | DecisionPointKind::ManaColor { .. }, + LoopAnswerValue::May(_) | LoopAnswerValue::Targets(_), + ) => return None, + }); + } + // (4) The template. `replay.count` carries the offer's own SUGGESTION; the driving count + // comes off `GameAction::DeclareShortcut` and nothing reads this copy (see + // `build_recast_template`'s note and `analysis::decision_template::resolve`'s doc). + let template = DecisionTemplate { + owner: proposer, + decisions, + replay: ReplayMode::Scheduled { + count: schema.iteration_count.clone(), + }, + key: DecisionGroupKey::from_sources( + &schema + .points + .iter() + .map(|point| point.slot.source.clone()) + .collect::>(), + DecisionKind::LoopChoice, + ), + }; + // (5) VALIDATE BEFORE PUBLISHING — the same authority `handle_declare_shortcut` accepts + // under. See this function's "Published is validated" doc section for why the range is the + // schema's OWN count and why this is not a third derivation. + crate::analysis::decision_template::declaration_conforms( + schema, + &template, + shortcut_validated_range(&schema.iteration_count, Some(&template)), + state, + ) + .then_some(template) +} + /// CR 704.5a / CR 704.5c: a determinate lethal drain (0-or-less life / 10-poison) repeats /// UntilLethal; every other CR 732.1b win seeds a `Fixed(1)` frontend count picker. Extracted /// as a pure classifier so the exhaustive `WinKind` mapping is unit-testable without a @@ -2494,7 +2663,7 @@ fn pinned_decisions_to_points( /// CR 115.2 + CR 732.2a: does the ability's HEAD effect declare the "target opponent" PLAYER /// filter — a `Typed` filter with no type constraints, no object properties, and /// `controller: Opponent`, the shape `game::targeting::find_legal_targets` collapses to -/// players-only (`crates/engine/src/game/targeting.rs:192-193`)? +/// players-only? /// /// SHAPE ACCEPTANCE ONLY, and the `bool` return is what enforces it: the published legal /// set must come from the announcement authority (`ability_utils::build_target_slots`), never @@ -2531,9 +2700,11 @@ fn declares_opponent_player_target(ability: &crate::types::ability::ResolvedAbil /// What ONE accepted stack entry publishes: the slot keys, plus the legal set the /// ANNOUNCEMENT authority itself built for the target slot. pub(crate) struct EntryPinSlots { - /// CR 115.2 target choice — `index: 0`. `None` for shape (B), the may-only entry: - /// announcing it surfaces NO choice at all (`targets.is_empty()` and zero built slots), - /// so there is no CR 601.2c announcement choice for a pin to specify. + /// CR 115.2 target choice — `index: 0`. `None` in TWO shapes, and both are the absence of + /// a CR 601.2c *choice* rather than the absence of a target: shape (B), the may-only entry, + /// announces NO slot at all (`targets.is_empty()` and zero built slots); shape (A′) + /// announces one whose assignment is FORCED (`forced_unique_targeting`), which CR 732.2a + /// does not count as a game choice and which the dispatcher answers itself. pub(crate) target: Option, /// CR 603.5 "may" gate — `index: 1`, `Some` only if `ability.optional` — the mint /// additionally refuses on recipient, stored auto-choice and prompt-cardinality grounds @@ -2546,13 +2717,111 @@ pub(crate) struct EntryPinSlots { /// exactly one mandatory choice. Deriving it a second time from the head effect's /// filter would let the two disagree about WHICH choice is being published, which is /// the same class of divergence the cardinality conjunct closes about HOW MANY. - /// Empty for shape (B), which publishes no target slot to carry a legal set for. + /// Empty for shapes (B) and (A′), neither of which publishes a target slot to carry a + /// legal set for. + pub(crate) legal_targets: Vec, +} + +/// CR 601.2c (reached for a triggered ability via CR 603.3d): the ONE target an accepted +/// entry ANNOUNCES — the slot key, the legal set the announcement authority itself built, +/// and whether announcing it is a game CHOICE. +/// +/// THE TWO QUESTIONS THIS TYPE KEEPS APART, because conflating them was a measured +/// fail-OPEN. PUBLICATION answers CR 732.2a — *is this a game choice the player makes?* — +/// and shapes the schema. CHARGING answers CR 704.5a — *which seat is charged, and how +/// much?* — and shapes the bound. A forced announcement is not a choice, so it is withheld +/// from the schema; its victim still loses the life, so it is still charged. Deriving the +/// bound from the PUBLISHED point set made the CR 732.2a withhold silently drop the forced +/// victim into `elimination_bounds`' cheaper arm and RAISE `max_iterations`. +pub(crate) struct AnnouncedTarget { + /// CR 115.2 target choice — `index: 0`, the same key a published point carries, so a + /// charge and a publication of the same announcement can never land on different slots. + pub(crate) slot: crate::analysis::decision_template::DecisionSlot, + /// The legal set of the ONE announcement slot, taken VERBATIM from + /// `ability_utils::build_target_slots` — the same authority that decided there is + /// exactly one mandatory choice, and the same one `forced_unique_targeting` rebuilds + /// slots with. Never a second derivation from the head effect's filter. pub(crate) legal_targets: Vec, + pub(crate) announcement: TargetAnnouncement, +} + +/// CR 732.2a: whether announcing an [`AnnouncedTarget`] is a *game choice the PROPOSER makes +/// at a prompt of their own*. +/// +/// Not a `bool`: the two arms name two different CR readings, and the whole defect this +/// type exists to prevent came from a caller re-deriving "was it a choice?" from a +/// downstream artifact instead of reading the answer. +/// +/// ⚠ THE QUESTION IS THREE-AXIS, and this type answered ONE of them while carrying the name of +/// all three. CR 601.2c routes an announcement by WHO announces (`target_chooser`) as well as +/// by HOW MANY assignments are legal (`forced_unique_targeting`), and CR 115.1 is overridden +/// outright when the game selects at random (`TargetSelectionMode`). Only the middle axis was +/// read. The publication BEHAVIOUR that gap produced predates the commit this type ships in — +/// what was new is a named authority claiming to answer "is announcing this a game choice the +/// proposer makes" while covering one of its three members. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum TargetAnnouncement { + /// The PROPOSER announces this target, at a prompt that is really raised for them: + /// no other seat announces it (CR 601.2c `target_chooser`), the game does not select it + /// (CR 115.1 vs `TargetSelectionMode`), and `forced_unique_targeting` is false — so + /// `triggers::prepare_trigger_targets` routes it to `NeedsPlayerChoice`, a + /// `WaitingFor::TriggerTargetSelection` comes up under the proposer's own seat, and + /// `record_trigger_target_answer` can journal an answer AT THIS SLOT AND UNDER THIS KEY. + /// + /// ⚠ NOT "two or more legal assignments". That claim stood here and is FALSE: the conjunct + /// the code applies is the NEGATION of `forced_unique_targeting`, i.e. + /// `auto_select_targets_for_ability != Ok(Some(_))`. Two-or-more is its principal member, + /// but `Err` ("No legal target combinations available") negates it too, so this variant + /// carries no assignment COUNT — only the "nobody else and nothing else announced it, and + /// the dispatcher did not settle it" reading above. + Chosen, + /// The proposer makes no such announcement, so CR 732.2a publishes no decision point for + /// it. THREE DISJOINT ROUTES, each named at the code that takes it: + /// + /// * `forced_unique_targeting` — exactly one legal assignment, so the announcement is + /// determined rather than chosen and `triggers::prepare_trigger_targets` routes it to + /// `AutoAssigned` without asking anyone. + /// * CR 601.2c `target_chooser` ("of an opponent's choice") — a prompt IS raised, but for + /// ANOTHER seat. `ability_utils::auto_select_targets_for_ability` early-returns + /// `Ok(None)` whenever any slot carries a chooser, so `forced_unique_targeting` is false + /// even with ONE legal assignment. The writer journals under the ANNOUNCING seat while + /// the consumer reads `loop_answer(slot, proposer)` — an unanswerable published point, + /// which is the exact undeclarable-offer condition the bounded offer exists to remove. + /// * `TargetSelectionMode` other than `Chosen` — the game selects (CR 115.1 is overridden; + /// `triggers::prepare_trigger_targets` calls `random_select_targets_for_ability` and + /// routes to `AutoAssigned`), so no prompt is ever raised and a pin would be a + /// designation the RNG contradicts at drive time. + /// + /// CHARGED ALL THE SAME: CR 704.5a asks which seat loses how much life, and nobody having + /// made the choice changes neither who pays nor how much. Only the CR 732.2a publication + /// reader acts on this value. + NotProposerChoice, } -/// CR 732.2a: the per-iteration choice slots ONE stack entry publishes for `proposer`, or +/// CR 601.2c + CR 603.5: everything ONE accepted stack entry ANNOUNCES for `proposer`, +/// BEFORE the CR 732.2a question of how much of it is a published game choice. +/// +/// THE SINGLE ACCEPTANCE AUTHORITY. Both [`entry_publishes_pin_slots`] (publication) and +/// [`bounded_cycle_charged_targets_for_window`] (CR 704.5a charging) are thin readers of +/// this one function, so the two can never disagree about WHICH entries are in the cycle — +/// only about which of their announcements is a published choice. Two independent +/// acceptance chains that could disagree is exactly the shape gate (3)'s single-authority +/// rule exists to forbid. +struct EntryAnnouncement { + target: Option, + may: Option, +} + +/// CR 732.2a: the per-iteration choice slots ONE stack entry PUBLISHES for `proposer`, or /// `None` when it publishes none. /// +/// A THIN READER of [`entry_announces`], which owns every acceptance conjunct documented +/// below; this function contributes exactly one thing on top of it — the CR 732.2a +/// publication decision (a `Forced` announcement is not a game choice, so no point is +/// published for it). The CR 704.5a charging reader +/// ([`bounded_cycle_charged_targets_for_window`]) reads the SAME announcement, so the two +/// cannot disagree about which entries are in the cycle. +/// /// SINGLE AUTHORITY, and that is the whole point of its existence: the MINT /// ([`bounded_cycle_pin_slots_for_window`]) maps it over the certified period's announced /// pairs, of which `state.stack` is the zero-window degenerate case, and the RELIEF @@ -2573,6 +2842,12 @@ pub(crate) struct EntryPinSlots { /// `targets.is_empty()` and zero built slots); and its source object still exists, so the /// slot can re-bind (CR 400.7 incarnation, fail-closed on absence). /// +/// Shape (A) carries one further conjunct that is a property of the BOARD rather than of the +/// ability: the announcement must actually be a CHOICE. A slot with exactly one legal +/// assignment is FORCED (shape (A′)) — `triggers::prepare_trigger_targets` announces it +/// without asking anyone — so it publishes its CR 603.5 gate alone, or nothing. See the +/// `forced_unique_targeting` call in the body for why publishing it is the undeclarable case. +/// /// SCOPE OF THE ANSWER: because the relief is a `continue` at gate (3), the relief /// predicate must be no coarser than EVERY fact `stack_entry_has_no_ordering_input` /// rejects on — not just the target one. Correspondence, in that function's own order: @@ -2585,6 +2860,43 @@ pub(crate) fn entry_publishes_pin_slots( entry: &StackEntry, proposer: PlayerId, ) -> Option { + let announced = entry_announces(state, entry, proposer)?; + // CR 732.2a: a shortcut describes "a sequence of game choices", so ONLY a `Chosen` + // announcement earns a decision point. `NotProposerChoice` is withheld — see + // [`TargetAnnouncement::NotProposerChoice`] and the shape (A′) block in + // [`entry_announces`]. + let published = announced + .target + .filter(|target| target.announcement == TargetAnnouncement::Chosen); + // An entry that publishes NOTHING publishes no slot set at all. This is the fail-closed + // reading shapes (B) and (A′) each carried inline as a `Some(may?)`, stated ONCE here + // instead of once per shape; the observable result is identical. + if published.is_none() && announced.may.is_none() { + return None; + } + let (target, legal_targets) = match published { + Some(target) => (Some(target.slot), target.legal_targets), + None => (None, vec![]), + }; + Some(EntryPinSlots { + target, + may: announced.may, + legal_targets, + }) +} + +/// CR 601.2c + CR 603.5 (reached for a triggered ability via CR 603.3d): what ONE stack +/// entry ANNOUNCES for `proposer`, or `None` when the entry is not one this cycle accepts. +/// +/// Every acceptance conjunct documented on [`entry_publishes_pin_slots`] lives here. What +/// does NOT live here is the CR 732.2a publication decision: this function reports whether +/// the announcement is `Chosen` or `Forced` and lets its two readers apply that fact to the +/// question each is answering — the schema (publication) or the bound (CR 704.5a charging). +fn entry_announces( + state: &GameState, + entry: &StackEntry, + proposer: PlayerId, +) -> Option { use crate::analysis::decision_template::DecisionSlot; if entry.controller != proposer { return None; @@ -2655,8 +2967,13 @@ pub(crate) fn entry_publishes_pin_slots( // player shape while the ONE slot the announcement actually surfaces belongs to a // chained sub-ability targeting OBJECTS (measured: head `LoseLife` at // `TargetChoiceTiming::Resolution` contributing 0 slots + a chained - // `LoseLife{Typed{[Creature]}}` contributing 1, legal set three objects). A - // `TargetPin::Player` cannot specify such a choice, so publishing it would hand + // `LoseLife{Typed{[Creature]}}` contributing 1, legal set three objects). NO SEAT PIN + // can specify such a choice — neither spelling: not the CR 115.10a + // `TargetPin::Player`, and not the CR 601.2c + // `Scheduled(Constant(Ranking::one(AnnouncementSubject::Seat(..))))` the announcement + // journal now emits, since both resolve to a `ConcreteTarget::Player` and the slot + // wants objects. The provenance split changes which authority judges a seat, not what + // a seat can denote, so this conjunct is untouched by it. Publishing anyway would hand // gate (3)'s `continue` a slot no pin can answer. // // `Err` (no legal target, CR 603.3d) also yields `None` — fail-closed, matching this @@ -2723,10 +3040,7 @@ pub(crate) fn entry_publishes_pin_slots( .as_ref() .is_none_or(|key| state.may_trigger_auto_choice(key).is_none()) }) - .map(|_| DecisionSlot { - source: source.clone(), - index: 1, - }); + .map(|_| DecisionSlot::may(source.clone())); let mut slots = super::ability_utils::build_target_slots(state, ability).ok()?; // SHAPE (B) — may-only. The announcement authority surfaced NO choice, so there is no // CR 601.2c target for a pin to specify and the entry publishes its CR 603.5 gate @@ -2734,16 +3048,14 @@ pub(crate) fn entry_publishes_pin_slots( // nothing" rather than "declared something the builder declined"; `optional` is // inherited from the `may` expression, which is `None` without it. A `may` the three // conjunct groups above suppressed leaves shape (B) with NO slot at all, so the whole - // entry publishes `None` — the fail-closed direction. + // entry publishes `None` — the fail-closed direction, now applied by the publication + // reader rather than restated here. Shape (B) also charges NOTHING under CR 704.5a: + // there is no announced target, so there is no seat a declaration could aim at. if slots.is_empty() { if !ability.targets.is_empty() { return None; } - return Some(EntryPinSlots { - target: None, - may: Some(may?), - legal_targets: vec![], - }); + return Some(EntryAnnouncement { target: None, may }); } if slots.len() != 1 { return None; @@ -2766,12 +3078,97 @@ pub(crate) fn entry_publishes_pin_slots( if !declares_opponent_player_target(ability) { return None; } - // Shape (A) — targeted. Index 1 is kept for the may slot in BOTH shapes, so slot - // identity is stable across them. - Some(EntryPinSlots { - target: Some(DecisionSlot { source, index: 0 }), + // SHAPE (A′) — NOT THE PROPOSER'S CHOICE, so there is no CHOICE OF THEIRS to publish. + // CR 732.2a describes a shortcut as "a sequence of game choices, for all players": a + // decision point stands for a game choice, and the proposer makes none of these three. + // CR 603.3d routes a trigger's announcement through CR 601.2c–d, and CR 601.2c has the + // player "announce their choice of an appropriate object or player for each target". + // + // WITHHOLD, never journal-an-auto-selection, and the difference is observable rather + // than stylistic: `triggers::prepare_trigger_targets` sends this exact predicate's + // `Ok(Some(..))` to `PreparedTriggerTargets::AutoAssigned`, so no + // `WaitingFor::TriggerTargetSelection` is ever raised, so `record_trigger_target_answer` + // — whose only two call sites are that prompt's reducer arms — never runs. A point + // published here would demand a `predictability_gate` answer no writer can produce, and + // one unanswerable point makes the WHOLE offer undeclarable (the gate's `required` set is + // every published point). Journalling the auto-selection instead would model a decision + // the player never made. + // + // THE SAME AUTHORITY AS THE RELIEF, exported rather than re-derived: gate (3)'s + // `stack_entry_has_no_ordering_input` asks `forced_unique_targeting` about this same + // fact, so withholding the point loses no relief — the entry passes gate (3) on the + // ordering-input arm instead of the pin arm. Evaluated on `state`, which for a window + // mint IS the pair's own carrying frame (`bounded_cycle_pin_slots_for_window` passes + // `frame`), the same board `build_target_slots` above enumerated the legal set from; that + // function's doc records why the live board would be fail-open here. + // + // Consistent with the sibling refusal one level up: a `ControllerRef::You` head is + // already refused as "a single forced seat, not a per-opponent choice". Forced-unique + // targeting is that same condition measured on the legal SET rather than on the filter. + // The `may` survives — a CR 603.5 take/decline is a real choice on the same source — and + // an entry with neither publishes nothing at all, exactly as shape (B) does. + // + // ⚠ `forced_unique_targeting` ANSWERS THE ASSIGNMENT-COUNT AXIS ONLY, and CR 601.2c has + // two more that decide the same question — WHO announces, and whether anybody does. The + // two cheap conjuncts run FIRST because each independently makes the count irrelevant: + // + // * CR 601.2c `target_chooser` ("of an opponent's choice", e.g. Volcanic Offering). The + // prompt is raised for the CHOOSER, and `record_trigger_target_answer` journals under + // the seat that answered it, while every consumer of a published point reads + // `loop_answer(slot, proposer)`. So the point is unanswerable at the proposer's key and + // one unanswerable point makes the WHOLE offer undeclarable — the same failure the + // forced arm above avoids. Note the count axis CANNOT see this: + // `auto_select_targets_for_ability` early-returns `Ok(None)` when ANY slot carries a + // chooser (`ability_utils.rs`, whose own comment names the `TargetSelectionMode::Random` + // guard as its mirror), so `forced_unique_targeting` is false here even when exactly ONE + // legal assignment exists. `slots.len() == 1` is already enforced above, so this reads + // the single announcement slot. The `!= proposer` half is not redundant with + // `collect_target_slots`' own `player != ability.controller` filter: it keys on the seat + // the CONSUMER reads, which is `entry.controller`, and those two coincide in production + // but are separate fields. Same shape as the sibling `may` mint's + // `.filter(|gate| gate.prompt_player == proposer)` — direction: strictly FEWER offers. + // * `TargetSelectionMode` other than `Chosen` — CR 115.1's "require their controller to + // choose" is overridden and the GAME selects. `triggers::prepare_trigger_targets` sends + // this to `random_select_targets_for_ability` and then to `AutoAssigned`, so no prompt + // is raised at all; worse than merely unanswerable, a pin here also RELIEVES gate (3), + // so the offer would be minted because of a designation the RNG contradicts at drive + // time. Written as `!is_chosen()` rather than `is_random()` deliberately: a future + // variant is withheld by DEFAULT, which is this function's documented fail-closed + // contract that the schema can only ever UNDER-publish. + // + // SCOPING HONESTY: this publication behaviour PREDATES the commit these types ship in. + // What is new is [`TargetAnnouncement`] claiming authority over "is announcing this a game + // choice the proposer makes" while reading one of the three axes. This is not a repair of + // a defect this commit introduced. + // + // ⚠ WITHHELD FROM THE SCHEMA IS NOT UNCHARGED, and the two used to be the same act. + // CR 704.5a asks which seat loses how much life, and a forced victim loses it exactly as + // a chosen one does — nobody having made the choice changes who pays, not how much. + // Reporting the shape here rather than dropping the announcement is what lets + // [`bounded_cycle_charged_targets_for_window`] charge it while + // [`entry_publishes_pin_slots`] still withholds it. Before, the shape was destroyed at + // this line and the CR 704.5a bound — derived from the surviving PUBLISHED points — read + // the withhold as "no victim", charging bare `observed_life_loss` instead of + // `observed_life_loss.max(0) + declared_life_magnitude`, so `max_iterations` GREW: the + // offer stated more legal repetitions than CR 732.2a permits. + let announcement = if slot.chooser.is_some_and(|chooser| chooser != proposer) + || !ability.target_selection_mode.is_chosen() + || crate::analysis::resource::forced_unique_targeting(state, ability) + { + TargetAnnouncement::NotProposerChoice + } else { + TargetAnnouncement::Chosen + }; + // Shape (A) / (A′) — targeted. Index 1 is kept for the may slot in BOTH shapes, so slot + // identity is stable across them. Both sub-indices come from `DecisionSlot`'s own + // constructors, which the CR 603.5 and CR 601.2c journal writers also use. + Some(EntryAnnouncement { + target: Some(AnnouncedTarget { + slot: DecisionSlot::target(source), + legal_targets: slot.legal_targets, + announcement, + }), may, - legal_targets: slot.legal_targets, }) } @@ -2815,8 +3212,11 @@ pub(crate) fn entry_publishes_pin_slots( /// with an unbindable slot), so the schema can only ever under-publish. /// /// Class served: every proposer-controlled triggered ability on the stack whose declared -/// target is a player — never a named card. Command-zone sources (CR 114.2 emblems) are -/// included; [`slot_source_prompted`] is the matching half at replay time. +/// target is a player — never a named card. Command-zone sources are included — every +/// command-zone-functioning ability source, not emblems alone (emblem CR 114.4; plane, scheme, +/// conspiracy CR 113.6p; a face-up phenomenon CR 901.7; an Eminence commander per its own +/// ability's declared zones, CR 113.6b) — and [`slot_source_prompted`] is the matching half at +/// replay time. /// /// PER SOURCE, NOT PER ENTRY: N stack entries from ONE source mint N byte-identical /// `DecisionSlot`s (real boards reach 35 entries on one source), and the sub-index @@ -2916,6 +3316,106 @@ pub(crate) fn bounded_cycle_pin_slots_for_window( points } +/// CR 704.5a: what ONE CERTIFIED PERIOD CHARGES — the announcement slot of every accepted +/// entry, paired with the seats that announcement may name, whether or not CR 732.2a +/// publishes it as a decision point. +/// +/// DELIBERATELY NOT A FILTER OVER [`bounded_cycle_pin_slots_for_window`]'s OUTPUT, and that +/// is the entire reason this exists as its own reader. Publication answers CR 732.2a — "a +/// sequence of game choices, for all players" — so a FORCED announcement publishes nothing. +/// Charging answers CR 704.5a — "if a player has 0 or less life, that player loses the +/// game" — and the victim loses that life whether or not anybody chose it. Deriving the +/// bound from the published set therefore let the CR 732.2a withhold silently drop a forced +/// victim into `ResourceVector::elimination_bounds`' cheaper `observed_life_loss` arm, +/// RAISING `max_iterations`: the offer would state more legal repetitions than CR 732.2a +/// permits, on the very operator whose job is to prove the proposed sequence "may be legally +/// taken based on the current game state". +/// +/// SAME ACCEPTANCE AUTHORITY as the publication mint — both read [`entry_announces`] — so +/// the charged SLOT set is a superset of the published `Targets` slots by construction, never +/// an independently-derived one that could name an entry the schema does not. +/// +/// ⚠ THE SUPERSET IS OVER SLOTS, NOT OVER EACH SLOT'S LEGAL SET, and conflating the two is +/// what the dedup below exists to prevent. The two mints read the same announcements but keep +/// DIFFERENT ONES of a repeated slot: publication skips a `NotProposerChoice` frame entirely, +/// charging does not. So a first-wins charge could retain a narrow frame's legal set for a +/// slot the schema publishes from a WIDER later frame — the schema would offer a pin the bound +/// never charged, and `max_iterations` would GROW. +/// +/// PER SOURCE, NOT PER ENTRY, for the reason [`bounded_cycle_pin_slots`] documents at +/// length: one state-independent designation specifies every instance of that source's +/// announcement, so its slot is charged ONCE however many entries carry it. On a repeat the +/// victim lists are UNIONED rather than first-wins. +/// +/// # Why the union is MONOTONE — it can only tighten the bound, never loosen it +/// +/// The union changes exactly one input to +/// [`crate::analysis::resource::ResourceVector::elimination_bounds`]: +/// `declarable_victims` (its caller's flat union over these victim lists) can only GAIN +/// members. It cannot change `slot_magnitude`, which is keyed by SLOT and whose value is the +/// slot-independent `worst_seat_life_loss` — the union adds no slot. And for the one seat `p` +/// a union adds, that function's per-seat life magnitude moves from `observed_life_loss` to +/// `observed_life_loss.max(0) + S`, where `S = declared_life_magnitude >= 0` by construction +/// (its initializer filters `*m > 0` and sums; the empty sum is `0`). For `observed >= 0` that +/// is `observed + S >= observed`; for `observed < 0` it is `S >= 0 > observed`. So the +/// magnitude never decreases, and `narrow` — `bound.min(headroom.max(0) / magnitude)` over a +/// non-negative numerator, fired only when `magnitude > 0` — is monotone non-increasing in its +/// divisor. Hence the bound can only SHRINK. That is this repo's fail-closed direction. +/// +/// # Reachability of the shape this closes: NARROW, AND NOT CLOSED +/// +/// Stated honestly in both directions, because neither the reviewer nor the orchestrator built +/// the window. Divergent legal sets for ONE slot across a window need the legal PLAYER set to +/// GROW between frames. ELIMINATION — the realistic mechanism, and the one every tracked dump +/// exhibits — narrows it MONOTONICALLY (CR 800.4 + CR 102.1), which puts the widest frame +/// FIRST and lands first-wins fail-CLOSED. The fail-open direction needs a seat's +/// untargetability to END mid-window: a corpus census measured 14 cards granting a player +/// untargetability mid-loop, all self-protective and predominantly "until end of turn", which +/// does not expire mid-turn — so the path additionally needs the granting permanent to LEAVE, +/// or a shorter duration. `a_repeated_slots_victim_lists_are_unioned_not_first_wins` builds +/// exactly that board (CR 702.11c player hexproof whose grantor leaves between frames). It is +/// NOT a claim that a full production trajectory reaches it, and it is NOT "unreachable". +pub(crate) fn bounded_cycle_charged_targets_for_window( + touch: &crate::analysis::resource::PeriodTouch<'_>, + proposer: PlayerId, +) -> Vec<( + crate::analysis::decision_template::DecisionSlot, + Vec, +)> { + use crate::analysis::decision_template::DecisionSlot; + let mut charged: Vec<(DecisionSlot, Vec)> = Vec::new(); + for (frame, entry) in &touch.announced { + let Some(target) = entry_announces(frame, entry, proposer).and_then(|a| a.target) else { + continue; + }; + // CR 115.2: an object target is not a seat any CR 704 loss threshold applies to, so + // only players are collected — the same projection the bound always applied to the + // published set, moved to the authority that owns the legal set. + let victims: Vec = target + .legal_targets + .iter() + .filter_map(|t| match t { + TargetRef::Player(p) => Some(*p), + _ => None, + }) + .collect(); + // UNION, NOT FIRST-WINS. `position` (not `iter_mut().find`) so the immutable probe's + // borrow ends before the `None` arm pushes. + match charged.iter().position(|(slot, _)| *slot == target.slot) { + Some(i) => { + let seats = &mut charged[i].1; + for victim in victims { + if !seats.contains(&victim) { + seats.push(victim); + } + } + } + None => charged.push((target.slot, victims)), + } + } + charged +} + /// CR 732.2a: assemble a loop-shortcut offer's READ-side schema from its already-reified /// decision `points`, its proposed repeat mode, and its CR 704 count bound. /// @@ -3272,6 +3772,8 @@ fn until_lethal_fallback( // sampler with no seat semantics, so it clears unconditionally; the period is evidence about // the seat that recorded it, so only the proposer's own is theirs to discard. state.loop_detect_ring.clear(); + // CR 603.5: the recorded "may" answers describe the window that just ended. + state.loop_answer_journal = None; if state.loop_period_controller() == Some(proposer) { state.last_loop_action_sequence.clear(); } @@ -3283,17 +3785,36 @@ fn until_lethal_fallback( } /// CR 732.2a: how many whole cycles one shortcut drive must aggregate before the measured -/// delta is complete. A `RoundRobin`/`Piecewise` target schedule rotates its OBJECT sources -/// over its length, so a full period is that length; every other pin (a `Constant` target, a +/// delta is complete. A `RoundRobin`/`Piecewise` target schedule rotates its STEPS over its +/// length, so a full period is that length; every other pin (a `Constant` target, a /// `Player` pin, a non-target pin, or no template at all) settles in ONE cycle. Returns the -/// max schedule length over the template's `Targets` pins, defaulting to 1. +/// max schedule length over the template's `Targets` pins, defaulting to 1. A step's subject +/// is a `Ranking`, which lives INSIDE the step and never changes the count — this seam is +/// type-only across that parameterization. /// -/// DORMANT for every Stage-2 crownable loop (Ruling B): `TargetSchedule` rotates DecisionSource -/// objects, not players, and `live_mandatory_loop_winner` crowns on PLAYER fallers — an -/// object-rotating loop produces no player faller, so it never crowns; the only crownable >2p -/// player drain pins ALL opponents every cycle (`TargetPin::Player` is constant, period 1). The -/// seam is built for generality; a multi-cycle aggregation is fail-safe (an object loop reaching -/// the arm measures 1 cycle, finds no faller, does not crown). +/// DORMANT for every Stage-2 crownable loop (Ruling B) — and the REASON has now been restated +/// TWICE, because each restatement was falsified by the next commit and the history is the +/// useful part. (i) It was "`TargetSchedule` rotates DecisionSource objects, not players"; +/// parameterizing a step's subject admitted `AnnouncementSubject::Seat` and killed that. +/// (ii) It was then "no in-tree producer emits a `Seat` into a schedule", which is FALSE as of +/// the provenance split: `record_trigger_target_answer` and +/// `game::interaction::materialize_loop_shortcut_response` both mint +/// `Scheduled(TargetSchedule::Constant(Ranking::one(AnnouncementSubject::Seat(..))))` for a +/// CR 601.2c announced seat. +/// +/// (iii) The property that actually holds, and the one this function's return value depends on, +/// is about ROTATION rather than about subjects: **no in-tree producer emits a multi-STEP +/// schedule** (`RoundRobin` / `Piecewise`) **or a multi-entry `Ranking`**. Every seat-carrying +/// schedule the engine mints is a one-step `Constant`, which lands on the `1` arm of the match +/// below — the same `1` a `TargetPin::Player` lands on, so the split moved the spelling and not +/// the period. `live_mandatory_loop_winner` crowns on PLAYER fallers, and a loop whose targets +/// do not rotate produces no NEW player faller per cycle to aggregate. The only crownable >2p +/// player drain pins ALL opponents every cycle (constant, period 1 — via the +/// `Scheduled(Constant(_))` arm below since the split, via `TargetPin::Player(_)` before it, +/// and those two arms return the same `1`). The seam is built for generality and a multi-cycle +/// aggregation is fail-safe either way (a loop reaching the arm measures 1 cycle, finds no +/// faller, does not crown), so a future ROTATING producer changes what must be re-argued here, +/// not what this function returns. /// /// CR 732.2a SAFETY LIMIT: the returned period is clamped to `MAX_SHORTCUT_CYCLES`. Both /// consumers derive their `0..period` range from this one helper (`validate_pins` and @@ -3324,8 +3845,9 @@ fn shortcut_drive_period( .unwrap_or(1) // CR 732.2a SAFETY LIMIT: the drive period is STRUCTURALLY unbounded in the engine — // its length is the client template schedule's own length. On the WS transport the - // 8 KB inbound-frame cap (phase-server/src/main.rs:409/1420) already bounds a hostile - // schedule to a few hundred entries (~1-2 s stall, not a million-cycle remote DoS), + // inbound-frame cap (`phase-server`'s `MAX_WS_MESSAGE_BYTES`, 64 KB, applied at its + // `ws.max_message_size`) already bounds a hostile schedule to a finite entry count + // (a bounded stall, not a million-cycle remote DoS), // but in-process callers (WASM/Tauri/local) bypass that cap, so clamp here AT THE // SOURCE for every caller. Real schedules rotate over a handful of object sources // (period ≪ cap), so this is invisible to every legitimate loop; a clamped-shorter @@ -3684,42 +4206,24 @@ fn inject_pinned_answer( } } -/// CR 608.2b + CR 114.2: does this SLOT's source identify the ability instance that raised -/// the prompt carrying `source_id`? +/// CR 608.2b + CR 114.4 + CR 113.6p: does this SLOT's source identify the ability instance +/// that raised the prompt carrying `source_id`? /// -/// [`crate::analysis::decision_template::resolve_source`] is deliberately BATTLEFIELD-ONLY, -/// and that filter IS the CR 608.2b (`docs/MagicCompRules.txt:2789`) legality re-check for -/// `ByIdentity` **target** pins — a pinned target that left the battlefield must stop -/// matching. It must not be widened. But a SLOT's source only identifies WHICH ability -/// instance prompts, and CR 114.2 (`:828`) puts a planeswalker EMBLEM — "both owned and -/// controlled by that player" — in the **command zone**, where it stays for the whole game -/// and raises its triggers from. So the command-zone disjunct lives HERE, at the caller, -/// scoped to object identity + the pinned CR 400.7 incarnation. +/// The zone reasoning — why a SLOT's source admits the command zone while a PIN's source is +/// battlefield-only, and why graveyard / exile / hand still fail closed — now lives on +/// [`crate::analysis::decision_template::resolve_ability_instance`], the single accessor for +/// "which live ability instance is this". This call site is the identity comparison against +/// the prompting object; a `None` there means the caller aborts to manual play. /// -/// Graveyard / exile / hand sources still fail ⇒ the caller aborts to manual play. +/// FOUR production seams ask through here: `inject_pinned_answer`'s `TriggerTargetSelection` +/// and `MayChoice` `find_map` guards, and the drive's `pinned_targets_for_source` / +/// `pinned_mana_color_for_source`. fn slot_source_prompted( state: &GameState, src: &crate::analysis::decision_template::DecisionSource, source_id: ObjectId, ) -> bool { - if crate::analysis::decision_template::resolve_source(src, state) == Some(source_id) { - return true; - } - // CR 114.2: the command-zone arm. `AllCopies` is card-identity matching and an emblem - // has no card, so only `ThisObject` participates. - let crate::types::game_state::YieldTarget::ThisObject { - source_id: pinned_id, - incarnation, - .. - } = src - else { - return false; - }; - *pinned_id == source_id - && state.objects.get(pinned_id).is_some_and(|o| { - o.zone == crate::types::zones::Zone::Command - && (incarnation.is_none() || *incarnation == Some(o.incarnation)) - }) + crate::analysis::decision_template::resolve_ability_instance(src, state) == Some(source_id) } /// PR-7 Phase 4b: CR 732.2a finite materialization of a confirmed `Fixed(N)` loop @@ -3947,8 +4451,39 @@ fn materialize_fixed_shortcut( // partial-cycle event leak). Ring-clear BEFORE handback so this same `apply()` does // not instantly re-emit a fresh offer for the same (now-interrupted) loop; a later // beat re-detects genuinely. + // + // CR 732.2a: "The ending point of this sequence must be a place where a player has + // priority, though it need not be the player proposing the shortcut." THIS BLOCK IS + // THAT ENDING POINT, and it is the ending point for BOTH entry paths above — `n` + // cycles done with no cross-lethal, and `break 'cycles`. + // + // What the boundary means for a declared `Ranking`: within one accepted drive only its + // HEAD is ever resolved (`evaluate_schedule`), so this seam is where the NEXT episode + // may legitimately re-evaluate the tail. The reasoning is not restated here — it lives + // on `analysis::decision_template::Ranking` ("CONSUMED AT AN EPISODE BOUNDARY, NEVER + // MID-DRIVE"), and a second copy is the drift the R1 doc sweep exists to prevent. + // + // PROBE-PINNED (probe arm `MUT_SEAM`): the window clear here is load-bearing, not a + // backstop. MEASURED — skipping it on the f4 accepted drive leaves `loop_detect_ring` + // non-empty (12) and the journal populated (3 answers), and this same `apply()` + // re-emits a `LoopShortcut` offer. + // PROBE-PINNED: the abort entry reaches this seam ASYMMETRICALLY. MEASURED + // `ring=16, answers=0` on `bounded_fixed_drive_rolls_back_a_partial_crossing_cycle`: the + // ring is LIVE there, so the ring-clear stays load-bearing on this path, but the journal is + // ALREADY empty — the `loop_answer_journal = None` below is a ⚠ FORWARD TRIPWIRE on this + // entry path, not a co-equal half of the CR 603.5 claim. The DISCRIMINATING statement of the + // journal half is the f4 row + // `fantastic_four_bounded_loop::r3a_the_accepted_drive_ends_at_the_priority_point_with_the_window_cleared`. + // + // LABELLED INTERPRETATION, not a pinned claim: the `waiting_for` re-seat below is a + // NORMALIZATION whose load-bearing case no fixture in this repo exercises today. On + // all four fixtures measured reaching this seam the state is ALREADY + // `WaitingFor::Priority` on entry, and skipping the re-seat changes nothing observable + // (probe arm `MUT_PRIORITY`). *state = committed; state.loop_detect_ring.clear(); + // CR 603.5: the recorded "may" answers describe the window that just ended. + state.loop_answer_journal = None; priority::reset_priority(state); state.waiting_for = WaitingFor::Priority { player: living_priority_seat(state), @@ -4106,23 +4641,129 @@ pub(crate) fn object_decision_source( }) } +/// CR 608.2b + CR 601.2c (reached for a triggered ability via CR 603.3d) + CR 732.2a: +/// journal ONE seat's announced target choice for the current loop-detection window. THE +/// SINGLE WRITE AUTHORITY for the target axis — both `WaitingFor::TriggerTargetSelection` +/// reducer arms route through here, never inline, so the two cannot drift. +/// +/// FAIL-CLOSED ON A DEAD IDENTITY, and this DIVERGES DELIBERATELY from the proliferate +/// `record_loop_pin` site below, which `filter_map`s an unresolvable object away. There a +/// short pin vector still drives; here it would be journalled as a UNIFORM answer and then +/// fail `validate_pins` as an illegal pin value at declare time — a WRONG PIN rather than +/// no offer. `collect::>>()` makes any unresolvable member abandon the whole +/// write. +/// +/// FAIL-CLOSED ON A MULTI-SLOT ANNOUNCEMENT, and the key is why. `DecisionSlot::target` +/// hard-codes `index: 0` (its own doc: the sub-index disambiguates the two choices of ONE +/// ability instance — CR 601.2c target vs. CR 603.5 may — and nothing finer), so every slot of +/// a multi-slot announcement lands on ONE key. `LoopAnswerValue::Targets`' contract is "the +/// announced targets for one slot, in announcement order", so what gets stored is wrong in two +/// distinguishable ways: two slots taking DISTINCT targets latch `Conflicted` (fail-closed, +/// harmless), while two slots taking the SAME target — which CR 601.2c expressly permits, "if +/// the spell uses the word 'target' in multiple places, the same object or player can be chosen +/// once for each instance" — store a `Uniform` one-pin vector that LOOKS like a valid answer to +/// a two-choice announcement. Refusing the whole write is the only reading that cannot hand a +/// widened publisher a wrong pin. Deriving a real per-slot sub-index is the right long-term +/// answer and is deliberately NOT attempted here: `DecisionSlot`'s index namespace is shared +/// with the publisher and with `record_loop_pin`'s own numbering, so widening it is a design +/// change, not a guard. +/// +/// The slot count is read from the PROMPT IN HAND rather than passed by the caller. Both reducer +/// arms run BEFORE the handler replaces `waiting_for` (that is why the seat and source are only +/// readable there), so `state.waiting_for` here IS the `TriggerTargetSelection` the announcement +/// answers — the same value the arm matched on, since `apply_action`'s reducer matches a CLONE +/// and nothing writes the field in between. Reading it makes the guard un-driftable by +/// construction: a third arm cannot pass a stale or invented count, and a caller holding no +/// prompt at all has no announcement to journal and is refused. (A `debug_assert!` on the count +/// is deliberately NOT used: a multi-slot trigger announcement is legal and reachable in +/// production — `triggers.rs` measures a combat-damage trigger surfacing two target slots — so +/// asserting would panic a debug build on a correct game.) +/// +/// Gating is inherited, not restated: `record_loop_answer` carries the +/// `samples() && !in_simulation_probe()` gate, so this adds no second gate. +fn record_trigger_target_answer( + state: &mut GameState, + source_id: Option, + player: PlayerId, + targets: &[crate::types::ability::TargetRef], +) { + use crate::analysis::decision_template::{ + AnnouncementSubject, DecisionSlot, LoopAnswer, LoopAnswerValue, Ranking, TargetPin, + TargetSchedule, + }; + use crate::types::ability::TargetRef; + let announced_slots = match &state.waiting_for { + WaitingFor::TriggerTargetSelection { target_slots, .. } => target_slots.len(), + // No announcement in hand ⇒ nothing to journal. + _ => return, + }; + if announced_slots > 1 { + return; + } + let Some(source) = source_id.and_then(|id| object_decision_source(state, id)) else { + return; + }; + let Some(pins) = targets + .iter() + .map(|t| match t { + // CR 400.7: bind to the CURRENT incarnation, so a re-entered permanent stops + // matching instead of being falsely replayed. + TargetRef::Object(id) => object_decision_source(state, *id).map(TargetPin::ByIdentity), + // CR 601.2c: THIS PRODUCER IS TARGET CLASS, and the spelling says so. This + // writer is gated on `WaitingFor::TriggerTargetSelection`, i.e. on a CR 601.2c + // announcement ("the player announces … choices … including the targets"), so + // the seat it journals was TARGETED, not merely chosen. It therefore emits the + // announcement-subject spelling, whose resolver arm applies CR 702.11c hexproof / + // CR 702.18a shroud / CR 702.16b protection. A merely CHOSEN seat (CR 115.10a — + // e.g. the CR 701.34a proliferate arm) keeps `TargetPin::Player` and its + // existence-only authority; see that variant's own doc. Two questions, two + // spellings, and the spelling IS the provenance. + // + // CR 732.2a is still satisfied: a seat is state-independent by construction — it + // can never denote "the newest copy" — and a one-element `Ranking` under + // `Constant` is answered identically at every iteration index, so no iteration + // can turn the pin into a conditional action. + TargetRef::Player(pl) => Some(TargetPin::Scheduled(TargetSchedule::Constant( + Ranking::one(AnnouncementSubject::Seat(*pl)), + ))), + }) + .collect::>>() + else { + return; + }; + if pins.is_empty() { + // A declined / empty announcement is not an answer a pin can specify. + return; + } + state.record_loop_answer( + DecisionSlot::target(source), + player, + LoopAnswer::Uniform(LoopAnswerValue::Targets(pins)), + ); +} + /// FIX-1 (CR 608.2b): the concrete targets of the recorded `Targets` pin whose slot source /// re-binds LIVE to `source_id` this iteration (the beat's cost / trigger source, e.g. the Relic /// cost source for a tap-cost pin or the Kilo trigger source for a proliferate pin). Resolving the /// WHOLE `template` means ANY pin that no longer resolves to a live legal object (a target left /// its zone) aborts the whole beat fail-closed — a broken loop never certifies. `Err(RecastAbort)` /// if no `Targets` pin's source matches `source_id`. +/// +/// "Whose slot source re-binds to `source_id`" is asked through [`slot_source_prompted`], the one +/// spelling of that question — so the slot may name any ability instance the beat's prompt could +/// have come from (CR 608.2b keeps the pinned TARGETS battlefield-only through `resolve_source`; +/// the SLOT's source is a different question and admits the command zone). fn pinned_targets_for_source( template: &crate::analysis::decision_template::DecisionTemplate, iteration: crate::analysis::decision_template::IterationIndex, clone: &GameState, source_id: ObjectId, ) -> Result, RecastAbort> { - use crate::analysis::decision_template::{resolve, resolve_source, ConcreteDecision}; + use crate::analysis::decision_template::{resolve, ConcreteDecision}; let decisions = resolve(template, iteration, clone).map_err(|_| RecastAbort)?; for d in decisions { if let ConcreteDecision::Targets { slot, targets } = d { - if resolve_source(&slot.source, clone) == Some(source_id) { + if slot_source_prompted(clone, &slot.source, source_id) { return Ok(targets); } } @@ -4132,17 +4773,21 @@ fn pinned_targets_for_source( /// FIX-1 (CR 608.2d): the recorded mana color of the `ManaColor` pin whose slot source is /// `source_id` (the driving mana ability's source). `Err(RecastAbort)` if unpinned. +/// +/// The slot-source question is [`slot_source_prompted`]'s, exactly as in +/// [`pinned_targets_for_source`]: CR 608.2d is the choice this pin records, and which ability +/// instance offered that choice is what the predicate answers. fn pinned_mana_color_for_source( template: &crate::analysis::decision_template::DecisionTemplate, iteration: crate::analysis::decision_template::IterationIndex, clone: &GameState, source_id: ObjectId, ) -> Result { - use crate::analysis::decision_template::{resolve, resolve_source, ConcreteDecision}; + use crate::analysis::decision_template::{resolve, ConcreteDecision}; let decisions = resolve(template, iteration, clone).map_err(|_| RecastAbort)?; for d in decisions { if let ConcreteDecision::ManaColor { slot, color } = d { - if resolve_source(&slot.source, clone) == Some(source_id) { + if slot_source_prompted(clone, &slot.source, source_id) { return Ok(color); } } @@ -5128,6 +5773,8 @@ fn materialize_object_growth_shortcut( } } state.loop_detect_ring.clear(); + // CR 603.5: the recorded "may" answers describe the window that just ended. + state.loop_answer_journal = None; state.last_loop_action_sequence.clear(); priority::reset_priority(state); state.waiting_for = WaitingFor::Priority { @@ -5230,13 +5877,19 @@ struct LoopShortcutOffer<'a> { predicted_winner: Option, certificate: &'a crate::analysis::loop_check::LoopCertificate, schema: &'a crate::analysis::decision_template::ShortcutDecisionSchema, + /// The engine's OWN published declaration for this offer, borrowed. Cloned only on the + /// fallback path in `handle_declare_shortcut`, where a `template: None` declaration + /// resolves against it. + declaration: Option<&'a crate::analysis::decision_template::DecisionTemplate>, } -/// CR 732.2a (MagicCompRules.txt:6372) + CR 800.4a (MagicCompRules.txt:6408): reject a +/// CR 732.2a + CR 800.4a: reject a /// shortcut declaration and hand priority back to the next living seat — the manual-play /// handback every reject path in `handle_declare_shortcut` lands on. Single -/// authority: a sixth reject path added later cannot forget to sync -/// `result.waiting_for`. +/// authority: a SEVENTH reject path added later cannot forget to sync +/// `result.waiting_for` — six exist today (the sixth is the `template.owner` firewall). +/// Cited by CR number, not by `MagicCompRules.txt` line: that file is gitignored and +/// re-fetched, so its line coordinates rot on every rules release. fn reject_shortcut_declaration(state: &mut GameState, result: &mut ActionResult) { priority::reset_priority(state); state.waiting_for = WaitingFor::Priority { @@ -5292,7 +5945,8 @@ fn handle_declare_shortcut( // the single authority — BEFORE the proposal is built — into the same fail-closed // manual-play handback the pin validation above uses. This is THE catastrophic remote // vector: `Fixed(u32)` scalar-encodes up to ~4.3e9 cycles in ~10 bytes, sailing through - // the 8 KB WS frame cap → one GameState clone + drive per cycle. Both confirmation paths + // the WS frame cap (`phase-server`'s `MAX_WS_MESSAGE_BYTES`, 64 KB) → one GameState clone + + // drive per cycle. Both confirmation paths // (solitaire-immediate below, APNAP Accept) consume this one proposal, and both drive // helpers (materialize_fixed_shortcut / materialize_object_growth_shortcut) read `n` from // it, so this one check bounds every Fixed drive on every transport. The drive helpers do @@ -5332,6 +5986,41 @@ fn handle_declare_shortcut( crate::analysis::decision_template::IterationCount::Fixed(_) | crate::analysis::decision_template::IterationCount::UntilLethal => {} } + // A `template: None` declaration is not "no pins" — it is "no OVERRIDE of the pins this + // offer already published". Resolve it against the offer's own engine-issued declaration so + // the manual ingress and `ai_support::candidates` (which reads the identical field) declare + // the SAME proposal against the SAME offer. Until this line existed the engine published a + // declaration on the offer and then discarded it here, so the AI — which sends + // `Some(declaration)` — was accepted while the browser, which sends `None`, was refused on + // one and the same offer. No rules citation is minted here: the block immediately below + // carries this handler's, and `docs/MagicCompRules.txt` is absent from this tree, so an + // unverified restatement would be worse than none. + // + // PLACEMENT IS LOAD-BEARING, AND MEASURED: this sits ABOVE the `template.owner` firewall + // below. `declaration_conforms` is `predictability_gate && validate_pins` and reads no + // `owner` at all — measured: a template differing from a conforming one ONLY in `owner` + // still conforms. That firewall is therefore the SOLE refuser of a foreign-owner + // declaration, and moving this statement one line down would hand the firewall a `None` + // (which passes) and then hand the `Some(t)` arm a foreign-owner template it accepts. + // Pinned by `r3_placement_a_restored_foreign_owner_declaration_is_refused`. + // + // WHAT THIS DOES TO THE `None if …loop_period_controller() != Some(proposer)` ARM BELOW, + // stated because it reads like a loosening and is not: that arm is BYPASSED whenever the + // offer published a declaration, because `&template` then takes the `Some(t)` arm instead. + // That is intended. The arm exists so a PINLESS drive never runs — its own doc says "with + // nothing this proposer can re-derive from, a pin-consuming drive would run with no pins at + // all" — and a resolved declaration supplies exactly those pins. The substitute gate is + // `declaration_conforms`, which is strictly STRONGER for this case: the arm asserts only + // that a re-derivation SOURCE exists, while `declaration_conforms` validates the actual + // pins against the actual schema over the range the accepted count will drive. + // + // THE ARM IS NOT DEAD AFTERWARDS — do not "simplify" it away. It still decides every offer + // that published no declaration, and that set is non-empty by construction: + // `build_bounded_declaration` returns `None` on a journal miss or on a kind/value mismatch + // even with a non-empty schema, both non-bounded mints hard-code `declaration: None`, and a + // restored save may carry `None`. Pinned by + // `a_template_free_declaration_is_admitted_only_by_the_proposers_own_period`. + let template = template.or_else(|| offer.declaration.cloned()); // CR 732.2a + CR 603.5: the declared template's `owner` is CLIENT-SUPPLIED — the // `GameAction::DeclareShortcut { template }` payload arrives here verbatim — and it is // the comparand `inject_pinned_answer` uses to decide WHOSE CR 603.5 choice a pin may @@ -5355,8 +6044,6 @@ fn handle_declare_shortcut( if !offer.schema.points.is_empty() { match &template { Some(t) => { - let required: Vec = - offer.schema.points.iter().map(|p| p.slot.clone()).collect(); // CR 732.2a: validate over the range the ACCEPTED COUNT will drive, not // over the schedule's own period. `shortcut_drive_period` answers a // different question (how many cycles one measurement must aggregate), and @@ -5364,15 +6051,15 @@ fn handle_declare_shortcut( // set at an index the count reaches, and REFUSED conforming declarations // whose count is shorter than the schedule. let validated_range = shortcut_validated_range(&count, Some(t)); - if crate::analysis::decision_template::predictability_gate(t, &required).is_err() - || crate::analysis::decision_template::validate_pins( - offer.schema, - t, - validated_range, - state, - ) - .is_err() - { + // Coverage + value legality via the shared authority, so the predicate this + // handler ACCEPTS under is the same one `build_bounded_declaration` PUBLISHES + // under and the human ingress EMITS under. The range is this site's own. + if !crate::analysis::decision_template::declaration_conforms( + offer.schema, + t, + validated_range, + state, + ) { reject_shortcut_declaration(state, &mut result); return Ok(result); } @@ -5447,9 +6134,13 @@ fn handle_declare_shortcut( /// Re-offer suppression, by seam: /// - Interactive bridge (Seam 1, `find_live_loop_winner` reads `loop_detect_ring`, gated by /// `!stack.is_empty()`): suppressed by the GENERAL deliberate-action invariant, not by this -/// handler. `apply_action` (engine.rs:3006-3011) invalidates `loop_detect_ring` for every -/// deliberate (non-`PassPriority`/`OrderTriggers`) action; `DeclineShortcut` is a deliberate -/// break, so the ring is already empty before this handler runs. Seam-1 suppression is the +/// handler. `apply_action`'s deliberate-action ring clear invalidates `loop_detect_ring` for +/// every action that is neither `PassPriority`/`OrderTriggers` nor the answer to a +/// `WaitingFor::is_forced_cascade_window` window; `LoopShortcut` is in neither exemption (it +/// is not a member of that window class — see the `forced_cascade_window_class` test), so +/// `DeclineShortcut` is a deliberate break and the ring is already empty before this handler +/// runs. Cited by SYMBOL, not by line: this reference named a hard coordinate that rotted +/// twice, so a fresh number would only schedule a third rot. Seam-1 suppression is the /// shared invariant every cast/activate/play-land relies on — the handler does NOT re-clear /// the ring (re-clearing would special-case `DeclineShortcut` to distrust an engine-wide /// invariant). The interactive e2e's "no re-offer" assertion guards this end-to-end: a future @@ -5486,8 +6177,8 @@ fn handle_decline_shortcut( waiting_for: state.waiting_for.clone(), log_entries: vec![], }; - // Seam 1 (loop_detect_ring) is already invalidated by apply_action's deliberate-action - // ring-clear (engine.rs:3006-3011) — see doc. Only Seam 2 is the handler's gap, and only + // Seam 1 (loop_detect_ring) is already invalidated by `apply_action`'s deliberate-action + // ring clear — see doc. Only Seam 2 is the handler's gap, and only // for the decliner's OWN period (CR 732.2a): if state.loop_period_controller() == Some(proposer) { state.last_loop_action_sequence.clear(); @@ -5758,6 +6449,7 @@ pub(crate) fn drain_pending_cost_move_resume( | PendingCostMoveResume::ActivationMillPayment { .. } | PendingCostMoveResume::LoyaltyActivation { .. } | PendingCostMoveResume::CounterAdditionUnlessPayment { .. } + | PendingCostMoveResume::RandomDiscardUnlessPayment(..) ) ), // CR 606.4 + CR 616.1: a fully-prevented loyalty counter add (e.g. an @@ -5781,6 +6473,7 @@ pub(crate) fn drain_pending_cost_move_resume( | PendingCostMoveResume::ActivationMillPayment { .. } | PendingCostMoveResume::LoyaltyActivation { .. } | PendingCostMoveResume::CounterAdditionUnlessPayment { .. } + | PendingCostMoveResume::RandomDiscardUnlessPayment(..) ) ), CostMoveDrainBoundary::PriorityBoundary => matches!( @@ -5861,6 +6554,14 @@ pub(crate) fn drain_pending_cost_move_resume( events, matches!(boundary, CostMoveDrainBoundary::ReplacementDelivered { .. }), )? + } else if matches!( + state.pending_cost_move_resume, + Some(PendingCostMoveResume::RandomDiscardUnlessPayment(..)) + ) { + // CR 118.12: random discard pauses only after its Moved replacement + // returns a replacement choice; the delivered boundary resumes the + // payment through its already-authorized paid epilogue. + engine_payment_choices::resume_random_discard_unless_payment(state, events)? } else { unreachable!("eligible cost-move root must remain parked") }; @@ -6143,7 +6844,12 @@ fn drain_pending_deferred_life_cost_resume( } } })(); - if result.is_err() && state.pending_deferred_life_cost_resume.is_none() { + if result.is_err() + && !result + .as_ref() + .is_err_and(super::casting_costs::is_abandoned_cast_finalization) + && state.pending_deferred_life_cost_resume.is_none() + { state.pending_deferred_life_cost_resume = Some(resume_for_restore); } result @@ -6340,6 +7046,8 @@ fn pass_priority_once_with_pipeline( state.record_loop_detect_sample(); } else if !wf.is_forced_cascade_window() { state.loop_detect_ring.clear(); + // CR 603.5: the recorded "may" answers describe the window that just ended. + state.loop_answer_journal = None; } // CR 603.3b/603.3d/603.5/608.2/903.9a + CR 703.1/117.3a + CR 732.2a: leave the // ring intact on every FORCED PRE-PRIORITY window, not just trigger ordering. @@ -6442,7 +7150,7 @@ fn finish_completed_or_interrupted_until_stack_empty_sessions(state: &mut GameSt // against an absurd/hostile count — NOT a rules constraint. It bounds both a `Fixed(n)` // cycle count (handle_declare_shortcut) and a template drive period (shortcut_drive_period). // Motivating vector: a `u32` count scalar-encodes up to ~4.3e9 cycles in ~10 JSON bytes, so -// it sails through the 8 KB inbound WS frame cap (phase-server/src/main.rs:409/1420) yet +// it sails through the inbound WS frame cap (`phase-server`'s `MAX_WS_MESSAGE_BYTES`, 64 KB) yet // would force ~4.3e9 GameState clones — a byte cap cannot see it, only this count cap can. // 1_000 is generous vs any honest Fixed count (~10x KCI-style loops); worst-case bounded // cost is 1_000 cycles x <=10_000 beats = 1e7. @@ -7173,6 +7881,8 @@ fn apply_action( ) && !answering_forced_window { state.loop_detect_ring.clear(); + // CR 603.5: the recorded "may" answers describe the window that just ended. + state.loop_answer_journal = None; } // Keep the semantic owner of the prompt before reducing it. Under turn @@ -8671,7 +9381,36 @@ fn apply_action( GameAction::CancelCast, ) => engine_casting::cancel_pending_cast(state, *player, pending_cast, &mut events)?, // CR 608.2d: Player decided whether to perform an optional effect ("You may X"). - (WaitingFor::OptionalEffectChoice { .. }, GameAction::DecideOptionalEffect { accept }) => { + ( + WaitingFor::OptionalEffectChoice { + player, source_id, .. + }, + GameAction::DecideOptionalEffect { accept }, + ) => { + // CR 603.5 + CR 732.2a: journal the answer BEFORE the handler runs — it + // replaces `waiting_for`, so the prompt's own seat and source are only + // readable here. The key comes from `object_decision_source`, the same + // producer `entry_publishes_pin_slots` uses, so publish-side and record-side + // keys agree by construction rather than by coincidence. `record_loop_answer` + // carries the `samples() && !in_simulation_probe()` gate. + let (answering_player, may_source) = (*player, *source_id); + if let Some(source) = object_decision_source(state, may_source) { + use crate::analysis::decision_template::{ + DecisionSlot, LoopAnswer, LoopAnswerValue, MayChoiceOption, + }; + // CR 603.5 rides sub-index 1, via `DecisionSlot::may` — the SAME + // constructor `entry_publishes_pin_slots` publishes the gate with, so the + // sub-index is a literal on neither side of the journal. + state.record_loop_answer( + DecisionSlot::may(source), + answering_player, + LoopAnswer::Uniform(LoopAnswerValue::May(if accept { + MayChoiceOption::Take + } else { + MayChoiceOption::Decline + })), + ); + } engine_payment_choices::handle_optional_effect_choice(state, accept, &mut events)? } ( @@ -8733,6 +9472,13 @@ fn apply_action( predicted_winner, certificate, schema, + // Threaded: `handle_declare_shortcut` resolves a `template: None` declaration + // against the offer's own published declaration, so the manual ingress and + // `ai_support::candidates` (which reads this identical field) declare the SAME + // proposal against the SAME offer. The hostile-fixture obligations this bind + // used to defer — foreign period, restore ingress — are discharged by the rows + // named on that handler's `or_else`. + declaration, }, GameAction::DeclareShortcut { count, template }, ) => { @@ -8743,6 +9489,7 @@ fn apply_action( predicted_winner: *predicted_winner, certificate, schema, + declaration: declaration.as_ref(), }, count, template, @@ -9901,6 +10648,10 @@ fn apply_action( (WaitingFor::ReplacementChoice { .. }, GameAction::ChooseReplacement { index }) => { engine_replacement::handle_replacement_choice(state, index, &mut events)? } + ( + WaitingFor::EntryControllerChoice { .. }, + GameAction::ChooseEntryController { opponent }, + ) => engine_replacement::handle_entry_controller_choice(state, opponent, &mut events)?, // CR 603.3b: Player submits the chosen order for their pending triggers. // `actor` is already authorized as the prompted player by // `check_actor_authorization` (via `WaitingFor::acting_player`). @@ -10665,20 +11416,40 @@ fn apply_action( ( WaitingFor::TriggerTargetSelection { player, + source_id, target_slots, target_constraints, .. }, GameAction::SelectTargets { targets }, - ) => engine_stack::handle_trigger_target_selection_select_targets( - state, - *player, - target_slots, - target_constraints, - targets, - &mut events, - )?, - (WaitingFor::TriggerTargetSelection { .. }, GameAction::ChooseTarget { target }) => { + ) => { + // CR 608.2b + CR 732.2a: journal the announcement BEFORE the handler runs — it + // replaces `waiting_for`, so the prompt's own seat and source are only readable + // here, and the key reads the source object's CR 400.7 incarnation, which + // resolution can invalidate. `apply_action_boundary_core` snapshots the whole + // state and restores it on every `Err` return, so a write made before a handler + // that then errors is rolled back with everything else. + record_trigger_target_answer(state, *source_id, *player, targets.as_slice()); + engine_stack::handle_trigger_target_selection_select_targets( + state, + *player, + target_slots, + target_constraints, + targets, + &mut events, + )? + } + ( + WaitingFor::TriggerTargetSelection { + player, source_id, .. + }, + GameAction::ChooseTarget { target }, + ) => { + // Same write authority and same before-the-handler reason as the `SelectTargets` + // arm above. `target: None` yields an empty slice, which the helper's + // `pins.is_empty()` guard refuses — the fail-closed reading of a no-target + // announcement. + record_trigger_target_answer(state, *source_id, *player, target.as_slice()); let waiting_for = state.waiting_for.clone(); engine_stack::handle_trigger_target_selection_choose_target( state, @@ -10873,21 +11644,14 @@ fn apply_action( )); } } - if !effects::proliferate::apply_proliferate(state, p, &targets, &mut events) { - return Ok(ActionResult { - events, - waiting_for: state.waiting_for.clone(), - log_entries: vec![], - }); - } - // CR 701.34a: Emit player-action event so proliferate triggers fire. - events.push(GameEvent::PlayerPerformedAction { - player_id: p, - action: PlayerActionKind::Proliferate, - look_count: None, - scry_bottom_count: None, - scry_top_count: None, - }); + // CR 701.34a + issue #7384: take the frame BEFORE applying counters. + // A counter-placement replacement can pause mid-application, and any + // path that returns while this direct-choice frame is still resident + // strands it on the resolution stack — every later frame transition + // then fails `ResolutionStack::validate` against a prompt that has + // long since moved on. A wrong stack top degrades to a rejected + // action here rather than to silent corruption, because + // `take_active_proliferate_frame` reports `UnexpectedTop`. let pending = state .take_active_proliferate_frame() .map_err(|error| EngineError::InvalidAction(error.to_string()))? @@ -10900,6 +11664,15 @@ fn apply_action( // (Pentad's charge) — never "all eligible", which could grow an opponent's // counters/poison and introduce a loss axis. Slot source = the trigger source (Kilo); // `index: 0` (distinct source from the Relic tap-cost/color pins). + // + // Recorded BEFORE the counters are applied: a counter-placement + // replacement can pause `apply_proliferate`, and that path returns + // early. Leaving the pin below it would silently drop the pin on + // exactly the proliferate this fix made complete, falling back to + // the "all eligible" replay this comment rules out. Everything read + // here — `state`, `targets`, `p`, `completion_source` — is already + // settled, and `object_decision_source` resolves card identity, + // which the pending counters do not affect. if let Some(source) = object_decision_source(state, completion_source) { let target_pins: Vec = targets .iter() @@ -10927,17 +11700,37 @@ fn apply_action( ); } } - if !effects::proliferate::resume_proliferate_actions(state, pending, &mut events) { + // The player-action event and any remaining actions are owed once + // the counters land, so they ride the completion rather than being + // emitted here — `continue_proliferate_actions` is the single + // authority for both, on the synchronous and paused paths alike. + let completion = PendingEffectResolved::with_post_actions_without_effect( + crate::types::ability::EffectKind::Proliferate, + completion_source, + vec![PendingCounterPostAction::ContinueProliferateActions { + pending: pending.clone(), + }], + ); + if !effects::proliferate::apply_proliferate( + state, + p, + &targets, + completion, + &mut events, + ) { + return Ok(ActionResult { + events, + waiting_for: state.waiting_for.clone(), + log_entries: vec![], + }); + } + if !effects::proliferate::continue_proliferate_actions(state, pending, &mut events) { return Ok(ActionResult { events, waiting_for: state.waiting_for.clone(), log_entries: vec![], }); } - events.push(GameEvent::EffectResolved { - kind: crate::types::ability::EffectKind::Proliferate, - source_id: completion_source, - subject: None,}); state.waiting_for = WaitingFor::Priority { player: p }; state.priority_player = p; resume_pending_continuation_if_priority(state, &mut events)?; @@ -11588,14 +12381,14 @@ fn apply_action( // // BLAST RADIUS. Nothing this leaves in `state` survives to a consumer unrecomputed: // `finish_action_boundary` runs the SAME `sync_waiting_for` over `result.waiting_for` - // (`:1171`) and copies the outcome back into the result (`:1189`), and the reorder + // and copies the outcome back into the result, and the reorder // never changes `ActionResult.waiting_for` itself. That is an argument about // RE-DERIVATION, not reachability, because `apply_action_boundary` is not the only // route: `inject_pinned_answer`'s three dispatches and `drive_loop_action_iteration`'s // ten reach `apply_action` directly, and // `apply_interaction_pre_reconciliation_for_life_safety` returns `raw.result` without - // ever calling `finish_action_boundary` (`apply_action_boundary_core`'s own comment at - // `:1119` records it). All three drive a CLONE — `drive_one_shortcut_cycle`'s `work`, + // ever calling `finish_action_boundary` (`apply_action_boundary_core`'s own comment + // records it). All three drive a CLONE — `drive_one_shortcut_cycle`'s `work`, // the drive's `clone`, `preview_candidate_life_safety`'s `preview` — never the settled // board. MEASURED pre-reorder by an instrumented `debug_assert_eq!` census over the // full lib + integration corpus (per-site counts in PR #7005's history; one unit = @@ -14746,46 +15539,487 @@ mod shortcut_schema_tests { } } -/// PR-7 Combo-UI Stage 2: the mid-drive pin injector (item 4) + the drive-period seam (item 6). +/// item-4 C2b — `build_bounded_declaration`, the consumer that turns the window's observed +/// answers into the offer's own CR 732.2a declaration. +/// +/// TIER NOTE, stated because it is FORCED rather than chosen: rows D1 / D1-P / D1-P-sib drive +/// the real F4 dump through production `apply()` and live in +/// `crates/engine/tests/integration/fantastic_four_bounded_loop.rs`. The three rows HERE are the +/// ones no tracked board can reach — a `Decline`d CR 603.5 answer at a certifying offer, and a +/// point kind the bounded publisher cannot mint — so each states its own unreachability rather +/// than implying a wire row was available and skipped. #[cfg(test)] -mod stage2_injector_tests { - use super::*; +mod bounded_declaration_tests { + use super::{build_bounded_declaration, build_shortcut_schema}; use crate::analysis::decision_template::{ - DecisionGroupKey, DecisionKind, DecisionSlot, DecisionTemplate, IterationCount, - PinnedDecision, ReplayMode, TargetPin, TargetSchedule, + DecisionPoint, DecisionPointKind, DecisionSlot, IterationCount, LoopAnswer, + LoopAnswerValue, MayChoiceOption, PinnedDecision, ShortcutDecisionSchema, TargetPin, }; - use crate::game::scenario::GameScenario; - use crate::types::game_state::{LoopDetectionMode, YieldTarget}; - - const P0: PlayerId = PlayerId(0); - const P1: PlayerId = PlayerId(1); - const P2: PlayerId = PlayerId(2); - const TARGET_DRAIN: &str = "Whenever you gain life, target opponent loses that much life."; - const FEEDBACK: &str = "Whenever an opponent loses life, you gain that much life."; - const KICKOFF: &str = "You gain 1 life."; + use crate::types::ability::TargetRef; + use crate::types::game_state::{GameState, LoopDetectionMode, YieldTarget}; + use crate::types::identifiers::ObjectId; + use crate::types::mana::ManaColor; + use crate::types::player::PlayerId; - fn life(state: &GameState, p: PlayerId) -> i32 { - state.players.iter().find(|pl| pl.id == p).unwrap().life - } + const PROPOSER: PlayerId = PlayerId(0); + const AIMED: PlayerId = PlayerId(1); - fn this_object(id: ObjectId) -> YieldTarget { + /// A CR 400.7-stable source identity, built the way `object_decision_source` builds one. + fn source(id: u64) -> YieldTarget { YieldTarget::ThisObject { - source_id: id, - incarnation: None, + source_id: ObjectId(id), + incarnation: Some(1), trigger_description: None, } } - /// A template routing two distinct drainers to two distinct opponents by source identity. - fn two_drainer_template( - d0: ObjectId, - opp0: PlayerId, - d1: ObjectId, - opp1: PlayerId, - ) -> DecisionTemplate { - let s0 = this_object(d0); - let s1 = this_object(d1); - DecisionTemplate { + /// A board whose journal ACCEPTS writes: `record_loop_answer` is gated on + /// `loop_detection.samples()`, so a default board would silently record nothing and every + /// row below would measure the "seat never answered" path instead of its own subject. + fn recording_state() -> GameState { + let mut state = GameState::new_two_player(7); + state.loop_detection = LoopDetectionMode::Interactive; + state + } + + fn targets_kind() -> DecisionPointKind { + DecisionPointKind::Targets { + legal_targets: vec![TargetRef::Player(AIMED)], + min_targets: 1, + max_targets: 1, + ordered: false, + } + } + + /// The two-kind schema the bounded publisher actually mints: one CR 603.5 `may` gate and one + /// CR 601.2c target slot, on two distinct sources. + fn may_and_target_schema() -> ShortcutDecisionSchema { + build_shortcut_schema( + vec![ + DecisionPoint { + slot: DecisionSlot::may(source(100)), + kind: DecisionPointKind::MayChoice, + }, + DecisionPoint { + slot: DecisionSlot::target(source(200)), + kind: targets_kind(), + }, + ], + IterationCount::Fixed(4), + 4, + ) + } + + /// **Row D1-P-may — the `MayChoice` pin FOLLOWS THE JOURNAL, not a constant.** + /// + /// CR 603.5: an optional trigger's answer is `Take` or `Decline`, and the declaration must + /// state the one the proposer actually gave. A consumer that hard-codes + /// `MayChoiceOption::Take` is indistinguishable from this one on every tracked board, which + /// is exactly the vacuity this row closes. + /// + /// # Why this tier is FORCED, and not a shortcut + /// + /// A wire-tier may-provenance drive is measured UNREACHABLE: answering every CR 603.5 prompt + /// `Decline` on the tracked F4 board reaches NO offer at all (declining Sue's token breaks + /// the chain to Reed, so the loop never certifies — the drive policy's own doc records it). + /// The residual is filed rather than hidden: it needs a bounded board on which the proposer + /// DECLINES and the loop still certifies, and the lane's real-fixtures rule bars + /// synthesizing one. + /// + /// # Non-vacuity + /// + /// The `Take` case is asserted in the SAME test from the SAME fixture one field apart, so a + /// consumer that returned `None` — or that dropped the may pin entirely — fails the positive + /// arm rather than passing the negative one by omission. + /// + /// REVERT-PROBE: hard-code `take: MayChoiceOption::Take` in the `(MayChoice, May)` arm ⇒ the + /// `Decline` arm's assertion flips (`Take != Decline`) while the `Take` arm stays green. + /// That asymmetry is the row. + /// + /// *What wrong implementation would still pass this row?* One that reads the journal for the + /// may axis but pins a CONSTANT target — D1-P and D1-P-sib cover that axis on the real dump. + #[test] + fn d1p_may_the_may_pin_follows_the_journal_not_a_constant() { + let schema = may_and_target_schema(); + let [may_point, target_point] = &schema.points[..] else { + panic!("the fixture publishes exactly two points"); + }; + + for answered in [MayChoiceOption::Decline, MayChoiceOption::Take] { + let mut state = recording_state(); + state.record_loop_answer( + may_point.slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::May(answered)), + ); + state.record_loop_answer( + target_point.slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::Targets(vec![TargetPin::Player(AIMED)])), + ); + + // Reach-guard: the journal really holds the answer, under the PROPOSER's own key. + // Without this a gated-off `record_loop_answer` would make every arm below measure + // the "never answered" refusal instead. + assert_eq!( + state.loop_answer(&may_point.slot, PROPOSER), + Some(LoopAnswer::Uniform(LoopAnswerValue::May(answered))), + "reach-guard: the CR 603.5 answer must be journalled before the consumer runs" + ); + + let declaration = build_bounded_declaration(&state, PROPOSER, &schema) + .expect("both published points are answered, so the declaration is complete"); + assert_eq!( + declaration.decisions[0], + PinnedDecision::MayChoice { + slot: may_point.slot.clone(), + take: answered, + }, + "CR 603.5: the pinned option must be the one the proposer ANSWERED ({answered:?}), \ + not a constant" + ); + assert_eq!( + declaration.owner, PROPOSER, + "the declaration is the proposer's own, which is what the declare-time owner \ + firewall compares against" + ); + } + } + + /// **Row D3 — the consumer is TOTAL and FAIL-CLOSED over the four `DecisionPointKind`s the + /// bounded producer cannot mint.** + /// + /// CR 700.2 (`Mode`), CR 732.6 (`UnlessBreak`), CR 601.2h + CR 702.51a (`ConvokeTaps`) and + /// CR 608.2d + CR 605.3b (`ManaColor`) are real choice kinds with no observation-side answer + /// shape in `LoopAnswerValue`, so there is nothing to pin for them and the declaration must + /// refuse rather than guess. + /// + /// # ⚠ THE JOURNAL ENTRY ON THE FOUR-KIND POINT IS LOAD-BEARING, NOT DECORATION + /// + /// `build_bounded_declaration`'s body order is (1) empty check, (2) `state.loop_answer(..)?`, + /// (3) the `(kind, value)` match. An UNJOURNALLED four-kind point exits at step (2)'s `?` — + /// before control ever reaches the arm this row is about — and the reddening mutation returns + /// `None` there too, so real and mutant AGREE and nothing can red. Each case therefore + /// journals its own point and ASSERTS the entry is present before the consumer runs. + /// + /// # Unreachable today, and the row says so + /// + /// `bounded_cycle_pin_slots_for_window` constructs only `Targets` and `MayChoice` points. The + /// other four have one producer, `pinned_decisions_to_points`, which serves the two mints that + /// publish `declaration: None`. The row exists so a publisher relaxation gets a red test + /// instead of a silent pin. + /// + /// REVERT-PROBE: replace the four-kind arm with `_ => continue` ⇒ each case builds a + /// `Some(template)` with the four-kind point silently dropped ⇒ every `is_none()` flips while + /// the control stays green. + /// + /// *What wrong implementation would still pass this row?* One that returns `None` for + /// EVERYTHING — which the control arm (the same fixture with only mintable kinds) refuses. + #[test] + fn d3_the_consumer_fail_closes_on_every_kind_the_bounded_publisher_cannot_mint() { + let unmintable = [ + DecisionPointKind::Mode { + available_modes: vec![0, 1], + min_modes: 1, + max_modes: 1, + allow_repeats: false, + }, + DecisionPointKind::UnlessBreak, + DecisionPointKind::ConvokeTaps { + tappable: vec![ObjectId(31)], + }, + DecisionPointKind::ManaColor { + color: ManaColor::Blue, + }, + ]; + + // ── CONTROL, first: the same shape with only MINTABLE kinds yields `Some` ── + let control_schema = may_and_target_schema(); + let mut control = recording_state(); + control.record_loop_answer( + control_schema.points[0].slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::May(MayChoiceOption::Take)), + ); + control.record_loop_answer( + control_schema.points[1].slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::Targets(vec![TargetPin::Player(AIMED)])), + ); + assert!( + build_bounded_declaration(&control, PROPOSER, &control_schema).is_some(), + "CONTROL: a fully-answered two-kind schema DOES publish a declaration — without this \ + a consumer that refused everything would pass all four cases below" + ); + + for kind in unmintable { + let odd_slot = DecisionSlot::target(source(300)); + let schema = build_shortcut_schema( + vec![ + DecisionPoint { + slot: DecisionSlot::may(source(100)), + kind: DecisionPointKind::MayChoice, + }, + DecisionPoint { + slot: odd_slot.clone(), + kind: kind.clone(), + }, + ], + IterationCount::Fixed(4), + 4, + ); + let mut state = recording_state(); + // The OTHER point is answered, so the refusal cannot be attributed to it. + state.record_loop_answer( + schema.points[0].slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::May(MayChoiceOption::Take)), + ); + // `LoopAnswerValue` has exactly two variants and `May` pairs with NONE of the four + // kinds under test, so this entry is answerable and still unpinnable — which is the + // fail-closed disposition the row measures. + state.record_loop_answer( + odd_slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::May(MayChoiceOption::Take)), + ); + assert_eq!( + state.loop_answer(&odd_slot, PROPOSER), + Some(LoopAnswer::Uniform(LoopAnswerValue::May( + MayChoiceOption::Take + ))), + "⚠ VACUITY GUARD for kind {kind:?}: an UNJOURNALLED point exits at the \ + `loop_answer(..)?` one step EARLIER, where the reddening mutation also returns \ + `None` — real and mutant would agree and this row could never fire" + ); + + assert!( + build_bounded_declaration(&state, PROPOSER, &schema).is_none(), + "CR 732.2a: {kind:?} has no `LoopAnswerValue` shape, so the declaration must \ + refuse rather than pin a guess or silently drop the point" + ); + } + } + + /// **Row D4 — an empty schema publishes NO declaration.** + /// + /// LOAD-BEARING, not tidiness: `predictability_gate` and `validate_pins` both live inside + /// `handle_declare_shortcut`'s `if !offer.schema.points.is_empty()` block, so a declaration + /// minted against an empty schema would travel the one declare path that runs NEITHER gate. + /// The invariant is also staged at fixture level by + /// `tests/integration/loop_shortcut.rs::r28_empty_schema_offer`, which passes + /// `declaration: None` for this reason. + /// + /// REVERT-PROBE: delete the `schema.points.is_empty()` early return ⇒ the loop body never + /// runs, step (4) builds a template with ZERO decisions ⇒ `is_none()` flips. + /// + /// *What wrong implementation would still pass this row?* One that also refuses a + /// fully-answered NON-empty schema — which D1-P-may's positive arm and D3's control refuse. + #[test] + fn d4_an_empty_schema_publishes_no_declaration() { + let empty = build_shortcut_schema(Vec::new(), IterationCount::Fixed(4), 4); + assert!( + empty.points.is_empty(), + "reach-guard: this fixture is the empty-schema case" + ); + let mut state = recording_state(); + // Journalled anyway: the refusal must be keyed on the EMPTY POINT SET, not on an empty + // journal, and a populated journal is the only way to tell those two apart. + state.record_loop_answer( + DecisionSlot::may(source(100)), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::May(MayChoiceOption::Take)), + ); + assert!( + state.loop_answers_recorded() > 0, + "reach-guard: the journal is NON-empty, so the refusal below is the point set's" + ); + assert!( + build_bounded_declaration(&state, PROPOSER, &empty).is_none(), + "CR 732.2a: an offer that publishes no choice states no declaration" + ); + } + + /// **Row D8 — the PUBLISHER cannot publish what the HANDLER would refuse.** + /// + /// `ai_support::candidates` reads `declaration.is_some()` as *"`handle_declare_shortcut` + /// will accept this"* and hands the published template straight to `DeclareShortcut`. That + /// reading was unenforced: the publisher ran neither firewall half, and the two sides agreed + /// only because the publisher copies `legal_targets` from the same announcement the journal + /// answer came from. This row pins the implication itself. + /// + /// # The two halves are measured on DIFFERENT instruments, so this is not circular + /// + /// The "handler refuses it" half is measured by calling `validate_pins` DIRECTLY on the + /// template the pre-fix publisher would have emitted — the handler's own value-legality + /// firewall, at the range that handler validates a `Fixed(max_iterations)` declaration over. + /// Only then is the publisher asked. A row that asserted `is_none()` alone would pass on a + /// publisher that refuses for any unrelated reason. + /// + /// # Reach-guards, asserted BEFORE the claim + /// + /// The journal really holds the hostile answer (else the publisher exits one step earlier at + /// `loop_answer(..)?` and the refusal is not this one), and `predictability_gate` PASSES on + /// that template (both published slots are pinned) — so the refusal is attributable to the + /// VALUE half, not to coverage. + /// + /// # LATENT, not live — the row says so rather than implying a bug was shipped + /// + /// No tracked board reaches this: `record_trigger_target_answer` journals the seat it + /// ANNOUNCED, and the publisher's `legal_targets` come from that same announcement, so the + /// disagreement staged here is fixture-made. Reachability is NOT claimed. + /// + /// REVERT-PROBE: delete step (5)'s `declaration_conforms(..)` call (return `Some(template)`) + /// ⇒ the hostile arm's `is_none()` flips while the control stays green. + /// + /// *What wrong implementation would still pass this row?* One that validates COVERAGE only — + /// `predictability_gate` alone passes here, which is why the reach-guard asserts it. And one + /// that refuses everything, which the control arm refuses. + #[test] + fn d8_the_publisher_refuses_a_declaration_the_declare_handler_would_reject() { + use crate::analysis::decision_template::{ + declaration_conforms, predictability_gate, validate_pins, DecisionGroupKey, + DecisionKind, DecisionTemplate, ReplayMode, + }; + + let schema = may_and_target_schema(); + let [may_point, target_point] = &schema.points[..] else { + panic!("the fixture publishes exactly two points"); + }; + // CR 608.2b: the published legal set names AIMED only, so a pin naming PROPOSER is + // outside the offer's own legal set — an illegal pin VALUE at a legally exposed slot. + assert!( + !matches!(&target_point.kind, DecisionPointKind::Targets { legal_targets, .. } + if legal_targets.contains(&TargetRef::Player(PROPOSER))), + "reach-guard: PROPOSER must NOT be a published legal target, or the hostile pin \ + below is a conforming one and this row measures nothing" + ); + + for (label, pinned, expect_published) in [ + ("hostile", TargetPin::Player(PROPOSER), false), + ("control", TargetPin::Player(AIMED), true), + ] { + let mut state = recording_state(); + state.record_loop_answer( + may_point.slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::May(MayChoiceOption::Take)), + ); + state.record_loop_answer( + target_point.slot.clone(), + PROPOSER, + LoopAnswer::Uniform(LoopAnswerValue::Targets(vec![pinned.clone()])), + ); + assert_eq!( + state.loop_answer(&target_point.slot, PROPOSER), + Some(LoopAnswer::Uniform(LoopAnswerValue::Targets(vec![ + pinned.clone() + ]))), + "[{label}] reach-guard: the answer must be journalled, or the publisher exits at \ + `loop_answer(..)?` and the verdict below is the 'never answered' one" + ); + + // The template the UNVALIDATED publisher would have emitted, spelled out here so the + // handler-side half below is measured on it rather than on whatever the publisher + // now returns. + let as_published = DecisionTemplate { + owner: PROPOSER, + decisions: vec![ + PinnedDecision::MayChoice { + slot: may_point.slot.clone(), + take: MayChoiceOption::Take, + }, + PinnedDecision::Targets { + slot: target_point.slot.clone(), + targets: vec![pinned.clone()], + }, + ], + replay: ReplayMode::Scheduled { + count: schema.iteration_count.clone(), + }, + key: DecisionGroupKey::from_sources( + &schema + .points + .iter() + .map(|point| point.slot.source.clone()) + .collect::>(), + DecisionKind::LoopChoice, + ), + }; + let required: Vec<_> = schema.points.iter().map(|p| p.slot.clone()).collect(); + assert!( + predictability_gate(&as_published, &required).is_ok(), + "[{label}] reach-guard: COVERAGE passes on this template — every published slot \ + is pinned — so the handler's verdict below is the VALUE half's" + ); + + // ── HALF 1, on the handler's own instrument: would `handle_declare_shortcut` take + // it, at the range it validates the AI's `Fixed(max_iterations)` candidate over? + let handler_accepts = + validate_pins(&schema, &as_published, schema.max_iterations, &state).is_ok(); + assert_eq!( + handler_accepts, expect_published, + "[{label}] the declare-time pin firewall's verdict on the published shape" + ); + assert_eq!( + declaration_conforms(&schema, &as_published, schema.max_iterations, &state), + handler_accepts, + "[{label}] and the shared authority agrees with its own value half — it is the \ + conjunction of the two gates, not a third predicate" + ); + + // ── HALF 2: the PUBLISHER's verdict must be the same one ── + assert_eq!( + build_bounded_declaration(&state, PROPOSER, &schema).is_some(), + handler_accepts, + "[{label}] CR 732.2a: `declaration.is_some()` is read as 'the declare handler \ + will accept this'. A template the handler refuses must NOT be published, and a \ + template it accepts must be" + ); + } + } +} + +/// PR-7 Combo-UI Stage 2: the mid-drive pin injector (item 4) + the drive-period seam (item 6). +#[cfg(test)] +mod stage2_injector_tests { + use super::*; + use crate::analysis::decision_template::{ + ConcreteTarget, DecisionGroupKey, DecisionKind, DecisionSlot, DecisionTemplate, + IterationCount, PinnedDecision, ReplayMode, TargetPin, TargetSchedule, + }; + use crate::game::scenario::GameScenario; + use crate::types::game_state::{LoopDetectionMode, YieldTarget}; + + const P0: PlayerId = PlayerId(0); + const P1: PlayerId = PlayerId(1); + const P2: PlayerId = PlayerId(2); + const TARGET_DRAIN: &str = "Whenever you gain life, target opponent loses that much life."; + const FEEDBACK: &str = "Whenever an opponent loses life, you gain that much life."; + const KICKOFF: &str = "You gain 1 life."; + + fn life(state: &GameState, p: PlayerId) -> i32 { + state.players.iter().find(|pl| pl.id == p).unwrap().life + } + + fn this_object(id: ObjectId) -> YieldTarget { + YieldTarget::ThisObject { + source_id: id, + incarnation: None, + trigger_description: None, + } + } + + /// A template routing two distinct drainers to two distinct opponents by source identity. + fn two_drainer_template( + d0: ObjectId, + opp0: PlayerId, + d1: ObjectId, + opp1: PlayerId, + ) -> DecisionTemplate { + let s0 = this_object(d0); + let s1 = this_object(d1); + DecisionTemplate { owner: P0, decisions: vec![ PinnedDecision::Targets { @@ -15055,25 +16289,32 @@ mod stage2_injector_tests { }, key: DecisionGroupKey::from_sources(std::slice::from_ref(&a), DecisionKind::LoopChoice), }; + // A ranking lives INSIDE a schedule step; `shortcut_drive_period` still counts STEPS, + // which is why this migration is type-only at the seam under test. + let rank = |src| { + crate::analysis::decision_template::Ranking::one( + crate::analysis::decision_template::AnnouncementSubject::Object(src), + ) + }; let constant = mk(vec![TargetPin::Player(P1)]); assert_eq!(shortcut_drive_period(Some(&constant)), 1, "Player pin ⇒ 1"); let rr = mk(vec![TargetPin::Scheduled(TargetSchedule::RoundRobin( - vec![a.clone(), b.clone(), c.clone()], + vec![rank(a.clone()), rank(b.clone()), rank(c.clone())], ))]); assert_eq!(shortcut_drive_period(Some(&rr)), 3, "RoundRobin(3) ⇒ 3"); let pw = mk(vec![TargetPin::Scheduled(TargetSchedule::Piecewise(vec![ - (0, a.clone()), - (5, b.clone()), + (0, rank(a.clone())), + (5, rank(b.clone())), ]))]); assert_eq!(shortcut_drive_period(Some(&pw)), 2, "Piecewise(2) ⇒ 2"); // CR 732.2a SAFETY LIMIT: an over-cap schedule clamps to MAX_SHORTCUT_CYCLES. // Revert-probe: restore `.max(1)` (drop the `.clamp`) ⇒ returns MAX+5 (1005) ≠ 1000. let oversized = mk(vec![TargetPin::Scheduled(TargetSchedule::RoundRobin( - vec![a.clone(); (MAX_SHORTCUT_CYCLES + 5) as usize], + vec![rank(a.clone()); (MAX_SHORTCUT_CYCLES + 5) as usize], ))]); assert_eq!( shortcut_drive_period(Some(&oversized)), @@ -15098,64 +16339,599 @@ mod stage2_injector_tests { oid } - /// CR 114.2 + CR 608.2b: a pinned SLOT whose source is a command-zone emblem must match + /// Stand up the `WaitingFor::TriggerTargetSelection` prompt an announcement answers, with + /// `slot_count` announcement slot(s). + /// + /// `record_trigger_target_answer` reads the slot count off the prompt IN HAND — that is + /// production's own instrument, because both reducer arms run before the handler replaces + /// `waiting_for` — so a row that drives the writer has to stand the prompt up the way + /// production does rather than call the writer against a bare board. + fn stand_up_target_prompt( + state: &mut GameState, + player: PlayerId, + source: ObjectId, + slot_count: usize, + ) { + let slot = crate::types::game_state::TargetSelectionSlot { + legal_targets: vec![], + optional: false, + chooser: None, + effect_kind: crate::types::ability::EffectKind::NoOp, + effect_detail: crate::types::game_state::TargetEffectDetail::None, + }; + state.waiting_for = WaitingFor::TriggerTargetSelection { + player, + trigger_controller: None, + trigger_event: None, + trigger_events: vec![], + target_slots: vec![slot; slot_count], + mode_labels: vec![], + target_constraints: vec![], + selection: Default::default(), + source_id: Some(source), + description: None, + }; + } + + /// **Row T5.** CR 608.2b: an announcement one of whose members no longer resolves to a + /// live identity abandons the WHOLE journal write, rather than journalling a short + /// vector. + /// + /// This DIVERGES DELIBERATELY from the proliferate `record_loop_pin` site, which + /// `filter_map`s an unresolvable object away: there a short pin vector still drives, + /// while here a short vector would be journalled as a UNIFORM answer and then fail + /// `validate_pins` at declare time — a WRONG PIN rather than no offer. + /// + /// # Discrimination + /// + /// Replace `record_trigger_target_answer`'s `collect::>>()` with + /// `filter_map(..).collect::>()` (the `record_loop_pin` shape) ⇒ the negative + /// arm's `loop_answers_recorded()` rises to 1 with a one-pin vector and that assertion + /// flips. The mutation reds on the ASSERT, not on a compile error. + /// + /// # Paired positive / reach-guards + /// + /// The negative arm alone is satisfied by ANY no-op writer, so the positive arm runs + /// FIRST on the same state and asserts BOTH pins are journalled under the CR 601.2c + /// slot. The empty-announcement arm is the third case the helper's own guard names. + #[test] + fn c2a_row_t5_an_unresolvable_target_abandons_the_whole_journal_write() { + use crate::analysis::decision_template::{ + AnnouncementSubject, DecisionSlot, LoopAnswer, LoopAnswerValue, Ranking, TargetPin, + TargetSchedule, + }; + use crate::types::ability::TargetRef; + + let mut state = GameScenario::new_n_player(3, 7).build().state().clone(); + state.loop_detection = LoopDetectionMode::Interactive; + assert_eq!( + state.loop_answers_recorded(), + 0, + "reach-guard: the board starts with an EMPTY journal" + ); + let src = place(&mut state, 920, crate::types::zones::Zone::Battlefield); + let live = place(&mut state, 921, crate::types::zones::Zone::Battlefield); + let dead = ObjectId(922); + // The single-slot announcement this row is about — the writer refuses without the + // prompt it answers (see `stand_up_target_prompt`). + stand_up_target_prompt(&mut state, P0, src, 1); + assert!( + !state.objects.contains_key(&dead), + "reach-guard: the unresolvable member must genuinely be absent from `objects`, \ + else this row's negative arm tests nothing" + ); + let slot = DecisionSlot::target( + object_decision_source(&state, src).expect("the source object is live"), + ); + + // ── PAIRED POSITIVE: every member resolves ⇒ BOTH pins are journalled ── + record_trigger_target_answer( + &mut state, + Some(src), + P0, + &[TargetRef::Object(live), TargetRef::Player(P1)], + ); + assert_eq!( + state.loop_answers_recorded(), + 1, + "the fully-resolvable announcement must be journalled — without this the \ + negative arm below is satisfied by any no-op writer" + ); + assert_eq!( + state.loop_answer(&slot, P0), + Some(LoopAnswer::Uniform(LoopAnswerValue::Targets(vec![ + TargetPin::ByIdentity( + object_decision_source(&state, live).expect("the live target resolves") + ), + // CR 601.2c: an ANNOUNCED seat, so the TARGET-class spelling — not + // `TargetPin::Player`, which is the CR 115.10a choice class. + TargetPin::Scheduled(TargetSchedule::Constant(Ranking::one( + AnnouncementSubject::Seat(P1) + ))), + ]))), + "CR 601.2c: the pins are journalled in ANNOUNCEMENT ORDER, and CR 400.7 binds \ + the object member to its current incarnation" + ); + + // ── THE ROW'S OWN CLAIM: one dead member abandons the whole write ── + let before = state.loop_answers_recorded(); + record_trigger_target_answer( + &mut state, + Some(src), + P1, + &[TargetRef::Object(dead), TargetRef::Player(P1)], + ); + assert_eq!( + state.loop_answers_recorded(), + before, + "CR 608.2b: an unresolvable member abandons the WHOLE write. Under a \ + `filter_map` this rises by one and journals a one-pin vector, which \ + `validate_pins` would later reject as an illegal pin value — a wrong pin \ + instead of no offer" + ); + + // ── the empty announcement the `ChooseTarget` arm's `target: None` produces ── + record_trigger_target_answer(&mut state, Some(src), P1, &[]); + assert_eq!( + state.loop_answers_recorded(), + before, + "an empty announcement is not an answer a pin can specify, so nothing is \ + journalled" + ); + } + + /// **Row F2.** CR 601.2c (reached for a triggered ability via CR 603.3d) + CR 608.2b: a + /// MULTI-SLOT announcement is refused outright, because `DecisionSlot::target` hard-codes + /// `index: 0` and would collapse every slot of it onto ONE journal key. + /// + /// # The value that makes this a defect rather than a rounding error + /// + /// Two slots taking DISTINCT targets latch `Conflicted` — fail-closed, harmless. Two slots + /// taking the SAME target — which CR 601.2c expressly permits ("if the spell uses the word + /// `target` in multiple places, the same object or player can be chosen once for each + /// instance") — stored `Uniform(Targets([one pin]))`: a TRUNCATED answer that satisfies + /// `LoopAnswerValue::Targets`' own contract ("the announced targets for ONE slot") only by + /// accident, and that a widened publisher would spend as a valid pin. Arm (c) below is that + /// exact shape. + /// + /// # Discrimination — and the axis it is keyed to + /// + /// Arms (a) and (b) differ in EXACTLY ONE fact, the prompt's `target_slots.len()`: the same + /// source, the same seat, the same single announced target. So the row cannot pass by + /// accident on the announcement's own shape. + /// + /// A `targets.len() > 1` guard — the plausible wrong reading, keyed to how many targets were + /// announced rather than how many slots were asked — passes (a) and FAILS (b) and (c), + /// because the `ChooseTarget` walk announces ONE target per beat no matter how many slots + /// the prompt carries. That is why (b)/(c) announce a single target against a two-slot + /// prompt rather than two targets at once. + /// + /// REVERT-PROBE (measured, in the fix report): delete the `announced_slots > 1` early return + /// ⇒ (b) and (c) FLIP TO FAILING, (c) with the truncated one-pin `Uniform` value named + /// above. Delete the `WaitingFor::TriggerTargetSelection` read instead ⇒ that is a compile + /// error, since `announced_slots` has no other source. + /// + /// # Reach-guard + /// + /// Arm (a) runs FIRST and asserts a POSITIVE write, so none of the refusals below is + /// satisfied by a writer that journals nothing at all. + #[test] + fn c2a_row_f2_a_multi_slot_announcement_is_refused_rather_than_collapsed() { + use crate::analysis::decision_template::{ + AnnouncementSubject, DecisionSlot, LoopAnswer, LoopAnswerValue, Ranking, TargetPin, + TargetSchedule, + }; + use crate::types::ability::TargetRef; + + let mut state = GameScenario::new_n_player(3, 7).build().state().clone(); + state.loop_detection = LoopDetectionMode::Interactive; + let src = place(&mut state, 930, crate::types::zones::Zone::Battlefield); + let slot = DecisionSlot::target( + object_decision_source(&state, src).expect("the source object is live"), + ); + + // ── (a) POSITIVE CONTROL: one announcement slot ⇒ the answer is journalled ── + stand_up_target_prompt(&mut state, P0, src, 1); + record_trigger_target_answer(&mut state, Some(src), P0, &[TargetRef::Player(P1)]); + assert_eq!( + state.loop_answer(&slot, P0), + Some(LoopAnswer::Uniform(LoopAnswerValue::Targets(vec![ + // CR 601.2c TARGET class: an announced seat takes the ranked spelling. + TargetPin::Scheduled(TargetSchedule::Constant(Ranking::one( + AnnouncementSubject::Seat(P1) + ))) + ]))), + "a single-slot announcement is exactly what this journal key describes" + ); + let after_positive = state.loop_answers_recorded(); + assert_eq!( + after_positive, 1, + "reach-guard: exactly one key exists so far" + ); + + // ── (b) THE CLAIM: the SAME announcement under a TWO-slot prompt is refused ── + stand_up_target_prompt(&mut state, P1, src, 2); + record_trigger_target_answer(&mut state, Some(src), P1, &[TargetRef::Player(P2)]); + assert_eq!( + state.loop_answer(&slot, P1), + None, + "CR 601.2c: two announcement slots are two choices, and `DecisionSlot::target`'s \ + `index: 0` can key only one of them — refuse rather than collapse" + ); + assert_eq!( + state.loop_answers_recorded(), + after_positive, + "and no key is created at all: the whole write is abandoned" + ); + + // ── (c) THE DANGEROUS SHAPE: two slots, the SAME target, answered slot by slot ── + // Pre-fix this stored `Uniform(Targets([Player(P2)]))` — a one-pin answer to a + // two-choice announcement, indistinguishable from a legitimate single-slot answer. + for _ in 0..2 { + record_trigger_target_answer(&mut state, Some(src), P2, &[TargetRef::Player(P2)]); + } + assert_eq!( + state.loop_answer(&slot, P2), + None, + "CR 601.2c permits the same target for each instance of `target`, so repeating it \ + must not read as a UNIFORM answer to the whole announcement" + ); + assert_eq!( + state.loop_answers_recorded(), + after_positive, + "(c) creates no key either" + ); + + // ── (d) no prompt in hand ⇒ no announcement to journal ── + state.waiting_for = WaitingFor::Priority { player: P0 }; + record_trigger_target_answer(&mut state, Some(src), P1, &[TargetRef::Player(P2)]); + assert_eq!( + state.loop_answers_recorded(), + after_positive, + "the writer is the `TriggerTargetSelection` reducer's authority; with no such \ + prompt there is no announced choice for it to record" + ); + } + + /// CR 114.4 + CR 608.2b: a pinned SLOT whose source is a command-zone emblem must match /// the prompt that emblem raised; a graveyard or exile source must NOT. /// - /// This is the zone predicate `inject_pinned_answer`'s `TriggerTargetSelection` arm - /// dispatches on. Its production drive lands with the bounded offer in a later commit, - /// so it is pinned here at the seam — the shipped BATTLEFIELD arm is exercised - /// end-to-end by `injector_routes_pinned_targets_per_source` above and by the - /// `kilo_live_offer_from_real_dump` rows, and this row asserts that arm is unchanged. + /// This is the zone predicate `inject_pinned_answer`'s `TriggerTargetSelection` arm + /// dispatches on, and it now has two further production callers on the drive side: + /// `pinned_targets_for_source` and `pinned_mana_color_for_source` ask the same question + /// through the same `slot_source_prompted` predicate. The shipped BATTLEFIELD arm is + /// exercised end-to-end by `injector_routes_pinned_targets_per_source` above and by the + /// `kilo_live_offer_from_real_dump` rows, and this row asserts that arm is unchanged. + /// + /// The zone disjuncts this row pins now live one call down, in + /// [`crate::analysis::decision_template::resolve_ability_instance`], which + /// `slot_source_prompted` delegates to — so all three probes below are run THERE. That + /// delegation is why this row is also the maintained-invariant row for the factoring: it + /// stays green unmodified, and a factoring that silently widened the zone set reds here. + /// (Measured: dropping the `Zone::Command` filter in that accessor fails the graveyard + /// assertion below.) + /// + /// REVERT-PROBES: (a) delete the command-zone disjunct ⇒ the + /// Command assertion FAILS (and `inject_pinned_answer` would `RecastAbort` on an + /// emblem-pinned drive); (b) widen the disjunct to accept any zone ⇒ the graveyard and + /// exile assertions FAIL; (c) drop the incarnation conjunct ⇒ the CR 400.7 assertion + /// FAILS. + #[test] + fn command_zone_sourced_slot_matches_and_graveyard_still_aborts() { + use crate::types::zones::Zone; + let mut state = GameScenario::new_n_player(2, 7).build().state().clone(); + let battlefield = place(&mut state, 900, Zone::Battlefield); + let emblem = place(&mut state, 901, Zone::Command); + let graveyard = place(&mut state, 902, Zone::Graveyard); + let exiled = place(&mut state, 903, Zone::Exile); + + let pin = |id: ObjectId, inc: Option| YieldTarget::ThisObject { + source_id: id, + incarnation: inc, + trigger_description: None, + }; + + // Shipped behaviour, unchanged: the battlefield arm still matches. + assert!( + slot_source_prompted(&state, &pin(battlefield, Some(3)), battlefield), + "the shipped CR 608.2b battlefield arm must be untouched" + ); + // CR 114.2 — an emblem lives in the command zone; CR 114.4 — its abilities function + // there, which is why the slot may prompt from there. + assert!( + slot_source_prompted(&state, &pin(emblem, Some(3)), emblem), + "CR 114.4: a command-zone emblem's slot must match the prompt it raised" + ); + // Fail-closed: every other off-battlefield zone still misses ⇒ `RecastAbort`. + assert!( + !slot_source_prompted(&state, &pin(graveyard, Some(3)), graveyard), + "a graveyard-sourced slot must NOT match — the drive aborts to manual" + ); + assert!( + !slot_source_prompted(&state, &pin(exiled, Some(3)), exiled), + "an exile-sourced slot must NOT match" + ); + // CR 400.7: the command arm re-binds ONE incarnation, exactly like the + // battlefield arm — a re-created emblem does not answer the old pin. + assert!( + !slot_source_prompted(&state, &pin(emblem, Some(2)), emblem), + "CR 400.7: a stale incarnation must not match even in the command zone" + ); + // A pin naming a DIFFERENT object never answers this prompt. + assert!( + !slot_source_prompted(&state, &pin(emblem, Some(3)), battlefield), + "the matcher is keyed on identity, not merely on zone" + ); + } + + /// **T1 — CR 608.2b + CR 114.4: the drive's tap-cost / proliferate seam matches a + /// COMMAND-zone slot source, and still refuses every other way of missing.** + /// + /// `pinned_targets_for_source` asks "which pinned `Targets` belongs to the ability + /// instance that raised this beat?" through [`slot_source_prompted`] — the predicate + /// `inject_pinned_answer` already used — instead of through the battlefield-only + /// `resolve_source`. CR 114.4 (CR 113.6p for the plane / scheme / conspiracy members of + /// the same class) is why an ability may prompt from the command zone at all; CR 608.2b + /// is why the pinned TARGETS stay battlefield-only, which row c pins. + /// + /// # Non-vacuity / discrimination + /// + /// Every row is ONE field from row b and comes out OPPOSITE it. An input that never + /// arrived cannot produce that table — it would answer the same way on both sides of the + /// field. Each negative row asserts its subject exists in the intended zone BEFORE the + /// negative assertion, so it provably fails for the stated reason rather than because the + /// object was never built. + /// + /// REVERT-PROBES: restore `resolve_source` at this seam ⇒ row **b** fails alone; widen + /// `resolve_source` to admit `Zone::Command` ⇒ row **c** fails; widen the accessor's + /// command disjunct to any zone ⇒ row **d** fails; drop the CR 400.7 incarnation + /// conjunct ⇒ row **e** fails; make the accessor answer "any command-zone object" ⇒ row + /// **f** fails. + #[test] + fn pinned_targets_for_source_matches_a_command_zone_slot_and_still_refuses_elsewhere() { + use crate::types::zones::Zone; + let mut state = GameScenario::new_n_player(2, 7).build().state().clone(); + let battlefield = place(&mut state, 900, Zone::Battlefield); + let command = place(&mut state, 901, Zone::Command); + let graveyard = place(&mut state, 902, Zone::Graveyard); + let other_command = place(&mut state, 904, Zone::Command); + let command_target = place(&mut state, 905, Zone::Command); + + let live_src = |id: ObjectId| object_decision_source(&state, id).expect("placed above"); + let bf_src = live_src(battlefield); + let cmd_src = live_src(command); + let gy_src = live_src(graveyard); + let stale_cmd_src = YieldTarget::ThisObject { + source_id: command, + incarnation: Some(2), + trigger_description: None, + }; + let bf_target = TargetPin::ByIdentity(live_src(battlefield)); + let cmd_target = TargetPin::ByIdentity(live_src(command_target)); + + let template = |slot_source: &YieldTarget, target: &TargetPin| DecisionTemplate { + owner: P0, + decisions: vec![PinnedDecision::Targets { + slot: DecisionSlot { + source: slot_source.clone(), + index: 0, + }, + targets: vec![target.clone()], + }], + replay: ReplayMode::Scheduled { + count: IterationCount::UntilLethal, + }, + key: DecisionGroupKey::from_sources( + std::slice::from_ref(slot_source), + DecisionKind::LoopChoice, + ), + }; + + // row a — the shipped battlefield arm, unchanged. Also the control that proves the + // instrument can return targets at all. + assert_eq!( + pinned_targets_for_source(&template(&bf_src, &bf_target), 0, &state, battlefield) + .expect("row a: a battlefield-sourced slot still answers its own beat"), + vec![ConcreteTarget::Object(battlefield)], + "row a: control" + ); + + // row b — THE FIX. One field from row a: the slot source's ZONE. + assert_eq!( + pinned_targets_for_source(&template(&cmd_src, &bf_target), 0, &state, command) + .expect("row b: CR 114.4 — a command-zone ability instance's slot matches"), + vec![ConcreteTarget::Object(battlefield)], + "row b: the command-zone slot source answers the beat it raised" + ); + + // row c — one field from b: the TARGET's zone. CR 608.2b is not widened. + assert_eq!( + state + .objects + .get(&command_target) + .expect("reach-guard: row c's target object was built") + .zone, + Zone::Command, + "reach-guard: row c's TARGET is in the command zone, so its Err is about the \ + target path and not about a missing object" + ); + assert!( + pinned_targets_for_source(&template(&cmd_src, &cmd_target), 0, &state, command) + .is_err(), + "row c: CR 608.2b — a pinned TARGET off the battlefield stays illegal; widening \ + the SLOT question does not widen the TARGET question" + ); + + // row d — one field from b: the slot source's zone again, the other way. + assert_eq!( + state + .objects + .get(&graveyard) + .expect("reach-guard: row d's slot-source object was built") + .zone, + Zone::Graveyard, + "reach-guard: row d's slot source exists and is in the graveyard" + ); + assert!( + pinned_targets_for_source(&template(&gy_src, &bf_target), 0, &state, graveyard) + .is_err(), + "row d: the zone set is {{Battlefield, Command}} and nothing else — a graveyard \ + source aborts the drive to manual play" + ); + + // row e — one field from b: the pinned CR 400.7 incarnation. + assert_ne!( + state + .objects + .get(&command) + .expect("reach-guard: row e's slot-source object was built") + .incarnation, + 2, + "reach-guard: the LIVE incarnation differs from the pinned one, so row e's Err \ + is about CR 400.7 and not about an absent object" + ); + assert!( + pinned_targets_for_source(&template(&stale_cmd_src, &bf_target), 0, &state, command) + .is_err(), + "row e: CR 400.7 — a re-created source is a new object and does not answer the \ + old pin, in the command zone exactly as on the battlefield" + ); + + // row f — one field from b: the identity being asked about. + for (id, label) in [(command, "the pinned"), (other_command, "the asking")] { + assert_eq!( + state + .objects + .get(&id) + .unwrap_or_else(|| panic!("reach-guard: {label} object was built")) + .zone, + Zone::Command, + "reach-guard: BOTH command-zone objects exist, so row f cannot pass by \ + absence — only by identity" + ); + } + assert!( + pinned_targets_for_source(&template(&cmd_src, &bf_target), 0, &state, other_command) + .is_err(), + "row f: the matcher is keyed on IDENTITY, not on 'some object in the command \ + zone' — a second command-zone source does not inherit this slot's answer" + ); + } + + /// **T2 — CR 608.2d + CR 114.4: the same migration at the mana-color seam.** + /// + /// `pinned_mana_color_for_source` records the CR 608.2d color choice a mana ability + /// offered; which ability instance offered it is [`slot_source_prompted`]'s question, now + /// asked with the same spelling as at the tap-cost seam. Rows a/b/d/e/f mirror T1's; a + /// `ManaColor` pin carries no targets, so T1's target-legality row c has no analogue. /// - /// REVERT-PROBES: (a) delete the command-zone disjunct in `slot_source_prompted` ⇒ the - /// Command assertion FAILS (and `inject_pinned_answer` would `RecastAbort` on an - /// emblem-pinned drive); (b) widen the disjunct to accept any zone ⇒ the graveyard and - /// exile assertions FAIL; (c) drop the incarnation conjunct ⇒ the CR 400.7 assertion - /// FAILS. + /// # Non-vacuity / discrimination + /// + /// Same shape as T1: one field from row b, opposite verdict, every negative row reach- + /// guarded on its subject's existence and zone. + /// + /// REVERT-PROBES: restore `resolve_source` at this seam ⇒ row **b** fails alone; the + /// accessor-side probes (any-zone widening, dropped incarnation conjunct, zone-not- + /// identity matching) fail rows **d**, **e**, **f** respectively. #[test] - fn command_zone_sourced_slot_matches_and_graveyard_still_aborts() { + fn pinned_mana_color_for_source_matches_a_command_zone_slot_and_still_refuses_elsewhere() { + use crate::types::mana::ManaColor; use crate::types::zones::Zone; let mut state = GameScenario::new_n_player(2, 7).build().state().clone(); let battlefield = place(&mut state, 900, Zone::Battlefield); - let emblem = place(&mut state, 901, Zone::Command); + let command = place(&mut state, 901, Zone::Command); let graveyard = place(&mut state, 902, Zone::Graveyard); - let exiled = place(&mut state, 903, Zone::Exile); - - let pin = |id: ObjectId, inc: Option| YieldTarget::ThisObject { - source_id: id, - incarnation: inc, + let other_command = place(&mut state, 904, Zone::Command); + + let live_src = |id: ObjectId| object_decision_source(&state, id).expect("placed above"); + let bf_src = live_src(battlefield); + let cmd_src = live_src(command); + let gy_src = live_src(graveyard); + let stale_cmd_src = YieldTarget::ThisObject { + source_id: command, + incarnation: Some(2), trigger_description: None, }; - // Shipped behaviour, unchanged: the battlefield arm still matches. - assert!( - slot_source_prompted(&state, &pin(battlefield, Some(3)), battlefield), - "the shipped CR 608.2b battlefield arm must be untouched" + let template = |slot_source: &YieldTarget| DecisionTemplate { + owner: P0, + decisions: vec![PinnedDecision::ManaColor { + slot: DecisionSlot { + source: slot_source.clone(), + index: 0, + }, + color: ManaColor::Blue, + }], + replay: ReplayMode::Scheduled { + count: IterationCount::UntilLethal, + }, + key: DecisionGroupKey::from_sources( + std::slice::from_ref(slot_source), + DecisionKind::LoopChoice, + ), + }; + + // row a — shipped battlefield arm + the control that the instrument returns a color. + assert_eq!( + pinned_mana_color_for_source(&template(&bf_src), 0, &state, battlefield) + .expect("row a: a battlefield-sourced slot still answers its own beat"), + ManaColor::Blue, + "row a: control" ); - // NEW: CR 114.2 — an emblem lives in the command zone and prompts from there. - assert!( - slot_source_prompted(&state, &pin(emblem, Some(3)), emblem), - "CR 114.2: a command-zone emblem's slot must match the prompt it raised" + + // row b — THE FIX, one field from a: the slot source's zone. + assert_eq!( + pinned_mana_color_for_source(&template(&cmd_src), 0, &state, command) + .expect("row b: CR 114.4 — a command-zone ability instance's slot matches"), + ManaColor::Blue, + "row b: the command-zone slot source answers the CR 608.2d choice it offered" ); - // Fail-closed: every other off-battlefield zone still misses ⇒ `RecastAbort`. - assert!( - !slot_source_prompted(&state, &pin(graveyard, Some(3)), graveyard), - "a graveyard-sourced slot must NOT match — the drive aborts to manual" + + // row d — one field from b: the slot source's zone, the other way. + assert_eq!( + state + .objects + .get(&graveyard) + .expect("reach-guard: row d's slot-source object was built") + .zone, + Zone::Graveyard, + "reach-guard: row d's slot source exists and is in the graveyard" ); assert!( - !slot_source_prompted(&state, &pin(exiled, Some(3)), exiled), - "an exile-sourced slot must NOT match" + pinned_mana_color_for_source(&template(&gy_src), 0, &state, graveyard).is_err(), + "row d: a graveyard source aborts the drive to manual play" + ); + + // row e — one field from b: the pinned CR 400.7 incarnation. + assert_ne!( + state + .objects + .get(&command) + .expect("reach-guard: row e's slot-source object was built") + .incarnation, + 2, + "reach-guard: the LIVE incarnation differs from the pinned one" ); - // CR 400.7: the command arm re-binds ONE incarnation, exactly like the - // battlefield arm — a re-created emblem does not answer the old pin. assert!( - !slot_source_prompted(&state, &pin(emblem, Some(2)), emblem), - "CR 400.7: a stale incarnation must not match even in the command zone" + pinned_mana_color_for_source(&template(&stale_cmd_src), 0, &state, command).is_err(), + "row e: CR 400.7 — a stale incarnation does not match in the command zone either" ); - // A pin naming a DIFFERENT object never answers this prompt. + + // row f — one field from b: the identity being asked about. + for (id, label) in [(command, "the pinned"), (other_command, "the asking")] { + assert_eq!( + state + .objects + .get(&id) + .unwrap_or_else(|| panic!("reach-guard: {label} object was built")) + .zone, + Zone::Command, + "reach-guard: BOTH command-zone objects exist, so row f cannot pass by absence" + ); + } assert!( - !slot_source_prompted(&state, &pin(emblem, Some(3)), battlefield), - "the matcher is keyed on identity, not merely on zone" + pinned_mana_color_for_source(&template(&cmd_src), 0, &state, other_command).is_err(), + "row f: identity, not zone, selects whose color this is" ); } @@ -15273,7 +17049,7 @@ mod stage2_injector_tests { /// row is what fails when ONE conjunct is dropped. /// /// Why each matters: `find_legal_targets` collapses a `Typed` filter to PLAYERS ONLY - /// when both `type_filters` and `properties` are empty (`targeting.rs:192-193`, issue + /// when both `type_filters` and `properties` are empty (issue /// #2004). A type- or property-bearing filter therefore falls through to OBJECT /// enumeration — publishing it would put a point whose legal set is object refs into /// player-pin machinery. `controller: You` does collapse to players, but to exactly ONE @@ -15603,7 +17379,7 @@ mod stage2_injector_tests { ); } - /// CR 114.2 + CR 608.2b, on a REAL restored 4p board: `inject_pinned_answer` accepts a + /// CR 114.4 + CR 608.2b, on a REAL restored 4p board: `inject_pinned_answer` accepts a /// pin whose slot source is the COMMAND-zone emblem (obj 541) that raised the prompt. /// /// This is the production-path row for [`slot_source_prompted`]. The seam is live @@ -15664,7 +17440,7 @@ mod stage2_injector_tests { let src = object_decision_source(&state, EMBLEM).expect("the emblem object exists"); // The control that makes this row non-vacuous: the shipped battlefield-only // `resolve_source` does NOT match this source, so an accept can only come from the - // CR 114.2 disjunct. + // CR 114.4 disjunct. assert_eq!( crate::analysis::decision_template::resolve_source(&src, &state), None, @@ -15690,7 +17466,7 @@ mod stage2_injector_tests { // ── ACCEPT: the command-zone pin answers the prompt on the real board ── let mut work = state.clone(); inject_pinned_answer(&mut work, Some(&template(src.clone())), 0, &prompt) - .expect("CR 114.2: the emblem's own pin must answer the prompt it raised"); + .expect("CR 114.4: the emblem's own pin must answer the prompt it raised"); assert_ne!( work.waiting_for, prompt, "the prompt was actually consumed, not silently skipped" @@ -15734,6 +17510,307 @@ mod stage2_injector_tests { ); } + /// **T3 — the slot-source VALUE is one a REAL board produces, and the migrated drive + /// seam accepts it** (CR 114.4 + CR 608.2b, on the restored 4p dellian board). + /// + /// Modelled on `a_command_zone_pin_answers_a_real_restored_boards_prompt` above, whose + /// structure — reach guards read off the loaded board, then a `resolve_source == None` + /// non-vacuity control, then the accept — is reused here at the OTHER consumer. + /// + /// # What this row does NOT claim + /// + /// It does not claim that a shipped card drives a command-zone-sourced tap-cost / + /// mana-color / proliferate beat *in a recorded loop period* today. It claims the + /// slot-source VALUE is one a real board produces and that `pinned_targets_for_source` + /// accepts it. + /// + /// # Non-vacuity / discrimination + /// + /// The control is load-bearing: `resolve_source` answers `None` for this very source on + /// this very board, so row a's `Ok` can only have come from the command-zone disjunct. + /// Rows b–e are each one field from row a and come out opposite. Every object is chosen + /// by PREDICATE off the loaded board (except `EMBLEM`, already a module const), so a + /// re-derived fixture cannot silently blank a row. + /// + /// REVERT-PROBES: restore `resolve_source` at this seam ⇒ row **a** fails; widen the + /// accessor to any zone ⇒ row **b** fails; drop the CR 400.7 conjunct ⇒ row **c** fails; + /// match on zone rather than identity ⇒ rows **d** and **e** fail. + #[test] + fn a_command_zone_slot_from_the_real_4p_board_answers_the_recast_beat() { + use crate::types::zones::Zone; + let state = load_dellian_dump(); + + // ── reach guards, all read off the loaded board ── + let emblem = state + .objects + .get(&EMBLEM) + .expect("reach-guard: dump B carries the emblem object"); + assert_eq!( + emblem.zone, + Zone::Command, + "reach-guard: CR 114.2 puts the emblem in the command zone" + ); + assert!( + emblem.is_emblem, + "reach-guard: CR 114.4 is the rule under test, so the object must really be an \ + emblem" + ); + let emblem_incarnation = emblem.incarnation; + + let lowest_battlefield = state + .objects + .values() + .filter(|o| o.zone == Zone::Battlefield) + .min_by_key(|o| o.id.0) + .map(|o| o.id) + .expect("reach-guard: the board has battlefield objects to target"); + let graveyard_object = state + .objects + .values() + .filter(|o| o.zone == Zone::Graveyard) + .min_by_key(|o| o.id.0) + .map(|o| o.id) + .expect("reach-guard: the board has a graveyard object for row b"); + + let src = object_decision_source(&state, EMBLEM).expect("the emblem object exists"); + // Non-vacuity control: the shipped battlefield-only `resolve_source` does NOT match + // this source, so an accept can only come from the CR 114.4 disjunct. + assert_eq!( + crate::analysis::decision_template::resolve_source(&src, &state), + None, + "CR 608.2b: `resolve_source` is battlefield-only and must stay so" + ); + + let template = |slot_source: YieldTarget| DecisionTemplate { + owner: P0, + decisions: vec![PinnedDecision::Targets { + slot: DecisionSlot { + source: slot_source.clone(), + index: 0, + }, + targets: vec![TargetPin::ByIdentity(object_decision_source_of( + &state, + lowest_battlefield, + ))], + }], + replay: ReplayMode::Scheduled { + count: IterationCount::UntilLethal, + }, + key: DecisionGroupKey::from_sources(&[slot_source], DecisionKind::LoopChoice), + }; + + // row a — positive: the real board's command-zone source answers its own beat. + assert_eq!( + pinned_targets_for_source(&template(src.clone()), 0, &state, EMBLEM) + .expect("CR 114.4: the emblem's own slot answers the beat it raised"), + vec![ConcreteTarget::Object(lowest_battlefield)], + "row a: the drive seam accepts a slot source a REAL board produced" + ); + + // row b — one field: a graveyard source off the same board. + let gy_src = object_decision_source_of(&state, graveyard_object); + assert!( + pinned_targets_for_source(&template(gy_src), 0, &state, graveyard_object).is_err(), + "row b: the zone set is {{Battlefield, Command}}; a graveyard source still aborts" + ); + + // row c — one field: the pinned CR 400.7 incarnation. + let stale = YieldTarget::ThisObject { + source_id: EMBLEM, + incarnation: Some(emblem_incarnation + 1), + trigger_description: None, + }; + assert!( + pinned_targets_for_source(&template(stale), 0, &state, EMBLEM).is_err(), + "row c: CR 400.7 — a re-created emblem does not answer the old pin" + ); + + // row d — one field: the identity being asked about. + assert!( + pinned_targets_for_source(&template(src.clone()), 0, &state, lowest_battlefield) + .is_err(), + "row d: identity, not zone, selects whose slot this is" + ); + + // row e — MULTI-AUTHORITY hostile fixture. This board holds TWO command-zone + // objects, and the second is not an emblem: object 400's two TRIGGERS declare + // `trigger_zones == ["Battlefield"]` while its cost-reduction STATIC declares + // `active_zones ∋ Command`; one object, two abilities, two different zones of + // function (CR 113.6b). The accessor admits it by zone either way, so only identity + // can exclude it. + let commander = state + .objects + .values() + .filter(|o| o.zone == Zone::Command && o.id != EMBLEM) + .min_by_key(|o| o.id.0) + .map(|o| o.id) + .expect("reach-guard: dump B carries a SECOND command-zone object"); + assert!( + !state.objects[&commander].is_emblem, + "reach-guard: the second command-zone object is not an emblem, so row e is a \ + genuine multi-authority case and not a duplicate of row a" + ); + let commander_src = object_decision_source_of(&state, commander); + assert!( + pinned_targets_for_source(&template(commander_src), 0, &state, EMBLEM).is_err(), + "row e: a DIFFERENT command-zone ability instance's slot must not answer the \ + emblem's beat — zone admits both, identity separates them" + ); + } + + /// `object_decision_source` with the test's own existence guard folded in, so a row that + /// picks its object by predicate cannot silently degrade into `None`. + fn object_decision_source_of(state: &GameState, id: ObjectId) -> YieldTarget { + object_decision_source(state, id) + .unwrap_or_else(|| panic!("reach-guard: object {id:?} is on the loaded board")) + } + + /// **T6 — the capability-neutrality pin.** Five rows on ONE board with ONE template + /// value each, pinning what resolving a command-zone `Order` pin does and does not buy. + /// + /// Both sides of the Order-only claim return `Err(RecastAbort)` — a unit struct — so the + /// two abort points are not distinguishable by return value. The instrument therefore + /// observes the abort point INDIRECTLY, through a second pin in the same template, using + /// the mechanism `resolve` is built on: it is a per-pin `Result` collect, so one failing + /// pin discards every other pin's answer. + /// + /// | row | template | seam | expect | + /// |---|---|---|---| + /// | **R** (delivery) | `order_only` | `decision_template::resolve` | `Ok`, carrying the `Order` element | + /// | **N** (neutrality, `ok_or` half) | `order_only`, the SAME value | `pinned_targets_for_source` | `Err(RecastAbort)` | + /// | **N2** (neutrality, `find_map` half) | `order_only`, the SAME value | `inject_pinned_answer` | `Err(RecastAbort)` | + /// | **P** (poisoning removed) | `mixed` | `pinned_targets_for_source` | `Ok` | + /// | **P-minus** (omit-the-pin equivalence) | `mixed` minus its `Order` element | `pinned_targets_for_source` | `Ok`, EQUAL to P's | + /// + /// # Why no row is vacuous + /// + /// * **P vs N** differ by exactly one field — the presence of the `Targets` pin — and + /// come out OPPOSITE. + /// * **N and N2 are revert-INSENSITIVE on purpose, and that insensitivity IS the + /// measurement**: "the beat still fails closed" is the claim that the verdict does not + /// move. **R is their reach guard.** R calls the same `resolve` with the same + /// `(template, 0, &state)` triple those two seams call internally, so `R == Ok` proves + /// the input arrived and resolved; N's and N2's `Err` can then only be the + /// post-resolve fall-through, never "the input never arrived". **This is the + /// load-bearing condition of the whole test: R, N and N2 must use the byte-same + /// template VALUE on the same `&state` VALUE.** If R built its own template, N and N2 + /// would become indistinguishable from non-delivery and this row would read as proof + /// while measuring nothing. + /// * **P-minus** is the security row. Its discriminating assertion is the EQUALITY, not + /// the `Ok`: it fails the moment an `Order` element contributes anything to the answer. + /// `mixed_no_order` is `mixed` minus exactly its `Order` element, built from the same + /// `targets_pin` value on the same board — if the two templates were constructed + /// independently, the `assert_eq!` would measure two hand-written templates agreeing + /// instead of the element contributing nothing. + /// + /// # What this does NOT measure + /// + /// ACCEPTANCE. Whether `mixed_no_order` is *submittable* is `declaration_conforms`' + /// question; no row here asks it, and the answer is not always yes (an `Order` pin can be + /// the sole cover of a required point, in which case dropping it fails + /// `predictability_gate` — pre-existing and zone-independent). + /// + /// REVERT-PROBES: reverting the `Order` arm to `resolve_source` ⇒ rows **R** and **P** + /// fail; **N**, **N2** and **P-minus** are deliberately revert-insensitive. + #[test] + fn a_command_zone_order_pin_stops_poisoning_the_template_without_gaining_capability() { + use crate::types::zones::Zone; + let mut state = GameScenario::new_n_player(2, 7).build().state().clone(); + let battlefield = place(&mut state, 900, Zone::Battlefield); + let command = place(&mut state, 901, Zone::Command); + stand_up_target_prompt(&mut state, P0, command, 1); + let prompt = state.waiting_for.clone(); + + let order_source = object_decision_source(&state, command).expect("placed above"); + let order_pin = PinnedDecision::Order { + source: order_source.clone(), + pos: 0, + }; + // ONE template value, bound once, shared by rows R, N and N2 (the preservation + // condition above). + let order_only = DecisionTemplate { + owner: P0, + decisions: vec![order_pin.clone()], + replay: ReplayMode::Scheduled { + count: IterationCount::UntilLethal, + }, + key: DecisionGroupKey::from_sources( + std::slice::from_ref(&order_source), + DecisionKind::LoopChoice, + ), + }; + + // ── row R (delivery): the pin re-binds through the public `resolve`. ── + let resolved = crate::analysis::decision_template::resolve(&order_only, 0, &state) + .expect("row R: a command-zone Order pin re-binds to its live ability instance"); + assert!( + resolved.iter().any(|d| matches!( + d, + crate::analysis::decision_template::ConcreteDecision::Order { source, .. } + if *source == command + )), + "row R: the resolved vec carries THIS source's Order element — the delivery \ + this row exists to prove for N and N2" + ); + + // ── row N (neutrality, the trailing-`Err` half): the SAME value, same board. ── + assert!( + pinned_targets_for_source(&order_only, 0, &state, battlefield).is_err(), + "row N: an ORDER-only template still fails closed at this element reader. Row R \ + proved the internal `resolve` returned Ok on this exact value, so this Err is \ + the post-resolve fall-through — the abort MOVED, it was not avoided" + ); + + // ── row N2 (neutrality, the `find_map` half): the SAME value, cloned board. ── + let mut injector_work = state.clone(); + assert!( + inject_pinned_answer(&mut injector_work, Some(&order_only), 0, &prompt).is_err(), + "row N2: the injector beat still fails closed too — its `find_map` looks for a \ + `Targets` element and an `Order` element is not one" + ); + + // ── the mixed pair. `mixed_no_order` is `mixed` with the FIRST element dropped and + // NOTHING else changed: one `targets_pin` value, cloned into both. ── + let targets_pin = PinnedDecision::Targets { + slot: DecisionSlot { + source: object_decision_source(&state, battlefield).expect("placed above"), + index: 0, + }, + targets: vec![TargetPin::ByIdentity( + object_decision_source(&state, battlefield).expect("placed above"), + )], + }; + let mixed = DecisionTemplate { + decisions: vec![order_pin.clone(), targets_pin.clone()], + ..order_only.clone() + }; + let mixed_no_order = DecisionTemplate { + decisions: vec![targets_pin.clone()], + ..order_only.clone() + }; + + // ── row P (poisoning removed): the OTHER pin in the same template now answers. ── + let p = pinned_targets_for_source(&mixed, 0, &state, battlefield).expect( + "row P: with the Order pin re-binding and the Targets pin resolving, the seam \ + returns the Targets pin's own answer", + ); + assert_eq!( + p, + vec![ConcreteTarget::Object(battlefield)], + "row P: one field from row N (the second pin) and OPPOSITE it" + ); + + // ── row P-minus (omit-the-pin equivalence): the security row. ── + let p_minus = pinned_targets_for_source(&mixed_no_order, 0, &state, battlefield) + .expect("row P-minus: the Order-less template resolves at both revisions"); + assert_eq!( + p, p_minus, + "row P-minus: a re-binding `Order` element contributes NOTHING to this \ + consumer's answer — the equality is the discriminating assertion, and it \ + fails the moment any consumer starts reading `ConcreteDecision::Order`" + ); + } + // ───────────────────────── 5d U2 — the shape-(B) mint ───────────────────────── use crate::types::ability::ResolvedAbility; @@ -15956,6 +18033,29 @@ mod stage2_injector_tests { /// adds nothing below; (3) the total stays **37** and the partition stays **5/7/25**, so /// neither a producer nor a reader was gained or lost. Same set, one new line number ⇒ /// benign, re-baselined here. + /// + /// ⚠ **RE-ADJUDICATED BY C1 (the CR 603.5 may-answer journal), NOT RELAXED.** `37 ⇒ 38`, + /// partition `5/7/25 ⇒ 5/8/25`. The PRODUCER half is unchanged at **5** and four of the + /// five coordinates did not move at all. The `+1` READER is **`game/engine.rs:8626`** — + /// `apply_action`'s `(OptionalEffectChoice, DecideOptionalEffect)` arm, which C1 widened + /// from `{ .. }` to bind `player` and `source_id` so it can journal the answer under + /// `(DecisionSource, PlayerId)`. It READS the (cloned) `state.waiting_for` scrutinee and + /// never writes it, so it is a reader by this instrument's own rule, and it is the same + /// benign class as U4's `inject_pinned_answer` arm. Note WHY it became visible at all: + /// the instrument deliberately skips multi-line read destructures by excluding lines + /// containing `..`, and rustfmt puts `..` on the needle's own line only while the + /// pattern body is narrow — adding two bindings pushes it to the next line. The + /// exclusion is an approximation, and this is it losing one case, not a new prompt. + /// + /// The fifth producer's coordinate moved `engine.rs:11942 ⇒ :11977`, and the shift is + /// measured rather than assumed: `git diff -U0 HEAD -- game/engine.rs` has + /// exactly six hunks above it — five `+2` journal clears paired with the ring clears at + /// `:3274/:3951/:5130/:6334/:7139`, and `+25` for the reducer arm above — summing to + /// **+35**, so predicted `11942 + 35 = 11977` equals the observed coordinate exactly. + /// Identity re-established, not assumed: the line is **sha256-identical** + /// (`8a544e87…5cc7d63`) at the old coordinate in the pre-C1 tree and at the new one + /// here, and it is still inside `begin_pending_trigger_target_selection`. C1 adds no + /// line matching the needle in a producing position anywhere. #[test] fn the_cr_603_5_prompt_census_is_pinned_so_a_sixth_producer_is_a_counted_event() { /// Every `.rs` under the crate's `src`, and the `#[cfg(test)]`-attributed @@ -16006,6 +18106,15 @@ mod stage2_injector_tests { files.sort(); assert!(files.len() > 100, "reach-guard: the walker found the crate"); + // COMMENT TEXT IS NOT A CENSUS SITE — this instrument counts CODE, and it had no + // comment rule at all until now. The consequence was measured, not theorised: a doc + // comment that quoted the needle verbatim counted ITSELF, reading 39 against a pin of + // 38, and was worked around by deleting the brace from the quotation (`aa313f122`). + // That repaired one sentence and left the counter broken for the next one. The rule now + // has ONE home for the whole repository, `crate::source_census`, which the integration + // binary compiles from the same file. + use crate::source_census::code as code_of; + // The needle is ASSEMBLED so this row's own source cannot be counted by its own // instrument. `..` excludes multi-line READ destructures whose rest-pattern sits on // a later line — the inflation the raw grep suffers from. @@ -16027,20 +18136,23 @@ mod stage2_injector_tests { .replace('\\', "/"); let test_file = rel.trim_end_matches(".rs").ends_with("_tests"); for (n, line) in lines.iter().enumerate() { - if !line.contains(&needle) || line.contains("..") { + let code = code_of(line); + if !code.contains(&needle) || code.contains("..") { continue; } if test_file || spans.iter().any(|(a, b)| (*a..=*b).contains(&n)) { in_test += 1; - } else if line.contains("waiting_for = ") - || line.contains("Ok(Some(") + } else if code.contains("waiting_for = ") + || code.contains("Ok(Some(") // `install_direct_choice_frame` owns the actual // `state.waiting_for` write. Its typed prompt argument is // still a production mint, not a reader; the call sits - // within this bounded argument expression. + // within this bounded argument expression. Read through + // `code_of` as well, so prose naming the call cannot + // promote a reader to a producer. || lines[n.saturating_sub(32)..n] .iter() - .any(|prior| prior.contains(".install_direct_choice_frame(")) + .any(|prior| code_of(prior).contains(".install_direct_choice_frame(")) { producers.push(format!("{rel}:{}", n + 1)); } else { @@ -16161,6 +18273,28 @@ mod stage2_injector_tests { // with a delegation; it sits above this producer and below the first two. // The merge tree therefore retains main's first two coordinates // (`:6177`/`:6254`) and shifts this one by −16 to `:9442`. + // Random-discard-as-a-cost (#7320, review round 1): `engine.rs:12004 ⇒ + // :12019`, +15, and ONLY the engine.rs entry moved — the four + // effects/mod.rs + scoped_library_search entries did not, which is the + // set-preservation evidence. `git diff -U0` on this file has exactly three + // hunks, ALL inside `drain_pending_cost_move_resume` at `:5761`/ + // `:5865` (+1/+13 = +14, zero deletions), i.e. entirely ABOVE this + // producer; predicted `12004+14` equals the observed coordinate exactly. + // They add the `RandomDiscardUnlessPayment` delivery resume and its + // dispatch arm — a cost-payment continuation, not + // a prompt mint: it RESUMES an already-minted `UnlessPayment` rather than + // creating a recipient, so it is correctly absent from this census. + // Identity re-established, not assumed: the producer at `:12019` is the + // same announcement-time modal mint this row NAMES — an `Ok(Some(..))` of + // the optional-effect prompt over `player` / `source_id` / + // `trigger_description` / `may_trigger_key` — still inside + // `begin_pending_trigger_target_selection`. (Spelled out rather than + // quoted: the needle above is ASSEMBLED so this row cannot be counted by + // its own instrument, and a verbatim quote here re-introduces exactly the + // self-count that defends against — it inflates `in_test` and reds the + // TOTAL assert instead of this one.) The two asserts + // above this one fired GREEN on the run that caught it — total still 37, + // partition still 5/7/25 — so no producer was added or lost. // // ⚠ THIS ROW FAILS IN CI BEFORE IT FAILS LOCALLY, and that is not a bug in the // row. CI checks out `refs/pull//merge` — this branch merged with CURRENT @@ -16180,10 +18314,369 @@ mod stage2_injector_tests { // 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. - // Re-derived against this merged tree. The production set remains five entries. - "game/effects/mod.rs:6922".to_string(), - "game/effects/mod.rs:6999".to_string(), - "game/effects/mod.rs:10237".to_string(), + // Current-main port: #7221's typed player-action completion seam and the + // contemporaneous upstream changes moved these three producers. Re-derived + // in the merged source, still in their named production functions. + // + // Fight for the Throne commander-gate unit (base 8035813e6): + // `:6640/:6717/:9922 ⇒ :6647/:6724/:9929`, uniform +7. LOCAL, not upstream, + // so the CI-vs-local diagnosis in the header does not apply. `git diff -U0` + // on effects/mod.rs has exactly four hunks: `@@ -12,7 +12,7 @@` (net 0 — the + // `CommanderOwnership` import reflow), `@@ -3238,0 +3239,3 @@` (+3, the + // `AbilityCondition::ControlsCommander` leaf arm in + // `condition_reads_filter_population`), `@@ -3639,0 +3643,4 @@` (+4, the same + // variant joining `should_resolve_subability_on_optional_decline`'s live-gate + // list), and `@@ -12474,0 +12482,23 @@` (+23, the `evaluate_condition` + // delegation to `game::commander`) which sits BELOW all three producers and + // therefore moves none of them. 3 + 4 = the whole +7, and predicted + // `6640+7`/`6717+7`/`9922+7` equal the observed coordinates exactly. None of + // the three arms mints a prompt — they are condition CLASSIFICATION arms plus + // one boolean predicate delegation. Identity re-established, not assumed: each + // producer at its new coordinate is sha256-identical to + // `8035813e6:effects/mod.rs` at its old one (`a8512b402f8675b7`, + // `82c6c569182ae4ed`, `c9d8e7ba3b9e29e2`) and still sits inside the enclosing + // function this row NAMES (`drive_sequential_repeated_optional_payment` ×2, + // `resolve_chain_body`). The diff instrument discriminates: the three OLD + // coordinates now hold a `PendingRepeatedOptionalPayment` field init, a + // `payment_unit` argument, and a `trigger_events` clone — none of which mints + // anything. Set preservation: the two asserts above this one ran FIRST and + // both fired GREEN on the run that caught this (total still 37, partition + // still 5/7/25), and the other two entries did not move. + // + // Fight for the Throne review-fix round (same base 8035813e6): + // `:6647/:6724/:9929 ⇒ :6680/:6757/:9964`, i.e. `+40/+40/+42` measured + // from BASE (`:6640/:6717/:9922`), not from the row above. LOCAL, not + // upstream, so the CI-vs-local diagnosis in the header does not apply. + // `git diff -U0 8035813e6` on effects/mod.rs now has eight hunks; the + // ones that move a producer are, in order: `@@ -2935,0 +2936,33 @@` + // (+33, `condition_survives_false_parent_gate` — the single authority + // the CR 603.4 delayed-hoist carve-out now shares with + // `resolve_chain_body`), `@@ -3238,0 +3272,3 @@` (+3) and + // `@@ -3639,0 +3676,4 @@` (+4) — the two condition-classification arms + // already logged above. 33 + 3 + 4 = the `+40` on the first two. The + // third takes a further `+2` from the two hunks INSIDE + // `resolve_chain_body` and above its own gate: + // `@@ -9679,4 +9719,10 @@` (+6, the twin sub-gate clauses collapsed + // into the shared `condition_survives_false_parent_gate` call) and + // `@@ -9701,5 +9747 @@` (−4, the corresponding conjunct removal); the + // remaining hunks (`@@ -12,7 +12,7 @@` net 0, the `evaluate_condition` + // delegation, and `mod tests`) are net-zero or BELOW all three. + // Predicted `6640+40`/`6717+40`/`9922+42` equal the observed + // coordinates exactly. None of the moved code mints a prompt: it is one + // boolean condition classifier plus the call sites that consume it. + // Identity re-established, not assumed: each producer at its new + // coordinate is sha256-identical to `8035813e6:effects/mod.rs` at its + // old one (`9869a19f28c791ee`, `2bc316e3aa0297f8`, `8df98486627bfe15`) + // and still sits inside the enclosing production function it always + // did — `drive_sequential_repeated_optional_payment` (6653-6687), + // `resolve_repeated_optional_payment_choice` (6695-6779) and + // `resolve_chain_body`. (The row above names the first two as + // `drive_sequential_repeated_optional_payment` ×2; re-derived here, the + // second is the `resolve_repeated_optional_payment_choice` resume arm, + // which is what the older entries called it.) The diff instrument + // discriminates: the three OLD coordinates now hold a blank line, an + // `.active_repeated_optional_payment_frame_mut()` call and a `//` + // comment, none of which mints anything. Set preservation: the two + // asserts above this one ran FIRST and both fired GREEN on the run that + // caught this (total still 37, partition still 5/7/25), and the other + // two entries did not move. + // + // Fight for the Throne review-fix round 2 (same base 8035813e6): + // `:6715/:6792/:9994`, i.e. `+75/+75/+72` measured from BASE + // (`:6640/:6717/:9922`), not from the row above. LOCAL, not upstream, so + // the CI-vs-local diagnosis in the header does not apply. `git diff -U0 + // 8035813e6` on effects/mod.rs has eight hunks; above the first two + // producers are `@@ -2935,0 +2936,68 @@` (+68 — the condition classifier + // of the row above, now also carrying the shared + // `sub_outlives_false_parent_gate` authority the CR 603.4 delayed-hoist + // carve-out and `resolve_chain_body` both call), `@@ -3238,0 +3307,3 @@` + // (+3) and `@@ -3639,0 +3711,4 @@` (+4): 68 + 3 + 4 = the `+75`. The third + // producer nets `−3` more from the two hunks INSIDE `resolve_chain_body` + // and above its own gate — `@@ -9679,4 +9753,0 @@` (−4, the twin + // `sub_survives_false_parent_gate` / `sub_is_replicated_or_branch` locals + // collapsed into the one shared call) and `@@ -9698,9 +9769,10 @@` (+1) — + // giving `+72`; the remaining hunks (`@@ -12,7 +12,7 @@` net 0, the + // `evaluate_condition` delegation, and `mod tests`) are net-zero or BELOW + // all three. Predicted `6640+75`/`6717+75`/`9922+72` equal the observed + // coordinates exactly. None of the moved code mints a prompt: it is one + // boolean sub-classification predicate plus the call site that consumes it. + // Identity re-established, not assumed: each producer at its new coordinate + // is sha256-identical to `8035813e6:effects/mod.rs` at its old one + // (`9869a19f28c791ee`, `2bc316e3aa0297f8`, `8df98486627bfe15`) and still + // sits inside the enclosing production function this row NAMES — + // `drive_sequential_repeated_optional_payment` (opens 6702), + // `resolve_repeated_optional_payment_choice` (opens 6730) and + // `resolve_chain_body`. The diff instrument discriminates: the three OLD + // coordinates now hold a `return Ok(());`, an + // `optional_cost_payments_this_resolution` binding and a + // `resolve_optional_effect_decision(` call, none of which mints anything. + // Set preservation: the two asserts above this one ran FIRST and both + // fired GREEN on the run that caught this (total still 37, partition still + // 5/7/25), and the other two entries did not move. + // Fight for the Throne, same branch, same base `8035813e6`: + // `:6715/:6792/:9994 ⇒ :6722/:6799/:10001`, a UNIFORM `+7` on all three, + // i.e. `+82/+82/+79` measured from the base. The row above measured + // `+75/+75/+72` from that same base, so the delta is `+7` and its sole + // cause is that the FIRST hunk GREW: `@@ -2935,0 +2936,68 @@ ⇒ + // @@ -2935,0 +2936,75 @@`, the `condition_survives_false_parent_gate` + // doc/authority block picking up seven more lines. No hunk was added or + // removed and none changed size: the other four are byte-for-byte the + // sizes the row above names (`+3` at `:3307 ⇒ :3314`, `+4` at + // `:3711 ⇒ :3718`, `−4` at `:9753 ⇒ :9760`, `+1` at `:9769 ⇒ :9776`) and + // merely shifted by the same `+7`, which is what makes the shift uniform + // even for the third producer (its `−4/+1` pair still nets the same `−3` + // below the other two). Predicted `6640+82`/`6717+82`/`9922+79` against + // `8035813e6` equal the observed coordinates exactly. Identity + // re-established, not assumed: each producer at its new coordinate is + // sha256-identical to `8035813e6:effects/mod.rs` at its old one + // (`7067db50922da31f`, `975791a569b1f587`, `967e35eb66a5780b` over the + // 15-line mint expression at each site). This card's own effects/mod.rs + // edits — the two `AbilityCondition::ControlsCommander` registrations at + // `:3314`/`:3718` and the `evaluate_condition` arm below all three — mint + // nothing: they are classifier list entries and one condition evaluator. + // + // Wheel of Misfortune (#7266), MEASURED ON THE MERGE TREE. This row's + // own header warns that a fork branch's pins are correct for the branch + // and wrong for `refs/pull//merge`; both sides of this conflict were + // that kind of local-correct. `origin/main` carried `:6306/:6383/:9578` + // and the branch carried `:6261/:6338/:9550`; NEITHER is right here, so + // the merged file was re-measured rather than either side taken: + // `:6306/:6383/:9578 => :6315/:6392/:9606`, i.e. `+9/+9/+28`. + // + // The asymmetry IS the measurement. This branch's non-test additions to + // effects/mod.rs, in file order: + // `pub mod reveal_chosen_numbers;` — 1 line, above all three. + // the `Effect::RevealChosenNumbers` dispatch arm — 3 lines, above all + // three (the dispatch table precedes every producer). + // the `QuantityRef::PlayerChosenNumber` arm in + // `candidate_player_scalar` — 5 lines, above all three. + // 1 + 3 + 5 = the uniform `+9` the first two producers take. The third + // takes a further `+19` from the depth-0 per-player secret-number ledger + // reset in `resolve_ability_chain` (16 lines, plus 3 widening the clear + // to retain both `Number` and `RevealedNumber`), which sits above it and + // below the first two: 9 + 19 = 28. Predicted and observed agree. + // + // Nothing added here raises a `WaitingFor`: the two clears and the scalar + // read are pure state reads/writes, and the dispatch arm delegates to + // `reveal_chosen_numbers::resolve`, which converts + // `ChosenAttribute::Number` to `RevealedNumber` and emits an event. The + // census set is therefore still exactly 5. + // + // NOTE for the next drift: upstream refactored the third producer from a + // `state.waiting_for = …` assignment form into a bare struct-literal value + // inside a returned tuple. It is still one producer and still matches this + // row's assembled needle, but a grep for the old assignment form now finds + // only two — measure with the needle, not with the assignment. + // + // And do NOT spell the needle literally in this comment. It is assembled + // at the top of this row precisely so the row cannot count itself, but the + // walker reads every line of this file: writing the struct-literal form + // out in prose here adds a phantom `in_test` hit per mention. Two such + // mentions in an earlier draft of this very note pushed the partition to + // 27 and reded the row — the instrument working exactly as intended. + // + // SECOND merge with main (#7221's typed player-action completion seam and + // its contemporaries). Same rule, applied again: `main` re-derived these to + // `:6640/:6717/:9922` for ITS tree and the branch carried `:6315/:6392/:9606` + // for its own; the merged file measures `:6653/:6730/:9954`, a uniform `+13` + // over main's coordinates. That `+13` is exactly this branch's four + // additions above all three producers: `pub mod reveal_chosen_numbers;` (1), + // the `Effect::RevealChosenNumbers` dispatch arm (3), the + // `QuantityRef::PlayerChosenNumber` arm in `candidate_player_scalar` (5), + // and its arm in main's new `quantity_ref_counts_population_matching` (4). + // It is uniform this time — unlike the first merge — because main's own + // churn moved the depth-0 ledger reset and the third producer together, so + // the branch's extra offset there is already inside main's baseline rather + // than stacked on top of it. + // + // Measure AFTER the last edit to effects/mod.rs, not during: an earlier + // pass here recorded `+9` from a measurement taken before that fourth arm + // was added, and the row caught the 4-line discrepancy. + // Unbounded-number round (same PR): `:6653/:6730/:9954 => + // `:6655/:6732/:9956`, a uniform `+2` — the unbounded-range arm + // added to `compute_options`' sibling classifier in this file, + // which sits above all three producers. Nothing added raises a + // `WaitingFor`; the census set is still exactly 5. + // + // MERGE OF `origin/main` (`59f5a51e`) INTO THIS BRANCH (First Family's + // characteristic-set union). This array conflicted, and the header's rule + // applied a third time: `origin/main` carried `:6656/:6733/:9974` and this + // branch carried `:6648/:6725/:9947`, each correct for its own tree and + // neither correct for the merge. NEITHER SIDE WAS TAKEN — the merged file + // was re-measured: `:6656/:6733/:9974 => :6664/:6741/:9982`, a uniform + // `+8` over main's coordinates. + // + // The `+8` is exactly this branch's net insertion into effects/mod.rs, and + // all of it sits above the FIRST producer, which is why the shift is + // uniform rather than staggered. `git diff -U0 origin/main` on that file + // has exactly four hunks, ALL between `:2966` and `:3047`: + // `filter_contains_last_created`'s characteristic-source arm (+1), + // `card_type_set_source_counts_population_matching`'s zone/tracked-set/ + // union population cases (+7), + // the `quantity_ref_counts_population_matching` arm the union folds + // into the shared helper (-1), and + // its replacement delegation (+1). + // 1 + 7 - 1 + 1 = 8, with nothing below `:3047` — predicted and observed + // agree. None of the four raises a prompt: they are population COUNTS + // (pure reads over zones, tracked sets and unions), so the census set is + // still exactly 5. + // + // Identity re-established at the new coordinates rather than assumed. Each + // producer line is byte-identical by sha256 to the same producer on BOTH + // parents — `9869a19f…`, `2bc316e3…` and `8df98486…` respectively, the + // same three digests the line carries at `:6656/:6733/:9974` on main and + // at `:6648/:6725/:9947` on this branch. The two asserts above this one + // fired GREEN on the merged tree — total still 38, partition still 5/8/25 + // — and the other two entries did not move (`scoped_library_search.rs:452` + // unmoved, `engine.rs:12773` unmoved, both re-read and sha256-confirmed in + // place). A merge that had gained or lost a producer could not leave two + // entries byte-identical AND at their coordinates while moving the other + // three by a figure the diff predicts exactly. + // + // CR-CITATION ROUND (review follow-up), LOCAL not upstream — so the + // CI-vs-local diagnosis in the header does not apply, the shift + // originates in this same diff. `:6664/:6741/:9982 => :6670/:6747/:9988`, + // a uniform `+6`. + // + // A COMMENT-ONLY round, and the census caught it, which is the row + // working exactly as designed rather than a defect in the row. + // effects/mod.rs's entire delta is two comment hunks in + // `card_type_set_source_counts_population_matching`, both ABOVE all three + // producers: `@@ -2976,2 +2976,5 @@` (+3, the `TurnJournal` arm's + // citation corrected off CR 601.2a) and `@@ -2979 +2982,4 @@` (+3, the + // `AnyOf` arm's off CR 109.2). 3 + 3 = 6, with nothing below `:2985` — + // predicted and observed agree. Prose cannot mint a prompt, and the + // census agrees: the two asserts above this one fired GREEN on the run + // that caught this (total still 38, partition still 5/8/25) and the + // panic was on this third assert alone, which is what makes it a + // coordinate shift rather than a set change. + // + // Identity re-established rather than assumed: the three producer lines + // are byte-identical by sha256 at their new coordinates to the same + // producers at `:6664/:6741/:9982` — `9869a19f…`, `2bc316e3…`, + // `8df98486…`, the same digests this log recorded one entry above. The + // other two entries did not move (`scoped_library_search.rs:452` and + // `engine.rs:12773`, both re-read and sha256-confirmed in place); this + // round does not touch either file's producer region at all. + // + // BOUNDED-UNION-WALKER ROUND (review follow-up), LOCAL not upstream. + // `:6670/:6747/:9988 => :6685/:6762/:10003`, a uniform `+15`. + // + // effects/mod.rs's whole delta is the split of + // `card_type_set_source_counts_population_matching` into a bounded + // walker plus a leaf classifier, both ABOVE all three producers: + // `@@ -2971,0 +2972,16 @@` (+16, the walker and its truncation + // contract) and `@@ -2982,7 +2998,6 @@` (-1, the `AnyOf` recursion arm + // collapsing to a no-op now that unions are unrolled before the + // classifier sees them). 16 - 1 = 15, with nothing below `:3004` — + // predicted and observed agree. + // + // The split moves a recursion; it mints nothing. The census agrees: the + // two asserts above this one fired GREEN (total still 38, partition + // still 5/8/25) and the panic was on this third assert alone. Identity + // re-established rather than assumed — `9869a19f…`, `2bc316e3…`, + // `8df98486…` at the new coordinates, the same digests recorded one + // entry above — and the other two entries did not move. + // + // THIRD merge with main (this branch × `origin/main` @ 59f5a51e, which + // by now carries Wheel of Misfortune's unbounded-number round). Same rule + // as the two merges logged above, applied a third time: each side's pins + // were local-correct and BOTH are wrong for the merged tree, so the merged + // file was re-measured rather than either side taken. `origin/main` + // carried `:6656/:6733/:9974`; this branch carried `:6722/:6799/:10001`; + // the merged file measures `:6738/:6815/:10053`. + // + // The merged coordinates are PREDICTED, not merely observed, and the + // prediction is what makes this a measurement rather than a fixup: + // `main`'s pins plus this branch's own base-relative offsets — `+82/+82/+79`, + // the figure the row immediately above derives from base `8035813e6` and + // re-derives twice — give `6656+82`/`6733+82`/`9974+79` = + // `:6738`/`:6815`/`:10053`, equal to the observed coordinates exactly. + // That the branch's offsets compose additively onto main's is the evidence + // the merge introduced no new producer and displaced none: a merge that had + // gained or lost one would break the additivity, not just shift a pin. + // + // Set preservation: the assembled needle finds exactly five hits in the + // merged effects/mod.rs (`:6738`, `:6815`, `:10053`, `:14805`, `:15290`); + // the last two fall inside the `#[cfg(test)]` span opening at `:13563` and + // so are the partition's test half, leaving the same three production + // producers this row has always pinned. Total still 37, partition still + // 5/7/25. The merge added no `WaitingFor` producer on either side — main's + // contribution here is the unbounded-range arm in `compute_options`' sibling + // classifier and this branch's is the CR 603.4 delayed-hoist carve-out, both + // pure classification code. + // + // FOURTH MERGE (this branch × `origin/main` @ `2ae92459`). The header's + // rule applies again and for the same reason: `origin/main` carried + // `:6738/:6815/:10053` and this branch carried `:6685/:6762/:10003`, each + // correct for its own tree and NEITHER correct for the merge. Neither side + // was taken — the merged file was re-measured to + // `:6767/:6844/:10082`, a uniform `+29` over main's coordinates. + // + // The `+29` is this branch's CUMULATIVE net insertion into + // effects/mod.rs relative to main, not any single round's: + // `git diff --numstat origin/main` on that file reads `33 4` = `+29`, and + // its five hunks all sit between `:3041` and `:3144`, above the first + // producer with nothing below. It is the sum of the three rounds this log + // records — `+8` (union population), `+6` (CR citations), `+15` (bounded + // walker) — which is exactly why the per-round figure is the WRONG one to + // compose here. + // + // Recorded because the first attempt at this entry got it wrong: it + // composed only the last round's `+15` onto main and predicted + // `:6753/:6830/:10068`, which the measurement contradicted. The pins below + // come from measuring the merged tree, and the arithmetic is reconciled + // to that measurement rather than the other way round. A prediction is + // evidence only when it is made against the cumulative offset. + // + // Identity re-established rather than assumed — `9869a19f…`, `2bc316e3…`, + // `8df98486…` at the new coordinates, the same three digests this log has + // carried since the first merge — and the other two entries did not move: + // `scoped_library_search.rs:452`, and `engine.rs:12796`, which is main's + // own coordinate for that producer (this branch's engine.rs edits are all + // in the census array far below it). + // + // NOTE on the prose above from main: that entry's "total still 37, + // partition still 5/7/25" describes an older census. The asserts in this + // file read 38 and 5/8/25, and both fired GREEN on the merged tree. + // + // FIFTH MERGE (this branch × `origin/main` @ `0f37d27b`, Doomsday). + // Main's own entry for this round, preserved: "#7403/#7389 move main's + // three production pins to `:6738/:6815/:10053`; the Doomsday tracked-set + // publication adds seven lines above each. Re-measured in this merged + // tree: `:6745/:6822/:10060`. The three sites remain the existing + // producers." That is main's coordinate, correct for main. + // + // Neither side taken, again. Main carried `:6745/:6822/:10060` and this + // branch carried `:6767/:6844/:10082`; the merged file measures + // `:6774/:6851/:10089`. + // + // Predicted with the CUMULATIVE offset, which is the lesson the previous + // entry records: main's `:6745` plus this branch's `+29` net insertion + // into effects/mod.rs gives `6745+29`/`6822+29`/`10060+29` = + // `:6774`/`:6851`/`:10089`, equal to the measurement. Main's `+7` + // (Doomsday) and this branch's `+29` compose additively, which is the + // set-preservation evidence: a merge that gained or lost a producer would + // break the additivity rather than merely shift a pin. + // + // Identity re-established: `9869a19f…`, `2bc316e3…`, `8df98486…` at the + // new coordinates. The other two entries did not move. + // + // CONVERGENT RE-MEASUREMENT, and the strongest evidence in this log. The + // maintainer merged the same upstream commit into this branch + // independently and in parallel, and recorded it thus: "#7404's Doomsday + // tracked-set publication and this branch's characteristic-source work + // both shift the producer coordinates. Re-measured in this merged tree: + // `:6774/:6851/:10089`. The three sites remain the existing producers." + // + // Two independent measurements of the same merged tree, agreeing to the + // line on all three coordinates. That is what a coordinate this log can + // trust looks like — and it is why the two prose entries are BOTH kept + // rather than one overwriting the other: they are separate witnesses, not + // duplicates. + "game/effects/mod.rs:6774".to_string(), + "game/effects/mod.rs:6851".to_string(), + "game/effects/mod.rs:10089".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. @@ -16500,12 +18993,376 @@ mod stage2_injector_tests { // card entry boundary. The producer remains byte-identical; only its coordinate moves. // // SET PRESERVATION: unchanged. Upstream adds no line matching the needle to this file and - // neither does this branch — total still 37, partition still 5/7/25. + // neither does this branch. + // + // REBASE — this coordinate has absorbed THREE independent shifts and all are folded + // here rather than each overwriting the last. From the merge base at `:12004`: + // upstream #7303 round 3: -1 (the `ReturnAsAuraTarget` resume arm's two raw + // attach calls became one call to the entering-Aura attachment authority, + // `-8 +7`, in a hunk ABOVE this producer) + // upstream #4155: +5 (seven lines for abandoned-cast finalization, less two + // removed by its deferred-resume cleanup — also entirely above this producer) + // lane C1 (CR 603.5 may-answer journal): +35 (five `+2` journal clears paired + // with the five ring clears, plus `+25` for the `DecideOptionalEffect` arm) + // No two of those hunks overlap, so the shifts compose: 12004 -1 +5 +35 = 12043. + // The value below is MEASURED in the rebased file by content digest, never computed + // from that sum; the sum is retained only as the prediction it agreed with, and it + // did agree. The offset from the enclosing fn is the control and is unchanged at 134. // - // Maintainer port: current main's producer is at `:12003`; this PR's owner - // boundary/trigger-construction additions occur above it, producing `:12058` - // in the merged tree. It remains inside `begin_pending_trigger_target_selection`. - "game/engine.rs:12058".to_string(), + // Producer identity re-established rather than assumed: the line at the new + // coordinate is byte-identical to the base's `:12004` and to upstream's `:12003` + // (`return Ok(Some(WaitingFor::OptionalEffectChoice {`), and it is still inside + // `begin_pending_trigger_target_selection`. THE OPENING BRACE IS QUOTED WHOLE + // AGAIN, and that is the point: it was dropped as a workaround because the census + // had no comment filter and this sentence counted ITSELF as the 39th hit against + // a pin of 38. Mutilating the prose repaired the sentence, not the counter — the + // next whole quotation anywhere in the crate would have broken it again. The + // counter now excludes comment text, so this line is a comment and is not a site; + // restoring the brace is what makes that repair MEASURED on the real tree rather + // than latent. Under the old rule this exact line reds the census at 39. + // + // TO BE UNAMBIGUOUS FOR THE NEXT READER: the `+1` in `apply_action`'s + // `DecideOptionalEffect` arm is a READER, NOT A SIXTH PRODUCER. It destructures the cloned `state.waiting_for` + // scrutinee to journal the answer and never assigns `state.waiting_for`; the + // producer count in this vec is still five and this branch mints no new prompt. + // + // ⚠ RE-ADJUDICATED BY C2a (the CR 608.2b target axis on the same journal), NOT + // RELAXED. `:11977 ⇒ :12052`, **+75**, and ONLY this entry moved — the three + // `effects/mod.rs` pins and `scoped_library_search.rs:452` are in files C2a does + // not touch and did not move at all, which is the set-preservation evidence. The + // total stays **38** and the partition **5/8/25**: both of those asserts ran and + // fired GREEN on the run that caught this, so no producer or reader was gained. + // The `+75` is fully accounted for by C2a's own hunks ABOVE this line, measured + // with `git diff -U0 70fcd851a -- game/engine.rs`: `-3` (`entry_publishes_pin_slots`'s + // may-slot literal collapsing into `DecisionSlot::may`), `+1` (its target-slot + // comment), `+53` (`record_trigger_target_answer` and its doc), `+6`/`-2` (the + // `DecideOptionalEffect` arm re-expressed over `DecisionSlot::may` + + // `LoopAnswerValue::May`), `+1` (`source_id` bound in the `SelectTargets` arm) and + // `+19` (both `TriggerTargetSelection` arms' journal calls and their comments) — + // summing to exactly `+75`, so predicted `11977 + 75 = 12052` equals the observed + // coordinate. EVERY OTHER HUNK IN THIS FILE IS BELOW THIS PRODUCER — row T5 and + // this comment block, both inside `mod stage2_injector_tests` — which is why the + // shift equals the sum above it exactly. (No whole-file total is quoted here on + // purpose: this comment is itself part of that total, so the number could not be + // stated without falsifying itself.) Identity + // re-established rather than assumed: the line is sha256-identical + // (`8a544e87…5cc7d63` — the SAME digest this doc already recorded above) and is + // still inside `begin_pending_trigger_target_selection` (`:11843 ⇒ :11918`, the + // same `+75`). The diff instrument discriminates: the NEW tree at the OLD + // coordinate `:11977` holds a bare `source_id,` struct-field line, which mints + // nothing. C2a adds NO line matching the needle in a producing position. + // + // ⚠ C2a FIX ROUND (round 2/3, closing an independent review's F1/F2): `:12052 ⇒ :12132`, + // **+80**. RE-ADJUDICATED BY THE ORCHESTRATOR, NOT BY THE IMPLEMENTER — the executor was + // instructed to REPORT the shift and leave the literal alone, precisely so the number could + // not be nudged until the row passed. It complied; this line is the orchestrator's. + // + // Located BY CONTENT FIRST, arithmetic afterwards as a CHECK, per the doctrine at the head of + // this log. The line whose sha256 (WITH trailing newline) is + // `8a544e878d3e77fb80391b95af8f74059540d5ce4ad6fb83559f364df5cc7d63` sits at `:12132`; that + // digest matches exactly ONE line under a whole-file scan, so the coordinate is unambiguous. + // It is still inside `begin_pending_trigger_target_selection`, which opens at `:11998` with no + // intervening `fn`. The checks, computed AFTER locating the line and never used as its source: + // `12052 + 80 = 12132` for the producer and `11918 + 80 = 11998` for the function's opening + // line — the SAME `+80`, which is what a set of hunks lying wholly above one producer requires. + // + // The `+80` is accounted for by six hunks above this producer: `+2` (`EntryPinSlots.target` + // doc), `+1` (`legal_targets` doc), `+6` (fn doc), `+37` (the forced-target withhold), `+26` + // (writer doc) and `+8` (the writer's multi-slot guard). The remaining hunks are inside + // `mod stage2_injector_tests` and therefore below it. + // + // SET PRESERVATION: unchanged, and this is the conjunct that makes the move a SHIFT rather + // than a census drift. The other four entries are byte-identical AND unmoved + // (`effects/mod.rs:6252/6329/9522`, `scoped_library_search.rs:452`) — this round touches + // neither file. The total (**38**) and partition (**5/8/25**) asserts both ran FIRST and fired + // GREEN; the panic was on the third assert alone. Withholding a published `Targets` point + // removes a DECISION POINT, not a prompt producer, so no line matching the needle is added or + // removed by this round. + // + // ⚠ C2a FIX ROUND 3 (the cap round, closing the CR 704.5a bound regression the round-2 review + // found): `:12132 ⇒ :12302`, **+170**. Orchestrator's adjudication; the executor reported the + // shift and left the literal alone, as instructed. + // + // PURELY POSITIONAL, and that is measured rather than asserted: every hunk this round adds + // sits above `engine.rs:3133` (the announcement/charging split — `entry_announces`, + // `AnnouncedTarget`/`TargetAnnouncement`/`EntryAnnouncement`, and + // `bounded_cycle_charged_targets_for_window`), and there is NO hunk between there and this + // producer. The other four entries are byte-identical AND unmoved. + // + // Located BY CONTENT FIRST, arithmetic afterwards as a CHECK. The line whose sha256 (WITH + // trailing newline) is `8a544e878d3e77fb80391b95af8f74059540d5ce4ad6fb83559f364df5cc7d63` + // sits at `:12425`, and that digest matches exactly ONE line under a whole-file scan. It is + // still inside `begin_pending_trigger_target_selection`, which opens at `:12291` with no + // intervening `fn`. Checks computed AFTER locating it: `12302 + 123 = 12425` for the producer + // and `12168 + 123 = 12291` for the function's opening line — the SAME `+123`. + // + // FOURTH re-derivation of this one coordinate (`:12052 → :12132 → :12302 → :12425`), + // and the reason it keeps moving is that it is a LINE NUMBER in the most-edited function + // of the most-edited file. Every move has been resolved BY CONTENT FIRST — the digest + // above has been this producer's identity since `a6d1a0e62` and has never itself changed + // — with arithmetic used only as a check that agrees afterwards. A coordinate re-derived + // four times to the same content is evidence the pin tracks the right line, not evidence + // the pin is fragile. + // + // SET PRESERVATION (C2a round 4): that round adds a withhold CONDITION, not a prompt + // producer. `entry_announces` reports an announcement; it does not assign + // `state.waiting_for`, so no line matching the needle is added or removed (grep-counted 0 + // on both the `+` and `-` sets). Total (38) and partition (5/8/25) both fire GREEN first; + // the panic was on the third assert alone, which is what makes it a coordinate shift + // rather than a population change. + // + // item-4 C2b (`WaitingFor::LoopShortcut.declaration`), base `1bc45bb8c`: `:12425 ⇒ :12552`, + // `+127`, and ONLY this entry moved — the other four live in `effects/` and + // `scoped_library_search.rs`, which this commit does not touch. LOCAL, not upstream, so the + // CI-vs-local diagnosis in the header does not apply. + // + // LOCATED BY CONTENT FIRST, as this log requires: the line at `:12552` is sha256-identical + // (`8a544e878d3e77fb80391b95…`, the digest this producer has carried since `a6d1a0e62`) to + // `1bc45bb8c:game/engine.rs:12425`, and it is still inside + // `begin_pending_trigger_target_selection`, which moved by the same `+127` (opens + // `:12291 ⇒ :12418`). Arithmetic afterwards as a CHECK: `git diff -U0` on this file has five + // hunks above the producer — `+4` and `+3` (the two `declaration: None` mints with their + // reasons, in `reconcile_terminal_result` and `interactive_loop_bridge`), `+5` (the mint + // wiring in `certified_bounded_cycle_offer`), `+110` (`build_bounded_declaration` and its + // doc), and `+5` (`apply_action`'s `declaration: _` discharge and its deferral note) — which + // sum to exactly `+127`. The file's remaining two hunks (`mod bounded_declaration_tests`, + // `+302`, and one `#[cfg(test)]` field, `+1`) sit BELOW it. + // + // SET PRESERVATION: this commit adds a FIELD to `WaitingFor::LoopShortcut` and one + // declaration consumer; neither assigns `state.waiting_for` to an + // `OptionalEffectChoice`, so no line matching the needle is added or removed. The total (38) + // and the partition (5/8/25) both fired GREEN on the run that caught this; the panic was on + // this third assert alone, which is what makes it a coordinate shift rather than a + // population change. + // + // item-4 C2b FIX ROUND (F1: `declaration_conforms`, the shared declare-legality + // authority), base `908720e6f`: `:12552 ⇒ :12582`, `+30`, and ONLY this entry moved — + // the other four live in `effects/` and `scoped_library_search.rs`, untouched here. + // LOCAL, not upstream, so the CI-vs-local diagnosis in the header does not apply. + // + // LOCATED BY CONTENT FIRST, as this log requires: the line at `:12582` is + // sha256-identical (`8a544e878d3e77fb80391b95…`, the digest this producer has carried + // since `a6d1a0e62`) to `908720e6f:game/engine.rs:12552`, and it is still inside + // `begin_pending_trigger_target_selection`, which moved by the same `+30` (opens + // `:12418 ⇒ :12448`). Arithmetic afterwards as a CHECK: `git diff -U0` on this file has + // five hunks above the producer — `+22` (`build_bounded_declaration`'s "PUBLISHED IS + // VALIDATED" doc section), `0` (the `Some(..)` tail rebound to `let template = ..`), + // `+10` (step (5)'s `declaration_conforms` call), `-2` (the `required` derivation + // DELETED from `handle_declare_shortcut`, now derived once inside the authority) and + // `0` (that site's condition rewritten in place) — summing to exactly `+30`. The + // file's remaining hunk (row D8 in `mod bounded_declaration_tests`, `+139`) is BELOW. + // + // SET PRESERVATION: this round adds one validation call and one `#[cfg(test)]` row; + // neither assigns `state.waiting_for` an `OptionalEffectChoice`, so no line matching + // the needle is added or removed. The total (38) and the partition (5/8/25) both fired + // GREEN on the run that caught this; the panic was on this third assert alone. + // + // ⚠ C3 (the stale-coordinate comment sweep), REBASED ONTO THE C2b FIX ROUND: + // `:12582 ⇒ :12590`, `+8`, LOCAL — measured at this tip, not carried. C3's own + // hunks above this producer are unchanged and have always summed to `+8`: `+1` in + // `shortcut_drive_period` and `+1` in `handle_declare_shortcut` (both replacing a + // measured-wrong "8 KB" WS frame cap with `phase-server`'s `MAX_WS_MESSAGE_BYTES`, + // 64 KB), `+2` on `reject_shortcut_declaration`'s doc (rotted `MagicCompRules.txt` line + // numbers dropped in favour of the CR numbers, which are the stable identifiers), and + // `+4` on `handle_decline_shortcut`'s doc (the twice-rotted `engine.rs:3006-3011` + // ring-clear coordinate replaced by a SYMBOL reference). `engine.rs`'s entire delta in + // C3 is COMMENT HUNKS and nothing else, so a comment round cannot mint a prompt. + // + // THIS ENTRY'S BASE HAS NOW BEEN RE-DERIVED SIX TIMES, and recording that is the point. + // C3 was authored against `70fcd851a` (`:11977 ⇒ :11985`); successive rebases moved its + // base to C2a's `:12052`, then `:12132`, then `:12302`, then C2a round 4's `:12425`, + // then C2b's `:12552`, and now the C2b fix round's `:12582`. Every stored number was + // correct only for the parent it was written against, and every time the CONTENT was + // unchanged. **A coordinate is a fact about a tree, not a property of this commit** — + // which is exactly why C3 replaces line coordinates with SYMBOL references everywhere + // else, and why this row's own pin is the one place that cannot take its own advice. + // + // Resolved BY CONTENT FIRST, arithmetic afterwards as a CHECK: the line whose sha256 + // (WITH trailing newline) is `8a544e878d3e77fb80391b95af8f74059540d5ce4ad6fb83559f364df5cc7d63`, + // which must match exactly ONE line under a whole-file scan and must still sit inside + // `begin_pending_trigger_target_selection` with no intervening `fn`. + // + // SET PRESERVATION (C3): unchanged. The other four entries live in `game/effects/mod.rs` + // and `game/effects/scoped_library_search.rs`, neither of which C3 touches, and a comment + // round adds no line matching the needle — total still 38, partition still 5/8/25. + // + // ⚠ item-4 R1 (the `Ranking` parameterization): `:12590 ⇒ :12575`, `-15`, LOCAL. + // Resolved BY CONTENT FIRST per the protocol above: the sha256 above matched exactly + // ONE line under a whole-file scan, at `:12575`, and the nearest preceding `fn` is + // still `begin_pending_trigger_target_selection` (`:12441`) with none intervening. + // Arithmetic CHECK afterwards: `git diff -U0` against the parent shows exactly four + // non-zero hunks above the old coordinate — `+2` and `+5` on `shortcut_drive_period`'s + // doc (Ruling B's dormancy REASON restated: the type now admits a seat subject, so the + // dormancy is a measured producer property rather than a structural one) and `-5`/`-17` + // for `slot_source_prompted`'s factoring into + // `analysis::decision_template::resolve_ability_instance` (a doc block and its two + // inlined zone arms, replaced by one delegating call and a pointer) — summing to `-15`. + // SET PRESERVATION: all four hunks are a doc block or a delegating call; none assigns + // `state.waiting_for` and none mints a prompt, and this round's remaining `engine.rs` + // hunks are inside `#[cfg(test)]` below this producer. The total (38) and the + // partition (5/8/25) both fired GREEN on the run that caught this; only this third + // assert panicked. + // + // ⚠ item-4 R2 (the seat-pin provenance split): `:12575 ⇒ :12606`, `+31`, LOCAL. + // Resolved BY CONTENT FIRST per the protocol above: the sha256 recorded there + // matched exactly ONE line under a whole-file scan, at `:12606`, and the nearest + // preceding `fn` is still `begin_pending_trigger_target_selection` (`:12472`) with + // none intervening. Arithmetic CHECK afterwards: `git diff -U0` against the parent + // shows exactly four hunks above the old coordinate — `+5` on `entry_announces`' + // withhold rationale (a comment), `+12` on `shortcut_drive_period`'s dormancy doc + // (a comment), and `+1`/`+13` inside `record_trigger_target_answer` (its `use` + // list and the `TargetRef::Player` arm re-spelled to + // `Scheduled(Constant(Ranking::one(AnnouncementSubject::Seat(..))))`) — summing to + // `+31`, and `12575 + 31 = 12606` exactly. SET PRESERVATION: two of the four hunks + // are pure comment; the other two are a `use` list and ONE expression inside a + // `LoopAnswerValue::Targets` mapping, which assigns no `state.waiting_for` and + // mints no `OptionalEffectChoice` prompt. This round's remaining `engine.rs` hunks + // are all inside `#[cfg(test)] mod stage2_injector_tests`, BELOW this producer. The + // total (38) and the partition (5/8/25) both fired GREEN on the run that caught + // this — only this third assert (`:17342`) panicked. + // + // R2b (the slot-question accessor migration at the last three call sites): + // `:12606 ⇒ :12622`, +16. LOCAL, not upstream — the CI-vs-local diagnosis in the + // header does not apply. Arithmetic CHECK: `git diff -U0` against the parent has + // NINE hunks above the old coordinate, netting exactly `+16`, and + // `12606 + 16 = 12622`. SET PRESERVATION: every one of those nine is either a + // doc/comment rewrite (the CR 114.2 → CR 114.4 / CR 113.6p sweep, the two + // `pinned_*` headers, `slot_source_prompted`'s header, `bounded_cycle_pin_slots`' + // class list) or ONE of the two production call-site swaps + // (`resolve_source(&slot.source, clone) == Some(source_id)` ⇒ + // `slot_source_prompted(clone, &slot.source, source_id)`), which changes which + // predicate answers a slot question and assigns no `state.waiting_for` — it mints + // no `OptionalEffectChoice` prompt. This round's remaining `engine.rs` hunks are + // inside `#[cfg(test)] mod stage2_injector_tests`, BELOW this producer. The total + // (38) and the partition (5/8/25) both fired GREEN on the run that caught this — + // only this third assert panicked. Identity re-established rather than assumed: + // line `:12622` is byte-identical by sha256 + // (`8a544e878d3e77fb…5cc7d63`, the SAME hash this log recorded for `:11549` and + // `:11583`) to `10e80db9c:engine.rs:12606`, and it is still inside + // `begin_pending_trigger_target_selection`, which moved by the same +16 (opens + // `:12472 ⇒ :12488`). + // + // ⚠ item-4 R3 (the drive-end seam's CR 732.2a doc amendment): `:12622 ⇒ :12646`, + // `+24`. LOCAL, and a COMMENT-ONLY round. Resolved BY CONTENT FIRST per the + // protocol above: the sha256 this log already records for this producer + // (`8a544e878d3e77fb…5cc7d63`) matches EXACTLY ONE line under a whole-file scan + // of the new tree, at `:12646` — and exactly one in the parent, at `:12622` — and + // it is still inside `begin_pending_trigger_target_selection`, which moved by the + // same +24 (opens `:12488 ⇒ :12512`). Arithmetic CHECK afterwards, never as the + // source: `git diff -U0` against the parent shows exactly ONE hunk ABOVE this + // producer, `@@ -4452,0 +4453,24 @@` inside `materialize_fixed_shortcut` — the + // CR 732.2a episode-boundary amendment — and `12622 + 24 = 12646` exactly. (The + // file carries a SECOND hunk, this very comment block; it is BELOW the producer + // and so contributes nothing to the coordinate. Counting whole-file hunks instead + // of hunks-above-the-producer is the arithmetic slip to avoid here.) + // SET PRESERVATION: all 24 inserted lines are `//` comments, so no + // `waiting_for = ` or `Ok(Some(` line was added and a comment round cannot mint a + // prompt. R3's `crates/engine/src` diff is comment-only APART FROM THIS PIN + // STRING: with comment lines stripped, `analysis/decision_template.rs` is + // byte-identical to the parent and `game/engine.rs` differs in exactly one line — + // the pin literal directly below. The total (38) and the partition + // (5/8/25) both fired GREEN on the run that caught this — only this third assert + // panicked. + // + // ⚠ item-4 R3 FIX-ROUND 3 (reword of that same block's abort-entry PROBE-PINNED + // clause, which called the window "equally live" while reporting `answers=0`): + // `:12646 ⇒ :12651`, `+5`. LOCAL, COMMENT-ONLY again, same protocol: the recorded + // sha256 (`8a544e878d3e77fb…5cc7d63`, verbatim line + trailing newline) matches + // EXACTLY ONE line under a whole-file scan of the new tree, at `:12651` — and + // exactly one in the parent, at `:12646` — and it is still inside + // `begin_pending_trigger_target_selection`, which moved by the same +5 (opens + // `:12512 ⇒ :12517`). Arithmetic CHECK afterwards, never as the source: `git diff + // -U0` shows exactly ONE hunk ABOVE this producer, `@@ -4469,2 +4469,7 @@` inside + // `materialize_fixed_shortcut` (2 comment lines ⇒ 7), and `12646 + 5 = 12651`. + // The other hunk is this very block plus the pin below it — BELOW the producer, + // contributing nothing, the same slip the entry above flags. SET PRESERVATION + // holds identically: all 5 net inserted lines are `//` comments, so no + // `waiting_for = ` or `Ok(Some(` line was added, and the pin below is once more + // the ONLY non-comment line in this round's `crates/engine/src` diff. + // ⚠ RE-REBASE onto upstream `7127326673`: `:12712 ⇒ :12717`, the **+5** that + // upstream #4155 inserts above this producer (seven lines for abandoned-cast + // finalization, less two removed by its deferred-resume cleanup). LOCATED BY + // CONTENT DIGEST, never by arithmetic: the line whose sha256 is + // `8a544e87…5cc7d63` matches exactly ONE line under a whole-file scan and is + // still inside `begin_pending_trigger_target_selection`, at the invariant offset + // 134. `12712 + 5` is the CHECK that agreed, not the derivation. + // + // ⚠ item-4 C2 (the manual declare path honours the offer's own published + // declaration): `:12717 ⇒ :12759`, `+42`. LOCAL, not upstream. LOCATED BY + // CONTENT DIGEST, never by arithmetic: the line whose sha256 is + // `8a544e87…5cc7d63` — the digest this log has carried since `a6d1a0e62` — + // matches EXACTLY ONE line under a whole-file scan of the new tree, at `:12759`, + // and exactly one in the parent, at `:12717`. It is still inside + // `begin_pending_trigger_target_selection` (`:12625`) with no intervening `fn`, + // at the INVARIANT OFFSET 134 — `12759 - 12625`, and the parent's + // `12717 - 12583`. Arithmetic CHECK afterwards, never as the source: `git diff + // -U0` against the parent shows FOUR hunks, ALL above this producer — `+4` + // (`LoopShortcutOffer`'s new `declaration` field and its doc), `+35` + // (`handle_declare_shortcut`'s `or_else` and the placement rationale above it), + // `+2` net (`apply_action`'s `declaration: _` discharge rewritten as a bind, + // `-5`/`+7`) and `+1` (`declaration: declaration.as_ref(),` in the struct + // literal) — summing to exactly `+42`, and `12717 + 42 = 12759`. + // + // DERIVED TWICE, ACROSS A REBASE, AND THAT IS THE ENTRY'S POINT. This value was + // first measured pre-rebase against `b51e45c59`, then DISCARDED unused and + // re-derived from scratch against the rebased tree rather than carried — the + // discipline the entry six above states as *"a coordinate is a fact about a + // tree, not a property of this commit"*. The two derivations agreeing is a + // result, not a shortcut that was taken. (The rebase moved this file's OTHER + // stale element for us: upstream `d11529d0c` re-pinned + // `game/effects/mod.rs:9922 ⇒ :9932`, which arrived through the rebase already + // correct and is not this commit's to touch.) + // + // SET PRESERVATION: C2 adds ONE production statement (an `Option::or_else`) and + // one struct field, and rewrites a match-arm binding from `declaration: _` to a + // bind. None of the three assigns `state.waiting_for`, so no line matching the + // needle is added or removed and no `OptionalEffectChoice` prompt can be minted. + // Confirmed by the failure shape rather than by inspection alone: the total (38) + // and the partition (5/8/25) both fired GREEN on the run that caught this, and + // the panic was on this third assert alone — which is what makes it a coordinate + // shift rather than a population change. + // ⚠ REBASE onto upstream `635c51ec4` (#7382, pre-entry opponent controller): + // `:12759 ⇒ :12763`, +4 from a hunk at `apply_action` `@@ -9867,0 +9868,4 @@`, + // entirely above this producer. MEASURED in the rebased file, never computed: the + // offset from `begin_pending_trigger_target_selection` is the control and is STILL + // 134, which is what re-establishes identity — the same mint text occurs at several + // coordinates in this crate, so the offset discriminates where the text cannot. + // This rebase raised the literal as a CONFLICT twice and then drifted it SILENTLY a + // third time at the tip; only the offset control caught the silent one. That is the + // drift class FU-4 (content-hash coordinate anchor) exists to end. + // #7320's random-discard continuation adds ten lines above this producer in the + // merged tree. Re-derived by the exact producer text at `:12773`, not by carrying + // the prior coordinate. + // Proliferate frame-orphan fix (#7384): `:12773 ⇒ :12796`, +23, and ONLY this + // engine.rs entry moved — the four `effects/mod.rs` + + // `scoped_library_search` entries were re-read byte-identical AND in + // place, which is the set-preservation evidence. `git diff -U0` on this + // file has exactly seven hunks; six sit at `:11`–`:11687`, entirely ABOVE + // this producer: net `0` (a dropped `PlayerActionKind` import), `+1` (the + // `game_state` import list gaining a line), `-7` and `+9` (the + // `ProliferateChoice` handler taking its frame BEFORE applying counters), + // `+24` (the loop-pin block moved above `apply_proliferate`, plus the + // completion construction) and `-4` (the terminal `EffectResolved` push + // moving into `continue_proliferate_actions`). `0+1-7+9+24-4 = +23`, and + // predicted `12773+23` equals the observed coordinate exactly. The seventh + // and only remaining hunk is THIS drift note, which sits at `:18964` — + // below the producer — so nothing that moved it is unaccounted for. + // Deliberately stated WITHOUT pinning that hunk's own line count or the + // whole-file delta: this note is self-referential, its length feeds any + // such total, and the previous revision of this row asserted a + // whole-file figure that its own next wording edit falsified by exactly + // the size of that edit. The six above-producer hunks are the whole + // load-bearing claim; the seventh is identified by position, which no + // rewording can invalidate. + // None of it mints a prompt: the handler consumes an ALREADY-minted + // `ProliferateChoice`, and the completion defers a keyword action rather + // than creating a recipient, so the census set is still exactly 5. + // Identity re-established, not assumed, on BOTH controls this row uses: + // the line at `:12796` is sha256-identical (`8a544e87…5cc7d63`) to + // `origin/main:crates/engine/src/game/engine.rs:12773`, and its offset from + // `begin_pending_trigger_target_selection` (`:12662`) is STILL 134 — the + // control that caught this row's one historical SILENT drift. + "game/engine.rs:12796".to_string(), ], "the five production producers, NAMED: the CR 603.5 gate in `resolve_chain_body` \ plus the two repeated-optional-payment drivers, the per-player acceptance cursor \ @@ -16519,15 +19376,15 @@ mod stage2_injector_tests { let effects_src = std::fs::read_to_string(root.join("game/effects/mod.rs")) .expect("readable effects module"); let authority = format!("{}_prompt_player", "optional"); - // CODE LINES ONLY. A whole-file `matches()` also counted PROSE, and this PR's C1 adds a - // doc link to the authority in `upfront_optional_gate`'s comment — a mention that is - // neither a definition nor a call. Excluding `//` lines makes the instrument STRICTLY - // MORE specific to the thing it names (a second CALL) rather than less: the pinned - // count is unchanged at 2, and a real second call still trips it because a call cannot - // live on a comment line. + // CODE ONLY, and now the CODE HALF of each line rather than only non-comment lines: + // a whole-file `matches()` counted PROSE, and this PR's C1 adds a doc link to the + // authority in `upfront_optional_gate`'s comment — a mention that is neither a + // definition nor a call. `crate::source_census::code` is the shared rule; the pinned + // count is unchanged at 2 (re-measured), and a real second call still trips it because + // a call cannot live in comment text. let authority_code_hits = effects_src .lines() - .filter(|l| !l.trim_start().starts_with("//")) + .map(crate::source_census::code) .filter(|l| l.contains(&authority)) .count(); assert_eq!( @@ -16813,9 +19670,11 @@ mod stage2_injector_tests { /// dispatched" from "the injector returned `Ok(())` having done nothing": an empty board /// would answer `Ok(())` just as happily. /// - /// The pinned source is a BATTLEFIELD object because `resolve_source` is battlefield-only - /// (CR 400.7 incarnation binding) — on any other zone `slot_source_prompted` would refuse - /// every arm below for a reason none of them is about. + /// The pinned source is a BATTLEFIELD object because `slot_source_prompted` asks + /// `resolve_ability_instance`, whose zone set is {Battlefield, Command} at the pinned + /// CR 400.7 incarnation — on a graveyard / exile / hand source it would refuse every arm + /// below for a reason none of them is about. (A command-zone source would be admitted; + /// the battlefield one is chosen because these rows are not about the zone at all.) fn u4_may_board(asked: PlayerId) -> (GameState, ObjectId) { use crate::types::ability::{Effect, QuantityExpr, TargetFilter}; let mut state = GameScenario::new_n_player(3, 7).build().state().clone(); @@ -17126,6 +19985,7 @@ mod stage2_injector_tests { per_cycle: None, }, schema: ShortcutDecisionSchema::default(), + declaration: None, }; } @@ -17557,7 +20417,7 @@ mod kilo_interruptibility_tests { /// combo-interruptibility-acceptance-criterion). A declined `Counters`/`Life` axis leaves its /// ∞ capability marker in `unbounded_resources` intentionally (CR 732.2b never forces a /// shortcut). This test guards the MEASURED retirement path (a) documented at the boundary - /// seam: the empty-stack offer hook `try_offer_object_growth_shortcut` (engine.rs:472) is NOT + /// seam: the empty-stack offer hook `try_offer_object_growth_shortcut` is NOT /// gated by existing ∞ marks, so a later genuine re-detection RE-OFFERS the loop and can /// re-collapse the declined axis once the observer is gone. /// @@ -18412,11 +21272,13 @@ mod bounded_offer_conjunct_tests { /// Code lines (comments excluded, per R8's ruling: a comment reads nothing) of an extent /// that contain `needle`, as absolute line indices. + /// + /// "Comments excluded" means the shared `crate::source_census::code` rule — whole-line AND + /// trailing — not a private `starts_with("//")` test. #[cfg(test)] fn engine_code_hits(lines: &[&str], extent: (usize, usize), needle: &str) -> Vec { (extent.0..=extent.1) - .filter(|i| !lines[*i].trim_start().starts_with("//")) - .filter(|i| lines[*i].contains(needle)) + .filter(|i| crate::source_census::code(lines[*i]).contains(needle)) .collect() } @@ -18846,9 +21708,9 @@ mod bounded_offer_conjunct_tests { .replace('\\', "/"); let test_file = rel.trim_end_matches(".rs").ends_with("_tests"); for (n, line) in lines.iter().enumerate() { - if line.trim_start().starts_with("//") { - continue; - } + // The shared comment rule, not a private one: comment text declares no + // predicate and calls none. + let line = crate::source_census::code(line); if test_file || spans.iter().any(|(a, b)| (*a..=*b).contains(&n)) { continue; } diff --git a/crates/engine/src/game/triggers.rs b/crates/engine/src/game/triggers.rs index ba3f572cec..9dd2d3de11 100644 --- a/crates/engine/src/game/triggers.rs +++ b/crates/engine/src/game/triggers.rs @@ -1255,9 +1255,7 @@ fn contextual_batched_trigger_event( let defending_player = matching .first() .map(|(_, target)| { - super::trigger_matchers::attack_target_defending_player( - state, *target, fallback, - ) + super::combat::defending_player_for_target_or(state, *target, fallback) }) .unwrap_or(fallback); (defending_player, matching) @@ -5189,7 +5187,12 @@ fn collect_pending_triggers_with_collection( if let Some(attacker) = state.objects.get(source_id) { let new_monarch = attacker.controller; if new_monarch != *target_player { - let become_effect = Effect::BecomeMonarch; + // CR 725.2: the synthetic trigger's controller IS the + // new monarch, so the printed-default subject axis is + // exactly right here. + let become_effect = Effect::BecomeMonarch { + target: TargetFilter::Controller, + }; let source_context = trigger_source_context_for_latch(state, attacker); let mut become_ability = ResolvedAbility::new( become_effect, @@ -9741,7 +9744,7 @@ pub(crate) fn filter_consumed_trigger_events( /// CR 724 end-the-turn / end-the-combat-phase EFFECTS, and by elimination /// — NOT by the turn boundary. /// * R2 — deserialized states bypass the recorder. -/// `PersistedGameState::into_game_state` (`types/game_state.rs:9024`) +/// `PersistedGameState::into_game_state` (`types/game_state.rs`) /// reconstructs `ZoneChanged` straight into live buffers with /// `#[serde(default)]` indices, so a restored state can carry index `0` on /// distinct occurrences. Pre-existing and out of scope here. @@ -9898,16 +9901,655 @@ fn expand_multi_fire_damage_occurrences( } } +/// CR 603.4 + CR 608.2c: does this delayed body still carry work that the +/// resolution-time reading performs when the gate is FALSE — work the fire-time +/// hoist would silently DELETE? +/// +/// This is the carve-out for [`delayed_intervening_if`], and it is deliberately +/// narrower than the "any `sub_ability`" test it replaces. The two cases the old +/// blanket conflated are NOT the same: +/// +/// * `else_ability` ("…, if X, A. Otherwise, B.") is not an intervening-`if` in +/// the CR 603.4 sense at all — the ability has printed work on the false path, +/// so it MUST still trigger and resolve its else branch. Always declines. +/// * An UNCONDITIONAL `SequentialSibling` sub is the second clause of the ONE +/// gated sentence ("…, if you control your commander, draw a card and create a +/// token"). CR 603.4 gates the whole ability, so that clause must not happen +/// either — yet `resolve_ability_chain` (`effects/mod.rs`, the +/// `sub_link == SequentialSibling && condition.is_none()` escape) resolves it +/// even when the parent gate is false. Declining the hoist here is what LEAVES +/// that violation in place, so this class must hoist. +/// +/// The three sub shapes that genuinely survive a false parent gate mirror +/// `resolve_ability_chain`'s own escape hatches one-for-one — literally, by +/// calling the SAME predicate (`effects::sub_outlives_false_parent_gate`) rather +/// than restating it, so the pair cannot drift: +/// +/// 1. a reflexive / performed gate (`condition_depends_on_effect_performed`, +/// CR 603.12 — Council's Deliberation's `OptionalEffectPerformed` rider, the +/// only conditioned delayed body with a sub in the card pool today). Its truth +/// is only knowable at resolution; hoisting the parent gate is CONSERVATIVELY +/// declined so that in-pool shape keeps byte-identical behaviour; +/// 2. an INDEPENDENT per-event gate (CR 615.5 +/// `PostReplacementDamageSourceMatchesFilter`, Comeuppance's mutually-exclusive +/// riders), which resolves on its own predicate regardless of the parent's; +/// 3. a `SiblingCondition::ReplicatedOrBranch` on a `SequentialSibling` link +/// (CR 702.1c keyword-list replication, Kathril / Mutable Pupa), an +/// independent OR-branch gated on its own keyword. +/// +/// Inspects the DIRECT sub only, because that is the whole of what +/// `resolve_chain_body`'s condition-false path inspects: it resolves +/// `else_ability` if present, else the direct `sub_ability` when +/// `sub_outlives_false_parent_gate` (or the unconditional-`SequentialSibling` +/// escape) accepts it, and otherwise RETURNS — nothing deeper in the chain runs. +/// A grandchild's `else_ability` or CR 603.12 reflexive gate is therefore reached +/// only THROUGH a direct sub that already qualified, so recursing past the direct +/// sub would decline the hoist for chains whose resolution does nothing at all, +/// leaving CR 603.4's fire-time half unenforced for that shape. Pinned by +/// `two_deep_chain_mirrors_the_resolvers_direct_sub_test`. +fn delayed_body_outlives_a_false_gate(ability: &ResolvedAbility) -> bool { + if ability.else_ability.is_some() { + return true; + } + let Some(sub) = ability.sub_ability.as_deref() else { + return false; + }; + crate::game::effects::sub_outlives_false_parent_gate(sub) +} + +/// CR 603.4: does this gate read a binding that the FIRE-TIME leg of the hoist +/// resolves DIFFERENTLY from the resolution-time leg it is supposed to mirror? +/// +/// The two legs of the CR 603.4 pair must be the same predicate over the same +/// values. They are not the same *evaluator*: the fire-time leg runs +/// `check_trigger_condition_with_source`, whose `QuantityContext` +/// (`quantity::resolve_quantity_for_trigger_check`) is built from the delayed +/// ability's controller, its CR 400.7 source context and the matched event — it +/// has NO access to the delayed ability's snapshotted `targets` and no +/// resolution-scoped player. So a gate that reads one of those resolves against +/// the wrong object or player at fire time and would gate the ability off the +/// stack (and, for a consumed one-shot, delete it) on a value the resolution-time +/// reader would never have computed. +/// +/// Rather than let the two legs disagree, such a gate DECLINES the hoist and +/// keeps today's resolution-only reading — the same conservative treatment the +/// two bridges already give every other resolution-context predicate. +/// +/// Only `QuantityCheck` carries a `QuantityExpr` (and hence a scope or filter) +/// among the arms `ability_condition_to_static_condition` can bridge — the +/// others (`IsYourTurn`, `CompletedDungeon`, `SourceAttachedToCreature`, +/// `ControlsCommander`) are payload-free. `And`/`Or` do not bridge today; they +/// recurse here anyway so adding them to the bridge cannot silently bypass this. +fn gate_binding_diverges_at_fire_time(condition: &AbilityCondition) -> bool { + match condition { + AbilityCondition::QuantityCheck { lhs, rhs, .. } => { + quantity_expr_binding_diverges(lhs) || quantity_expr_binding_diverges(rhs) + } + AbilityCondition::Not { condition } => gate_binding_diverges_at_fire_time(condition), + AbilityCondition::And { conditions } | AbilityCondition::Or { conditions } => { + conditions.iter().any(gate_binding_diverges_at_fire_time) + } + _ => false, + } +} + +/// CR 603.4: the `QuantityExpr` half of [`gate_binding_diverges_at_fire_time`]. +/// Exhaustive over `QuantityExpr` so a new arithmetic wrapper cannot hide a +/// divergent leaf; the leaf test is [`quantity_ref_binding_diverges`]. +fn quantity_expr_binding_diverges(expr: &QuantityExpr) -> bool { + match expr { + QuantityExpr::Ref { qty } => quantity_ref_binding_diverges(qty), + QuantityExpr::Offset { inner, .. } + | QuantityExpr::ClampMin { inner, .. } + | QuantityExpr::Multiply { inner, .. } + | QuantityExpr::DivideRounded { inner, .. } + | QuantityExpr::UpTo { max: inner } + | QuantityExpr::Power { + exponent: inner, .. + } => quantity_expr_binding_diverges(inner), + QuantityExpr::Sum { exprs } | QuantityExpr::Max { exprs } => { + exprs.iter().any(quantity_expr_binding_diverges) + } + QuantityExpr::Difference { left, right } => { + quantity_expr_binding_diverges(left) || quantity_expr_binding_diverges(right) + } + QuantityExpr::Fixed { .. } => false, + } +} + +/// CR 115.1 + CR 608.2c: an object-axis scope the fire-time `QuantityContext` +/// cannot bind the way the resolving ability does. +/// +/// FAIL-CLOSED, and exhaustive so a new scope must be adjudicated: only the +/// three referents `quantity::resolve_quantity_for_trigger_check` is actually +/// handed — the CR 400.7 source context and the matched event's source/target — +/// are known to read the same on both legs. Everything else is a +/// RESOLUTION-scoped referent: `Target` reads `ability.targets`, `Recipient` is +/// passed as `None`, and the `CostPaidObject` / anaphor / per-resolution-local +/// family resolves through `ResolvedAbility` fields and `effect_context_object`, +/// none of which exist at detection time. Declining costs nothing but the +/// fire-time half of CR 603.4 for shapes no card in the pool has; guessing wrong +/// deletes a real ability. +fn object_scope_unbound_at_fire_time(scope: ObjectScope) -> bool { + match scope { + ObjectScope::Source | ObjectScope::EventSource | ObjectScope::EventTarget => false, + ObjectScope::Target + | ObjectScope::Recipient + | ObjectScope::CostPaidObject + | ObjectScope::Anaphoric + | ObjectScope::Demonstrative + | ObjectScope::OtherRevealedCard + | ObjectScope::AmassedArmy + | ObjectScope::OwnedLinkedExileCard + | ObjectScope::BatchSource => true, + } +} + +/// CR 115.10 + CR 608.2c: the player-axis counterpart, same fail-closed rule. +/// +/// `ScopedPlayer` is the per-iteration player of the RESOLVING ability, but the +/// fire-time context derives its `scoped_player` from the triggering event +/// (`extract_player_from_event`); `Target`, `RecipientController` and +/// `ParentObjectTargetController` all read `ability.targets` or the layer +/// recipient. The rest are derived from the controller (CR 109.5), the attacking +/// source (CR 508.5) or the source's own persisted choice (CR 613.1), all of +/// which the fire-time check has. `AnyTurn` is duration-timing-only and never +/// reaches a quantity, but is adjudicated here rather than wildcarded. +fn player_scope_unbound_at_fire_time(scope: &PlayerScope) -> bool { + match scope { + PlayerScope::ScopedPlayer + | PlayerScope::Target + | PlayerScope::RecipientController + | PlayerScope::ParentObjectTargetController => true, + PlayerScope::AllPlayers { exclude, .. } => exclude + .as_deref() + .is_some_and(player_scope_unbound_at_fire_time), + PlayerScope::Controller + | PlayerScope::Opponent { .. } + | PlayerScope::DefendingPlayer + | PlayerScope::SourceChosenPlayer + // CR 109.4: a concrete `PlayerId` already SNAPSHOTTED at resolution — a + // literal, so there is nothing left to bind and both legs read the same + // value. Same argument as `WhenLeavesPlay`'s already-resolved `ObjectId`. + // Like `AnyTurn` it is duration-timing-only and never reaches a quantity, + // but is adjudicated here rather than wildcarded. + | PlayerScope::SpecificPlayer { .. } + | PlayerScope::AnyTurn => false, + } +} + +/// CR 603.4: the leaf test of [`gate_binding_diverges_at_fire_time`]. +/// +/// Two independent reasons a leaf diverges: +/// +/// * it is scoped to an object or player the fire-time context cannot bind +/// (`object_scope_unbound_at_fire_time` / `player_scope_unbound_at_fire_time`); +/// * it counts a POPULATION whose filter the trigger-side bridge REWRITES. +/// `oracle_trigger::static_condition_to_trigger_condition` substitutes +/// `FilterProp::Another` → `FilterProp::OtherThanTriggerObject` on the +/// fire-time leg only (CR 603.4, Valakut's ruling), while +/// `effects::evaluate_condition` keeps the source-exclusion reading — so the +/// same printed "two or more OTHER creatures" counts a different population on +/// each leg. Declining is the conservative half of that pair; the alternative +/// (substituting on both legs) would change the resolution-time reading of +/// every non-delayed consumer of the same condition. +/// +/// * it reads a RESOLUTION-SCOPED tally that only exists while the ability is +/// resolving. The fire-time leg calls `resolve_quantity_for_trigger_check`, +/// which resolves with `targets = &[]`, `chosen_x = None`, `ability = None` +/// and no resolution-local ledger, so a payload-free leaf can diverge just as +/// badly as a scoped one (CR 608.2c: the chain tallies are established BY the +/// resolution). `TrackedSetSize` and friends read the most recent tracked set +/// at READ time — at fire time that is whatever an unrelated earlier +/// resolution left behind. +/// +/// EXHAUSTIVE and wildcard-free (matching `object_scope_unbound_at_fire_time` / +/// `quantity_expr_binding_diverges`), so a new `QuantityRef` must be adjudicated +/// here rather than silently defaulting to "cannot diverge" — the earlier +/// `_ => false` tail rested on exactly that claim and it was false for the +/// resolution-scoped payload-free family below. When in doubt the answer is +/// `true`: declining costs only the fire-time half of CR 603.4 for that shape, +/// while a wrong `false` deletes a real ability off the stack. +fn quantity_ref_binding_diverges(qty: &QuantityRef) -> bool { + match qty { + QuantityRef::CountersOn { scope, .. } + | QuantityRef::Power { scope } + | QuantityRef::Intensity { scope } + | QuantityRef::Toughness { scope } + | QuantityRef::ObjectManaValue { scope } + | QuantityRef::ObjectColorCount { scope } + | QuantityRef::ObjectNameWordCount { scope } + | QuantityRef::ObjectTypelineComponentCount { scope } + | QuantityRef::ManaSymbolsInManaCost { scope, .. } => { + object_scope_unbound_at_fire_time(*scope) + } + QuantityRef::HandSize { player, .. } + | QuantityRef::LifeTotal { player } + | QuantityRef::GraveyardSize { player, .. } + | QuantityRef::LifeLostThisTurn { player } + | QuantityRef::LifeGainedThisTurn { player } + | QuantityRef::PartySize { player } + | QuantityRef::Speed { player } + | QuantityRef::CardsDrawnThisTurn { player } + | QuantityRef::CardsDiscardedThisTurn { player } + | QuantityRef::LoyaltyAbilitiesActivatedThisTurn { player } => { + player_scope_unbound_at_fire_time(player) + } + QuantityRef::SacrificedThisTurn { player, filter } + | QuantityRef::TokensCreatedThisTurn { player, filter } + | QuantityRef::BattlefieldEntriesThisTurn { player, filter } => { + player_scope_unbound_at_fire_time(player) || filter_binding_diverges(filter) + } + QuantityRef::LandsPlayedThisTurn { player, .. } + | QuantityRef::PlayerActionsThisTurn { player, .. } => { + player_scope_unbound_at_fire_time(player) + } + QuantityRef::ObjectCount { filter } + | QuantityRef::ObjectCountDistinct { filter, .. } + | QuantityRef::ObjectCountBySharedQuality { filter, .. } + | QuantityRef::CountersOnObjects { filter, .. } + | QuantityRef::Aggregate { filter, .. } + | QuantityRef::EnteredThisTurn { filter } + | QuantityRef::DistinctCounterKindsAmong { filter } + | QuantityRef::ControlledByEachPlayer { filter, .. } + | QuantityRef::ZoneChangeCountThisTurn { filter, .. } + | QuantityRef::ZoneChangeAggregateThisTurn { filter, .. } + | QuantityRef::CounterAddedThisTurn { + target: filter, + .. + } => filter_binding_diverges(filter), + QuantityRef::DamageDealtThisTurn { source, target, .. } => { + filter_binding_diverges(source) || filter_binding_diverges(target) + } + // Same filter axis, optional: a `None` filter names no population to + // re-scope, so only the `Some` arm can diverge. + QuantityRef::ZoneCardCount { filter, .. } + | QuantityRef::SpellsCastThisTurn { filter, .. } + | QuantityRef::SpellsCastThisGame { filter, .. } + | QuantityRef::AttackedThisTurn { filter, .. } => { + filter.as_ref().is_some_and(filter_binding_diverges) + } + // CR 205.2a + CR 205.3 + CR 105.1: the distinct-characteristic family + // carries its population as a `CardTypeSetSource` rather than a bare + // filter. The COLOURS head joins its two siblings here rather than the + // bare-filter arm above: it was lifted onto the shared population axis in + // this change, so `DistinctColorsAmongPermanents { filter }` no longer + // exists to sit alongside `ObjectCount`. + QuantityRef::DistinctCardTypes { source } + | QuantityRef::DistinctSubtypes { source, .. } + | QuantityRef::DistinctColorsAmong { source } => { + card_type_set_source_binding_diverges(source) + } + // CR 601.2h: `AbilityTarget` is a target-slot read that + // `quantity::resolve_event_scoped_ref` explicitly answers `None` for at + // fire time; `SelfObject` reads `ctx.source` and `TriggeringSpell` is + // resolved from the matched event itself, so both bind identically on + // both legs — except through a `FromSource` metric, which carries its own + // population filter. + QuantityRef::ManaSpentToCast { scope, metric } => { + *scope == crate::types::ability::CastManaObjectScope::AbilityTarget + || match metric { + CastManaSpentMetric::FromSource { source_filter } => { + filter_binding_diverges(source_filter) + } + CastManaSpentMetric::Total + | CastManaSpentMetric::DistinctColors + | CastManaSpentMetric::OfColor { .. } => false, + } + } + // ---- RESOLUTION-SCOPED, payload-free or target-bound: always diverges ---- + // + // CR 115.1: reads the resolving ability's declared targets, which the + // fire-time resolver is handed as `&[]`. + QuantityRef::TargetControllerCounter { .. } + | QuantityRef::TargetObjectManaValue { .. } + | QuantityRef::TargetZoneCardCount { .. } + // CR 107.3a: `X` comes from the resolving ability's `chosen_x`, which is + // `None` at fire time. + | QuantityRef::Variable { .. } + // CR 608.2c: chain-local tracked sets and per-resolution tallies. Each is + // established BY a resolution; at fire time these read whatever an + // unrelated earlier resolution left in the ledger (or nothing at all). + | QuantityRef::TrackedSetSize + | QuantityRef::FilteredTrackedSetSize { .. } + | QuantityRef::TrackedSetAggregate { .. } + | QuantityRef::ExiledFromHandThisResolution + | QuantityRef::PreviousEffectAmount { .. } + | QuantityRef::TimesCostPaidThisResolution + // CR 608.2c: the secret-number ledger is populated BY the + // resolution that ran the choice (Wheel of Misfortune, Menacing Ogre) and + // is cleared per resolution — players who chose no number THIS resolution + // are excluded from the aggregate entirely. At fire time the ledger holds + // an unrelated resolution's numbers or none, so the two legs cannot agree. + // The `player` payload is irrelevant to that verdict: the ledger itself is + // resolution-scoped whichever player the scope selects. + | QuantityRef::PlayerChosenNumber { .. } + // CR 701.38: the vote tally is published by the resolution that ran the + // vote block. + | QuantityRef::VoteCount { .. } + // CR 106.4: a mana pool the resolution's own costs and mana abilities + // fill and empty; the fire-time reading is a different moment's pool. + | QuantityRef::UnspentMana { .. } + // CR 607.2a + CR 406.6: the source's linked-exile set, read through the + // resolving ability's materialized candidate set + // (`quantity::materialized_linked_exile_candidates`, which reads + // `ability.targets`). + | QuantityRef::CardsExiledBySource + | QuantityRef::ExiledCardPower { .. } + // CR 603.7c + CR 608.2c: the event-context family resolves through + // `state.current_trigger_event(s)` and the resolution-local + // amount/die/substitution cascade (`resolve_ref`'s `EventContextAmount` + // arm), none of which is populated at DETECTION time — the detection-time + // event override is consumed only by `ObjectCount`'s + // `OtherThanTriggerObject` exclusion. + | QuantityRef::EventContextAmount + | QuantityRef::EventContextPlayerCount { .. } + | QuantityRef::EventContextSourceCostX + | QuantityRef::EventContextSourceModesChosen + | QuantityRef::AttachmentsOnLeavingObject { .. } + | QuantityRef::SpellsCastBeforeTriggeringSpell { .. } + | QuantityRef::TriggeringScryLookCount + | QuantityRef::TriggeringScryBottomCount => true, + // ---- Reads that bind IDENTICALLY on both legs ---- + // + // Global or format-level state (CR 103.4, CR 500, CR 117.1), a + // controller-keyed per-turn/per-game accumulator (CR 109.5 — the + // fire-time leg is handed the delayed ability's own controller), or a + // read of the CR 400.7 source object the fire-time context carries + // (`ctx.source`, the same object `ObjectScope::Source` is adjudicated + // non-divergent for above). + QuantityRef::LifeAboveStarting + | QuantityRef::StartingLifeTotal + | QuantityRef::TriggeringDiscoverValue + | QuantityRef::PlayerCount { .. } + | QuantityRef::PlayerCounter { .. } + | QuantityRef::SelfManaValue + | QuantityRef::Devotion { .. } + | QuantityRef::BasicLandTypeCount { .. } + | QuantityRef::CrimesCommittedThisTurn + | QuantityRef::BendTypesThisTurn + | QuantityRef::TurnsTaken + | QuantityRef::ChosenNumber + | QuantityRef::DescendedThisTurn + | QuantityRef::SpellsCastLastTurn + | QuantityRef::DungeonsCompleted + | QuantityRef::CostXPaid + | QuantityRef::KickerCount + | QuantityRef::AdditionalCostPaymentCount + | QuantityRef::AdditionalCostPaymentCountFor { .. } + | QuantityRef::ConvokedCreatureCount + | QuantityRef::ColorsInCommandersColorIdentity + | QuantityRef::CommanderCastFromCommandZoneCount + | QuantityRef::CommanderManaValue { .. } => false, + } +} + +/// CR 603.4: the `CardTypeSetSource` half of [`quantity_ref_binding_diverges`]. +/// Exhaustive for the same reason: each source names a different population, and +/// two of them are resolution-scoped. +fn card_type_set_source_binding_diverges(source: &CardTypeSetSource) -> bool { + let mut diverges = false; + let complete = + source.try_for_each_member(crate::types::ability::UNION_DEPTH_BUDGET, &mut |leaf| { + if diverges { + return; + } + diverges = match leaf { + // CR 400.1: a zone census keyed by `CountScope` (controller / + // opponents / all) — the fire-time leg has the controller and + // reads the same zones. + CardTypeSetSource::Zone { .. } => false, + CardTypeSetSource::Objects { filter } => filter_binding_diverges(filter), + // CR 601.2a: the journal's optional narrowing filter is the only + // re-scopable part; the journal itself is per-player history that + // binds the same on both legs. + CardTypeSetSource::TurnJournal { filter, .. } => { + filter.as_ref().is_some_and(filter_binding_diverges) + } + // CR 607.2a + CR 608.2i: the same two resolution-scoped + // populations `CardsExiledBySource` / `TrackedSetSize` are + // declined for. + CardTypeSetSource::ExiledBySource | CardTypeSetSource::TrackedSet { .. } => true, + // Unrolled by the walker; never reaches this arm. + CardTypeSetSource::AnyOf { .. } => false, + }; + }); + // CR 603.4: a truncated walk DECLINES the hoist. Declining costs a delayed + // trigger its fire-time shortcut; wrongly allowing it re-scopes a population + // against the wrong binding, which is a rules error. + diverges || !complete +} + +/// CR 603.4: the filter half of [`quantity_ref_binding_diverges`]. Recurses +/// through exactly the shapes `oracle_trigger::substitute_another_in_filter` +/// rewrites — `Typed` property lists plus the `And`/`Or`/`Not` combinators — so +/// the decline covers precisely the population the fire-time leg would have +/// re-scoped, no more and no less. +fn filter_binding_diverges(filter: &TargetFilter) -> bool { + match filter { + TargetFilter::Typed(tf) => tf + .properties + .iter() + .any(|prop| matches!(prop, FilterProp::Another)), + TargetFilter::Not { filter } => filter_binding_diverges(filter), + TargetFilter::And { filters } | TargetFilter::Or { filters } => { + filters.iter().any(filter_binding_diverges) + } + // CR 115.1 + CR 115.10: resolution-scoped anaphora read `ability.targets` + // / the per-iteration player, neither of which the fire-time context has + // — the population-level counterpart of `ObjectScope::Target`. + TargetFilter::ParentTarget + | TargetFilter::ParentTargetSlot { .. } + | TargetFilter::ParentTargetController + | TargetFilter::ParentTargetOwner + | TargetFilter::ScopedPlayer => true, + _ => false, + } +} + +/// CR 400.1 + CR 603.4: does the STATIC intermediate of the hoist LOSE the zone +/// axis on its way to the fire-time leg? +/// +/// `conditions::ability_condition_to_static_condition` folds the +/// `ObjectCount{filter} >= 1` shape into `StaticCondition::IsPresent`, and +/// `oracle_trigger::static_condition_to_trigger_condition` lowers the AFFIRMATIVE +/// `IsPresent` to `TriggerCondition::ControlsType`, whose evaluator +/// (`check_trigger_condition_with_source`) scans `state.battlefield` and nothing +/// else. The resolution-time leg counts in the FILTER'S OWN zone +/// (`quantity::object_count_matching_ids`, via `TargetFilter::extract_in_zone`). +/// For a filter that names a non-battlefield zone ("if you have a creature card +/// in your graveyard") those are two DIFFERENT predicates — TRUE at resolution, +/// FALSE at fire time — so the hoist would gate the ability off the stack and, +/// for a consumed one-shot, delete it outright. The zone axis is invisible to +/// [`gate_binding_diverges_at_fire_time`], which screens the object, player and +/// `FilterProp::Another` axes of the SAME leaf but not this one, so it is +/// adjudicated here, on the intermediate that actually loses the information. +/// +/// Only the affirmative arm is affected: the NEGATED `IsPresent` bridge already +/// lowers to `QuantityComparison { ObjectCount(f) EQ 0 }`, which resolves through +/// the same zone-aware `QuantityRef::ObjectCount` reader on both legs. +/// `And`/`Or` recurse because the trigger-side bridge maps them member-wise; no +/// `AbilityCondition` reaches them today (`ability_condition_to_static_condition` +/// declines `And`/`Or`), so the recursion is future-proofing, not live behaviour. +/// +/// Declining is the conservative half of the pair, exactly like +/// `gate_binding_diverges_at_fire_time`: the gate keeps today's resolution-only +/// reading rather than being evaluated as a different predicate. +fn static_gate_bridge_loses_zone(condition: &StaticCondition) -> bool { + match condition { + StaticCondition::IsPresent { filter: Some(f) } => f + .extract_in_zone() + .is_some_and(|zone| zone != crate::types::zones::Zone::Battlefield), + StaticCondition::And { conditions } | StaticCondition::Or { conditions } => { + conditions.iter().any(static_gate_bridge_loses_zone) + } + _ => false, + } +} + +/// CR 603.4: recover the intervening-`if` of a DELAYED triggered ability as a +/// trigger-level `TriggerCondition`. +/// +/// A printed "When [event], if [condition], [effect]" trigger carries its gate +/// on `TriggerDefinition.condition`, so both halves of CR 603.4 apply to it: the +/// collection gate refuses to put the ability on the stack, and +/// `stack.rs`'s recheck removes it if the gate flipped. A DELAYED triggered +/// ability parses the very same sentence shape into +/// `Effect::CreateDelayedTrigger`, but the gate lands on the delayed BODY as an +/// `AbilityCondition`, which only the resolution-time reader +/// (`effects::evaluate_condition`) ever consults. That is half of CR 603.4: the +/// ability would still be put onto the stack, be respondable, and count as a +/// triggered ability put onto the stack, when per CR 603.4 it must never have +/// triggered at all. +/// +/// Rather than add a fifth condition-vocabulary mirror, this composes the two +/// EXISTING single-authority bridges — +/// `conditions::ability_condition_to_static_condition` then +/// `oracle_trigger::static_condition_to_trigger_condition`. Both legs decline +/// every resolution-context predicate (`WhenYouDo`, `EffectOutcome`, +/// `HasObjectTarget`, `CoinFlipOutcome`, the casting-context family, …), so only +/// gates that are genuine game-state intervening-`if`s reach the fire-time +/// check; anything else keeps today's resolution-only behaviour unchanged. +/// +/// CR 603.4 governs only an `if` that IMMEDIATELY FOLLOWS the trigger event and +/// gates the WHOLE ability, so a body that still has work to do on the +/// condition-false path is NOT hoisted — see +/// [`delayed_body_outlives_a_false_gate`], which is deliberately NARROWER than +/// "has any sub-ability". +/// +/// The hoisted gate must also be one the two legs read IDENTICALLY, or the pair +/// CR 603.4 requires would be two different predicates — see +/// [`gate_binding_diverges_at_fire_time`] for the object/player/population axes +/// of the leaf, and [`static_gate_bridge_loses_zone`] for the zone axis the +/// `IsPresent` intermediate drops. +/// +/// CR 400.7: there is deliberately NO guard here for a delayed ability carrying +/// no `trigger_source` context, and the absence is load-bearing rather than an +/// oversight. The worry it would answer is real in shape — a source-relative gate +/// with no context would read nothing, evaluate false for want of a reading +/// rather than on the game state, and (via [`false_gate_consumes_one_shot`]) +/// DELETE a one-shot outright. It cannot happen, because matching runs BEFORE +/// gating and already requires that context: `delayed_trigger_event_with_index` +/// opens its `WhenNextEvent` arm with `let source_context = source_context?;`, so +/// a contextless one-shot never matches an event, never reaches this gate, and is +/// never discarded. The condition is checked at most where a reading exists. +/// +/// A guard here would therefore be unreachable code that also declines the hoist +/// for gates whose fire-time reading is perfectly well-defined without a source +/// (the controller-scoped `QuantityCheck` populations pinned by +/// `non_battlefield_presence_gate_declines_the_fire_time_hoist` and its +/// siblings) — buying nothing and costing CR 603.4's fire-time half. +fn delayed_intervening_if(ability: &ResolvedAbility) -> Option { + if delayed_body_outlives_a_false_gate(ability) { + return None; + } + let condition = ability.condition.as_ref()?; + if gate_binding_diverges_at_fire_time(condition) { + return None; + } + let static_condition = + crate::parser::oracle_effect::conditions::ability_condition_to_static_condition(condition)?; + if static_gate_bridge_loses_zone(&static_condition) { + return None; + } + crate::parser::oracle_trigger::static_condition_to_trigger_condition(&static_condition) +} + +/// CR 603.4 + CR 603.7b: when the hoisted intervening-`if` was FALSE, is this +/// ONE-SHOT delayed ability CONSUMED by the occurrence it just declined? +/// +/// CR 603.4 says a false gate means the ability "does nothing" — it never +/// triggered. CR 603.7b says a delayed triggered ability "will trigger only +/// once — the next time its trigger event occurs — unless it has a stated +/// duration, such as 'this turn.'" The two only agree on DROPPING the ability +/// when its stated trigger event can occur at most once: +/// +/// * a PHASE-NAMED time ("at the beginning of the next end step"). That named +/// time has now come and gone; a false gate there must not let the ability lie +/// in wait for the FOLLOWING end step; +/// * a zone change of ONE ALREADY-BOUND object ("when THAT creature dies" — +/// `TargetFilter::names_bound_single_object`). The bound object departs once +/// per incarnation, and an object that returns is a NEW object (CR 400.7 / +/// CR 603.7c), so a retained ability could never match it anyway; +/// * EVERY `WhenNextEvent`, whatever its `DelayedTriggerLifetime` — because this +/// variant IS CR 603.7b's once-only half. The rule's two outcomes are modelled +/// as two SIBLING conditions, not as a lifetime: `WheneverEvent` is the +/// "unless it has a stated duration" carve-out (multi-fire, purged by +/// `WheneverEventExpiry`), and `WhenNextEvent` is documented on its own +/// declaration as the "one-shot variant of `WheneverEvent`". A stated-duration +/// ability that must keep watching is therefore a `WheneverEvent` and CANNOT +/// reach this function: `effects::delayed_trigger` computes +/// `one_shot = !matches!(condition, WheneverEvent { .. })` and the only caller +/// gates on `delayed.one_shot`. The lifetime then bounds only how long the +/// single shot WAITS — `ThisTurn` to cleanup, `Persistent` open-ended +/// (The Pandorica), `Reflexive` to its creation batch (CR 603.12). +/// +/// Do NOT re-derive that from the wording "next": the discriminator is WotC's +/// "When" / "Whenever" templating, and the parser already keys on exactly that. +/// The distinction is load-bearing and easy to get backwards, so it is worth +/// naming the case that proves it. `parse_dealt_damage_this_way_dies_trigger` +/// (`oracle_effect/mod.rs`) parses "[subject] dealt damage this way dies +/// [this turn]" — a BROAD filter with no "next" anywhere — and is reached from +/// two call sites that lower it differently, on the templating alone: +/// - the `"whenever "` site → `WheneverEvent` (multi-fire). Ghired's +/// Belligerence and Reckless Blaze, both of which spread damage over many +/// creatures, so many deaths can qualify; +/// - the `"when "` site → `WhenNextEvent { ThisTurn }` (one-shot). Skeletonize, +/// whose damage goes to a SINGLE target creature, so at most one death can +/// ever qualify — one occurrence, correctly consumed. +/// +/// Those are the only three cards in the pool with that wording, and the split is +/// exact for all three. So "broad filter" alone never implies multi-fire here, +/// and this arm does not need to inspect the filter: the routing decision was +/// already made, correctly, one layer up. +/// +/// Exhaustive on purpose: a new `DelayedTriggerCondition` must decide whether its +/// stated event is a single occurrence before it can be discarded on a false gate. +fn false_gate_consumes_one_shot(condition: &DelayedTriggerCondition) -> bool { + match condition { + DelayedTriggerCondition::AtNextPhase { .. } + | DelayedTriggerCondition::AtNextPhaseForPlayer { .. } + // A concrete `ObjectId`: the same single-occurrence argument as a + // bound-single-object filter, already resolved to one object. + | DelayedTriggerCondition::WhenLeavesPlay { .. } => true, + DelayedTriggerCondition::WhenDies { filter } + | DelayedTriggerCondition::WhenLeavesPlayFiltered { filter } + | DelayedTriggerCondition::WhenEntersBattlefield { filter } + | DelayedTriggerCondition::WhenDiesOrExiled { filter } => { + filter.names_bound_single_object() + } + // CR 603.4 + CR 603.7b (+ CR 603.12 for `Reflexive`): this variant IS the + // one-shot half of the CR 603.7b split — its own doc calls it the + // "one-shot variant of `WheneverEvent`" — so the matched event has spent + // its single occurrence whatever lifetime it carries. Retaining it would + // silently rewrite "when X, if C" into "when X for which C holds", letting + // a later X fire an ability CR 603.4 already resolved as doing nothing. + // A trigger that must keep watching is a `WheneverEvent` and cannot reach + // here at all — see the doc above. + DelayedTriggerCondition::WhenNextEvent { .. } => true, + // Never one-shot (`effects::delayed_trigger` computes `one_shot` as + // "not `WheneverEvent`"), so this arm is unreachable from the caller; + // spelled out rather than wildcarded to keep the match exhaustive. + DelayedTriggerCondition::WheneverEvent { .. } => false, + } +} + fn delayed_trigger_to_context( state: &GameState, trigger: DelayedTrigger, trigger_event: GameEvent, ) -> PendingTriggerContext { + // CR 603.4 (second half): carry the hoisted intervening-`if` onto the stack + // entry so `stack.rs`'s resolution recheck applies to a delayed triggered + // ability exactly as it does to a printed one. `delayed_intervening_if` is + // the SAME authority the collection gate below used, so the two halves of + // the CR 603.4 pair cannot read different predicates. + let condition = delayed_intervening_if(&trigger.ability); PendingTriggerContext::delayed( PendingTrigger { source_id: trigger.source_id, controller: trigger.controller, - condition: None, + condition, ability: trigger.ability, timestamp: state.turn_number, target_constraints: Vec::new(), @@ -9967,6 +10609,44 @@ fn collect_matching_delayed_triggers( if !scope.accepts(&trigger_event) { continue; } + // CR 603.4 (first half): "When the trigger event occurs, the ability + // checks whether the stated condition is true. The ability triggers + // only if it is; otherwise it does nothing." The delayed body's + // intervening-`if` was previously consulted ONLY at resolution, so a + // failing gate still put a respondable ability on the stack — and a + // player could then make the gate true in response (getting a + // commander onto the battlefield for Fight for the Throne), which + // CR 603.4 forbids outright. + // + // `check_trigger_condition_with_source` is the same fire-time + // evaluator printed triggers use, given the delayed ability's own + // CR 400.7 source context and the matched event. + if let Some(condition) = delayed_intervening_if(&delayed.ability) { + if !check_trigger_condition_with_source( + state, + &condition, + delayed.controller, + delayed.ability.trigger_source.as_ref(), + Some(&trigger_event), + ) { + // CR 603.4 + CR 603.7b: the ability did not trigger. It is + // removed without firing, tagged `InterveningIfFalse`, ONLY + // when its stated event was a single occurrence that this + // check has now consumed — see + // `false_gate_consumes_one_shot`. Everything else (a + // multi-fire "whenever … this turn", and a one-shot watching + // a BROAD event filter) stays installed and gets its gate + // re-checked on the next occurrence, per CR 603.7b's + // stated-duration clause. + if delayed.one_shot && false_gate_consumes_one_shot(&delayed.condition) { + to_discard.push(( + idx, + super::lifecycle::DelayedTerminalDisposition::InterveningIfFalse, + )); + } + continue; + } + } if delayed.one_shot { to_remove.push((idx, event_index, trigger_event)); } else { @@ -10944,6 +11624,27 @@ pub(crate) fn check_trigger_condition_with_source( return false; } + // CR 603.4 + CR 109.4: polarity-safe fail-closed for designation leaves. + // + // A leaf whose PLAYER ANCHOR cannot be resolved is UNANSWERABLE, not false. + // Returning `false` from inside the recursion inverts to `true` under + // `TriggerCondition::Not` — the shape every "unless" grammar and the + // "if you're not the monarch" bridge produce — firing the trigger precisely + // when the engine cannot identify the player. Reject here, at the same outer + // boundary that already rejects incoherent zone-change provenance directly + // above, so the boolean combinators in + // `evaluate_trigger_condition_with_source` can never reinterpret it as an + // ordinary false operand. + if !trigger_condition_designation_anchors_resolvable( + state, + condition, + controller, + source_context, + trigger_event, + ) { + return false; + } + evaluate_trigger_condition_with_source( state, condition, @@ -10953,6 +11654,62 @@ pub(crate) fn check_trigger_condition_with_source( ) } +/// CR 603.4 + CR 109.4: boundary predicate — does every designation leaf in this +/// tree have a resolvable player anchor? +/// +/// NOT a second evaluator: it recurses only the boolean combinators and +/// delegates every anchor question to +/// `quantity::resolve_player_scope_for_trigger_check`, the same single authority +/// the leaves use, via the compiler-forced +/// [`TriggerCondition::designation_player_anchor`] accessor. The `_ => true` +/// leaf arm is safe precisely because that accessor is exhaustive: a future +/// anchored leaf is a compile error there, not a silent fail-open here. +/// +/// Deliberately conservative: an unresolvable anchor anywhere rejects the whole +/// condition, INCLUDING inside an `Or` whose other operand is true. No corpus +/// card places a designation leaf under `Or`; the choice is pinned by +/// `unresolvable_designation_anchor_absorbs_or_cr_603_4` so a future card +/// needing the looser reading has a failing test to point at. +fn trigger_condition_designation_anchors_resolvable( + state: &GameState, + condition: &TriggerCondition, + controller: PlayerId, + source_context: Option<&TriggerSourceContext>, + trigger_event: Option<&GameEvent>, +) -> bool { + if let Some(scope) = condition.designation_player_anchor() { + return crate::game::quantity::resolve_player_scope_for_trigger_check( + state, + scope, + controller, + source_context, + trigger_event, + ) + .is_some(); + } + match condition { + TriggerCondition::And { conditions } | TriggerCondition::Or { conditions } => { + conditions.iter().all(|inner| { + trigger_condition_designation_anchors_resolvable( + state, + inner, + controller, + source_context, + trigger_event, + ) + }) + } + TriggerCondition::Not { condition } => trigger_condition_designation_anchors_resolvable( + state, + condition, + controller, + source_context, + trigger_event, + ), + _ => true, + } +} + /// Evaluates a condition after the outer event-provenance boundary has accepted /// its input. Boolean combinators recurse here so invalid provenance cannot be /// reinterpreted as an ordinary false operand. @@ -11747,8 +12504,29 @@ fn evaluate_trigger_condition_with_source( TriggerCondition::SpellCastWithVariantThisTurn { variant } => { crate::game::restrictions::spell_cast_with_variant_this_turn(state, variant) } - // CR 725.1: True when the controller is the monarch. - TriggerCondition::IsMonarch => eval_is_monarch(state, controller), + // CR 725.1 + CR 603.4: the monarch check is evaluated against the player + // the condition names. `PlayerScope::Controller` is CR 109.5's "you"; + // every other scope is an event/combat anchor resolved from the SAME + // explicit `trigger_event` this function threads everywhere else, so the + // fire-time check and the CR 603.4 resolution-time recheck read the same + // player. + // + // An unresolvable scope cannot reach this arm: the entry boundary in + // `check_trigger_condition_with_source` has already rejected the whole + // condition (see `trigger_condition_designation_anchors_resolvable`). + // The `is_some_and` below is therefore a total-function formality, not + // the fail-closed mechanism — putting the rejection here instead would + // fail OPEN under `TriggerCondition::Not`. + TriggerCondition::IsMonarch { player } => { + crate::game::quantity::resolve_player_scope_for_trigger_check( + state, + player, + controller, + source_context, + trigger_event, + ) + .is_some_and(|pid| eval_is_monarch(state, pid)) + } // CR 726.3: True when the controller has the initiative. TriggerCondition::IsInitiative => eval_is_initiative(state, controller), // CR 725.1: True when no player holds the monarch designation. @@ -12708,6 +13486,37 @@ fn quantity_expr_refs_cost_paid_object(expr: &QuantityExpr) -> bool { } } +/// Does a [`CardTypeSetSource`] population route a filter that references the +/// cost-paid object? Only the object filter and the journal's optional +/// narrowing filter can; `AnyOf` recurses so a union member's reference is not +/// dropped. +/// +/// Uncited: a structural query over which arms hold a `TargetFilter`, not a rule +/// implementation. (It cited CR 109.2, the battlefield-default rule for a bare +/// type description, which does not speak to filter routing.) +fn characteristic_source_references_cost_paid_object(source: &CardTypeSetSource) -> bool { + let mut found = false; + let complete = + source.try_for_each_member(crate::types::ability::UNION_DEPTH_BUDGET, &mut |leaf| { + if found { + return; + } + found = match leaf { + CardTypeSetSource::Objects { filter } => filter.references_cost_paid_object(), + CardTypeSetSource::TurnJournal { filter, .. } => filter + .as_ref() + .is_some_and(TargetFilter::references_cost_paid_object), + CardTypeSetSource::Zone { .. } + | CardTypeSetSource::ExiledBySource + | CardTypeSetSource::TrackedSet { .. } + | CardTypeSetSource::AnyOf { .. } => false, + }; + }); + // A truncated walk claims the reference: this gate exists to stop a + // cost-paid-object read from escaping, so exhaustion must not let one past. + found || !complete +} + /// CR 400.7d + CR 608.2k: True when this `QuantityRef` reads the cost-paid /// object, by either of the two structural axes a ref can carry it on: /// @@ -12748,7 +13557,6 @@ fn quantity_ref_refs_cost_paid_object(qty: &QuantityRef) -> bool { | QuantityRef::ZoneChangeAggregateThisTurn { filter, .. } | QuantityRef::CounterAddedThisTurn { target: filter, .. } | QuantityRef::TokensCreatedThisTurn { filter, .. } - | QuantityRef::DistinctColorsAmongPermanents { filter } | QuantityRef::DistinctCounterKindsAmong { filter } => filter.references_cost_paid_object(), // Filter-bearing refs (boxed `TargetFilter`): recurse (auto-deref). @@ -12771,21 +13579,13 @@ fn quantity_ref_refs_cost_paid_object(qty: &QuantityRef) -> bool { source.references_cost_paid_object() || target.references_cost_paid_object() } - // Card-type counting embeds a `TargetFilter` through its source enum. - QuantityRef::DistinctCardTypes { source } => match source { - CardTypeSetSource::Objects { filter } => filter.references_cost_paid_object(), - CardTypeSetSource::Zone { .. } - | CardTypeSetSource::ExiledBySource - | CardTypeSetSource::TrackedSet { .. } => false, - }, - - // Subtype counting embeds a `TargetFilter` through its source enum too. - QuantityRef::DistinctSubtypes { source, .. } => match source { - CardTypeSetSource::Objects { filter } => filter.references_cost_paid_object(), - CardTypeSetSource::Zone { .. } - | CardTypeSetSource::ExiledBySource - | CardTypeSetSource::TrackedSet { .. } => false, - }, + // Card-type / subtype / colour counting all embed their `TargetFilter`s + // through the shared population enum. + QuantityRef::DistinctCardTypes { source } + | QuantityRef::DistinctSubtypes { source, .. } + | QuantityRef::DistinctColorsAmong { source } => { + characteristic_source_references_cost_paid_object(source) + } // Mana-spent metering embeds a `TargetFilter` through its metric enum. QuantityRef::ManaSpentToCast { metric, .. } => match metric { @@ -12840,6 +13640,7 @@ fn quantity_ref_refs_cost_paid_object(qty: &QuantityRef) -> bool { | QuantityRef::LandsPlayedThisTurn { .. } | QuantityRef::TurnsTaken | QuantityRef::ChosenNumber + | QuantityRef::PlayerChosenNumber { .. } | QuantityRef::DescendedThisTurn | QuantityRef::LoyaltyAbilitiesActivatedThisTurn { .. } | QuantityRef::SpellsCastLastTurn @@ -16925,6 +17726,7 @@ pub mod tests { card_id: CardId(2), controller: player, object_id: spell, + cast_mana_value: None, }], ); @@ -18400,6 +19202,7 @@ pub mod tests { card_id: CardId(10), controller: PlayerId(0), object_id: spell, + cast_mana_value: None, }]; process_triggers(&mut state, &events); @@ -18453,6 +19256,7 @@ pub mod tests { card_id: CardId(10), controller: PlayerId(0), object_id: creature_spell, + cast_mana_value: None, }]; process_triggers(&mut state, &events); @@ -18506,6 +19310,7 @@ pub mod tests { card_id: CardId(10), controller: PlayerId(1), object_id: spell, + cast_mana_value: None, }]; process_triggers(&mut state, &events); @@ -18726,6 +19531,92 @@ pub mod tests { ); } + /// The shared characteristic-source branch, which the three distinct-count + /// heads all route through. Sibling to the two tests above, which cover the + /// per-`QuantityRef` arms but never reach this population axis. + /// + /// Each filter-BEARING arm is exercised, plus the recursion and the + /// fixed-vocabulary arms that must stay false — a gate that answered `true` + /// for everything would pass a positive-only test. + #[test] + fn cost_paid_object_gate_covers_every_characteristic_source_arm() { + use crate::types::ability::{CardTypeSetSource, CountScope, TurnJournalKind, ZoneRef}; + + let objects = |filter| CardTypeSetSource::Objects { filter }; + let journal = |filter| CardTypeSetSource::TurnJournal { + journal: TurnJournalKind::SpellsCast, + scope: CountScope::Controller, + filter, + }; + + // Every head shares one population axis, so detection must not depend on + // which characteristic is being counted. + for qty in [ + QuantityRef::DistinctColorsAmong { + source: objects(TargetFilter::CostPaidObject), + }, + QuantityRef::DistinctCardTypes { + source: objects(TargetFilter::CostPaidObject), + }, + ] { + assert!( + quantity_ref_refs_cost_paid_object(&qty), + "an Objects population over the cost-paid object must be detected: {qty:?}" + ); + } + + // The journal's optional narrowing filter is the second filter-bearing + // arm, and `None` there must not be mistaken for a reference. + assert!( + quantity_ref_refs_cost_paid_object(&QuantityRef::DistinctCardTypes { + source: journal(Some(TargetFilter::CostPaidObject)), + }), + "a cost-paid-object reference in the journal's narrowing filter must be detected" + ); + assert!( + !quantity_ref_refs_cost_paid_object(&QuantityRef::DistinctCardTypes { + source: journal(None), + }), + "an unfiltered journal references nothing" + ); + + // `AnyOf` recursion: a reference in ANY member is a reference, including + // one nested a union deep, and a union of clean members stays false. + let clean = CardTypeSetSource::Zone { + zone: ZoneRef::Graveyard, + scope: CountScope::Controller, + }; + let nested = CardTypeSetSource::any_of(vec![ + clean.clone(), + CardTypeSetSource::any_of(vec![ + CardTypeSetSource::ExiledBySource, + objects(TargetFilter::CostPaidObject), + ]) + .expect("two-member union"), + ]) + .expect("two-member union"); + assert!( + quantity_ref_refs_cost_paid_object(&QuantityRef::DistinctSubtypes { + source: nested, + exclude: Default::default(), + }), + "a reference nested inside a union of unions must be detected" + ); + + // Fixed-vocabulary arms carry no filter and must stay false — this is + // what stops the gate from degenerating into "always true". + for source in [ + clean, + CardTypeSetSource::ExiledBySource, + CardTypeSetSource::TrackedSet { caused_by: None }, + ] { + assert!( + !characteristic_source_references_cost_paid_object(&source), + "a fixed-vocabulary population routes no filter: {source:?}" + ); + } + } + /// CR 400.7d end-to-end: `build_triggered_ability` propagates the emerge /// `cast_cost_paid_object` snapshot onto a sub-ability whose magnitude reads /// "the number of counters on the sacrificed creature" @@ -19316,6 +20207,1607 @@ pub mod tests { ); } + /// CR 603.4 (fire-time half) for a DELAYED triggered ability. + /// + /// Stages Fight for the Throne's shape — a one-shot `WhenDies` delayed + /// ability whose body carries the intervening-`if` "if you control your + /// commander" — and drives the production collection entry point + /// (`check_delayed_triggers`) on the death event. + /// + /// REVERT-TO-RED: with the fire-time hoist removed, `state.stack.len()` is + /// `1` in the no-commander half, because the ability was put onto the stack + /// and only skipped later, at resolution. That is precisely the CR 603.4 + /// violation — the ability is respondable, counts as a triggered ability put + /// onto the stack, and a player could make the gate TRUE in response. + /// + /// The commander half is the reachability proof: the very same fixture with + /// a commander staged DOES reach the stack, so the empty-stack assertion + /// below cannot be passing because the delayed trigger never matched. + /// + /// The two `DelayedSubject` halves pin the CR 603.7b survival split that + /// `false_gate_consumes_one_shot` decides — see the assertions below. + #[test] + fn delayed_intervening_if_gates_the_ability_off_the_stack_at_fire_time() { + /// Which shape of "when [it] dies" the fixture installs. + #[derive(Clone, Copy)] + enum DelayedSubject { + /// "when A creature dies this turn" — a CLASS filter that can match + /// again later in the turn. + BroadFilter, + /// "when THAT creature dies" — one already-bound object. + BoundObject, + } + + fn run(stage_commander: bool, subject: DelayedSubject) -> (usize, usize) { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_0401), + controller, + "Fight for the Throne".to_string(), + Zone::Battlefield, + ); + if stage_commander { + // CR 903.3 + CR 903.3d: owned AND controlled, on the battlefield. + let commander = make_creature(&mut state, controller, "Your Commander", 2, 2); + state + .objects + .get_mut(&commander) + .expect("staged commander") + .is_commander = true; + } + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + // The intervening-`if` as the parser lowers it onto the delayed BODY. + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + let filter = match subject { + DelayedSubject::BroadFilter => TargetFilter::Any, + DelayedSubject::BoundObject => TargetFilter::SpecificObject { id: victim }, + }; + assert_eq!( + filter.names_bound_single_object(), + matches!(subject, DelayedSubject::BoundObject), + "reach-guard: the fixture's filter must land on the side of \ + `names_bound_single_object` the case under test intends, or both halves \ + take the same retention branch" + ); + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { filter }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + (state.stack.len(), state.delayed_triggers.len()) + } + + let (gated_stack, gated_remaining) = run(false, DelayedSubject::BroadFilter); + assert_eq!( + gated_stack, 0, + "CR 603.4: with the intervening-if FALSE when the trigger event occurs, the \ + delayed ability must never be put onto the stack. A 1 here is the shipped \ + defect — a respondable ability that CR 603.4 says never triggered" + ); + assert_eq!( + gated_remaining, 1, + "CR 603.4 + CR 603.7b: the ability DID NOT TRIGGER, and a broad-filter form \ + has a stated duration, so it must keep watching for a later qualifying \ + death. A 0 here is the defect where the first non-qualifying death silently \ + destroys the ability for the rest of the turn" + ); + + let (fired_stack, fired_remaining) = run(true, DelayedSubject::BroadFilter); + assert_eq!( + fired_stack, 1, + "reachability proof: with an owned-and-controlled commander the SAME fixture \ + puts the delayed ability on the stack, so the gated assertion above is not \ + passing because the trigger never matched" + ); + assert_eq!( + fired_remaining, 0, + "the fired one-shot is removed from delayed_triggers as before" + ); + + let (bound_gated_stack, bound_gated_remaining) = run(false, DelayedSubject::BoundObject); + assert_eq!( + bound_gated_stack, 0, + "CR 603.4: the bound-object form is gated off the stack on a false gate too" + ); + assert_eq!( + bound_gated_remaining, 0, + "CR 603.7b + CR 400.7: the ONE bound object has now died, and an object that \ + returns is a new object, so nothing is left for this ability to watch — it \ + is consumed. A 1 here would leak a permanently inert delayed trigger" + ); + + let (bound_fired_stack, bound_fired_remaining) = run(true, DelayedSubject::BoundObject); + assert_eq!( + bound_fired_stack, 1, + "reachability proof for the bound-object half: the same fixture with the gate \ + TRUE reaches the stack, so the retention assertion above is about the gate" + ); + assert_eq!( + bound_fired_remaining, 0, + "the fired one-shot is removed from delayed_triggers as before" + ); + } + + /// CR 603.7b for the PHASE-NAMED one-shot class — the case the discard was + /// always sound for. "At the beginning of the next end step, if [gate], …" + /// names ONE specific later time; when that time arrives with the gate false + /// the ability did nothing (CR 603.4) AND has nothing left to wait for, so it + /// must not lie in wait for the FOLLOWING end step. + /// + /// REVERT-TO-RED: drop the `AtNextPhase` arm from + /// `false_gate_consumes_one_shot` and `remaining` below is `1` — a delayed + /// ability that outlives its own named deadline. + /// + /// The gate-true half is the reachability proof that this fixture's delayed + /// trigger genuinely matches the end-step event. + #[test] + fn phase_named_one_shot_is_consumed_by_a_false_intervening_if() { + fn run(stage_commander: bool) -> (usize, usize) { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_0406), + controller, + "End Step Rider".to_string(), + Zone::Battlefield, + ); + if stage_commander { + let commander = make_creature(&mut state, controller, "Your Commander", 2, 2); + state + .objects + .get_mut(&commander) + .expect("staged commander") + .is_commander = true; + } + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::AtNextPhase { phase: Phase::End }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + check_delayed_triggers(&mut state, &[GameEvent::PhaseChanged { phase: Phase::End }]); + (state.stack.len(), state.delayed_triggers.len()) + } + + let (gated_stack, gated_remaining) = run(false); + assert_eq!( + gated_stack, 0, + "CR 603.4: the ability must never be put onto the stack with the gate false" + ); + assert_eq!( + gated_remaining, 0, + "CR 603.7b: \"at the beginning of the NEXT end step\" names one specific later \ + time, which has now passed — the ability is consumed, not left waiting" + ); + + let (fired_stack, fired_remaining) = run(true); + assert_eq!( + fired_stack, 1, + "reachability proof: with the gate TRUE the same fixture reaches the stack, so \ + the consumption above is not a never-matched trigger" + ); + assert_eq!( + fired_remaining, 0, + "the fired one-shot is removed as before" + ); + } + + /// CR 603.4 + CR 603.7b for the `WhenNextEvent` one-shot class, driven through + /// the production `check_delayed_triggers` path over TWO separate event + /// batches. + /// + /// "When you next [event] this turn, if [gate], …" names ONE occurrence. When + /// that occurrence arrives with the gate false the ability "does nothing" + /// (CR 603.4) and its single shot is spent (CR 603.7b) — making the gate true + /// afterwards must NOT let a second matching event resurrect it. Retaining it + /// would silently rewrite the ability into "when you next [event] for which + /// [gate] holds". + /// + /// REVERT-TO-RED: restore `is_reflexive_lifetime(condition)` on the + /// `WhenNextEvent` arm of `false_gate_consumes_one_shot` and this + /// non-reflexive `ThisTurn` trigger survives batch one, so `after_first` + /// is `1` and the second batch puts the ability on the stack. + #[test] + fn when_next_event_one_shot_is_consumed_by_a_false_intervening_if() { + use crate::types::ability::DelayedTriggerLifetime; + use crate::types::triggers::TriggerMode; + + /// Installs "when you next play a land this turn, if you control your + /// commander, you become the monarch" and returns the land whose play is + /// the matching event. + fn install(state: &mut GameState) -> ObjectId { + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + state, + CardId(0x0603_0407), + controller, + "Next Land Rider".to_string(), + Zone::Battlefield, + ); + let land = create_object( + state, + CardId(0x0603_0408), + controller, + "Played Land".to_string(), + Zone::Battlefield, + ); + + // CR 305.1 + CR 603.2: scope the delayed event to the controller's + // own land drop, exactly as the parser's `WhenNextEvent` builders do. + let mut trigger_def = TriggerDefinition::new(TriggerMode::LandPlayed); + trigger_def.valid_target = Some(TargetFilter::Controller); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + // `WhenNextEvent` matching requires the delayed ability's CR 400.7 + // source context; without it `delayed_trigger_event_with_index` + // returns `None` and the fixture would prove nothing. + let source_object = state.objects.get(&source).expect("installed source"); + ability.trigger_source = Some(trigger_source_context_for_latch(state, source_object)); + + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenNextEvent { + trigger: Box::new(trigger_def), + or_trigger: None, + lifetime: DelayedTriggerLifetime::ThisTurn, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + land + } + + fn stage_commander(state: &mut GameState) { + // CR 903.3 + CR 903.3d: owned AND controlled, on the battlefield. + let commander = make_creature(state, PlayerId(0), "Your Commander", 2, 2); + state + .objects + .get_mut(&commander) + .expect("staged commander") + .is_commander = true; + } + + fn land_played(land: ObjectId) -> GameEvent { + GameEvent::LandPlayed { + object_id: land, + player_id: PlayerId(0), + from_zone: Zone::Hand, + } + } + + // Reachability proof: the same fixture with the gate TRUE on the first + // matching event does reach the stack, so the assertions below are about + // the gate and not about a trigger that never matched `LandPlayed`. + let mut reachable = setup(); + let reachable_land = install(&mut reachable); + stage_commander(&mut reachable); + check_delayed_triggers(&mut reachable, &[land_played(reachable_land)]); + assert_eq!( + reachable.stack.len(), + 1, + "reachability proof: with the gate TRUE the first land drop fires the delayed ability" + ); + + let mut state = setup(); + let land = install(&mut state); + + // Batch one: the ability's single named occurrence, gate FALSE. + check_delayed_triggers(&mut state, &[land_played(land)]); + assert_eq!( + state.stack.len(), + 0, + "CR 603.4: a false intervening-`if` means the ability never triggers, so it must \ + not be put onto the stack" + ); + let after_first = state.delayed_triggers.len(); + assert_eq!( + after_first, 0, + "CR 603.7b: \"when you NEXT play a land this turn\" names ONE occurrence, and that \ + occurrence has now happened — the one-shot is consumed, not left installed" + ); + + // Batch two: the gate is now TRUE and another land is played. A consumed + // one-shot must stay consumed; a retained one would fire here. + stage_commander(&mut state); + check_delayed_triggers(&mut state, &[land_played(land)]); + assert_eq!( + state.stack.len(), + 0, + "CR 603.4 + CR 603.7b: the ability already spent its single occurrence with the \ + gate false; making the gate true afterwards must not let a LATER land drop fire it" + ); + } + + /// The OTHER half of CR 603.7b, and the discriminating counterpart to the + /// test above: a STATED-DURATION delayed ability whose first matching event + /// fails the intervening-`if` must SURVIVE and still fire on a later matching + /// event in the same turn. + /// + /// CR 603.7b caps a delayed ability at one trigger "unless it has a stated + /// duration, such as 'this turn.'" The engine models that carve-out as + /// `DelayedTriggerCondition::WheneverEvent` (multi-fire), the sibling of the + /// one-shot `WhenNextEvent` — so the protection is structural, and this test + /// pins the structure rather than trusting it: `effects::delayed_trigger` + /// computes `one_shot = !matches!(condition, WheneverEvent { .. })`, and the + /// discard in `check_delayed_triggers` is gated on `delayed.one_shot`, so a + /// false gate here must decline the fire WITHOUT consuming the ability. + /// + /// Two matching events, gate false then true, is what makes this + /// discriminating: a one-event fixture would pass even if the ability were + /// wrongly discarded, because both readings put nothing on the stack the + /// first time. + /// + /// REVERT-TO-RED, and the exact recipe matters here: `WheneverEvent` is + /// protected TWICE over, so neither half alone reds this test. Flipping only + /// `false_gate_consumes_one_shot`'s `WheneverEvent` arm to `true` changes + /// nothing, because the caller never consults it for a multi-fire trigger; + /// dropping only the caller's `delayed.one_shot &&` conjunct changes nothing, + /// because the arm still answers `false`. Do BOTH — flip the arm to `true` + /// AND drop the conjunct — and `after_first` becomes `0` and the second event + /// fires nothing (measured, not asserted from reading). + /// + /// That redundancy is the finding, not a weakness of the fixture: this test + /// pins the CONJUNCTION that is the actual guarantee, so it stays red for any + /// change that removes the protection outright rather than merely moving it + /// between the two layers. + #[test] + fn stated_duration_multi_fire_survives_a_false_intervening_if() { + use crate::types::triggers::TriggerMode; + + /// Installs "whenever you play a land this turn, if you control your + /// commander, you become the monarch" — the stated-duration shape. + /// Deliberately the same fixture as the one-shot test above, differing + /// ONLY in the condition sibling, so the two tests isolate exactly the + /// `WhenNextEvent` / `WheneverEvent` distinction and nothing else. + fn install(state: &mut GameState) -> ObjectId { + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + state, + CardId(0x0603_0409), + controller, + "Stated Duration Rider".to_string(), + Zone::Battlefield, + ); + let land = create_object( + state, + CardId(0x0603_040a), + controller, + "Played Land".to_string(), + Zone::Battlefield, + ); + + // CR 305.1 + CR 603.2: scope the delayed event to the controller's + // own land drop, exactly as the one-shot fixture above does. + let mut trigger_def = TriggerDefinition::new(TriggerMode::LandPlayed); + trigger_def.valid_target = Some(TargetFilter::Controller); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + let source_object = state.objects.get(&source).expect("installed source"); + ability.trigger_source = Some(trigger_source_context_for_latch(state, source_object)); + + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WheneverEvent { + trigger: Box::new(trigger_def), + expiry: crate::types::ability::WheneverEventExpiry::EndOfTurn, + }, + ability: Box::new(ability), + controller, + source_id: source, + // CR 603.7b: the stated duration is what makes this multi-fire. + // Mirrors `effects::delayed_trigger`'s own computation + // (`one_shot = !matches!(condition, WheneverEvent { .. })`). + one_shot: false, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + land + } + + fn stage_commander(state: &mut GameState) { + // CR 903.3 + CR 903.3d: owned AND controlled, on the battlefield. + let commander = make_creature(state, PlayerId(0), "Your Commander", 2, 2); + state + .objects + .get_mut(&commander) + .expect("staged commander") + .is_commander = true; + } + + fn land_played(land: ObjectId) -> GameEvent { + GameEvent::LandPlayed { + object_id: land, + player_id: PlayerId(0), + from_zone: Zone::Hand, + } + } + + let mut state = setup(); + let land = install(&mut state); + + // Batch one: a matching event with the gate FALSE (no commander staged). + check_delayed_triggers(&mut state, &[land_played(land)]); + assert_eq!( + state.stack.len(), + 0, + "CR 603.4: a false intervening-`if` means the ability does not trigger on this \ + occurrence" + ); + let after_first = state.delayed_triggers.len(); + assert_eq!( + after_first, 1, + "CR 603.7b: a STATED-DURATION delayed ability is not capped at one trigger, so a \ + false gate must decline the occurrence WITHOUT consuming the ability" + ); + + // Batch two: make the gate TRUE, then a second matching event. The + // ability must still be installed and must now fire. + stage_commander(&mut state); + check_delayed_triggers(&mut state, &[land_played(land)]); + assert_eq!( + state.stack.len(), + 1, + "CR 603.7b + CR 603.4: the ability survived the declined occurrence and its gate is \ + re-checked on the next one, which now passes" + ); + + // CR 603.4 (second half) + CR 608.2a: passing the FIRE-TIME gate must not + // disarm the RESOLUTION-TIME one. The hoist is a pair, not a move: the + // same gate has to ride onto the stack entry so `stack.rs`'s + // `bind_resolution_scope` re-checks it, which is what denies the ability + // if the condition stops holding between triggering and resolution (a + // commander leaving the battlefield in response). An entry carrying + // `condition: None` would resolve unconditionally — the very bug this PR + // fixes, reintroduced one layer later. + let entry = state.stack.last().expect("the fired delayed ability"); + assert!( + matches!( + &entry.kind, + StackEntryKind::TriggeredAbility { + condition: Some(_), + .. + } + ), + "CR 603.4: the hoisted intervening-`if` must also be carried onto the stack entry \ + for the resolution-time recheck, got {:?}", + entry.kind + ); + } + + /// HOSTILE fixture for the CR 603.4 hoist's carve-out. + /// + /// "…, if X, A. Otherwise, B." is NOT an intervening-`if` in the CR 603.4 + /// sense — the ability has work to do when the gate is false, so it MUST + /// still be put onto the stack and resolve its `else_ability`. + /// `delayed_body_outlives_a_false_gate` declines every body carrying an + /// `else_ability` for exactly this reason. + /// + /// REVERT-TO-RED: drop the `else_ability` arm from + /// `delayed_body_outlives_a_false_gate` and the gate hoists, the ability + /// never reaches the stack, and the Otherwise branch is silently deleted — + /// life stays 20 and the stack stays empty. + #[test] + fn delayed_body_with_an_otherwise_branch_is_never_gated_off_the_stack() { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + state.players[0].life = 20; + + let source = create_object( + &mut state, + CardId(0x0603_0402), + controller, + "Otherwise Rider".to_string(), + Zone::Battlefield, + ); + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + // Gate is FALSE (no commander anywhere), so the Otherwise branch is the + // one that must run. + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + ability.else_ability = Some(Box::new(ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + controller, + ))); + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + assert_eq!( + state.stack.len(), + 1, + "a body with an Otherwise branch must still reach the stack even though its \ + own condition is false" + ); + + let mut events = Vec::new(); + crate::game::stack::resolve_top(&mut state, &mut events); + assert_eq!( + state.players[0].life, 21, + "CR 608.2c: the Otherwise branch must resolve. 20 here means the CR 603.4 \ + hoist swallowed a non-intervening-if gate and deleted the else branch" + ); + assert_eq!( + state.monarch, None, + "the gated primary effect must still not happen" + ); + } + + /// CR 603.4 for a CHAINED delayed body — the case the old blanket + /// `sub_ability.is_some()` guard silently got wrong. + /// + /// "When [event], if [gate], A and B" lowers B as an UNCONDITIONAL + /// `SequentialSibling` sub of the gated body. CR 603.4's `if` gates the WHOLE + /// ability, so with the gate false NEITHER clause may happen. Under the old + /// guard the hoist was skipped AND `resolve_ability_chain`'s + /// `SequentialSibling && condition.is_none()` escape resolved B anyway — the + /// ability went on the stack and B happened with the gate false. + /// + /// REVERT-TO-RED: restore `|| ability.sub_ability.is_some()` in + /// `delayed_body_outlives_a_false_gate` and the gated half below reports + /// `stack == 1` and life 21 — B resolved through a false CR 603.4 gate. + /// + /// The commander half is the reachability proof AND the non-deletion proof: + /// the same fixture with the gate TRUE runs both clauses. + #[test] + fn delayed_body_with_an_unconditional_sequential_sub_is_gated_as_one_ability() { + fn run(stage_commander: bool) -> (usize, i32, Option) { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + state.players[0].life = 20; + + let source = create_object( + &mut state, + CardId(0x0603_0403), + controller, + "Chained Rider".to_string(), + Zone::Battlefield, + ); + if stage_commander { + let commander = make_creature(&mut state, controller, "Your Commander", 2, 2); + state + .objects + .get_mut(&commander) + .expect("staged commander") + .is_commander = true; + } + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + // "…and gain 1 life" — the second clause of the ONE gated sentence. + let mut sub = ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + sub.sub_link = crate::types::ability::SubAbilityLink::SequentialSibling; + assert!( + sub.condition.is_none(), + "reach-guard: the sub must be UNCONDITIONAL, or it takes a different \ + resolve_ability_chain branch than the one under test" + ); + ability.sub_ability = Some(Box::new(sub)); + + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + let stack_len = state.stack.len(); + if stack_len == 1 { + let mut events = Vec::new(); + crate::game::stack::resolve_top(&mut state, &mut events); + } + (stack_len, state.players[0].life, state.monarch) + } + + let (gated_stack, gated_life, gated_monarch) = run(false); + assert_eq!( + gated_stack, 0, + "CR 603.4: the intervening-if gates the WHOLE ability, chain and all" + ); + assert_eq!( + gated_life, 20, + "CR 603.4: the chained second clause must NOT happen through a false gate. \ + 21 means the unconditional SequentialSibling escaped the gate" + ); + assert_eq!( + gated_monarch, None, + "the gated primary effect must not happen" + ); + + let (fired_stack, fired_life, fired_monarch) = run(true); + assert_eq!( + fired_stack, 1, + "reachability proof: with the gate TRUE the same fixture reaches the stack" + ); + assert_eq!( + fired_monarch, + Some(PlayerId(0)), + "the gated primary effect resolves when the gate is true" + ); + assert_eq!( + fired_life, 21, + "non-deletion proof: the chained clause still resolves when the gate is true, \ + so the gated assertion above is not passing because the chain was dropped" + ); + } + + /// CR 603.4: the two legs of the hoist must be the SAME predicate over the + /// SAME values. A gate whose quantity reads a binding only the + /// resolution-time leg has — the delayed ability's snapshotted `targets` + /// (`ObjectScope::Target`), the per-iteration player + /// (`PlayerScope::ScopedPlayer`), or a population the trigger bridge + /// re-scopes (`FilterProp::Another` → `OtherThanTriggerObject`) — must + /// DECLINE the hoist rather than gate the ability off the stack on a value + /// the resolver would never have computed. + /// + /// Each case is a MINIMAL PAIR: the two halves differ only in the scope or + /// the one filter property. The `hoisted` half proves a `QuantityCheck` gate + /// really does bridge and really is evaluated at fire time (so the declined + /// half's `stack == 1` is the decline, not a non-bridging condition); the + /// `declined` half proves the divergent reading never reaches the gate. + /// + /// REVERT-TO-RED: drop the `gate_binding_diverges_at_fire_time` call from + /// `delayed_intervening_if` and every `declined` half below reports + /// `stack == 0` — the ability deleted off the stack on a fire-time reading + /// (empty targets / event-derived player / trigger-object exclusion) that the + /// resolution-time reader does not share. + #[test] + fn divergent_gate_bindings_decline_the_fire_time_hoist() { + fn run(condition: AbilityCondition) -> usize { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_0407), + controller, + "Divergent Rider".to_string(), + Zone::Battlefield, + ); + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(condition); + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + state.stack.len() + } + + let counters_on = |scope| AbilityCondition::QuantityCheck { + lhs: QuantityExpr::Ref { + qty: QuantityRef::CountersOn { + scope, + counter_type: Some(crate::types::counter::CounterType::Plus1Plus1), + }, + }, + comparator: crate::types::ability::Comparator::GE, + rhs: QuantityExpr::Fixed { value: 3 }, + }; + // No object in the fixture carries a +1/+1 counter, so BOTH readings are + // false — the halves differ only in whether the gate is consulted at all. + assert_eq!( + run(counters_on(ObjectScope::Source)), + 0, + "CR 603.4: a Source-scoped counter gate binds identically on both legs, so it \ + hoists and gates the ability off the stack" + ); + assert_eq!( + run(counters_on(ObjectScope::Target)), + 1, + "CR 115.1: `ObjectScope::Target` reads the delayed ability's snapshotted \ + targets, which the fire-time context does not carry. 0 here means the hoist \ + gated the ability off the stack on an empty-targets reading" + ); + + let life_total = |player| AbilityCondition::QuantityCheck { + lhs: QuantityExpr::Ref { + qty: QuantityRef::LifeTotal { player }, + }, + comparator: crate::types::ability::Comparator::GE, + rhs: QuantityExpr::Fixed { value: 100 }, + }; + assert_eq!( + run(life_total(PlayerScope::Controller)), + 0, + "reach-guard: the same comparison scoped to the controller DOES hoist (nobody \ + has 100 life), so the declined half below is the scope and nothing else" + ); + assert_eq!( + run(life_total(PlayerScope::ScopedPlayer)), + 1, + "CR 115.10: `ScopedPlayer` is the RESOLVING ability's per-iteration player; \ + the fire-time context derives its scoped player from the event instead" + ); + + let controls_creature = |props: Vec| AbilityCondition::QuantityCheck { + lhs: QuantityExpr::Ref { + qty: QuantityRef::ObjectCount { + filter: TargetFilter::Typed( + TypedFilter::creature() + .controller(ControllerRef::You) + .properties(props), + ), + }, + }, + comparator: crate::types::ability::Comparator::GE, + rhs: QuantityExpr::Fixed { value: 1 }, + }; + assert_eq!( + run(controls_creature(vec![])), + 0, + "reach-guard: the unqualified population gate hoists (the controller controls \ + no creature), so the declined half below is the `Another` prop alone" + ); + assert_eq!( + run(controls_creature(vec![FilterProp::Another])), + 1, + "CR 603.4: `oracle_trigger` rewrites `Another` to `OtherThanTriggerObject` on \ + the fire-time leg only, so the two legs would count DIFFERENT populations" + ); + } + + /// CR 400.1 + CR 603.4: the ZONE axis of the same two-legs-must-agree rule. + /// + /// `ObjectCount{filter} >= 1` folds into `StaticCondition::IsPresent`, whose + /// affirmative trigger-side bridge is `TriggerCondition::ControlsType` — a + /// BATTLEFIELD-ONLY scan. The resolution leg counts in the FILTER'S OWN zone. + /// A gate on a graveyard population therefore reads TRUE at resolution and + /// FALSE at fire time, and hoisting it would gate the ability off the stack + /// (deleting a consumed one-shot) on a predicate the resolver never computes. + /// + /// MINIMAL PAIR: the two halves differ only in the `InZone { Graveyard }` + /// property. The battlefield half is the reach-guard — it proves an + /// `ObjectCount >= 1` gate really does bridge and really is evaluated at fire + /// time, so the graveyard half's `stack == 1` is the decline and nothing else. + /// + /// REVERT-TO-RED: drop the `static_gate_bridge_loses_zone` call from + /// `delayed_intervening_if` and the graveyard half reports `stack == 0` (and + /// `monarch == None`) — the ability deleted at fire time on a battlefield scan + /// of a graveyard population. + #[test] + fn non_battlefield_presence_gate_declines_the_fire_time_hoist() { + fn run(props: Vec) -> (usize, Option) { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_0409), + controller, + "Graveyard Rider".to_string(), + Zone::Battlefield, + ); + // The counted population lives in the GRAVEYARD, never on the + // battlefield: the controller controls no creature there, so a + // battlefield scan of this filter is false either way. + let buried = create_object( + &mut state, + CardId(0x0603_040A), + controller, + "Buried Squire".to_string(), + Zone::Graveyard, + ); + { + let obj = state.objects.get_mut(&buried).expect("staged card"); + obj.card_types.core_types.push(CoreType::Creature); + obj.base_card_types = obj.card_types.clone(); + } + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::QuantityCheck { + lhs: QuantityExpr::Ref { + qty: QuantityRef::ObjectCount { + filter: TargetFilter::Typed( + TypedFilter::creature() + .controller(ControllerRef::You) + .properties(props), + ), + }, + }, + comparator: crate::types::ability::Comparator::GE, + rhs: QuantityExpr::Fixed { value: 1 }, + }); + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::SpecificObject { id: victim }, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + let stack_len = state.stack.len(); + if stack_len == 1 { + let mut events = Vec::new(); + crate::game::stack::resolve_top(&mut state, &mut events); + } + (stack_len, state.monarch) + } + + let (battlefield_stack, battlefield_monarch) = run(vec![]); + assert_eq!( + battlefield_stack, 0, + "reach-guard: a battlefield-scoped `ObjectCount >= 1` gate bridges and IS \ + evaluated at fire time — the controller controls no creature, so CR 603.4 \ + keeps the ability off the stack" + ); + assert_eq!( + battlefield_monarch, None, + "nothing resolved on the battlefield-scoped half" + ); + + let (graveyard_stack, graveyard_monarch) = run(vec![FilterProp::InZone { + zone: Zone::Graveyard, + }]); + assert_eq!( + graveyard_stack, 1, + "CR 400.1 + CR 603.4: the affirmative `IsPresent` bridge is battlefield-only, \ + so a graveyard population would read FALSE at fire time while the resolver \ + reads TRUE. The hoist must decline and the ability must reach the stack" + ); + assert_eq!( + graveyard_monarch, + Some(PlayerId(0)), + "divergence proof: the RESOLUTION-time leg reads the same gate as TRUE (one \ + creature card in the controller's graveyard), so a fire-time deletion would \ + have destroyed an ability that was supposed to resolve" + ); + } + + /// CR 608.2c + CR 603.4: a RESOLUTION-SCOPED quantity leaf carries no scope, + /// player or filter payload, yet the fire-time leg cannot reproduce it — the + /// resolver is called with no resolving ability, no targets and no chain + /// ledger, so `TrackedSetSize` reads whatever an unrelated earlier resolution + /// left behind (here: nothing). + /// + /// MINIMAL PAIR against another PAYLOAD-FREE leaf whose reading is identical + /// on both legs (`TurnsTaken`), so the decline below cannot be explained by + /// "payload-free gates never hoist". + /// + /// REVERT-TO-RED: restore the `_ => false` tail of + /// `quantity_ref_binding_diverges` and the tracked-set half reports + /// `stack == 0` with `delayed_triggers` emptied — the one-shot deleted by + /// `false_gate_consumes_one_shot` on a value the resolver never computed. + #[test] + fn resolution_scoped_quantity_gate_declines_the_fire_time_hoist() { + // Unit pins for the two adjudications the production pair below drives. + assert!( + quantity_ref_binding_diverges(&QuantityRef::TrackedSetSize), + "CR 608.2c: the chain tracked set exists only during a resolution" + ); + assert!( + !quantity_ref_binding_diverges(&QuantityRef::TurnsTaken), + "CR 500: turns taken is global state both legs read identically" + ); + + fn run(qty: QuantityRef, threshold: i32) -> (usize, usize) { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_040B), + controller, + "Tracked Rider".to_string(), + Zone::Battlefield, + ); + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::QuantityCheck { + lhs: QuantityExpr::Ref { qty }, + comparator: crate::types::ability::Comparator::GE, + rhs: QuantityExpr::Fixed { value: threshold }, + }); + state.delayed_triggers.push(DelayedTrigger { + // A bound single object, so a false gate would CONSUME the + // one-shot (`false_gate_consumes_one_shot`) rather than leave it + // installed — the deletion half of the defect. + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::SpecificObject { id: victim }, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + (state.stack.len(), state.delayed_triggers.len()) + } + + // `TurnsTaken >= 100` is false on both legs — the reach-guard that a + // payload-free `QuantityCheck` gate does hoist and does gate at fire time. + let (turns_stack, turns_remaining) = run(QuantityRef::TurnsTaken, 100); + assert_eq!( + turns_stack, 0, + "reach-guard: a payload-free gate whose reading is leg-independent hoists, so \ + the declined half below is the RESOLUTION scope and nothing else" + ); + assert_eq!( + turns_remaining, 0, + "CR 603.7b: the bound-object one-shot is consumed by its own event" + ); + + let (tracked_stack, tracked_remaining) = run(QuantityRef::TrackedSetSize, 1); + assert_eq!( + tracked_stack, 1, + "CR 608.2c: `TrackedSetSize` is published BY a resolution; the fire-time leg \ + reads a foreign (here empty) ledger, so the gate must keep its \ + resolution-only reading and the ability must reach the stack" + ); + assert_eq!( + tracked_remaining, 0, + "the ability FIRED (it is off the delayed list because it went on the stack), \ + not because a false gate deleted it — see the stack assertion above" + ); + } + + /// CR 603.4 + CR 608.2c: `delayed_body_outlives_a_false_gate` inspects the + /// DIRECT sub only, because that is all `resolve_chain_body`'s + /// condition-false path inspects. A 2-deep chain whose direct sub does NOT + /// qualify resolves nothing at all, so the hoist must still apply even though + /// a GRANDCHILD carries an `else_ability`. + /// + /// REVERT-TO-RED: restore the `|| delayed_body_outlives_a_false_gate(sub)` + /// recursion and the gated half below reports `stack == 1` — an ability put + /// onto the stack (respondable, and CR 603.4 says it never triggered) to + /// resolve exactly nothing. + /// + /// The resolver-mirror assertion drives `effects::resolve_ability_chain` + /// directly on the SAME body with the SAME false gate and proves it performs + /// no work, so the hoist deletes nothing. + #[test] + fn two_deep_chain_mirrors_the_resolvers_direct_sub_test() { + /// "…, if [gate], become the monarch" whose direct sub is an ORDINARY + /// conditional continuation and whose GRANDCHILD carries an + /// `else_ability` (life gain) that only runs if the chain gets that far. + fn body(source: ObjectId, controller: PlayerId) -> ResolvedAbility { + let mut grandchild = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + grandchild.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + grandchild.else_ability = Some(Box::new(ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + controller, + ))); + + let mut sub = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + // NOT one of the surviving classes: an ordinary game-state gate on a + // continuation link, so `resolve_chain_body` stops here when the + // parent gate is false. + sub.condition = Some(AbilityCondition::IsYourTurn); + sub.sub_link = crate::types::ability::SubAbilityLink::ContinuationStep; + sub.sub_ability = Some(Box::new(grandchild)); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + ability.sub_ability = Some(Box::new(sub)); + ability + } + + fn run(stage_commander: bool) -> (usize, i32, Option) { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + state.players[0].life = 20; + + let source = create_object( + &mut state, + CardId(0x0603_040C), + controller, + "Two Deep Rider".to_string(), + Zone::Battlefield, + ); + if stage_commander { + let commander = make_creature(&mut state, controller, "Your Commander", 2, 2); + state + .objects + .get_mut(&commander) + .expect("staged commander") + .is_commander = true; + } + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let ability = body(source, controller); + // Reach-guard: the shape under test is a 2-deep chain whose DIRECT sub + // is not one of the surviving classes but whose grandchild is. + let direct = ability.sub_ability.as_deref().expect("staged direct sub"); + assert!( + !crate::game::effects::sub_outlives_false_parent_gate(direct), + "the direct sub must NOT qualify, or the hoist declines for the shallow \ + reason and the 2-deep case is never exercised" + ); + assert!( + direct + .sub_ability + .as_deref() + .is_some_and(|grandchild| grandchild.else_ability.is_some()), + "the GRANDCHILD must carry the surviving shape the old recursion tripped on" + ); + + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + let stack_len = state.stack.len(); + if stack_len == 1 { + let mut events = Vec::new(); + crate::game::stack::resolve_top(&mut state, &mut events); + } + (stack_len, state.players[0].life, state.monarch) + } + + let (gated_stack, gated_life, gated_monarch) = run(false); + assert_eq!( + gated_stack, 0, + "CR 603.4: with the gate false the resolver would run NOTHING (the direct sub \ + does not survive), so the fire-time half must gate the ability off the stack" + ); + assert_eq!(gated_life, 20, "no chain work happened"); + assert_eq!(gated_monarch, None, "no chain work happened"); + + // Resolver mirror: the same body, the same false gate, resolved directly. + // Nothing runs — which is exactly why hoisting deletes nothing. + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + state.players[0].life = 20; + let source = create_object( + &mut state, + CardId(0x0603_040D), + controller, + "Two Deep Rider".to_string(), + Zone::Battlefield, + ); + let mut events = Vec::new(); + crate::game::effects::resolve_ability_chain( + &mut state, + &body(source, controller), + &mut events, + 0, + ) + .expect("chain resolution"); + assert_eq!( + state.players[0].life, 20, + "CR 608.2c: `resolve_chain_body` stops at the non-surviving DIRECT sub, so the \ + grandchild's else branch never runs — 21 here would mean the hoist above \ + deleted printed work" + ); + assert_eq!( + state.monarch, None, + "the gated primary effect and the gated grandchild both stay unperformed" + ); + + let (fired_stack, _fired_life, fired_monarch) = run(true); + assert_eq!( + fired_stack, 1, + "reachability proof: the SAME fixture with the gate TRUE reaches the stack" + ); + assert_eq!( + fired_monarch, + Some(PlayerId(0)), + "and resolves its gated effect" + ); + } + + /// CR 603.4 + CR 702.1c: `delayed_body_outlives_a_false_gate` and + /// `resolve_chain_body` must decline / run on EXACTLY the same sub shapes. + /// The `ReplicatedOrBranch` marker only means "independent OR-branch" on a + /// `SequentialSibling` link — that is the conjunct + /// `effects::sub_outlives_false_parent_gate` now carries for BOTH consumers. + /// + /// REVERT-TO-RED: drop the `sub_link == SequentialSibling` conjunct from + /// `sub_outlives_false_parent_gate` and the `ContinuationStep` half below + /// reports `stack == 1` — the hoist declined for a sub the resolver would + /// never have run through a false gate, leaving CR 603.4 unenforced. + #[test] + fn replicated_or_branch_carve_out_requires_a_sequential_sibling_link() { + fn run(sub_link: crate::types::ability::SubAbilityLink) -> usize { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_0408), + controller, + "Replicated Rider".to_string(), + Zone::Battlefield, + ); + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + // Gate FALSE: no commander is staged anywhere. + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + let mut sub = ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + sub.sibling_condition = crate::types::ability::SiblingCondition::ReplicatedOrBranch; + sub.sub_link = sub_link; + // Reach-guard: the sub must carry the replication marker, or both + // halves take the plain no-sub branch and the pair proves nothing. + assert_eq!( + sub.sibling_condition, + crate::types::ability::SiblingCondition::ReplicatedOrBranch + ); + ability.sub_ability = Some(Box::new(sub)); + + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + state.stack.len() + } + + assert_eq!( + run(crate::types::ability::SubAbilityLink::SequentialSibling), + 1, + "CR 702.1c: a replicated OR-branch on a SequentialSibling link is exactly what \ + `resolve_chain_body` still runs through a false parent gate, so the hoist \ + must decline and the ability must still reach the stack" + ); + assert_eq!( + run(crate::types::ability::SubAbilityLink::ContinuationStep), + 0, + "CR 603.4: on a ContinuationStep link the resolver would NOT have run the sub \ + either, so nothing survives the false gate and the whole ability must be \ + gated off the stack" + ); + } + + /// CR 603.4 + CR 903.3d for the NEGATED commander gate ("if you don't + /// control your commander"). + /// + /// The hoist composes two bridges, and only the FIRST had a negated + /// commander arm: `ability_condition_to_static_condition` produced + /// `Not{ControlsCommander}` while + /// `static_condition_to_trigger_condition`'s `Not` sub-match fell through to + /// `_ => None`, so `delayed_intervening_if` returned `None` and the negated + /// gate kept only the resolution-time half of CR 603.4. + /// + /// REVERT-TO-RED: remove the `StaticCondition::ControlsCommander` arm from + /// that `Not` sub-match (`oracle_trigger.rs`) and the no-commander half below + /// reports `stack == 1` — the ability was put onto the stack instead of + /// having never triggered. + #[test] + fn negated_delayed_intervening_if_gates_at_fire_time_too() { + fn run(stage_commander: bool) -> usize { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_0405), + controller, + "Negated Rider".to_string(), + Zone::Battlefield, + ); + if stage_commander { + let commander = make_creature(&mut state, controller, "Your Commander", 2, 2); + state + .objects + .get_mut(&commander) + .expect("staged commander") + .is_commander = true; + } + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::Not { + condition: Box::new(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }), + }); + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + state.stack.len() + } + + assert_eq!( + run(true), + 0, + "CR 603.4: with a commander on the battlefield the NEGATED gate is false at \ + fire time, so the ability must never be put onto the stack" + ); + assert_eq!( + run(false), + 1, + "reachability proof: with no commander the negated gate is TRUE and the same \ + fixture reaches the stack" + ); + } + + /// HOSTILE fixture for the narrowed carve-out's OTHER arm (CR 603.12). + /// + /// A sub whose own gate is a performed/reflexive one ("…, if you do, …" — + /// Council's Deliberation's shape, the only conditioned delayed body with a + /// sub in the pool today) is re-evaluated on its own at resolution, so + /// `delayed_body_outlives_a_false_gate` conservatively declines the CR 603.4 + /// hoist and this class keeps byte-identical behaviour: the ability still + /// reaches the stack even with the parent gate false. + /// + /// REVERT-TO-RED: drop the `condition_survives_false_parent_gate` arm and the + /// hoist fires, the stack stays empty, and the in-pool reflexive shape changes + /// behaviour as a side effect of a commander-gate fix. + #[test] + fn delayed_body_with_a_reflexive_sub_is_never_gated_off_the_stack() { + let mut state = setup(); + let controller = PlayerId(0); + state.active_player = controller; + state.priority_player = controller; + + let source = create_object( + &mut state, + CardId(0x0603_0404), + controller, + "Reflexive Rider".to_string(), + Zone::Battlefield, + ); + let victim = make_creature(&mut state, PlayerId(1), "Doomed Squire", 1, 1); + + let mut ability = ResolvedAbility::new( + Effect::BecomeMonarch { + target: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + ability.condition = Some(AbilityCondition::ControlsCommander { + ownership: CommanderOwnership::Own, + }); + let mut sub = ResolvedAbility::new( + Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }, + vec![], + source, + controller, + ); + sub.condition = Some(AbilityCondition::effect_performed()); + // Reach-guard: the carve-out is keyed on this classification, not on the + // mere presence of a sub. + assert!( + crate::game::effects::condition_survives_false_parent_gate( + sub.condition.as_ref().expect("staged reflexive gate") + ), + "the fixture must carry a gate the shared authority classifies as \ + surviving a false parent gate" + ); + ability.sub_ability = Some(Box::new(sub)); + + state.delayed_triggers.push(DelayedTrigger { + condition: DelayedTriggerCondition::WhenDies { + filter: TargetFilter::Any, + }, + ability: Box::new(ability), + controller, + source_id: source, + one_shot: true, + provenance: DelayedInstallIdentity::LegacyDelayed, + }); + + let death = zone_changed_event( + victim, + Zone::Battlefield, + Zone::Graveyard, + vec![CoreType::Creature], + Vec::new(), + ); + check_delayed_triggers(&mut state, &[death]); + assert_eq!( + state.stack.len(), + 1, + "a body whose sub carries a CR 603.12 performed gate keeps its resolution-time \ + reading and must still reach the stack" + ); + } + #[test] fn delayed_phase_trigger_batches_with_normal_phase_trigger_before_priority() { let mut state = setup(); @@ -19687,6 +22179,7 @@ pub mod tests { controller, object_id: source, card_id: CardId(0x98), + cast_mana_value: None, }), modal: Some(ModalChoice { min_choices: 1, @@ -21572,6 +24065,7 @@ pub mod tests { card_id: CardId(1), controller: PlayerId(0), object_id: spell_id, + cast_mana_value: None, }]; process_triggers(&mut state, &events); @@ -23757,6 +26251,391 @@ pub mod tests { )); } + /// Three-player state with a battlefield source controlled by P0, used by + /// the monarch-subject fixtures below. + fn monarch_setup() -> (GameState, ObjectId) { + let mut state = GameState::new(crate::types::format::FormatConfig::commander(), 3, 42); + let source = create_object( + &mut state, + CardId(1), + PlayerId(0), + "Monarch subject source".to_string(), + Zone::Battlefield, + ); + (state, source) + } + + fn attackers_declared(attacker: ObjectId, defender: PlayerId) -> GameEvent { + GameEvent::AttackersDeclared { + attacker_ids: vec![attacker], + defending_player: defender, + attacks: vec![( + attacker, + crate::game::combat::AttackTarget::Player(defender), + )], + } + } + + /// CR 725.1 + CR 109.5: the controller subject is CR 109.5's "you". + #[test] + fn is_monarch_controller_subject_reads_the_ability_controller() { + let (mut state, source) = monarch_setup(); + let condition = TriggerCondition::IsMonarch { + player: PlayerScope::Controller, + }; + + state.monarch = Some(PlayerId(2)); + assert!(!check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + None + )); + + state.monarch = Some(PlayerId(0)); + assert!(check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + None + )); + } + + /// CR 725.1 + CR 508.5 + CR 603.4: the defending-player subject reads the + /// player the TRIGGERING creature is attacking, not the ability controller. + /// + /// Revert-failing: an arm that ignores `player` and calls + /// `eval_is_monarch(state, controller)` answers `false` for the positive + /// case (P0 is not the monarch) and `true` for the negative one. + #[test] + fn is_monarch_defending_player_subject_reads_the_attacked_player_cr_508_5() { + let (mut state, source) = monarch_setup(); + let attacker = create_object( + &mut state, + CardId(2), + PlayerId(0), + "Attacker".to_string(), + Zone::Battlefield, + ); + let condition = TriggerCondition::IsMonarch { + player: PlayerScope::DefendingPlayer, + }; + + state.monarch = Some(PlayerId(2)); + assert!( + check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + Some(&attackers_declared(attacker, PlayerId(2))), + ), + "the attacked player (P2) is the monarch" + ); + assert!( + !check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + Some(&attackers_declared(attacker, PlayerId(1))), + ), + "the attacked player (P1) is not the monarch" + ); + } + + /// CR 603.4 + CR 109.4: an unresolvable anchor makes the condition + /// UNANSWERABLE. It must be false in BOTH polarities — the whole point of + /// rejecting at the entry boundary instead of inside the recursion. + /// + /// Revert-failing on the negated case: without the boundary gate the leaf + /// returns `false` and `Not` inverts it to `true`, firing the trigger + /// precisely when the engine cannot identify the player. + #[test] + fn unresolvable_designation_anchor_is_false_in_both_polarities_cr_603_4() { + let (mut state, source) = monarch_setup(); + state.monarch = Some(PlayerId(2)); + + for scope in [ + PlayerScope::DefendingPlayer, + PlayerScope::Target, + // CR 611.2a: serde-reachable duration-timing-only scope; must fail + // closed rather than hit `resolve_single_player_scope`'s panic. + PlayerScope::AnyTurn, + // CR 603.4: the scope `parse_monarch_identity_subject` actually + // EMITS for "that player is the monarch". Every anchor that the + // attack-trigger rebind does not convert to `DefendingPlayer` + // arrives here as `ScopedPlayer`, so this is the one row that must + // not inherit `resolve_single_player_scope`'s value-context + // `unwrap_or(controller)` fallback. + PlayerScope::ScopedPlayer, + ] { + let affirmative = TriggerCondition::IsMonarch { + player: scope.clone(), + }; + assert!( + !check_trigger_condition(&state, &affirmative, PlayerId(0), Some(source), None), + "{scope:?}: affirmative must be false with no attack in scope" + ); + let negated = TriggerCondition::Not { + condition: Box::new(affirmative), + }; + assert!( + !check_trigger_condition(&state, &negated, PlayerId(0), Some(source), None), + "{scope:?}: NEGATED must also be false — `Not` must not invert an \ + unanswerable anchor into a firing trigger" + ); + } + + // Reach-guard: the arm IS reached — a resolvable subject still answers. + state.monarch = Some(PlayerId(0)); + assert!(check_trigger_condition( + &state, + &TriggerCondition::IsMonarch { + player: PlayerScope::Controller + }, + PlayerId(0), + Some(source), + None + )); + } + + /// CR 603.4 + CR 109.4: `PlayerScope::ScopedPlayer` — the scope + /// `parse_monarch_identity_subject` emits for "that player is the monarch" + /// — must read the player NAMED BY THE TRIGGERING EVENT, and must be + /// unanswerable when no event names one. It must never inherit + /// `resolve_single_player_scope`'s value-context `unwrap_or(controller)` + /// fallback. + /// + /// Revert-failing: drop the `ScopedPlayer`/`scoped_player.is_none()` guard + /// in `quantity::resolve_player_scope_for_trigger_check` and the + /// no-event affirmative below answers `true` — the ability CONTROLLER (P0) + /// is the monarch in that arm, which is precisely the wrong player — while + /// the negated case answers `false` for the wrong reason. + #[test] + fn is_monarch_scoped_player_subject_needs_an_event_anchor_cr_603_4() { + let (mut state, source) = monarch_setup(); + let condition = TriggerCondition::IsMonarch { + player: PlayerScope::ScopedPlayer, + }; + let life_changed = |player_id: PlayerId| GameEvent::LifeChanged { + player_id, + amount: -1, + }; + + // Reach-guard: with an event that NAMES a player, the arm resolves and + // discriminates between the named player and everyone else. + state.monarch = Some(PlayerId(2)); + assert!( + check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + Some(&life_changed(PlayerId(2))), + ), + "the event's player (P2) is the monarch" + ); + assert!( + !check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + Some(&life_changed(PlayerId(1))), + ), + "the event's player (P1) is not the monarch" + ); + + // The controller IS the monarch, and no event names a player. Both + // polarities must be false: the anchor is missing, so the question is + // unanswerable — not silently re-pointed at the controller. + state.monarch = Some(PlayerId(0)); + assert!( + !check_trigger_condition(&state, &condition, PlayerId(0), Some(source), None), + "an unanchored `that player` must not fall back to the controller" + ); + assert!( + !check_trigger_condition( + &state, + &TriggerCondition::Not { + condition: Box::new(condition.clone()), + }, + PlayerId(0), + Some(source), + None + ), + "`Not` must not invert the missing anchor into a firing trigger" + ); + + // Reach-guard for the negative rows: the same anchored event that + // resolves above still resolves with the controller as monarch, so the + // rows above are about the MISSING ANCHOR, not about a dead arm. + assert!(check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + Some(&life_changed(PlayerId(0))), + )); + } + + /// The boundary gate is deliberately conservative: an unresolvable anchor + /// rejects the whole condition even inside an `Or` whose other operand is + /// true. No corpus card places a designation leaf under `Or`; this pins the + /// choice so a future card needing the looser reading has a failing test to + /// point at rather than a silent behaviour change. + #[test] + fn unresolvable_designation_anchor_absorbs_or_cr_603_4() { + let (mut state, source) = monarch_setup(); + state.monarch = Some(PlayerId(0)); + + let true_operand = TriggerCondition::IsMonarch { + player: PlayerScope::Controller, + }; + // Reach-guard: on its own the true operand fires. + assert!(check_trigger_condition( + &state, + &true_operand, + PlayerId(0), + Some(source), + None + )); + + let disjunction = TriggerCondition::Or { + conditions: vec![ + TriggerCondition::IsMonarch { + player: PlayerScope::DefendingPlayer, + }, + true_operand, + ], + }; + assert!( + !check_trigger_condition(&state, &disjunction, PlayerId(0), Some(source), None), + "documented conservative choice, not an accident" + ); + } + + /// CR 725.1: vacancy and identity stay distinct predicates after the + /// subject axis is added. + #[test] + fn monarch_vacancy_and_identity_remain_distinct_cr_725_1() { + let (mut state, source) = monarch_setup(); + state.monarch = None; + + assert!(!check_trigger_condition( + &state, + &TriggerCondition::IsMonarch { + player: PlayerScope::Controller + }, + PlayerId(0), + Some(source), + None + )); + assert!(check_trigger_condition( + &state, + &TriggerCondition::NoMonarch, + PlayerId(0), + Some(source), + None + )); + } + + /// CR 508.5a / CR 802.2a: `DefendingPlayerControlsNone` quantifies over + /// EVERY live defender rather than the one defending player CR 508.5a + /// specifies. That is a real gap on Siege Dragon / Spectral Force / + /// Spectral Bears / Fear of the Dark, but it is an all-defenders + /// QUANTIFIER bug, not an anchor-resolution bug: it shares no code with the + /// three `defending_player_cr508_5` doors and fixing it would change those + /// four cards' firing behaviour. + /// + /// This test pins TODAY's behaviour so a later change that routes the arm + /// through the CR 508.5 authority fails here and forces an explicit + /// decision instead of a silent behaviour swap. + #[test] + fn defending_player_controls_none_quantifies_all_defenders_cr_508_5a_gap() { + let (mut state, source) = monarch_setup(); + // Two attackers, two DIFFERENT defenders. Only P2 controls a Wall. + let attacker_a = create_object( + &mut state, + CardId(2), + PlayerId(0), + "Attacker A".to_string(), + Zone::Battlefield, + ); + let attacker_b = create_object( + &mut state, + CardId(3), + PlayerId(0), + "Attacker B".to_string(), + Zone::Battlefield, + ); + let wall = create_object( + &mut state, + CardId(4), + PlayerId(2), + "Wall".to_string(), + Zone::Battlefield, + ); + state.objects.get_mut(&wall).unwrap().card_types.core_types = vec![CoreType::Creature]; + + let mut combat = crate::game::combat::CombatState::default(); + combat.attackers = vec![ + crate::game::combat::AttackerInfo::new( + attacker_a, + crate::game::combat::AttackTarget::Player(PlayerId(1)), + PlayerId(1), + ), + crate::game::combat::AttackerInfo::new( + attacker_b, + crate::game::combat::AttackTarget::Player(PlayerId(2)), + PlayerId(2), + ), + ]; + state.combat = Some(combat); + + let condition = TriggerCondition::DefendingPlayerControlsNone { + filter: TargetFilter::Typed(TypedFilter { + type_filters: vec![TypeFilter::Creature], + controller: None, + properties: vec![], + }), + }; + + // TODAY: P1 controls no creature but P2 does, and the `all` quantifier + // over BOTH defenders makes the condition false. Under CR 508.5a the + // per-attacker defending player would be determined individually, so + // the P1 attacker's copy of this ability should see "controls none". + assert!( + !check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + Some(&attackers_declared(attacker_a, PlayerId(1))), + ), + "pins the all-defenders quantifier; if this now passes, the arm was \ + routed through combat::defending_player_cr508_5 — re-derive Siege \ + Dragon, Spectral Force, Spectral Bears and Fear of the Dark \ + explicitly rather than letting their firing behaviour change silently" + ); + + // Reach-guard: with the only creature removed, the arm reports true, so + // the assertion above is about the quantifier and not about an + // unreachable arm. + state.battlefield.retain(|id| *id != wall); + state.objects.remove(&wall); + assert!(check_trigger_condition( + &state, + &condition, + PlayerId(0), + Some(source), + Some(&attackers_declared(attacker_a, PlayerId(1))), + )); + } + /// CR 603.4 + CR 810.9a: "if you have N or more life" reads the /// controller's TEAM total in a 2HG game. Team A = 30 + 25 = 55 satisfies a /// minimum of 50, even though neither individual reaches 50. Reverting Site @@ -25905,7 +28784,7 @@ pub mod tests { #[test] fn suppress_triggers_does_not_block_transform_on_reentry() { - // CR 603.2g + CR 701.28: SuppressTriggers only gates triggered-ability + // CR 603.2g + CR 701.27: SuppressTriggers only gates triggered-ability // registration. A permanent returning to the battlefield with // `enter_transformed=true` (e.g., Ajani, Nacatl Pariah's flip trigger) // must still transform — transform is NOT a triggered ability. Any @@ -25982,6 +28861,7 @@ pub mod tests { None, true, // enter_transformed crate::types::zones::EtbTapState::Unspecified, + false, // enters_attacking None, // controller_override &[], // effect_enter_with_counters None, // face_down_profile @@ -25999,7 +28879,7 @@ pub mod tests { ); assert!( obj.transformed, - "Ajani must flip to his back face — SuppressTriggers must not block CR 701.28 transform" + "Ajani must flip to his back face — SuppressTriggers must not block CR 701.27 transform" ); assert_eq!( obj.name, "Ajani, Nacatl Avenger", @@ -26661,6 +29541,7 @@ pub mod tests { card_id: CardId(2), controller: PlayerId(0), object_id: spell, + cast_mana_value: None, }; // 2 mana spent: 2 > 3 false, 2 > 4 false — trigger does NOT fire. @@ -26762,6 +29643,7 @@ pub mod tests { card_id: CardId(2), controller: PlayerId(1), object_id: spell, + cast_mana_value: None, }; state @@ -26807,6 +29689,7 @@ pub mod tests { card_id: CardId(2), controller: PlayerId(1), object_id: spell, + cast_mana_value: None, }; state @@ -26865,6 +29748,7 @@ pub mod tests { card_id: CardId(1), controller: PlayerId(0), object_id: ObjectId(1000), + cast_mana_value: None, }; // Case A: first qualifying spell — record has exactly one X-cost cast. @@ -27113,6 +29997,7 @@ pub mod tests { card_id: CardId(1), controller: PlayerId(0), object_id: opponent_spell, + cast_mana_value: None, }], ); assert!( @@ -27137,6 +30022,7 @@ pub mod tests { card_id: CardId(2), controller: PlayerId(1), object_id: controller_spell, + cast_mana_value: None, }], ); @@ -28878,6 +31764,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(2), + cast_mana_value: None, }], ); @@ -28923,6 +31810,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(1), + cast_mana_value: None, }], ); @@ -28985,6 +31873,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(2), + cast_mana_value: None, }], ); @@ -29028,6 +31917,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(1), + cast_mana_value: None, }], ); @@ -29106,6 +31996,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(2), + cast_mana_value: None, }], ); @@ -29162,6 +32053,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(1), + cast_mana_value: None, }], ); @@ -29202,6 +32094,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(1), + cast_mana_value: None, }], ); @@ -29240,6 +32133,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(1), + cast_mana_value: None, }], ); @@ -29314,6 +32208,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(2), + cast_mana_value: None, }], ); @@ -29401,6 +32296,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(2), + cast_mana_value: None, }], ); @@ -29483,6 +32379,7 @@ pub mod tests { object_id: spell, controller: caster, card_id: CardId(1), + cast_mana_value: None, }], ); @@ -36769,6 +39666,7 @@ pub mod tests { target: TargetFilter::Typed(TypedFilter::default().with_type(TypeFilter::Creature)), enters_under: None, enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enters_attacking: false, enter_with_counters: vec![], face_down_profile: None, library_position: None, @@ -37110,6 +40008,7 @@ pub mod tests { target: TargetFilter::Typed(TypedFilter::default().with_type(TypeFilter::Creature)), enters_under: None, enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enters_attacking: false, enter_with_counters: vec![], face_down_profile: None, library_position: None, @@ -37170,6 +40069,7 @@ pub mod tests { target: TargetFilter::Typed(TypedFilter::default().with_type(TypeFilter::Creature)), enters_under: None, enter_tapped: crate::types::zones::EtbTapState::Unspecified, + enters_attacking: false, enter_with_counters: vec![], face_down_profile: None, library_position: None, @@ -40517,6 +43417,7 @@ pub mod tests { card_id: CardId(999), object_id: ObjectId(999), controller, + cast_mana_value: None, }; // Attacking: the copy trigger lands on the stack. diff --git a/crates/engine/src/types/ability.rs b/crates/engine/src/types/ability.rs index ac9648c115..3e654884de 100644 --- a/crates/engine/src/types/ability.rs +++ b/crates/engine/src/types/ability.rs @@ -548,9 +548,31 @@ pub enum ChoiceType { }, CardName, /// "Choose a number between X and Y" — generates string options "0", "1", ..., "Y". + /// CR 107.1a/b + CR 608.2d: choose a number from `min` up to `max`. + /// + /// `max: None` is the UNBOUNDED form — "choose a number 0 or greater" (Wheel + /// of Misfortune, Menacing Ogre, Itazura). The rules state no maximum, so the + /// engine must not invent one: a bounded stand-in silently makes a legal + /// choice illegal, and on Wheel the magnitude of the number IS the decision. + /// + /// The practical ceiling on an unbounded choice is `i32::MAX`, enforced at the + /// answer seam rather than here. That is not an arbitrary UI cap but the + /// engine's own arithmetic domain: every quantity resolves through `i32` + /// (`game::quantity`), and damage and life totals are `i32`, so a number the + /// engine could not represent could not be acted on either. Within that + /// domain, every value the rules permit is accepted. + /// + /// Unbounded ranges enumerate no options — `compute_options` returns empty and + /// `options_supplied_by_player` is true, the same free-entry path `CardName` + /// already uses — so the client renders a numeric input instead of a button + /// per value. NumberRange { - min: u8, - max: u8, + min: u32, + /// `None` = no maximum (CR 107.1a/b). Bounded card text ("a number + /// between 1 and 5") keeps `Some`. The serde attributes that keep the + /// bounded form byte-identical on the wire live on the `ChoiceTypeData` + /// mirror, because `ChoiceType` itself has a hand-written `Serialize`. + max: Option, /// CR 609.3: distinctness requirement, parse-detected from "that hasn't /// been chosen". Default `Repeatable` for every existing card. distinctness: NumberDistinctness, @@ -781,10 +803,80 @@ impl ChoiceType { /// predicate is true) from an impossible choice that must resolve as a /// no-op per CR 609.3 (this predicate is false). pub fn options_supplied_by_player(&self) -> bool { - matches!(self, Self::CardName | Self::Word | Self::Artist) + matches!( + self, + Self::CardName + | Self::Word + | Self::Artist + // CR 107.1a/b: an unbounded number choice cannot be enumerated, + // so the player supplies the value. Bounded ranges keep their + // option list and their button-per-value rendering. + | Self::NumberRange { max: None, .. } + ) + } + + /// CR 107.1a/b + CR 608.2d: Is `answer` a legal value for this choice when + /// the engine cannot offer an option list to check it against? + /// + /// The single authority for validating a free-entry answer, shared by the + /// interactive handler and the AI's legal-action enumeration so a value one + /// accepts cannot be rejected by the other. Returns `None` for choice kinds + /// whose answers are validated by membership instead. + /// + /// Delegates to [`ChoiceType::free_entry`] so the rule this enforces and the + /// contract published to clients are the same value, not two statements of + /// the same intent. + pub fn accepts_free_entry_answer(&self, answer: &str) -> Option { + match self.free_entry()? { + FreeEntry::Number { min, max } => { + let parsed = answer.trim().parse::(); + Some(parsed.is_ok_and(|n| n >= min && n <= max)) + } + } + } + + /// CR 107.1a/b: The free-entry contract for this choice, or `None` when the + /// answer is picked from an option list instead. + /// + /// This is the ONE definition of what a free-entry answer may be. It is what + /// [`ChoiceType::accepts_free_entry_answer`] validates against, what the AI's + /// legal-action enumeration samples within, and — published on + /// `WaitingFor::NamedChoice` — what a client renders and bounds its input by. + /// A client that reads this contract cannot reject a value the engine accepts, + /// because there is no second statement of the domain to drift from. + pub fn free_entry(&self) -> Option { + match self { + // CR 107.1a/b: an unbounded number choice cannot be enumerated, so + // the player supplies the value. Bounded ranges keep their option + // list and are validated by membership. + Self::NumberRange { min, max: None, .. } => Some(FreeEntry::Number { + min: *min, + // Not a UI cap, but the engine's own arithmetic domain: every + // quantity resolves through `i32`, so a number beyond this could + // not be dealt as damage or compared against a life total. + // Within that domain every value the rules permit is accepted. + max: i32::MAX as u32, + }), + _ => None, + } } } +/// CR 107.1a/b: A choice whose answer the player supplies rather than picks from +/// an enumerated list, together with the bounds that make an answer legal. +/// +/// Published on the prompt (`WaitingFor::NamedChoice::free_entry`) so a client +/// renders and bounds the input from engine-stated values instead of +/// re-deriving them from the choice's own shape. `Number`'s bounds are both +/// INCLUSIVE. `CardName` and the other unbounded-string choices are deliberately +/// absent: their answers are validated against the card corpus, not a range, so +/// they have no contract of this form to publish. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind")] +pub enum FreeEntry { + Number { min: u32, max: u32 }, +} + impl Serialize for ChoiceType { fn serialize(&self, serializer: S) -> Result where @@ -840,7 +932,17 @@ impl Serialize for ChoiceType { } => { // Emit `distinctness` only when non-default so existing // `{min,max}` card-data stays byte-stable. - let field_count = 2 + (*distinctness != NumberDistinctness::Repeatable) as usize; + // + // CR 107.1a/b: emit `max` only when the range HAS one. This is a + // hand-written `Serialize`, so the `skip_serializing_if` on the + // `ChoiceTypeData` deserialize mirror does not apply here and has + // to be mirrored by hand — otherwise an unbounded range writes + // `"max": null`, which round-trips correctly but needlessly + // changes the wire shape and reads as "a null bound" rather than + // "no bound". + let field_count = 1 + + max.is_some() as usize + + (*distinctness != NumberDistinctness::Repeatable) as usize; let mut variant = serializer.serialize_struct_variant( "ChoiceType", 6, @@ -848,7 +950,9 @@ impl Serialize for ChoiceType { field_count, )?; variant.serialize_field("min", min)?; - variant.serialize_field("max", max)?; + if let Some(max) = max { + variant.serialize_field("max", max)?; + } if *distinctness != NumberDistinctness::Repeatable { variant.serialize_field("distinctness", distinctness)?; } @@ -977,8 +1081,13 @@ impl<'de> Deserialize<'de> for ChoiceType { excluded: Vec, }, NumberRange { - min: u8, - max: u8, + min: u32, + /// CR 107.1a/b: absent = no maximum. A bounded range keeps + /// emitting `"max": N` exactly as before, so existing card-data + /// round-trips byte-identically; only the unbounded form omits + /// the key. + #[serde(default, skip_serializing_if = "Option::is_none")] + max: Option, #[serde(default)] distinctness: NumberDistinctness, }, @@ -1380,7 +1489,22 @@ pub enum ChosenAttribute { OddOrEven(Parity), CardName(String), /// Stores a chosen number (e.g., "choose a number" for Talion). - Number(u8), + /// + /// On the PLAYER axis (`Player::chosen_attributes`) this is the SECRET half + /// of the CR 101.4 secret-number ledger: `game::visibility` redacts it from + /// every viewer but its owner. `Effect::RevealChosenNumbers` converts it to + /// [`ChosenAttribute::RevealedNumber`], which is public — that conversion is + /// the card's "reveal" instruction as an observable state transition. + Number(u32), + /// CR 101.4 + CR 608.2c: A chosen number that a reveal instruction has + /// PUBLISHED ("then all players reveal those numbers simultaneously"). + /// Identical in value to [`ChosenAttribute::Number`] and read + /// interchangeably with it by `Player::chosen_number`; the two differ only + /// in visibility, which is exactly what the reveal changes. Kept as a + /// distinct variant rather than a flag so the secret and published states + /// cannot be confused at a read site, and so `game::visibility` redacts on + /// the type rather than on a condition it might forget to check. + RevealedNumber(u32), /// Stores the chosen opponent/player ID (CR 800.4a). Player(PlayerId), /// Stores two chosen colors as a pair. @@ -1464,9 +1588,14 @@ impl ChosenAttribute { Self::CardType(_) => ChoiceType::card_type(), Self::OddOrEven(_) => ChoiceType::OddOrEven, Self::CardName(_) => ChoiceType::CardName, - Self::Number(_) => ChoiceType::NumberRange { + // CR 101.4: the secret and the published number came from the same + // `NumberRange` prompt; revealing changes visibility, not category. + // CR 107.1a/b: recovering the CATEGORY from a stored value cannot + // recover the card's original bounds, so report the widest form the + // rules allow rather than inventing a ceiling this value never had. + Self::Number(_) | Self::RevealedNumber(_) => ChoiceType::NumberRange { min: 0, - max: 20, + max: None, distinctness: NumberDistinctness::Repeatable, }, // Player covers both Player and Opponent choice types @@ -1567,7 +1696,7 @@ pub enum ChoiceValue { CardType(CoreType), OddOrEven(Parity), CardName(String), - Number(u8), + Number(u32), Label(String), CardPredicate(CardPredicateChoice), LandType(String), @@ -1601,7 +1730,7 @@ impl ChoiceValue { } ChoiceType::OddOrEven => value.parse::().ok().map(Self::OddOrEven), ChoiceType::CardName => Some(Self::CardName(value.to_string())), - ChoiceType::NumberRange { .. } => value.parse::().ok().map(Self::Number), + ChoiceType::NumberRange { .. } => value.parse::().ok().map(Self::Number), ChoiceType::Labeled { .. } => Some(Self::Label(value.to_string())), ChoiceType::CardPredicate { options } | ChoiceType::CardPredicateGuess { options } => { let predicate = CardPredicateChoice::from_label(value)?; @@ -1717,6 +1846,24 @@ pub enum ZoneRef { Hand, } +impl ZoneRef { + /// CR 400.1: The game zone this reference denotes. + /// + /// The single authority for the `ZoneRef` → [`Zone`](crate::types::zones::Zone) + /// mapping. Exhaustive by construction: a new `ZoneRef` variant fails to + /// compile here rather than silently reading as "some other zone" at a call + /// site that pattern-matched only the four it knew about. + pub fn zone(&self) -> crate::types::zones::Zone { + use crate::types::zones::Zone; + match self { + ZoneRef::Graveyard => Zone::Graveyard, + ZoneRef::Exile => Zone::Exile, + ZoneRef::Library => Zone::Library, + ZoneRef::Hand => Zone::Hand, + } + } +} + /// CR 701.10d-f: What aspect to double (counters, life total, or mana pool). /// Used by `Effect::Double` per locked decision D-05. /// DoublePT/DoublePTAll handle CR 701.10a-c (power/toughness) separately. @@ -2157,7 +2304,10 @@ pub enum ManaProduction { /// not colors (CR 105.1), so each of W/U/B/R/G contributes at most once. /// Used by Faeburrow Elder's "{T}: For each color among permanents you /// control, add one mana of that color." Mirrors the structure of - /// `QuantityRef::DistinctColorsAmongPermanents`. + /// [`QuantityRef::DistinctColorsAmong`], which is a DIFFERENT enum and + /// which is parameterized on [`CardTypeSetSource`] because a colour COUNT + /// can read a union or a non-object population (First Family). This mana + /// variant is deliberately NOT parameterized: no mana ability reads either. DistinctColorsAmongPermanents { filter: TargetFilter }, /// CR 106.1 + CR 109.1: Produce N mana of one chosen color from the distinct /// colors present among permanents matching `filter`. Mox Amber class: @@ -4052,6 +4202,28 @@ pub enum ControllerRef { /// player (Siren's Call, Maddening Imp), cast/activated only during an /// opponent's turn. ActivePlayer, + /// CR 109.4 + CR 611.2c: a player id SNAPSHOTTED at resolution — the lowered + /// form the dynamic siblings above collapse to once the resolving ability is + /// gone. Never produced by the parser; produced only by resolvers that + /// install a durable continuous effect whose *object set* must stay dynamic + /// while its *player reference* must not. + /// + /// Gideon Jura's "+2: During target opponent's next turn, creatures that + /// player controls attack Gideon Jura if able" is the canonical member. Per + /// CR 611.2c the requirement modifies no characteristics and changes no + /// controller, so the affected creature set is re-derived every + /// declare-attackers step (official ruling: the ability "doesn't lock in what + /// it applies to … includes creatures that come under that player's control + /// after the ability has resolved"). The *player*, by contrast, is fixed when + /// the ability resolves — and `ControllerRef::TargetPlayer` resolves by + /// reading `ability.targets`, which no longer exists at layer-evaluation + /// time, so `force_attack::resolve` lowers it to this arm on install. + /// + /// Mirrors the identical lower-at-resolution contract already documented on + /// [`RestrictionPlayerScope::SpecificPlayer`] and [`TargetFilter::SpecificPlayer`]. + SpecificPlayer { + id: PlayerId, + }, } /// CR 301 / CR 303: Kinds of attachments to permanents. @@ -5145,6 +5317,16 @@ pub enum TargetFilter { Any, Player, Controller, + /// CR 608.2h + CR 113.7a: The controller of this ability's source object. + /// + /// Unlike [`Self::Controller`], which is the controller of the resolving + /// ability (and therefore the activator for an activated ability), this + /// follows the source's exact incarnation. Triggered abilities use their + /// [`TriggerSourceContext`] live-or-LKI authority; other stack abilities + /// use their captured `source_incarnation` and the incarnation-keyed LKI + /// history. This keeps "~'s controller" from rebinding to a later object + /// that reuses the same storage id. + SourceController, /// CR 615 + CR 614.1a: Compound damage recipient "you and [type] permanents /// you control" (Comeuppance's "you and planeswalkers you control"; Channel /// Harm's "you and permanents you control"). A PARSE-LAYER recipient @@ -5667,6 +5849,67 @@ pub enum PlayerScope { /// `Duration::UntilNextStepOf` — never from a value/quantity/player-selection /// position. AnyTurn, + /// CR 109.4 + CR 611.2 + CR 514.2: a player id SNAPSHOTTED at resolution — + /// the player-scalar-axis analogue of [`ControllerRef::SpecificPlayer`] and + /// [`RestrictionPlayerScope::SpecificPlayer`], and the lowered form the + /// dynamic siblings above collapse to once the resolving ability is gone. + /// + /// DURATION-TIMING-ONLY, like [`AnyTurn`](Self::AnyTurn): never produced by + /// the parser and never read from a value/quantity/player-selection + /// position. It is constructed solely by resolvers that install a durational + /// continuous effect whose expiry keys on a player OTHER than the effect's + /// controller. + /// + /// Gideon Jura's "+2: During target opponent's next turn, …" is the + /// canonical member: the parser emits + /// `UntilEndOfNextTurnOf { player: PlayerScope::Target }`, and + /// `force_attack::resolve` lowers `Target` to this arm. Without the + /// lowering, the prune in `layers.rs::prune_until_next_turn_effects` — which + /// arms `UntilEndOfNextTurnOf` by comparing the ACTIVE player against the + /// effect's own `controller` — could never see the targeted opponent, and + /// the requirement would never arm nor expire. Overloading the effect's + /// `controller` field with the target instead would break CR 109.5's meaning + /// of "you" for every other consumer of that field. + SpecificPlayer { id: PlayerId }, +} + +/// CR 109.5: SINGLE serde default for every [`PlayerScope`] subject axis — a +/// clause with no printed subject means "you", the ability's controller. Keeps +/// pre-field rows (`{"type":"IsMonarch"}`, `GrantNextSpellAbility` without +/// `player`) deserializing unchanged. +/// +/// A named function rather than `#[serde(default)]` because [`PlayerScope`] +/// deliberately has no `Default` impl: most of its variants are only meaningful +/// relative to a context, so a blanket default would be wrong everywhere else. +fn player_scope_controller() -> PlayerScope { + PlayerScope::Controller +} + +/// Skip-serialization predicate paired with [`player_scope_controller`]: omit +/// `player` from JSON when it is the printed default, so existing card-data +/// rows stay byte-identical. +fn is_player_scope_controller(player: &PlayerScope) -> bool { + matches!(player, PlayerScope::Controller) +} + +impl PlayerScope { + /// CR 611.2a + CR 514.2: [`PlayerScope::AnyTurn`] and + /// [`PlayerScope::SpecificPlayer`] exist ONLY to key a `Duration`'s expiry. + /// They are never produced by the parser and carry no value / quantity / + /// player-selection reading, which is why + /// `quantity::resolve_single_player_scope` marks them `unreachable!()`. + /// + /// Any resolver reachable from DESERIALIZED data must reject them here + /// rather than reaching that `unreachable!()`: `IsMonarch { player }` is + /// serde-constructible from `card-data.json` and from mtgish input, so a + /// malformed or hand-authored row must fail closed, not panic the engine + /// inside a trigger-condition check. + pub(crate) fn duration_timing_only(&self) -> bool { + matches!( + self, + PlayerScope::AnyTurn | PlayerScope::SpecificPlayer { .. } + ) + } } /// Scope selector for object-axis quantities (Round Π-5). Picks WHICH object @@ -5787,7 +6030,36 @@ pub enum ObjectScope { BatchSource, } -/// Source set for counting distinct card types. +/// CR 601.2a: A per-turn action journal — a chronological record of a kind of +/// action taken this turn, cleared at the turn boundary. +/// +/// The parameterization axis for [`CardTypeSetSource::TurnJournal`]. Introduced +/// already parameterized rather than as a bare `SpellsCastThisTurn` leaf so the +/// journal axis cannot grow an X / X′ sibling cluster on +/// `CardTypeSetSource` itself. +/// +/// NEXT MEMBER, ALREADY IDENTIFIED: `PermanentsSacrificed` (Korvold, Gleeful +/// Glutton — "for each card type among permanents you've sacrificed this turn"). +/// BLOCKER: `GameState` has no sacrifice journal. Verified absent — the only +/// per-turn journals today are `spells_cast_this_turn_by_player` and its +/// game-scoped mirror. Adding Korvold costs one variant here plus its state and +/// write site; it costs NOTHING in `CardTypeSetSource`, whose shape absorbs it +/// unchanged. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum TurnJournalKind { + /// CR 601.2a + CR 112.1: spells cast this turn, as recorded in + /// `GameState::spells_cast_this_turn_by_player` at `finalize_cast`. + SpellsCast, +} + +/// Source set (population) whose members' characteristics are counted. +/// +/// CR 109.2 + CR 400.1 + CR 601.2a: the population axis shared by every +/// distinct-characteristic count — card types (CR 205.2), subtypes (CR 205.3), +/// and colors (CR 105.1). The CHARACTERISTIC axis stays partitioned by CR +/// section in `QuantityRef`; this axis names only "the set whose members are +/// read". #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum CardTypeSetSource { @@ -5806,8 +6078,253 @@ pub enum CardTypeSetSource { #[serde(default, skip_serializing_if = "Option::is_none")] caused_by: Option, }, + /// CR 601.2a + CR 112.1: The members of a per-turn action journal for the + /// scoped players ("spells you've cast this turn"). + /// + /// A resolved spell is no longer an object (CR 400.7), so characteristics + /// come from the snapshot captured when the action was journaled — not from + /// a live object scan. `scope` / `filter` mirror + /// `QuantityRef::SpellsCastThisTurn` so the two name the same population; + /// `filter: None` admits every member. + TurnJournal { + journal: TurnJournalKind, + scope: CountScope, + #[serde(default, skip_serializing_if = "Option::is_none")] + filter: Option, + }, + /// CR 109.2: The union of two or more populations — "among \ and \" / + /// "among \ and/or \". + /// + /// Set union, not arithmetic sum: a member appearing in both contributes its + /// characteristics once (First Family). Contrast + /// `parse_greatest_among_conjunction`, which is allowed to decompose the same + /// surface form into `Max{[Aggregate, Aggregate]}` ONLY because max + /// distributes over union and a distinct-count does not. Named `AnyOf` to + /// match `FilterProp::AnyOf` / `TypeFilter::AnyOf` (set-union-of-alternatives), + /// not `Or` (reserved for boolean condition enums). + /// + /// INVARIANT: at least two members, carried by [`UnionSources`] rather than + /// asserted. A 0- or 1-member union is not a union, and an empty one is + /// actively unsound: `characteristic_source_read`'s fold would return + /// `RwProfile::empty()`, which is FAIL-OPEN for the CR 603.3b ordering gate, + /// and the resolver would return 0 with no diagnostic. + AnyOf { sources: UnionSources }, +} + +/// CR 109.2: the members of a [`CardTypeSetSource::AnyOf`], carrying the +/// at-least-two invariant IN THE TYPE. +/// +/// The `Vec` is private, so the only ways in are [`UnionSources::new`] and +/// `Deserialize` — both of which reject a 0- or 1-member list. This replaces a +/// public `Vec` field guarded by a `debug_assert!`: that assertion compiles out +/// of release builds, and the field let any caller in the crate write +/// `AnyOf { sources: vec![] }` directly, bypassing both the constructor and the +/// serde check. Several already did. An invariant that arbitrary callers can +/// step around is documentation, not an invariant. +/// +/// Derefs to `[CardTypeSetSource]`, so every existing `sources.iter()` / +/// `sources.len()` read is unchanged — the type is a construction gate, not a +/// new collection API. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[serde(transparent)] +pub struct UnionSources(Vec); + +impl UnionSources { + /// The ONLY in-crate constructor. `None` when the arity invariant fails, so + /// a caller cannot get a degenerate union by ignoring an error. + /// + /// Callers that want the collapse-to-single behavior want + /// [`CardTypeSetSource::any_of`], which is written on top of this. + pub fn new(sources: Vec) -> Option { + (sources.len() >= 2).then_some(Self(sources)) + } + + pub fn into_vec(self) -> Vec { + self.0 + } +} + +impl std::ops::Deref for UnionSources { + type Target = [CardTypeSetSource]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> IntoIterator for &'a UnionSources { + type Item = &'a CardTypeSetSource; + type IntoIter = std::slice::Iter<'a, CardTypeSetSource>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +/// CR 109.2: Enforce the arity invariant on load, so a saved game or +/// hand-authored payload cannot smuggle a degenerate union past the constructor. +impl<'de> Deserialize<'de> for UnionSources { + fn deserialize>(deserializer: D) -> Result { + let sources = Vec::::deserialize(deserializer)?; + let len = sources.len(); + UnionSources::new(sources).ok_or_else(|| { + serde::de::Error::custom(format!( + "CardTypeSetSource::AnyOf requires at least 2 sources, got {len}" + )) + }) + } +} + +impl CardTypeSetSource { + /// CR 109.2: Arity-checked [`CardTypeSetSource::AnyOf`] constructor — the + /// only way a union is built. + /// + /// A single member collapses to itself rather than forming a degenerate + /// union; an empty list has no population and yields `None`. + pub fn any_of(mut sources: Vec) -> Option { + match sources.len() { + 0 => None, + 1 => sources.pop(), + _ => UnionSources::new(sources).map(|sources| CardTypeSetSource::AnyOf { sources }), + } + } + + /// CR 400.1 + CR 613.4a: Every zone this population reads. + /// + /// THE single authority for the population-zone axis. Both consumers ask + /// this and nothing else: + /// + /// * **Evaluation** — `game::quantity::visit_characteristic_source` walks + /// exactly these zones to enumerate members. + /// * **Dependency tracking** — [`reads_zone`](Self::reads_zone), which + /// `game::layers::characteristic_source_reads_zone` delegates to, dirties + /// a dependent characteristic when an object crosses one of them. + /// + /// They MUST agree. When they did not, a layer or CDA could retain a stale + /// distinct-characteristic value across a zone transition: the evaluator + /// scanned exile for a craft population (`And[ExiledBySource, …]`, Sunbird + /// Effigy) while the classifier reported that same population as reading no + /// zone at all, so nothing ever re-evaluated it. Splitting the two answers + /// across two functions is what made that divergence possible; one function + /// with two callers is what prevents it. + /// + /// EMPTY means "no zone is explicitly named", which each consumer resolves + /// per [`TargetFilter::population_zones`]: the walk substitutes the + /// battlefield default (CR 110.1), the dependency check does not. Two + /// populations are empty as a POSITIVE claim rather than a fallback, and for + /// them the walk substitutes nothing because it never scans a zone at all: + /// + /// * `TurnJournal` — characteristics come from the snapshot taken when the + /// action was journaled, because a resolved spell is no longer an object + /// (CR 400.7). Nothing re-reads a zone, so no transition can stale it. + /// * `TrackedSet` — membership is by object id and is fixed when the set is + /// published (CR 608.2i — an effect may look back at a previous action's + /// objects, which need not still be where they were); a member moving + /// zones changes neither the set + /// nor the card types its members have (CR 205.2a). + pub fn population_zones(&self) -> Vec { + self.population_zones_checked().0 + } + + /// [`population_zones`](Self::population_zones) plus whether the union walk + /// completed within its depth budget. Split out because the two consumers + /// need OPPOSITE things from a truncated walk: the evaluation walk can only + /// scan what it found, while [`reads_zone`](Self::reads_zone) must answer + /// `true` rather than miss an invalidation. + fn population_zones_checked(&self) -> (Vec, bool) { + let mut out: Vec = Vec::new(); + let complete = self.try_for_each_member(UNION_DEPTH_BUDGET, &mut |leaf| { + for zone in leaf.leaf_population_zones() { + if !out.contains(&zone) { + out.push(zone); + } + } + }); + (out, complete) + } + + /// The zones ONE non-union population reads. `AnyOf` is handled by + /// [`try_for_each_member`](Self::try_for_each_member), which is the only + /// caller and never hands a union here. + fn leaf_population_zones(&self) -> Vec { + match self { + CardTypeSetSource::Zone { zone, .. } => vec![zone.zone()], + // CR 607.2a + CR 406.6: a linked-exile pool lives in exile. + CardTypeSetSource::ExiledBySource => vec![crate::types::zones::Zone::Exile], + CardTypeSetSource::Objects { filter } => filter.population_zones(), + // Not zone reads — see the doc comment on `population_zones`. + CardTypeSetSource::TrackedSet { .. } | CardTypeSetSource::TurnJournal { .. } => { + Vec::new() + } + // Unreachable through the walker; empty rather than a panic so a + // future direct caller degrades instead of crashing a game. + CardTypeSetSource::AnyOf { .. } => Vec::new(), + } + } + + /// CR 613.4a: Does this population read `zone`? + /// + /// The dependency-tracking half of [`population_zones`](Self::population_zones), + /// kept as one call so a caller cannot accidentally ask a narrower question. + /// + /// A truncated union walk answers `true`: an over-report costs one redundant + /// layer recompute, an under-report strands a stale characteristic, and only + /// one of those is a correctness bug. + pub fn reads_zone(&self, zone: crate::types::zones::Zone) -> bool { + let (zones, complete) = self.population_zones_checked(); + !complete || zones.contains(&zone) + } + + /// CR 109.2: Visit every NON-union member of this population, depth-bounded. + /// + /// THE single bounded walker for the `AnyOf` axis. Every consumer that + /// recurses a `CardTypeSetSource` routes through this instead of writing its + /// own `AnyOf` arm, so the recursion is written once and bounded once. + /// + /// `AnyOf` nests without limit — its arity invariant bounds WIDTH, not + /// DEPTH — and a persisted or hand-authored payload can carry whatever + /// nesting it likes. Every consumer recursing independently meant every + /// consumer was a separate unbounded traversal. + /// + /// Returns `false` when the budget is exhausted before the walk completed, + /// and callers MUST treat that as "I did not see everything" and answer + /// conservatively — the same fail-safe contract + /// `target_filter_characteristic_reads_at` uses when it returns + /// `CharacteristicKinds::ALL`. The visitor still runs for everything reached + /// within budget, so a `false` return means incomplete, not empty. + /// + /// [`UNION_DEPTH_BUDGET`] is the depth every in-engine caller passes. + pub fn try_for_each_member( + &self, + depth: u32, + visit: &mut impl FnMut(&CardTypeSetSource), + ) -> bool { + let Some(depth) = depth.checked_sub(1) else { + return false; + }; + match self { + CardTypeSetSource::AnyOf { sources } => { + let mut complete = true; + for member in sources { + // Not short-circuited: a truncated branch must not stop the + // siblings a caller can still legitimately see. + complete &= member.try_for_each_member(depth, visit); + } + complete + } + leaf => { + visit(leaf); + true + } + } + } } +/// CR 109.2: Depth budget for [`CardTypeSetSource::try_for_each_member`]. +/// +/// Sized so no real card comes close — printed unions are two or three members +/// deep — while still bounding a hostile or corrupt payload. Mirrors the filter +/// walkers' budgets rather than inventing a second convention. +pub const UNION_DEPTH_BUDGET: u32 = 64; + /// CR 205.3: Which subtypes are excluded when counting distinct subtypes. /// /// A typed qualifier (not a `bool`) so the exclusion axis stays composable and @@ -6457,6 +6974,32 @@ pub enum QuantityRef { /// A number chosen as the source entered the battlefield (e.g., Talion, the Kindly Lord). /// Resolved from the source object's `ChosenAttribute::Number`. ChosenNumber, + /// CR 101.4 + CR 608.2d: The number a PLAYER chose during this resolution + /// ("each player secretly chooses a number 0 or greater"), read off + /// `Player::chosen_attributes` (`ChosenAttribute::Number`) — the player-axis + /// sibling of the object-axis [`QuantityRef::ChosenNumber`], which reads the + /// SOURCE object's persisted number instead. The two subjects have different + /// runtime resolvers (per-player scalar vs. source LKI), so they stay + /// separate variants rather than one subject-parameterized reference. + /// + /// A member of the per-player-scalar subset (`HandSize` / `LifeTotal` / + /// `GraveyardSize` / `PlayerCounter` / …), so `player` selects both WHICH + /// player is read and — for the aggregate scopes — HOW the per-player values + /// are folded: + /// - `AllPlayers { aggregate: Max }` — "the highest number" (Wheel of + /// Misfortune, Menacing Ogre, Life at Stake). + /// - `AllPlayers { aggregate: Min }` — "the lowest number" (Wheel of + /// Misfortune's discard clause). + /// - `ScopedPlayer` — the per-candidate read used by + /// [`PlayerFilter::PlayerAttribute`] to select "each player who chose the + /// highest number". + /// + /// Players who chose no number this resolution are EXCLUDED from the + /// aggregate populations (rather than contributing 0), so a card whose + /// choosers are a subset of the table — Life at Stake's "you and target + /// creature's controller" — still reads the extremum over the actual + /// choosers. + PlayerChosenNumber { player: PlayerScope }, /// CR 508.1a: Number of creatures that attacked this turn, scoped by /// `scope` and optionally narrowed by `filter` (e.g. "attacked with a /// token / a commander / a Wolf"). `Controller` + `filter: None` counts all @@ -6599,21 +7142,56 @@ pub enum QuantityRef { /// or in the command zone" pattern. The resolver selects the first matching commander /// (any one if multiple exist) and returns its mana value. CommanderManaValue { owner: ControllerRef }, - /// CR 106.1 + CR 109.1: Number of distinct colors among permanents matching - /// a filter. "Gold", "multicolor", and "colorless" are not colors (CR 105.1), - /// so each of W/U/B/R/G is counted at most once. Used by Faeburrow Elder's - /// "+1/+1 for each color among permanents you control" CDA and its companion - /// mana ability. Composes with `ObjectCount`-style filter predicates and is - /// the dual to `ManaProduction::DistinctColorsAmongPermanents`. - DistinctColorsAmongPermanents { filter: TargetFilter }, + /// CR 105.1 + CR 105.2: Number of distinct colors among the members of a + /// [`CardTypeSetSource`] population. + /// + /// There are exactly five colors (CR 105.1) and an object can be one or more + /// of them or none at all (CR 105.2) — "gold", "multicolor", and "colorless" + /// are not colors — so each of W/U/B/R/G is counted at most once and a + /// colorless member contributes nothing. Parameterized on the shared + /// population axis (rather than carrying a bare `TargetFilter`) so a union or + /// a non-object population is expressible: First Family's "the number of + /// colors among permanents you control **and spells you've cast this turn**" + /// is a set union over a live census and a cast journal, and `|A ∪ B|` is not + /// `|A| + |B|`. Faeburrow Elder's "+1/+1 for each color among permanents you + /// control" CDA is the single-source reading (`Objects { filter }`). + /// + /// Dual to `ManaProduction::DistinctColorsAmongPermanents`, which is a + /// DIFFERENT enum that happens to share the old variant name and is + /// deliberately left un-parameterized (no mana ability reads a union). + /// + /// SAVED-GAME MIGRATION: this variant was `DistinctColorsAmongPermanents + /// { filter: TargetFilter }`. Those nodes live in PERSISTED GAME STATE, not + /// only in regenerated card data — a battlefield token's continuous + /// modification, a mid-resolution stack object, an in-flight reconnect + /// payload, or an out-of-repo community scenario can all carry one. Both the + /// legacy tag (`#[serde(alias)]` on the variant, same precedent as + /// [`QuantityRef::ObjectCountDistinct`]) and the legacy payload key + /// (`#[serde(alias = "filter")]` + [`deserialize_distinct_colors_population`], + /// which lifts it to `Objects { filter }`) are accepted on load, so an old + /// snapshot rehydrates instead of failing with unknown-variant / missing-field. + /// Serialization is unmigrated-only: output always uses the new tag and key. + #[serde(alias = "DistinctColorsAmongPermanents")] + DistinctColorsAmong { + #[serde( + alias = "filter", + deserialize_with = "deserialize_distinct_colors_population" + )] + source: CardTypeSetSource, + }, /// CR 122.1: distinct counter kinds among filter-matched permanents /// (controller-relative, CR 109.4). Counter-side dual of - /// `DistinctColorsAmongPermanents` — counts each distinct `CounterType` + /// [`QuantityRef::DistinctColorsAmong`] — counts each distinct `CounterType` /// appearing on at least one permanent matching `filter` exactly once. /// Used by Bribe Taker's "for each kind of counter on permanents you /// control" iteration source. Kept a separate variant from the color - /// dual because counters (CR 122.1) and colors (CR 105/106) are distinct + /// dual because counters (CR 122.1) and colors (CR 105) are distinct /// rule sections the engine resolves independently. + /// + /// ASYMMETRY, deliberate: unlike its colour dual this variant still carries a + /// bare `TargetFilter` rather than a `CardTypeSetSource`. No card demands a + /// non-object counter population, so folding it onto the shared axis is + /// deferred rather than speculative. DistinctCounterKindsAmong { filter: TargetFilter }, /// CR 701.38 + CR 608.2c: Number of votes tallied for this choice index, /// summed from `state.last_vote_ballots`. Counts votes, not voters — a @@ -6624,6 +7202,113 @@ pub enum QuantityRef { VoteCount { choice_index: u32 }, } +impl QuantityRef { + /// CR 109.4: mutable access to this reference's single player-relativity + /// axis, when it has one. + /// + /// The mutable sibling of the read-only per-axis classifiers + /// (`ability_scan::scan_player_scope`, `ability_rw::rw_player_scope`), and + /// exhaustive for the same reason `ability_scan::scan_quantity_ref` is: a + /// future reference that carries a [`PlayerScope`] must be a COMPILE ERROR + /// here rather than silently escape an anaphor rebind. Object-axis + /// references and player-axis references that resolve through an + /// `AggregateFunction` population rather than a single scope return `None`. + pub(crate) fn player_scope_mut(&mut self) -> Option<&mut PlayerScope> { + match self { + QuantityRef::HandSize { player } + | QuantityRef::LifeTotal { player } + | QuantityRef::GraveyardSize { player } + | QuantityRef::LifeLostThisTurn { player } + | QuantityRef::PartySize { player } + | QuantityRef::Speed { player } + | QuantityRef::SacrificedThisTurn { player, .. } + | QuantityRef::LifeGainedThisTurn { player } + | QuantityRef::CardsDrawnThisTurn { player } + | QuantityRef::BattlefieldEntriesThisTurn { player, .. } + | QuantityRef::LandsPlayedThisTurn { player, .. } + | QuantityRef::PlayerChosenNumber { player } + | QuantityRef::LoyaltyAbilitiesActivatedThisTurn { player } + | QuantityRef::CardsDiscardedThisTurn { player } + | QuantityRef::TokensCreatedThisTurn { player, .. } + | QuantityRef::PlayerActionsThisTurn { player, .. } => Some(player), + QuantityRef::LifeAboveStarting + | QuantityRef::StartingLifeTotal + | QuantityRef::TriggeringDiscoverValue + | QuantityRef::TriggeringScryLookCount + | QuantityRef::TriggeringScryBottomCount + | QuantityRef::ObjectCount { .. } + | QuantityRef::ObjectCountDistinct { .. } + | QuantityRef::ObjectCountBySharedQuality { .. } + | QuantityRef::PlayerCount { .. } + | QuantityRef::CountersOn { .. } + | QuantityRef::CountersOnObjects { .. } + | QuantityRef::PlayerCounter { .. } + | QuantityRef::TargetControllerCounter { .. } + | QuantityRef::Variable { .. } + | QuantityRef::Power { .. } + | QuantityRef::Intensity { .. } + | QuantityRef::Toughness { .. } + | QuantityRef::ObjectManaValue { .. } + | QuantityRef::TargetObjectManaValue { .. } + | QuantityRef::ObjectColorCount { .. } + | QuantityRef::ObjectNameWordCount { .. } + | QuantityRef::ObjectTypelineComponentCount { .. } + | QuantityRef::ManaSymbolsInManaCost { .. } + | QuantityRef::SelfManaValue + | QuantityRef::Aggregate { .. } + | QuantityRef::ControlledByEachPlayer { .. } + | QuantityRef::TargetZoneCardCount { .. } + | QuantityRef::Devotion { .. } + | QuantityRef::DistinctCardTypes { .. } + | QuantityRef::DistinctSubtypes { .. } + | QuantityRef::CardsExiledBySource + | QuantityRef::ExiledCardPower { .. } + | QuantityRef::ZoneCardCount { .. } + | QuantityRef::BasicLandTypeCount { .. } + | QuantityRef::TrackedSetSize + | QuantityRef::FilteredTrackedSetSize { .. } + | QuantityRef::TrackedSetAggregate { .. } + | QuantityRef::ExiledFromHandThisResolution + | QuantityRef::PreviousEffectAmount { .. } + | QuantityRef::UnspentMana { .. } + | QuantityRef::EventContextAmount + | QuantityRef::EventContextPlayerCount { .. } + | QuantityRef::AttachmentsOnLeavingObject { .. } + | QuantityRef::EventContextSourceCostX + | QuantityRef::EventContextSourceModesChosen + | QuantityRef::SpellsCastThisTurn { .. } + | QuantityRef::SpellsCastBeforeTriggeringSpell { .. } + | QuantityRef::EnteredThisTurn { .. } + | QuantityRef::CrimesCommittedThisTurn + | QuantityRef::BendTypesThisTurn + | QuantityRef::TurnsTaken + | QuantityRef::ZoneChangeCountThisTurn { .. } + | QuantityRef::ZoneChangeAggregateThisTurn { .. } + | QuantityRef::DamageDealtThisTurn { .. } + | QuantityRef::ChosenNumber + | QuantityRef::AttackedThisTurn { .. } + | QuantityRef::DescendedThisTurn + | QuantityRef::SpellsCastLastTurn + | QuantityRef::SpellsCastThisGame { .. } + | QuantityRef::CounterAddedThisTurn { .. } + | QuantityRef::DungeonsCompleted + | QuantityRef::CostXPaid + | QuantityRef::KickerCount + | QuantityRef::AdditionalCostPaymentCount + | QuantityRef::AdditionalCostPaymentCountFor { .. } + | QuantityRef::ConvokedCreatureCount + | QuantityRef::TimesCostPaidThisResolution + | QuantityRef::ManaSpentToCast { .. } + | QuantityRef::ColorsInCommandersColorIdentity + | QuantityRef::CommanderCastFromCommandZoneCount + | QuantityRef::CommanderManaValue { .. } + | QuantityRef::DistinctColorsAmong { .. } + | QuantityRef::DistinctCounterKindsAmong { .. } + | QuantityRef::VoteCount { .. } => None, + } + } +} + /// CR 107.1a: Rounding direction for fractional Oracle-text expressions. /// Every "half X" phrase in Oracle text specifies whether to round up or /// down; this enum records that choice verbatim so resolution is deterministic. @@ -7513,22 +8198,25 @@ impl QuantityExpr { } } - /// CR 608.2c: Rebind a later clause's generic event-context amount to the - /// scalar result of the immediately preceding resolved instruction. + /// CR 608.2c: Rebind a later clause's generic event-context amount ("that + /// much", "that many") to the `antecedent` the surrounding grammar names. /// - /// Parser chain assembly uses this only when grammar proves that the - /// antecedent is the prior effect, rather than a triggering event or a - /// per-player iteration. The recursive walk preserves arithmetic wrappers - /// such as "twice that much". - pub fn rebind_event_context_amount_to_previous_effect(&mut self) { + /// `EventContextAmount` is the *unbound* demonstrative: it means "the amount + /// from the surrounding event context", which is correct only when a + /// triggering event or a per-player iteration supplies one. When chain + /// assembly can PROVE a different antecedent from the printed grammar — the + /// preceding instruction's scalar result, or a number a preceding clause had + /// a player choose — it rebinds the leaf here. The antecedent is a parameter + /// rather than one method per referent, so every provable binding shares one + /// recursive walk (which preserves arithmetic wrappers such as "twice that + /// much"); callers must not use it for merely plausible antecedents. + pub fn rebind_event_context_amount(&mut self, antecedent: &QuantityRef) { match self { QuantityExpr::Ref { qty: QuantityRef::EventContextAmount, } => { *self = QuantityExpr::Ref { - qty: QuantityRef::PreviousEffectAmount { - channel: DamageChannel::Total, - }, + qty: antecedent.clone(), }; } QuantityExpr::Offset { inner, .. } @@ -7538,15 +8226,15 @@ impl QuantityExpr { | QuantityExpr::UpTo { max: inner } | QuantityExpr::Power { exponent: inner, .. - } => inner.rebind_event_context_amount_to_previous_effect(), + } => inner.rebind_event_context_amount(antecedent), QuantityExpr::Sum { exprs } | QuantityExpr::Max { exprs } => { for expr in exprs { - expr.rebind_event_context_amount_to_previous_effect(); + expr.rebind_event_context_amount(antecedent); } } QuantityExpr::Difference { left, right } => { - left.rebind_event_context_amount_to_previous_effect(); - right.rebind_event_context_amount_to_previous_effect(); + left.rebind_event_context_amount(antecedent); + right.rebind_event_context_amount(antecedent); } QuantityExpr::Fixed { .. } | QuantityExpr::Ref { .. } => {} } @@ -7843,8 +8531,38 @@ pub enum StaticCondition { /// Once a creature is blocked, it remains blocked for the rest of combat even /// if all its blockers leave — mirrors `AttackerInfo.blocked` (sticky flag). SourceIsBlocked, - /// CR 725.1: True when the controller is the monarch. - IsMonarch, + /// CR 725.1: monarch IDENTITY — true when `player` currently holds the + /// monarch designation (CR 725.3: exactly one player at a time; CR 725.4 + /// governs reassignment). Distinct from [`NoMonarch`](Self::NoMonarch), + /// true only when the designation is VACANT, and from `Not(IsMonarch)`, + /// which is also true when vacant. + /// + /// CR 725.5: on the STATIC side, a continuous effect whose result depends + /// on who is currently the monarch does nothing while there is no monarch, + /// and begins to apply once a player becomes one. The + /// `layers::evaluate_condition_with_context` arm and its entry gate + /// implement exactly that. + /// + /// `player` is the CR 109.5 subject axis, parameterized within CR 725 + /// rather than proliferated into `ThatPlayerIsMonarch` siblings: + /// - [`PlayerScope::Controller`] ← "you're the monarch" (printed default) + /// - [`PlayerScope::ScopedPlayer`] ← "that player is the monarch" (an + /// anaphor to the player named by the triggering event) + /// - [`PlayerScope::DefendingPlayer`] ← the same anaphor once an attack + /// trigger's clause has bound it (CR 508.5; see + /// `parser::oracle_trigger::rebind_attack_anaphor_to_defending_player`) + /// + /// A scope the evaluator cannot resolve makes the condition UNANSWERABLE, + /// not false — see the entry-boundary gates in `game::triggers` and + /// `game::layers`, which reject the whole condition so `Not` cannot invert + /// a missing anchor into a firing trigger or an applied restriction. + IsMonarch { + #[serde( + default = "player_scope_controller", + skip_serializing_if = "is_player_scope_controller" + )] + player: PlayerScope, + }, /// CR 726.3: True when the controller has the initiative. IsInitiative, /// CR 725.1: True when no player holds the monarch designation. Distinct @@ -8110,6 +8828,86 @@ pub enum StaticCondition { None, } +impl StaticCondition { + /// CR 109.4 + CR 725.5: the player whose DESIGNATION this leaf tests, when + /// the leaf is a designation predicate at all. + /// + /// Exhaustive by design — there is deliberately no wildcard arm. This is + /// the guard that makes the static-side polarity boundary gate in + /// `game::layers` total: adding a future designation leaf that carries a + /// [`PlayerScope`] (e.g. an `IsInitiative { player }`) is a COMPILE ERROR + /// here, not a latent fail-open under [`StaticCondition::Not`]. + /// + /// Boolean combinators return `None`; the gate recurses them itself. + /// `QuantityComparison` returns `None` BY DEFINITION — it tests a quantity, + /// not a designation. + pub(crate) fn designation_player_anchor(&self) -> Option<&PlayerScope> { + match self { + StaticCondition::IsMonarch { player } => Some(player), + StaticCondition::DevotionGE { .. } + | StaticCondition::IsPresent { .. } + | StaticCondition::ChosenColorIs { .. } + | StaticCondition::ChosenLabelIs { .. } + | StaticCondition::QuantityComparison { .. } + | StaticCondition::HasMaxSpeed + | StaticCondition::SpeedGE { .. } + | StaticCondition::And { .. } + | StaticCondition::Or { .. } + | StaticCondition::Not { .. } + | StaticCondition::DayNightIs { .. } + | StaticCondition::HasCounters { .. } + | StaticCondition::CastVariantPaid { .. } + | StaticCondition::RecipientHasCounters { .. } + | StaticCondition::ClassLevelGE { .. } + | StaticCondition::DefendingPlayerControls { .. } + | StaticCondition::SourceAttackingAlone + | StaticCondition::SourceIsAttacking + | StaticCondition::SourceIsBlocking + | StaticCondition::SourceIsBlocked + | StaticCondition::IsInitiative + | StaticCondition::NoMonarch + | StaticCondition::HasCityBlessing + | StaticCondition::HasEnduringStory + | StaticCondition::CompletedADungeon + | StaticCondition::WasStartingPlayer { .. } + | StaticCondition::SpellCastWithVariantThisTurn { .. } + | StaticCondition::AnyPlayerAttackedYouLastTurn + | StaticCondition::OpponentPoisonAtLeast { .. } + | StaticCondition::UnlessPay { .. } + | StaticCondition::Unrecognized { .. } + | StaticCondition::DuringYourTurn + | StaticCondition::DuringOpponentsTurn + | StaticCondition::SharesColorWithMostCommonColorAmongPermanents + | StaticCondition::SourceEnteredThisTurn + | StaticCondition::SourceHasDealtDamage + | StaticCondition::WasCast { .. } + | StaticCondition::IsRingBearer + | StaticCondition::RingLevelAtLeast { .. } + | StaticCondition::ControlsCommander { .. } + | StaticCondition::SourceIsTapped + | StaticCondition::IsTapped { .. } + | StaticCondition::SourceIsFaceUp + | StaticCondition::SourceIsSaddled + | StaticCondition::SourceControllerEquals { .. } + | StaticCondition::SourceIsEquipped + | StaticCondition::SourceIsEnchanted + | StaticCondition::SourceIsMonstrous + | StaticCondition::SourceIsHarnessed + | StaticCondition::SourceAttachedToCreature + | StaticCondition::SourceMatchesFilter { .. } + | StaticCondition::TopOfLibraryMatches { .. } + | StaticCondition::RecipientMatchesFilter { .. } + | StaticCondition::RecipientAttackingOwnerTarget { .. } + | StaticCondition::SourceIsPaired + | StaticCondition::SourceInZone { .. } + | StaticCondition::EnchantedIsFaceDown + | StaticCondition::AdditionalCostPaid + | StaticCondition::CastingAsVariant { .. } + | StaticCondition::None => None, + } + } +} + // --------------------------------------------------------------------------- // ParsedCondition — typed restriction conditions parsed at build time // --------------------------------------------------------------------------- @@ -9492,22 +10290,7 @@ impl AbilityCost { filter: None, .. } => true, - // CR 702.24a + CR 122.1: The existing resolution payment - // authority can place a counter on the source through the - // replacement pipeline. This covers cumulative-upkeep costs such - // as Aboroth's "put a -1/-1 counter on this creature" without - // admitting arbitrary effect-as-cost shapes. - AbilityCost::EffectCost { effect } - if matches!( - effect.as_ref(), - Effect::PutCounter { - target: TargetFilter::SelfRef, - .. - } - ) => - { - true - } + AbilityCost::EffectCost { .. } if self.supports_effect_cost_payment() => true, // CR 118.12a: OneOf at the base must be a disjunction of mana // costs; mixed-shape disjunctions are not yet expanded into a // payable per-counter form. @@ -9524,6 +10307,28 @@ impl AbilityCost { } } + /// CR 118.3: Effect-as-cost forms the payment authority can resolve without + /// a player choice. This is shared by cumulative-upkeep synthesis and the + /// resolution-time payment gate so supported cards never install a trigger + /// whose cost will later be rejected. + pub fn supports_effect_cost_payment(&self) -> bool { + matches!( + self, + AbilityCost::EffectCost { effect } + if matches!( + effect.as_ref(), + Effect::PutCounter { + target: TargetFilter::SelfRef, + .. + } | Effect::Mana { + produced: ManaProduction::Fixed { .. }, + target: None, + .. + } + ) + ) + } + /// CR 118: Classify this cost into one or more `CostCategory` buckets. /// /// `Composite` recurses, flattening every sub-cost. Variants that pay @@ -11294,6 +12099,10 @@ pub enum Effect { skip_serializing_if = "EtbTapState::is_unspecified" )] enter_tapped: EtbTapState, + /// CR 508.4: Creatures enter combat during the mass move without being + /// declared as attackers. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + enters_attacking: bool, /// CR 122.1 + CR 122.1h: Counters placed on each object as it enters the /// battlefield during the mass move. Each entry is `(counter_type, /// count)`. Mirrors `Effect::ChangeZone.enter_with_counters` for the mass @@ -11368,6 +12177,9 @@ pub enum Effect { /// tapped when true (Planar Genesis — "onto the battlefield tapped"). #[serde(default)] enter_tapped: bool, + /// CR 508.4: Kept cards routed to the battlefield enter attacking. + #[serde(default)] + enters_attacking: bool, /// Determines where the resolver reads the card set from. See [`DigSource`]. #[serde(default, skip_serializing_if = "DigSource::is_library")] source: DigSource, @@ -11486,8 +12298,37 @@ pub enum Effect { /// CR 701.56a: Time travel — for each permanent you control with a time counter /// and each suspended card you own, you may add or remove a time counter. TimeTravel, - /// CR 725.1: Become the monarch. Sets GameState::monarch to the controller. - BecomeMonarch, + /// CR 725.1 + CR 725.3: Grant the monarch designation to `player`. Exactly + /// one player is the monarch at a time, so resolving this always MOVES the + /// designation rather than adding one. + /// + /// `target` is the CR 109.5 subject axis, parameterized within CR 725 rather + /// than proliferated into a `TargetBecomesMonarch` sibling. It is a + /// [`TargetFilter`] — NOT a [`PlayerScope`] — because the printed grammar is + /// "**target** opponent becomes the monarch", exactly like every sibling + /// "target player does X" effect ([`Effect::Draw`], [`Effect::Mill`], + /// [`Effect::GainLife`], …). Only a `TargetFilter` reaches + /// [`Effect::target_filter`], and only that makes `collect_target_slots` + /// declare a CR 115.1 target slot whose legality is the printed restriction + /// (opponents only). A `PlayerScope::Target` would name the slot without + /// ever creating one, so it would resolve to nobody: + /// - [`TargetFilter::Controller`] ← "you become the monarch" (printed + /// default, and the only shape the pre-axis unit variant could express). + /// A context ref, so it surfaces no target slot. + /// - a player filter ← "target opponent / target player becomes the monarch" + /// (M'Baku, Jabari Chieftain; Garland, Royal Kidnapper; Jared Carthalion, + /// True Heir; Éomer, King of Rohan; Denethor, Stone Seer) + /// + /// Serde-defaulted to `Controller` and skipped when it holds that value, so + /// every pre-existing `{"type":"BecomeMonarch"}` row in `card-data.json` + /// keeps deserializing AND re-serializing byte-identically. + BecomeMonarch { + #[serde( + default = "default_target_filter_controller", + skip_serializing_if = "is_target_filter_controller" + )] + target: TargetFilter, + }, /// CR 101.3 + CR 608.2: An instruction with no game action — "there's no /// effect." Used as the resolved outcome for a choice that has no printed /// clause, e.g. the losing/unlisted option of a single-conditional @@ -12422,6 +13263,33 @@ pub enum Effect { #[serde(default = "default_target_filter_self_ref")] target: TargetFilter, }, + /// CR 101.4 + CR 608.2c: Publish the numbers `players` secretly chose earlier + /// in this resolution — "then all players reveal those numbers + /// simultaneously" (Wheel of Misfortune), "then you reveal the number you + /// chose" (The Toymaker's Trap), "Then those numbers are revealed" (Menacing + /// Ogre). + /// + /// Deliberately NOT a member of the `Reveal` / `RevealTop` / `RevealHand` + /// family: CR 701.20a defines revealing a CARD ("show that card to all + /// players"), and those effects are parameterized over zone, count and card + /// filter. A committed number is not a card and has none of those axes — it + /// is a per-player choice made during resolution (CR 608.2d), so it gets its + /// own publication channel rather than a card-reveal variant bent to fit. + /// + /// A player's chosen number is private until this effect names them: the + /// resolver calls + /// [`crate::types::player::Player::reveal_chosen_number`], which swaps that + /// player's [`ChosenAttribute::Number`] for + /// [`ChosenAttribute::RevealedNumber`]. `game::visibility` redacts the + /// former from every other viewer and leaves the latter public, so privacy + /// is a property of the attribute kind rather than of any separate flag. + /// Naming a player who chose no number is a legal no-op (CR 609.3), which is + /// what makes `players: All` correct for a card whose choosers were only a + /// subset of the table. + RevealChosenNumbers { + #[serde(default)] + players: PlayerFilter, + }, /// CR 701.20a: Reveal the top N card(s) of a player's library. RevealTop { /// The player whose library to reveal from. @@ -12602,14 +13470,43 @@ pub enum Effect { #[serde(default = "default_duration_until_end_of_turn")] duration: Duration, }, - /// CR 508.1d: Target creature must attack the required player this turn/combat if able. + /// CR 508.1d + CR 506.3: The creatures matching `target` must attack the + /// required defender this turn/combat if able. + /// + /// `required_defender` is a `TargetFilter` because CR 506.3's defender + /// category ("a player, a planeswalker, or a battle") is already spanned by + /// that type — no second reference vocabulary is introduced. A filter that + /// denotes a PLAYER (`Controller`, a `ChosenPlayer` ref) grafts + /// `RequiredDefender::Fixed`; one that denotes an OBJECT (`SelfRef` — Gideon + /// Jura's "attack Gideon Jura if able") grafts `RequiredDefender::Permanent`. + /// `force_attack::resolve` is the single place that classifies it. + /// + /// `scope` is the single-vs-mass axis, exactly as on [`Effect::Transform`] + /// and [`Effect::SetTapState`] — parameterized rather than split into a + /// sibling `ForceAttackAll`. `Single` (the default, and every pre-Gideon-Jura + /// card) makes `target` a SELECTABLE target filter that surfaces a slot + /// ("Target creature attacks you this combat if able"). `All` makes it a + /// non-targeting POPULATION filter enumerated at resolution — Gideon Jura's + /// "creatures that player controls", which per CR 115.1 targets only the + /// opponent and never the creatures. `target_filter()` is `None` under `All`, + /// so no creature slot is built; the companion PLAYER slot still surfaces via + /// `mass_all_target_filter`. + /// + /// `serde`: pre-widening payloads named this field `required_player`, which + /// the alias below accepts; `scope` is absent from them and defaults to + /// `Single`, which is what every such payload meant. ForceAttack { #[serde(default = "default_target_filter_any")] target: TargetFilter, - #[serde(default = "default_target_filter_controller")] - required_player: TargetFilter, + #[serde( + default = "default_target_filter_controller", + alias = "required_player" + )] + required_defender: TargetFilter, #[serde(default = "default_duration_until_end_of_turn")] duration: Duration, + #[serde(default = "default_effect_scope_single")] + scope: EffectScope, }, /// CR 719.2: Solve the source Case — it becomes solved. SolveCase, @@ -12703,7 +13600,7 @@ pub enum Effect { /// `Controller` = "the next spell you cast"; `Target` = "the next /// spell they cast / that player casts" (the player this ability /// targets, e.g. the mana recipient on Bigger on the Inside). - #[serde(default = "default_player_scope_controller")] + #[serde(default = "player_scope_controller")] player: PlayerScope, #[serde(default, skip_serializing_if = "Option::is_none")] spell_filter: Option, @@ -14246,13 +15143,6 @@ fn default_duration_until_end_of_turn() -> Duration { Duration::UntilEndOfTurn } -/// CR 109.5: backward-compatible serde default for `Effect::GrantNextSpellAbility`'s -/// `player` field — pre-field data and "the next spell YOU cast" grants resolve to -/// the controller. -fn default_player_scope_controller() -> PlayerScope { - PlayerScope::Controller -} - fn default_comparator_ge() -> Comparator { Comparator::GE } @@ -14313,6 +15203,50 @@ fn default_distinct_names() -> Vec { vec![SharedQuality::Name] } +/// Backward-compat loader for the legacy +/// `QuantityRef::DistinctColorsAmongPermanents { filter }` payload, reached via +/// the `#[serde(alias = "filter")]` on +/// [`QuantityRef::DistinctColorsAmong`]'s `source` field. +/// +/// The legacy shape named exactly one population — the objects matching +/// `filter` — so it lifts to `CardTypeSetSource::Objects { filter }` with no +/// semantic change (this is a serialization shim, not rules logic; the rule +/// citations live on the variant it feeds). A saved game, an in-flight reconnect payload, or a +/// community scenario captured before the population axis was lifted therefore +/// still deserializes (Faeburrow Elder / Conqueror's Flail / Sunbird Effigy / +/// Aurora Awakener / Puca's Eye / Elemental Spectacle class). +/// +/// ORDERING, load-bearing: the current [`CardTypeSetSource`] reading is tried +/// FIRST, so a current payload is never re-read as a legacy one. Both types are +/// internally tagged on `"type"` and share exactly two tag names +/// (`ExiledBySource`, `TrackedSet` — verified against the two enum +/// declarations), which is the only place the two readings could collide; with +/// the current reading first, that collision can only ever mis-read a LEGACY +/// payload, and no legacy writer emitted either shape here. The two legacy +/// producers were `parse_number_of_distinct_colors_among_permanents_tail` +/// (craft materials → `And { [ExiledBySource, Typed] }`, or a `parse_type_phrase` +/// object filter) and `parse_for_each_distinct_colors_among_permanents` +/// (`parse_type_phrase` only), plus the mtgish-import converter (`Typed`) — +/// none of which can yield a BARE `ExiledBySource` / `TrackedSet` filter. +fn deserialize_distinct_colors_population<'de, D>( + deserializer: D, +) -> Result +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum Repr { + Current(CardTypeSetSource), + LegacyObjects(TargetFilter), + } + + Ok(match Repr::deserialize(deserializer)? { + Repr::Current(source) => source, + Repr::LegacyObjects(filter) => CardTypeSetSource::Objects { filter }, + }) +} + /// Backward-compat default for the legacy /// `FilterProp::MostPrevalentCreatureTypeInLibrary` shape. Old saves had no /// `scope` field; it always meant `your` library. @@ -14806,6 +15740,31 @@ pub enum VoteVisibility { } impl TargetFilter { + /// CR 508.3d + CR 508.5a: True when this filter denotes a PLAYER population + /// rather than an object population — the distinction + /// `trigger_matchers::matching_attack_events` uses to decide whether an + /// `Attacks` trigger's `valid_source` names the ATTACKING PLAYER ("Whenever + /// a player attacks you") or an attacking OBJECT (`valid_source_matches`). + /// + /// Single authority: the parser's intervening-if anaphor gate + /// (`oracle_trigger::attack_intervening_if_anaphor_is_defending_player`) + /// asks the same question and MUST ask it with this method, not with a + /// `valid_source.is_none()` proxy — that proxy silently excludes every + /// attack trigger whose attacker filter is an OBJECT filter in + /// `valid_source`, leaving the wrong `ScopedPlayer` anchor in place with no + /// compile error to catch it. + pub fn is_player_scope(&self) -> bool { + match self { + TargetFilter::Player | TargetFilter::Controller | TargetFilter::AllPlayers => true, + TargetFilter::Typed(TypedFilter { + type_filters, + controller: Some(_), + properties, + }) => type_filters.is_empty() && properties.is_empty(), + _ => false, + } + } + /// Clone this filter with any `Typed` property equal to `prop` removed. /// Used to strip a per-item discriminator leg (e.g. `IsChosenCreatureType`) /// so the residual base filter can be enumerated across candidate values. @@ -15064,6 +16023,33 @@ impl TargetFilter { } } + /// CR 603.7c + CR 400.7: Returns true when this filter names ONE object that + /// is already BOUND — a specific permanent or an anaphor to one — rather than + /// a CLASS of objects re-matched against the live board on every check. + /// + /// Used where "how many times can an event matching this filter occur?" is + /// the question, e.g. CR 603.7b's one-shot delayed triggers: a bound object + /// dies, leaves, or enters exactly once per incarnation (a returning object + /// is a NEW object, CR 400.7 / CR 603.7c), whereas a class filter ("a + /// creature") can match an unbounded number of later occurrences. + /// + /// Deliberately NARROWER than the parser's `is_single_object_ref`: that + /// helper also admits `TriggeringSource`, which is re-resolved from whichever + /// event is being examined and is therefore NOT bound in advance, and it has + /// no reason to admit `SpecificObject` / `ParentTargetSlot`. `TrackedSet` + /// and `LastCreated` are excluded for the same reason as a class filter — + /// they can name several objects, so several occurrences. + pub fn names_bound_single_object(&self) -> bool { + matches!( + self, + TargetFilter::SelfRef + | TargetFilter::SpecificObject { .. } + | TargetFilter::AttachedTo + | TargetFilter::ParentTarget + | TargetFilter::ParentTargetSlot { .. } + ) + } + /// CR 115.1: Returns true for filters that are NOT player-chosen targets — /// context references (triggering event participants per CR 603.7c), /// parent target anaphora, and self-references resolve automatically @@ -15085,6 +16071,10 @@ impl TargetFilter { | TargetFilter::SelfRef | TargetFilter::SourceOrPaired | TargetFilter::Controller + // CR 608.2h + CR 113.7a: "~'s controller" is resolved from + // the source's live-or-LKI incarnation, never chosen while + // announcing the ability. + | TargetFilter::SourceController | TargetFilter::OriginalController // CR 608.2c: the reanimator-Aura's pre-rebind source identity is // resolved (concretized to SpecificObject) during resolution, never @@ -15126,10 +16116,42 @@ impl TargetFilter { ) } - /// CR 608.2c + CR 109.4: If this filter is a player-only reference to the - /// Nth resolution-chosen player (a type-filter-free `Typed` whose only - /// distinguishing property is `controller: ChosenPlayer { index }`), return - /// that index. Used by effect-player resolvers to bind a "choose a player + /// CR 115.1a + CR 109.5: Returns true when this filter's TARGET SLOT holds a + /// player rather than an object — "target player", "target opponent", a + /// snapshotted specific player. + /// + /// This is the same rule `game::targeting::legal_targets` enumerates + /// players-only with, kept here as the single authority so any consumer that + /// must know whether a slot is player-valued asks it instead of re-deriving + /// the shape. The Aura-token host resolver is the second consumer: a token + /// created "attached to target opponent" (Selenia, the Cursed Heart) has to + /// reach the chosen PLAYER, and reading the ability's object targets for it + /// would attach the token to an unrelated permanent. + /// + /// The property-free requirement is load-bearing in both directions: + /// `Typed { properties: [Token] }` ("target token you control") names an + /// object characteristic that has no meaning for a player, and + /// `properties: [Another]` is the CR 115.4 "any other target" shape — both + /// denote objects and must fall through to object enumeration. + /// + /// Distinct from [`Self::is_context_ref`], which answers whether the filter + /// has a target slot at all: a resolution-chosen player is a context ref and + /// is NOT a player target, so [`Self::chosen_player_index`] must be consulted + /// first by callers that handle both. + pub fn denotes_player_target(&self) -> bool { + matches!( + self, + TargetFilter::Player | TargetFilter::SpecificPlayer { .. } + ) || matches!( + self, + TargetFilter::Typed(tf) if tf.type_filters.is_empty() && tf.properties.is_empty() + ) + } + + /// CR 608.2c + CR 109.4: If this filter is a player-only reference to the + /// Nth resolution-chosen player (a type-filter-free `Typed` whose only + /// distinguishing property is `controller: ChosenPlayer { index }`), return + /// that index. Used by effect-player resolvers to bind a "choose a player /// to " sub-effect's acting/recipient player without surfacing a /// target slot — the chosen player is fixed during resolution, not at /// target declaration. @@ -15247,6 +16269,43 @@ impl TargetFilter { _ => {} } } + + /// CR 400.1: Every zone this filter EXPLICITLY constrains its population to. + /// + /// The union of both zone readers, and never narrower than either. They + /// disagree on `StackSpell` / `StackAbility`: [`extract_in_zone`](Self::extract_in_zone) + /// reports `Stack`, while `collect_zones` has no arm for them and reports + /// nothing. A population walk that switched from the former to the latter + /// would stop scanning the stack, so the single-zone answer is unioned in + /// rather than assumed redundant. Deliberately fixed HERE rather than by + /// adding the arm to `collect_zones`: that function has ~15 callers asking + /// the narrower "what is written here" question, and widening it under them + /// is a change none of them requested. + /// + /// EMPTY IS MEANINGFUL, and is why no battlefield default is applied here. + /// A filter with no written zone constraint denotes permanents (CR 110.1), + /// but the two consumers of this list need opposite things from that fact: + /// + /// * a population WALK must scan the battlefield, so it substitutes the + /// default itself (`game::quantity::visit_characteristic_source`); + /// * a zone-transition DEPENDENCY must not claim to read the battlefield, + /// because battlefield moves are already escalated unconditionally by + /// `mark_layers_full` — reporting it here would add a redundant full + /// recompute to every battlefield move, and would break this function's + /// agreement with its `target_filter_reads_zone` siblings, none of which + /// report a defaulted zone. + /// + /// Order is deterministic (`extract_zones` order, then the single-zone + /// answer) so a walk's yield order does not depend on traversal incidentals. + pub fn population_zones(&self) -> Vec { + let mut zones = self.extract_zones(); + if let Some(single) = self.extract_in_zone() { + if !zones.contains(&single) { + zones.push(single); + } + } + zones + } } impl fmt::Debug for Effect { @@ -15330,6 +16389,11 @@ impl Effect { match self { // --- Effects with a `target: TargetFilter` field --- Effect::DealDamage { target, .. } + // CR 115.1 + CR 725.1: "target opponent becomes the monarch". The + // printed default (`Controller`, "you become the monarch") is a + // context ref, so `extract_target_filter_from_effect`'s final + // `is_context_ref` guard still surfaces no slot for it. + | Effect::BecomeMonarch { target } | Effect::Draw { target, .. } | Effect::Scry { target, .. } | Effect::Surveil { target, .. } @@ -15385,7 +16449,6 @@ impl Effect { | Effect::PhaseOut { target, .. } | Effect::PhaseIn { target, .. } | Effect::ForceBlock { target, .. } - | Effect::ForceAttack { target, .. } | Effect::BecomePrepared { target, .. } | Effect::BecomeUnprepared { target, .. } | Effect::BecomeSaddled { target, .. } @@ -15609,6 +16672,23 @@ impl Effect { .. } => None, + // CR 508.1d + CR 115.1: `ForceAttack` exposes its target only for the + // single-creature scope ("Target creature attacks you this combat if + // able"). The `All` scope is a non-targeting population enumerated at + // resolution — Gideon Jura's "creatures that player controls", whose + // only target is the opponent — so, like `Transform`/`SetTapState` + // above, its `target_filter()` is `None` and no creature slot or + // prompt is built. + Effect::ForceAttack { + scope: EffectScope::Single, + target, + .. + } => Some(target), + Effect::ForceAttack { + scope: EffectScope::All, + .. + } => None, + // CR 701.60a: `Suspect`/`Unsuspect` expose a target slot only for the // single-permanent scope (targeted/anaphoric "suspect target // creature" / "it's no longer suspected"). The `All` scope ("all @@ -15637,6 +16717,7 @@ impl Effect { Effect::StartYourEngines { .. } // CR 311.7: the chaos anchor swap is a non-targeting per-player effect. | Effect::SwapChosenLabels { .. } + | Effect::RevealChosenNumbers { .. } // CR 109.4: owner/type_filter are non-targeting resolution-time // filters; the copy source is chosen from the format pool, not // declared as a target. @@ -15668,7 +16749,6 @@ impl Effect { | Effect::Explore | Effect::Investigate | Effect::Tribute { .. } - | Effect::BecomeMonarch | Effect::NoOp | Effect::Proliferate | Effect::Populate @@ -16405,7 +17485,7 @@ impl Effect { | Effect::Attach { .. } | Effect::BecomeBlocked { .. } | Effect::BecomeCopy { .. } - | Effect::BecomeMonarch + | Effect::BecomeMonarch { .. } | Effect::BecomePrepared { .. } | Effect::BecomeSaddled { .. } | Effect::BecomeUnprepared { .. } @@ -16516,6 +17596,7 @@ impl Effect { | Effect::StartYourEngines { .. } | Effect::Suspect { .. } | Effect::SwapChosenLabels { .. } + | Effect::RevealChosenNumbers { .. } | Effect::SwitchPT { .. } | Effect::TakeTheInitiative | Effect::TargetOnly { .. } @@ -17026,7 +18107,7 @@ impl Effect { | Effect::Investigate | Effect::Tribute { .. } | Effect::TimeTravel - | Effect::BecomeMonarch + | Effect::BecomeMonarch { .. } | Effect::NoOp | Effect::Proliferate | Effect::ProliferateTarget { .. } @@ -17068,6 +18149,7 @@ impl Effect { | Effect::TargetOnly { .. } | Effect::Choose { .. } | Effect::SwapChosenLabels { .. } + | Effect::RevealChosenNumbers { .. } | Effect::ChooseDamageSource { .. } | Effect::Suspect { .. } | Effect::Unsuspect { .. } @@ -17275,7 +18357,7 @@ impl Effect { | Effect::Investigate | Effect::Tribute { .. } | Effect::TimeTravel - | Effect::BecomeMonarch + | Effect::BecomeMonarch { .. } | Effect::NoOp | Effect::Proliferate | Effect::ProliferateTarget { .. } @@ -17347,6 +18429,7 @@ impl Effect { | Effect::Cascade | Effect::Choose { .. } | Effect::SwapChosenLabels { .. } + | Effect::RevealChosenNumbers { .. } | Effect::ChooseAndSacrificeRest { .. } | Effect::EachPlayerCopyChosen { .. } | Effect::ChooseDamageSource { .. } @@ -17537,7 +18620,7 @@ impl Effect { | Effect::Investigate | Effect::Tribute { .. } | Effect::TimeTravel - | Effect::BecomeMonarch + | Effect::BecomeMonarch { .. } | Effect::NoOp | Effect::Proliferate | Effect::ProliferateTarget { .. } @@ -17609,6 +18692,7 @@ impl Effect { | Effect::Cascade | Effect::Choose { .. } | Effect::SwapChosenLabels { .. } + | Effect::RevealChosenNumbers { .. } | Effect::ChooseAndSacrificeRest { .. } | Effect::EachPlayerCopyChosen { .. } | Effect::ChooseDamageSource { .. } @@ -17752,7 +18836,7 @@ pub fn effect_variant_name(effect: &Effect) -> &str { Effect::Investigate => "Investigate", Effect::Tribute { .. } => "Tribute", Effect::TimeTravel => "TimeTravel", - Effect::BecomeMonarch => "BecomeMonarch", + Effect::BecomeMonarch { .. } => "BecomeMonarch", Effect::NoOp => "NoOp", Effect::Proliferate => "Proliferate", Effect::ProliferateTarget { .. } => "ProliferateTarget", @@ -17812,6 +18896,7 @@ pub fn effect_variant_name(effect: &Effect) -> &str { Effect::Choose { .. } => "Choose", Effect::OpponentGuess { .. } => "OpponentGuess", Effect::SwapChosenLabels { .. } => "SwapChosenLabels", + Effect::RevealChosenNumbers { .. } => "RevealChosenNumbers", Effect::ChooseDamageSource { .. } => "ChooseDamageSource", Effect::Suspect { .. } => "Suspect", Effect::Unsuspect { .. } => "Unsuspect", @@ -18262,7 +19347,7 @@ impl From<&Effect> for EffectKind { Effect::Investigate => EffectKind::Investigate, Effect::Tribute { .. } => EffectKind::Tribute, Effect::TimeTravel => EffectKind::TimeTravel, - Effect::BecomeMonarch => EffectKind::BecomeMonarch, + Effect::BecomeMonarch { .. } => EffectKind::BecomeMonarch, Effect::NoOp => EffectKind::NoOp, Effect::Proliferate => EffectKind::Proliferate, Effect::ProliferateTarget { .. } => EffectKind::ProliferateTarget, @@ -18327,6 +19412,10 @@ impl From<&Effect> for EffectKind { // CR 311.7: The chaos swap re-chooses each player's anchor, so it // reports as a `Choose`-kind resolution for event/AI purposes. Effect::SwapChosenLabels { .. } => EffectKind::Choose, + // CR 101.4: publishing a chosen number is a choice-ledger write, + // classified with the choice that produced it rather than with the + // CR 701.20 card reveals. + Effect::RevealChosenNumbers { .. } => EffectKind::Choose, Effect::ChooseDamageSource { .. } => EffectKind::ChooseDamageSource, Effect::Suspect { .. } => EffectKind::Suspect, Effect::Unsuspect { .. } => EffectKind::Unsuspect, @@ -18926,8 +20015,13 @@ pub struct AbilityDefinition { pub optional_targeting: bool, /// CR 608.2d: When true, the controller chooses whether to perform this effect ("You may X"). pub optional: bool, + /// CR 608.2d: Event-relative player named by an optional subject (for example, + /// "they may"). Unlike `optional_for` and `target_chooser`, this selects the + /// resolution-time optional actor; `None` uses this ability's controller. + pub optional_player: Option, /// CR 608.2d: When set, an opponent (not the controller) chooses whether to perform this - /// optional effect. Requires `optional: true`. Opponents are prompted in APNAP order. + /// optional effect. Unlike `optional_player` and `target_chooser`, this is an + /// any-opponent permission. Requires `optional: true`; prompts use APNAP order. pub optional_for: Option, /// Variable-count targeting: min/max targets the player can choose. /// When present, resolution enters MultiTargetSelection instead of immediate resolve. @@ -19007,8 +20101,8 @@ pub struct AbilityDefinition { /// CR 601.2c + CR 603.3d: When set, this player (not the controller) announces /// this ability's target(s) at stack placement. `None` = controller chooses /// (default). Mirrors `target_selection_mode` (the same "by-whom are targets - /// selected" axis). Distinct from CR 608.2d resolution-time "of their choice" - /// sacrifices. + /// selected" axis). Unlike `optional_player` and `optional_for`, this is a + /// stack-placement target choice, not a resolution-time optional actor. pub target_chooser: Option, /// CR 608.2c + CR 107.1c: per-iteration loop-continuation predicate, the /// non-count companion to `repeat_for`. When `Some`, the resolution chain @@ -19070,6 +20164,8 @@ struct AbilityDefinitionRepr<'a> { optional_targeting: bool, optional: bool, #[serde(skip_serializing_if = "Option::is_none")] + optional_player: &'a Option, + #[serde(skip_serializing_if = "Option::is_none")] optional_for: &'a Option, #[serde(skip_serializing_if = "Option::is_none")] multi_target: &'a Option, @@ -19136,6 +20232,7 @@ impl Serialize for AbilityDefinition { condition, optional_targeting, optional, + optional_player, optional_for, multi_target, target_constraints, @@ -19177,6 +20274,7 @@ impl Serialize for AbilityDefinition { condition, optional_targeting: *optional_targeting, optional: *optional, + optional_player, optional_for, multi_target, target_constraints, @@ -19268,6 +20366,8 @@ struct AbilityDefinitionDe { #[serde(default)] optional: bool, #[serde(default)] + optional_player: Option, + #[serde(default)] optional_for: Option, #[serde(default)] multi_target: Option, @@ -19340,6 +20440,7 @@ impl<'de> Deserialize<'de> for AbilityDefinition { condition: de.condition, optional_targeting: de.optional_targeting, optional: de.optional, + optional_player: de.optional_player, optional_for: de.optional_for, multi_target: de.multi_target, target_constraints: de.target_constraints, @@ -19536,6 +20637,7 @@ impl AbilityDefinition { condition: None, optional_targeting: false, optional: false, + optional_player: None, optional_for: None, multi_target: None, target_constraints: Vec::new(), @@ -20070,6 +21172,34 @@ pub enum AbilityCondition { HasCityBlessing, /// CR 702.195b: True when the ability controller has the enduring story designation. HasEnduringStory, + /// CR 903.3d + CR 608.2a: Resolution-time commander-control gate — "if you + /// control your commander" / "if you control a commander". CR 903.3d is the + /// authorizing rule: "If an effect refers to controlling a commander, it + /// refers to a permanent on the battlefield that is a commander." + /// CR 608.2a is the resolution-time half: an intervening-`if` condition is + /// re-checked as the ability resolves and the ability does nothing if it is + /// then false. + /// + /// The effect-resolution mirror of `StaticCondition::ControlsCommander` + /// (layers), `TriggerCondition::ControlsCommander` (intervening-if on a + /// printed trigger) and `ParsedCondition::ControlsCommander` + /// (activation/casting restrictions), carrying the same `ownership` axis. + /// + /// CR 903.3 + CR 109.5: `Own` ("your commander") requires the evaluating + /// player to both OWN and control it — CR 903.3 makes the designation an + /// attribute of the *card*, so a stolen commander remains its owner's, not + /// yours. `Any` ("a commander") is controller-only, any owner. + /// + /// CR 109.5: "you" is the resolving ability's controller; for a delayed + /// triggered ability that is the player who controlled the creating spell as + /// it resolved (CR 603.7d), which `Effect::CreateDelayedTrigger` already + /// stamps onto the delayed `ResolvedAbility`. + /// + /// Evaluated live at resolution against the single `game::commander` + /// authority — the same helpers the three sibling mirrors use — so the four + /// readings of one printed clause cannot drift, and the CR 702.26b + /// phased-out exclusion applies uniformly. + ControlsCommander { ownership: CommanderOwnership }, /// CR 701.9a + CR 608.2c: True when the card discarded by the directly /// preceding discard instruction matches `filter`. The resolver reads the /// discard operation's captured hand-time result, never current-zone state @@ -20366,6 +21496,14 @@ impl AbilityCondition { | AbilityCondition::IsInitiative | AbilityCondition::HasCityBlessing | AbilityCondition::HasEnduringStory + // CR 903.3d: a commander-control gate is a plain game-state predicate + // about the resolving ability's controller. It says nothing about + // whether an antecedent optional effect was performed, so a token + // minted under this gate is unconditionally live and the referent + // re-link must NOT fold the gate away as a reflexive "if you do". + // Same classification, for the same reason, as `IsMonarch` and + // `CompletedDungeon` below. + | AbilityCondition::ControlsCommander { .. } | AbilityCondition::DiscardedCardMatchesFilter { .. } | AbilityCondition::IsRingBearer | AbilityCondition::HasObjectTarget @@ -21054,8 +22192,27 @@ pub enum TriggerCondition { /// CR 702.178a: The trigger functions only while its controller has max speed. HasMaxSpeed, - /// CR 725.1: "if you're the monarch" is true when the controller is the monarch. - IsMonarch, + /// CR 725.1 + CR 603.4: monarch IDENTITY as an intervening-if — true when + /// `player` currently holds the monarch designation. Checked at fire time + /// and again as the ability resolves (CR 603.4). + /// + /// `player` is the CR 109.5 subject axis; see + /// [`StaticCondition::IsMonarch`] for the full axis rationale. "if you're + /// the monarch" is [`PlayerScope::Controller`]; "if that player is the + /// monarch" on an attack trigger is [`PlayerScope::DefendingPlayer`] + /// (CR 508.5 — the player the triggering creature is attacking). + /// + /// An unresolvable scope makes the condition UNANSWERABLE, not false: + /// `triggers::check_trigger_condition_with_source` rejects the whole + /// condition at its entry boundary so `Not` cannot invert a missing anchor + /// into a firing trigger. + IsMonarch { + #[serde( + default = "player_scope_controller", + skip_serializing_if = "is_player_scope_controller" + )] + player: PlayerScope, + }, /// CR 726.3: "if you have the initiative" is true when the controller has /// the initiative designation. IsInitiative, @@ -21302,6 +22459,104 @@ pub enum TriggerCondition { Not { condition: Box }, } +impl TriggerCondition { + /// CR 109.4 + CR 603.4: the player whose DESIGNATION this leaf tests, when + /// the leaf is a designation predicate at all. + /// + /// Exhaustive by design — there is deliberately no wildcard arm. This is + /// the guard that makes the polarity boundary gate in `game::triggers` + /// total: adding a future designation leaf that carries a [`PlayerScope`] + /// is a COMPILE ERROR here, not a latent fail-open under + /// [`TriggerCondition::Not`]. + /// + /// Boolean combinators return `None`; the gate recurses them itself. + /// `QuantityComparison` returns `None` BY DEFINITION — it tests a quantity, + /// not a designation. The pre-existing fail-open for unresolvable + /// [`PlayerScope`]s inside a `QuantityExpr` (an unresolved scope yields 0, + /// and `0 > 0` is false, which inverts under `Not`) is orthogonal, affects + /// every existing [`PlayerScope::DefendingPlayer`] card, and is deliberately + /// out of scope here. + pub(crate) fn designation_player_anchor(&self) -> Option<&PlayerScope> { + match self { + TriggerCondition::IsMonarch { player } => Some(player), + TriggerCondition::GainedLife { .. } + | TriggerCondition::LostLife + | TriggerCondition::Descended + | TriggerCondition::ControlsType { .. } + | TriggerCondition::NoSpellsCastLastTurn + | TriggerCondition::TwoOrMoreSpellsCastLastTurn + | TriggerCondition::DuringPlayersTurn { .. } + | TriggerCondition::SourceEnteredThisTurn + | TriggerCondition::SourceAttackedThisCombat + | TriggerCondition::EchoDue + | TriggerCondition::MinCoAttackers { .. } + | TriggerCondition::SolveConditionMet + | TriggerCondition::ClassLevelGE { .. } + | TriggerCondition::SourceIsHarnessed + | TriggerCondition::AttractionVisitRoll { .. } + | TriggerCondition::WasCast { .. } + | TriggerCondition::WasPlayed + | TriggerCondition::AdditionalCostPaid { .. } + | TriggerCondition::SourceIsAttacking + | TriggerCondition::CastVariantPaid { .. } + | TriggerCondition::CastVariantPaidPersistent { .. } + | TriggerCondition::ActivatedAbilityIsNonMana + | TriggerCondition::DealtDamageBySourceThisTurn + | TriggerCondition::DealtDamageThisTurnBySource { .. } + | TriggerCondition::FirstTimeObjectTappedThisTurn + | TriggerCondition::FirstTimeObjectCountersAddedThisTurn + | TriggerCondition::WasType { .. } + | TriggerCondition::LifeTotalGE { .. } + | TriggerCondition::ControlCount { .. } + | TriggerCondition::ControlsNone { .. } + | TriggerCondition::AttackedThisTurn + | TriggerCondition::FirstCombatPhaseOfTurn + | TriggerCondition::CastSpellThisTurn { .. } + | TriggerCondition::QuantityComparison { .. } + | TriggerCondition::HasMaxSpeed + | TriggerCondition::IsInitiative + | TriggerCondition::NoMonarch + | TriggerCondition::WasStartingPlayer { .. } + | TriggerCondition::SpellCastWithVariantThisTurn { .. } + | TriggerCondition::HasCityBlessing + | TriggerCondition::HasEnduringStory + | TriggerCondition::CompletedDungeon { .. } + | TriggerCondition::SourceIsTapped + | TriggerCondition::SourceIsTransformed + | TriggerCondition::SourceIsFaceUp + | TriggerCondition::SourceIsFaceDown + | TriggerCondition::SourceInZone { .. } + | TriggerCondition::CounterAddedThisTurn + | TriggerCondition::LostLifeLastTurn + | TriggerCondition::DefendingPlayerControlsNone { .. } + | TriggerCondition::TributeNotPaid + | TriggerCondition::CastDuringPhase { .. } + | TriggerCondition::CastTimingPermission { .. } + | TriggerCondition::ManaColorSpent { .. } + | TriggerCondition::ManaSpentCondition { .. } + | TriggerCondition::HadCounters { .. } + | TriggerCondition::ControlsCommander { .. } + | TriggerCondition::IsRenowned { .. } + | TriggerCondition::HasCounters { .. } + | TriggerCondition::ZoneChangeObjectMatchesFilter { .. } + | TriggerCondition::ZoneChangeObjectIsTapped + | TriggerCondition::SourceMatchesFilter { .. } + | TriggerCondition::EventDamageSourceMatchesFilter { .. } + | TriggerCondition::EventObjectMatchesFilter { .. } + | TriggerCondition::DamagedPlayerIsEventSourceOwner + | TriggerCondition::ChosenLabelIs { .. } + | TriggerCondition::AttackersDeclaredCount { .. } + | TriggerCondition::ExceptFirstDrawInDrawStep + | TriggerCondition::PlacedByAbilitySource + | TriggerCondition::TriggeringSpellTargetsFilter { .. } + | TriggerCondition::TriggeringSpellMatchesFilter { .. } + | TriggerCondition::And { .. } + | TriggerCondition::Or { .. } + | TriggerCondition::Not { .. } => None, + } + } +} + /// Condition that gates whether a replacement effect applies. /// Checked when determining if the replacement is a candidate for an event. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -21885,7 +23140,7 @@ pub struct TriggerDefinition { /// matcher requires the `DamageDealt` event's `amount` to satisfy /// `amount cmp n`. `None` means no amount restriction. Applies to all /// damage-event trigger modes (`DamageDone`, `DamageDoneOnce`, `DamageAll`, - /// `DamageDealtOnce`); ignored by other modes. + /// `DamageDealtOnce`, `DamageReceived`); ignored by other modes. #[serde(default, skip_serializing_if = "Option::is_none")] pub damage_amount: Option<(Comparator, u32)>, /// CR 119.3: Per-event life-change-amount constraint for life triggers @@ -22109,6 +23364,65 @@ impl TriggerEntry { } } +/// Classifies a persisted trigger list without inferring runtime provenance. +/// A list is either wholly legacy payloads or wholly identity-bearing entries; +/// a mixture cannot establish an exact occurrence mapping. +pub(crate) fn legacy_trigger_entry_list(entries: &[TriggerEntry]) -> Result { + let has_legacy = entries.iter().any(|entry| { + matches!( + entry.occurrence, + TriggerDefinitionOccurrenceRef::Unmaterialized + ) + }); + if has_legacy + && !entries.iter().all(|entry| { + matches!( + entry.occurrence, + TriggerDefinitionOccurrenceRef::Unmaterialized + ) + }) + { + return Err("legacy trigger list mixes payload-only and identity-bearing entries"); + } + Ok(has_legacy) +} + +/// Materializes a payload-only list only when an ordered printed base set proves +/// every slot. Runtime copied and granted triggers have no equivalent proof. +pub(crate) fn materialize_legacy_printed_trigger_entries( + entries: &mut Vec, + base_definitions: &[TriggerDefinition], + base_set: TriggerBaseSetInstanceRef, +) -> Result<(), &'static str> { + if !legacy_trigger_entry_list(entries)? { + return Ok(()); + } + if base_definitions.is_empty() + || entries.len() != base_definitions.len() + || !entries + .iter() + .zip(base_definitions) + .all(|(entry, base)| entry.definition == *base) + { + return Err("legacy runtime trigger payload has no provable producer or base slot"); + } + *entries = base_definitions + .iter() + .cloned() + .enumerate() + .map(|(printed_index, definition)| { + TriggerEntry::new( + TriggerDefinitionOccurrenceRef::Printed { + base_set, + printed_index, + }, + definition, + ) + }) + .collect(); + Ok(()) +} + #[derive(Deserialize)] #[serde(untagged)] enum TriggerEntryWire { @@ -22599,7 +23913,7 @@ pub struct StaticDefinition { pub source_controller: Option, /// CR 508.1d + CR 611.2c: The object that grafted this static onto its /// carrier (the ForceAttack/Encore/mass-coerce source for a - /// `MustAttackPlayer` requirement). Stamped at materialization from the + /// `MustAttackDefender` requirement). Stamped at materialization from the /// resolving continuous effect's `source_id`, but ONLY for static modes in /// the directing-source attribution class (see /// `static_mode_carries_directing_source` in game/layers.rs) — mirrors the @@ -24386,6 +25700,9 @@ pub struct ResolvedAbility { /// CR 608.2d: Optional effect — controller prompted before execution. #[serde(default)] pub optional: bool, + /// CR 608.2d: Event-relative player explicitly named by an optional subject. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub optional_player: Option, /// CR 608.2d: When set, an opponent chooses whether to perform this optional effect. #[serde(default, skip_serializing_if = "Option::is_none")] pub optional_for: Option, @@ -24631,6 +25948,7 @@ impl ResolvedAbility { context: SpellContext::default(), optional_targeting: false, optional: false, + optional_player: None, optional_for: None, multi_target: None, target_constraints: Vec::new(), @@ -25777,6 +27095,413 @@ mod tests { use crate::types::mana::ZoneSpendPolarity; use crate::types::zones::Zone; + /// Row 14, degenerate `AnyOf` cases. CR 109.2: a 0- or 1-member union is not + /// a union, and an EMPTY one is actively unsound — `characteristic_source_read` + /// would fold it to `RwProfile::empty()`, which is FAIL-OPEN for the CR 603.3b + /// same-event ordering gate, and the resolver would return 0 with no + /// diagnostic. Both boundaries are closed: construction and deserialization. + #[test] + fn any_of_arity_invariant_is_enforced_at_both_boundaries() { + let member = || CardTypeSetSource::Zone { + zone: ZoneRef::Graveyard, + scope: CountScope::Controller, + }; + + // Construction: empty yields nothing; a single member COLLAPSES to + // itself rather than forming a degenerate union. + assert_eq!(CardTypeSetSource::any_of(vec![]), None); + assert_eq!(CardTypeSetSource::any_of(vec![member()]), Some(member())); + assert!(matches!( + CardTypeSetSource::any_of(vec![member(), CardTypeSetSource::ExiledBySource]), + Some(CardTypeSetSource::AnyOf { ref sources }) if sources.len() == 2 + )); + + // Deserialization: a hand-authored or saved-game payload cannot smuggle + // a degenerate union past the constructor. + for payload in [ + r#"{"type":"AnyOf","sources":[]}"#, + r#"{"type":"AnyOf","sources":[{"type":"ExiledBySource"}]}"#, + ] { + assert!( + serde_json::from_str::(payload).is_err(), + "a degenerate union must be rejected on load: {payload}" + ); + } + assert!( + serde_json::from_str::( + r#"{"type":"AnyOf","sources":[{"type":"ExiledBySource"},{"type":"ExiledBySource"}]}"# + ) + .is_ok(), + "a two-member union must still load" + ); + } + + /// CR 109.2: the arity invariant is carried by the TYPE, so it holds in + /// release builds and against callers that never touch `any_of`. + /// + /// The previous form — a public `Vec` field plus a `debug_assert!` — was + /// unenforceable twice over: the assert compiled out of release, and any + /// in-crate caller could write the struct literal directly. Several did. + #[test] + fn union_sources_arity_is_unconstructible_below_two() { + let member = || CardTypeSetSource::ExiledBySource; + + // Construction: the ONLY in-crate way in rejects both degenerate arities. + assert!(UnionSources::new(vec![]).is_none()); + assert!(UnionSources::new(vec![member()]).is_none()); + assert!(UnionSources::new(vec![member(), member()]).is_some()); + + // `any_of` keeps its collapse-to-single behavior on top of that gate. + assert_eq!(CardTypeSetSource::any_of(vec![]), None); + assert_eq!(CardTypeSetSource::any_of(vec![member()]), Some(member())); + + // Deserialization: a saved game or hand-authored payload is rejected + // with a message naming the arity, not a generic length error. + for payload in [ + r#"{"type":"AnyOf","sources":[]}"#, + r#"{"type":"AnyOf","sources":[{"type":"ExiledBySource"}]}"#, + ] { + let err = serde_json::from_str::(payload) + .expect_err("a degenerate union must be rejected on load"); + assert!( + err.to_string().contains("at least 2"), + "the error should name the invariant, got {err}" + ); + } + + // Round-trip: the wire shape is unchanged by the newtype, so saved games + // written before it still load. + let json = + r#"{"type":"AnyOf","sources":[{"type":"ExiledBySource"},{"type":"ExiledBySource"}]}"#; + let loaded: CardTypeSetSource = + serde_json::from_str(json).expect("a two-member union must still load"); + assert_eq!( + serde_json::to_string(&loaded).expect("serialize"), + json, + "the newtype must be serde-transparent" + ); + } + + /// CR 109.2: the single bounded walker unrolls unions, visits every leaf + /// once, and reports truncation instead of recursing without limit. + #[test] + fn try_for_each_member_unrolls_unions_and_bounds_depth() { + let leaf = |n: u32| CardTypeSetSource::TrackedSet { + caused_by: (n > 0).then_some(ThisWayCause::Discarded), + }; + let union = CardTypeSetSource::any_of(vec![ + leaf(0), + CardTypeSetSource::any_of(vec![leaf(1), CardTypeSetSource::ExiledBySource]) + .expect("two-member union"), + ]) + .expect("two-member union"); + + // Every leaf, exactly once, with nested unions flattened. + let mut seen = Vec::new(); + assert!(union.try_for_each_member(UNION_DEPTH_BUDGET, &mut |m| seen.push(m.clone()))); + assert_eq!( + seen, + vec![leaf(0), leaf(1), CardTypeSetSource::ExiledBySource], + "unions unroll in declaration order and yield only non-union members" + ); + + // A non-union source is its own single member. + let mut single = Vec::new(); + assert!(CardTypeSetSource::ExiledBySource + .try_for_each_member(UNION_DEPTH_BUDGET, &mut |m| single.push(m.clone()))); + assert_eq!(single, vec![CardTypeSetSource::ExiledBySource]); + + // Exhaustion reports FALSE rather than recursing, and still visits what + // it reached — "incomplete", not "empty". + let mut partial = Vec::new(); + assert!( + !union.try_for_each_member(1, &mut |m| partial.push(m.clone())), + "a budget too small for the nesting must report truncation" + ); + assert!( + partial.len() < seen.len(), + "a truncated walk sees strictly fewer members" + ); + // Depth 0 cannot even visit a leaf. + assert!(!CardTypeSetSource::ExiledBySource.try_for_each_member(0, &mut |_| {})); + } + + /// CR 400.1: the population-zone authority reports EVERY zone a population + /// reads, per variant. The craft row is the one that was silently wrong: + /// evaluation scanned exile for `And[ExiledBySource, Owned{You}]` while the + /// dependency classifier reported that population as reading no zone, so no + /// exile transition ever dirtied a characteristic derived from it. + #[test] + fn population_zones_reports_every_zone_each_population_reads() { + // Craft linked-exile (Sunbird Effigy), in both the shapes that reach it: + // the dedicated variant and the filter the craft parser actually builds. + assert_eq!( + CardTypeSetSource::ExiledBySource.population_zones(), + vec![Zone::Exile] + ); + let craft = CardTypeSetSource::Objects { + filter: TargetFilter::And { + filters: vec![ + TargetFilter::ExiledBySource, + TargetFilter::Typed(TypedFilter::default().properties(vec![ + FilterProp::Owned { + controller: ControllerRef::You, + }, + ])), + ], + }, + }; + assert_eq!(craft.population_zones(), vec![Zone::Exile]); + assert!(craft.reads_zone(Zone::Exile), "the craft regression"); + assert!(!craft.reads_zone(Zone::Graveyard)); + + // An explicit single-zone constraint, and the ZoneRef mapping. + for (zone_ref, zone) in [ + (ZoneRef::Graveyard, Zone::Graveyard), + (ZoneRef::Exile, Zone::Exile), + (ZoneRef::Library, Zone::Library), + (ZoneRef::Hand, Zone::Hand), + ] { + let source = CardTypeSetSource::Zone { + zone: zone_ref, + scope: CountScope::Controller, + }; + assert_eq!(source.population_zones(), vec![zone]); + assert!(source.reads_zone(zone)); + assert!(!source.reads_zone(Zone::Stack)); + } + + // A snapshot population is NOT a zone read (CR 400.7 / CR 608.2c), and + // that is asserted rather than left implicit. + for snapshot in [ + CardTypeSetSource::TrackedSet { caused_by: None }, + CardTypeSetSource::TurnJournal { + journal: TurnJournalKind::SpellsCast, + scope: CountScope::Controller, + filter: None, + }, + ] { + assert!(snapshot.population_zones().is_empty()); + for zone in [Zone::Battlefield, Zone::Exile, Zone::Graveyard] { + assert!(!snapshot.reads_zone(zone)); + } + } + } + + /// CR 400.1: a multi-zone `InAnyZone` population enumerates EVERY zone. + /// `extract_in_zone` collapses it to one, which is what made the evaluator + /// undercount every zone after the first. + #[test] + fn population_zones_preserves_multi_zone_unions_that_extract_in_zone_collapses() { + let multi = + TargetFilter::Typed( + TypedFilter::default().properties(vec![FilterProp::InAnyZone { + zones: vec![Zone::Graveyard, Zone::Hand, Zone::Library], + }]), + ); + // The collapse this replaces: one zone out of three. + assert_eq!(multi.extract_in_zone(), None); + let source = CardTypeSetSource::Objects { + filter: multi.clone(), + }; + assert_eq!( + source.population_zones(), + vec![Zone::Graveyard, Zone::Hand, Zone::Library] + ); + for zone in [Zone::Graveyard, Zone::Hand, Zone::Library] { + assert!(source.reads_zone(zone), "{zone:?} is in the union"); + } + for zone in [Zone::Battlefield, Zone::Exile, Zone::Stack] { + assert!(!source.reads_zone(zone), "{zone:?} is not in the union"); + } + } + + /// The population-zone list must never be NARROWER than `extract_in_zone`. + /// `collect_zones` has no `StackSpell` arm, so a walk that read only + /// `extract_zones` would stop scanning the stack — the union is what keeps + /// Secret Arcade's "permanent spells you control" shape reachable. + #[test] + fn population_zones_is_never_narrower_than_either_zone_reader() { + for filter in [ + TargetFilter::StackSpell, + TargetFilter::And { + filters: vec![ + TargetFilter::StackSpell, + TargetFilter::Typed(TypedFilter::permanent().controller(ControllerRef::You)), + ], + }, + ] { + assert!( + filter.extract_zones().is_empty(), + "precondition: collect_zones has no stack arm" + ); + assert_eq!(filter.extract_in_zone(), Some(Zone::Stack)); + assert_eq!(filter.population_zones(), vec![Zone::Stack]); + } + } + + /// CR 110.1 + CR 611.3a: an unconstrained filter denotes permanents, but the + /// battlefield default is deliberately NOT reported here. Battlefield moves + /// are escalated unconditionally by `mark_layers_full`, so claiming the read + /// would add a redundant full recompute to every one of them; the population + /// WALK substitutes the default itself. This asymmetry is the whole reason + /// `population_zones` returns an empty vec instead of `[Battlefield]`. + #[test] + fn population_zones_leaves_the_battlefield_default_to_the_walk() { + let source = CardTypeSetSource::Objects { + filter: TargetFilter::Typed(TypedFilter::permanent().controller(ControllerRef::You)), + }; + assert!(source.population_zones().is_empty()); + assert!(!source.reads_zone(Zone::Battlefield)); + } + + /// CR 109.2: a union reads the union of its members' zones, deduplicated — + /// First Family's shape, plus a nested union to pin the recursion. + #[test] + fn population_zones_unions_member_zones_without_duplicates() { + let graveyard = CardTypeSetSource::Zone { + zone: ZoneRef::Graveyard, + scope: CountScope::Controller, + }; + let union = CardTypeSetSource::any_of(vec![ + graveyard.clone(), + CardTypeSetSource::ExiledBySource, + // Same zone twice — the dedup is what makes this a set union. + graveyard.clone(), + CardTypeSetSource::any_of(vec![ + CardTypeSetSource::Zone { + zone: ZoneRef::Hand, + scope: CountScope::Controller, + }, + CardTypeSetSource::TurnJournal { + journal: TurnJournalKind::SpellsCast, + scope: CountScope::Controller, + filter: None, + }, + ]) + .expect("two-member union"), + ]) + .expect("multi-member union"); + assert_eq!( + union.population_zones(), + vec![Zone::Graveyard, Zone::Exile, Zone::Hand] + ); + } + + /// SAVED-GAME MIGRATION, unit arm. The pre-lift shape + /// `{"type":"DistinctColorsAmongPermanents","filter":…}` must rehydrate as + /// the single-population reading. Both halves of the rename are load-bearing: + /// without the variant alias the tag is an unknown variant, and even with it + /// the renamed-and-retyped key would fail as a missing `source` field. + /// + /// The input is a VERBATIM node lifted out of the persisted 4p board in + /// `crates/engine/tests/fixtures/dina_noff_turn5_4p.json.gz`, so it is the + /// exact byte shape live snapshots carry rather than a hand-written + /// paraphrase. + #[test] + fn legacy_distinct_colors_among_permanents_payload_lifts_to_an_object_population() { + let expected = QuantityRef::DistinctColorsAmong { + source: CardTypeSetSource::Objects { + filter: TargetFilter::Typed( + TypedFilter::permanent().controller(ControllerRef::You), + ), + }, + }; + + let legacy = r#"{"filter":{"controller":"You","properties":[],"type":"Typed","type_filters":["Permanent"]},"type":"DistinctColorsAmongPermanents"}"#; + assert_eq!( + serde_json::from_str::(legacy) + .expect("a pre-lift persisted node must still deserialize"), + expected, + "the legacy tag + `filter` key must lift to `Objects {{ filter }}`", + ); + + // Reach-guard: the same payload under the CURRENT tag is still legacy on + // the key axis, and the current tag + key is unaffected. + let legacy_tag_only = r#"{"filter":{"controller":"You","properties":[],"type":"Typed","type_filters":["Permanent"]},"type":"DistinctColorsAmong"}"#; + assert_eq!( + serde_json::from_str::(legacy_tag_only) + .expect("the legacy key must be accepted under the current tag too"), + expected, + ); + + // Migration is load-only: a rehydrated node re-serializes in the CURRENT + // shape, so a save written by this build never re-emits the old names. + let round = serde_json::to_string(&expected).expect("serializes"); + assert!( + round.contains(r#""type":"DistinctColorsAmong""#) && round.contains(r#""source""#), + "serialization must emit the current tag and key: {round}", + ); + assert!( + !round.contains("DistinctColorsAmongPermanents"), + "serialization must not re-emit the legacy tag: {round}", + ); + assert_eq!( + serde_json::from_str::(&round).expect("round-trips"), + expected, + ); + + // NON-VACUITY: neither acceptance above comes from a tolerant decoder. + // `QuantityRef` has no `#[serde(other)]` fallback, so a near-miss tag is + // still an unknown-variant error — the alias is what admits the legacy + // one. And the population key stays REQUIRED: aliasing it did not turn + // it into a silent default, so a payload carrying neither key errors + // instead of decoding as an empty population. + assert!( + serde_json::from_str::( + r#"{"filter":{"type":"Typed"},"type":"DistinctColorsAmongPermanentsX"}"# + ) + .is_err(), + "an unaliased tag must still be rejected, or the row measures nothing", + ); + assert!( + serde_json::from_str::(r#"{"type":"DistinctColorsAmong"}"#).is_err(), + "the population key must stay required under both tags", + ); + assert!( + serde_json::from_str::(r#"{"type":"DistinctColorsAmongPermanents"}"#) + .is_err(), + "the population key must stay required under both tags", + ); + } + + /// The legacy lift must never capture a CURRENT payload. `TargetFilter` and + /// `CardTypeSetSource` are both internally tagged on `"type"` and share + /// exactly two tag names (`ExiledBySource`, `TrackedSet`), so those are the + /// only shapes where the two readings could collide — the shim tries the + /// current reading first, and this row pins that ordering. `Objects` and the + /// `AnyOf` union (First Family) are covered as the non-colliding controls. + #[test] + fn current_distinct_colors_population_payloads_are_never_read_as_legacy() { + let objects = CardTypeSetSource::Objects { + filter: TargetFilter::Typed(TypedFilter::permanent().controller(ControllerRef::You)), + }; + for source in [ + CardTypeSetSource::ExiledBySource, + CardTypeSetSource::TrackedSet { caused_by: None }, + objects.clone(), + CardTypeSetSource::any_of(vec![ + objects, + CardTypeSetSource::TurnJournal { + journal: TurnJournalKind::SpellsCast, + scope: CountScope::Controller, + filter: None, + }, + ]) + .expect("two-member union"), + ] { + let node = QuantityRef::DistinctColorsAmong { + source: source.clone(), + }; + let json = serde_json::to_string(&node).expect("serializes"); + assert_eq!( + serde_json::from_str::(&json).expect("round-trips"), + node, + "the current reading must win for {json}", + ); + } + } + #[test] fn first_optional_effect_gate_does_not_latch_a_later_independent_gate() { let mut first = ResolvedAbility::new(Effect::NoOp, vec![], ObjectId(1), PlayerId(0)); @@ -26075,7 +27800,7 @@ mod tests { legacy, ChoiceType::NumberRange { min: 1, - max: 5, + max: Some(5), distinctness: NumberDistinctness::Repeatable, } ); @@ -26085,10 +27810,32 @@ mod tests { "Repeatable must not emit the distinctness field" ); + // CR 107.1a/b: making `max` optional must not disturb the BOUNDED wire + // shape — the assertions above already prove `"max":5` both reads and + // writes unchanged, so existing card-data round-trips byte-identically. + // The UNBOUNDED form is the new shape: it omits the key entirely, and a + // payload with no `max` reads back as unbounded rather than defaulting to + // some ceiling. + let unbounded = ChoiceType::NumberRange { + min: 0, + max: None, + distinctness: NumberDistinctness::Repeatable, + }; + assert_eq!( + serde_json::to_string(&unbounded).unwrap(), + r#"{"NumberRange":{"min":0}}"#, + "an unbounded range must omit max rather than emit a stand-in" + ); + assert_eq!( + serde_json::from_str::(r#"{"NumberRange":{"min":0}}"#).unwrap(), + unbounded, + "a payload with no max is unbounded, not defaulted" + ); + // A DistinctFromSourceHistory value round-trips and emits the field. let distinct = ChoiceType::NumberRange { min: 1, - max: 5, + max: Some(5), distinctness: NumberDistinctness::DistinctFromSourceHistory, }; let json = serde_json::to_string(&distinct).unwrap(); @@ -29409,3 +31156,258 @@ mod mana_target_role_tests { ); } } + +/// CR 115.1a: `denotes_player_target` is read by both +/// `game::targeting::legal_targets` and the Aura-token host resolver, so its +/// contract is pinned here rather than only through either consumer. +#[cfg(test)] +mod player_target_slot_tests { + use super::*; + + /// CR 115.1a: `denotes_player_target` is read by both + /// `game::targeting::legal_targets` and the Aura-token host resolver, so its + /// contract is pinned here rather than only through either consumer. + /// + /// The property-free requirement is the load-bearing half: a `Typed` filter + /// carrying an object characteristic denotes objects however player-shaped + /// the rest of it looks. + #[test] + fn denotes_player_target_covers_the_player_slots_and_nothing_else() { + let empty_typed = |controller: Option| { + TargetFilter::Typed(TypedFilter { + type_filters: Vec::new(), + controller, + properties: Vec::new(), + }) + }; + + for filter in [ + TargetFilter::Player, + TargetFilter::SpecificPlayer { id: PlayerId(1) }, + empty_typed(Some(ControllerRef::Opponent)), + empty_typed(Some(ControllerRef::You)), + // A resolution-chosen player is a player slot by shape; callers that + // must tell it apart ask `chosen_player_index` first. + empty_typed(Some(ControllerRef::ChosenPlayer { index: 0 })), + empty_typed(None), + ] { + assert!( + filter.denotes_player_target(), + "{filter:?} names a player, not an object" + ); + } + + for filter in [ + TargetFilter::Any, + TargetFilter::Typed(TypedFilter::creature()), + TargetFilter::Opponent, + TargetFilter::Controller, + TargetFilter::SelfRef, + // "target token you control" — a characteristic no player has. + TargetFilter::Typed(TypedFilter { + type_filters: Vec::new(), + controller: Some(ControllerRef::You), + properties: vec![FilterProp::Token], + }), + // CR 115.4 "any other target": players AND objects, so not a + // player-only slot. + TargetFilter::Typed(TypedFilter { + type_filters: Vec::new(), + controller: None, + properties: vec![FilterProp::Another], + }), + ] { + assert!( + !filter.denotes_player_target(), + "{filter:?} does not name a player-only target slot" + ); + } + } +} + +#[cfg(test)] +mod monarch_subject_axis_tests { + use super::*; + + /// CR 725.1 + CR 109.5: every pre-existing `{"type":"IsMonarch"}` row in + /// `card-data.json` and in the committed integration-card fixture must keep + /// deserializing to the controller subject, and must re-serialize + /// byte-identically. + /// + /// Revert-failing twice: without `#[serde(default = ...)]` the deserialize + /// errors outright; without `skip_serializing_if` the re-serialize emits a + /// `player` key and every existing monarch row churns. + #[test] + fn is_monarch_round_trips_without_a_player_key_for_the_controller_subject() { + let trigger: TriggerCondition = serde_json::from_str(r#"{"type":"IsMonarch"}"#).unwrap(); + assert_eq!( + trigger, + TriggerCondition::IsMonarch { + player: PlayerScope::Controller + } + ); + assert_eq!( + serde_json::to_string(&trigger).unwrap(), + r#"{"type":"IsMonarch"}"# + ); + + let stat: StaticCondition = serde_json::from_str(r#"{"type":"IsMonarch"}"#).unwrap(); + assert_eq!( + stat, + StaticCondition::IsMonarch { + player: PlayerScope::Controller + } + ); + assert_eq!( + serde_json::to_string(&stat).unwrap(), + r#"{"type":"IsMonarch"}"# + ); + } + + /// CR 725.1 + CR 109.5: the EFFECT-side subject axis has the same serde + /// contract as the predicate-side one above. Every shipping + /// `{"type":"BecomeMonarch"}` row in `card-data.json` (~40 "you become the + /// monarch" cards) must keep deserializing to the controller subject and + /// re-serialize byte-identically, while a real target filter survives. + /// + /// Revert-failing twice: without `#[serde(default = ...)]` the deserialize + /// errors outright; without `skip_serializing_if` the re-serialize emits a + /// `target` key and every existing monarch row churns. + #[test] + fn become_monarch_round_trips_without_a_target_key_for_the_controller_subject() { + let default_row: Effect = serde_json::from_str(r#"{"type":"BecomeMonarch"}"#).unwrap(); + assert_eq!( + default_row, + Effect::BecomeMonarch { + target: TargetFilter::Controller + } + ); + assert_eq!( + serde_json::to_string(&default_row).unwrap(), + r#"{"type":"BecomeMonarch"}"# + ); + + // CR 115.1: "target opponent becomes the monarch" — the opponent + // restriction must survive export, or the re-imported row would offer + // the controller as a legal target. + let targeted = Effect::BecomeMonarch { + target: TargetFilter::Typed(TypedFilter { + type_filters: vec![], + controller: Some(ControllerRef::Opponent), + properties: vec![], + }), + }; + let json = serde_json::to_string(&targeted).unwrap(); + assert!( + json.contains("\"target\""), + "a non-default subject must be serialized: {json}" + ); + assert_eq!(serde_json::from_str::(&json).unwrap(), targeted); + } + + /// A non-default subject must survive the round trip, or the card-data + /// export would silently rebind M'Baku's anaphor to the controller. + #[test] + fn is_monarch_serializes_a_non_default_subject() { + let trigger = TriggerCondition::IsMonarch { + player: PlayerScope::DefendingPlayer, + }; + let json = serde_json::to_string(&trigger).unwrap(); + assert_eq!( + json, + r#"{"type":"IsMonarch","player":{"type":"DefendingPlayer"}}"# + ); + assert_eq!( + serde_json::from_str::(&json).unwrap(), + trigger + ); + } + + /// CR 611.2a + CR 514.2: a duration-timing-only scope is serde-reachable + /// from a malformed or hand-authored row. It must be REJECTED by + /// `duration_timing_only` before `resolve_single_player_scope`'s + /// `unreachable!()` can panic the engine inside a trigger check. + #[test] + fn duration_timing_only_scopes_are_flagged_for_fail_closed_rejection() { + let malformed: TriggerCondition = + serde_json::from_str(r#"{"type":"IsMonarch","player":{"type":"AnyTurn"}}"#).unwrap(); + let TriggerCondition::IsMonarch { player } = &malformed else { + panic!("expected IsMonarch, got {malformed:?}"); + }; + assert!(player.duration_timing_only()); + + assert!(PlayerScope::SpecificPlayer { id: PlayerId(3) }.duration_timing_only()); + // Everything the parser can actually emit must NOT be rejected. + for ok in [ + PlayerScope::Controller, + PlayerScope::ScopedPlayer, + PlayerScope::DefendingPlayer, + ] { + assert!(!ok.duration_timing_only(), "{ok:?} must resolve normally"); + } + } + + /// The polarity boundary gates are only sound because these accessors are + /// exhaustive. Pin the two answers they must give. + #[test] + fn designation_player_anchor_reports_the_monarch_subject_and_nothing_else() { + assert_eq!( + TriggerCondition::IsMonarch { + player: PlayerScope::DefendingPlayer + } + .designation_player_anchor(), + Some(&PlayerScope::DefendingPlayer) + ); + assert_eq!( + StaticCondition::IsMonarch { + player: PlayerScope::ScopedPlayer + } + .designation_player_anchor(), + Some(&PlayerScope::ScopedPlayer) + ); + // CR 725.1: vacancy is a different predicate and carries no subject. + assert_eq!( + TriggerCondition::NoMonarch.designation_player_anchor(), + None + ); + assert_eq!(StaticCondition::NoMonarch.designation_player_anchor(), None); + // A quantity tests a quantity, not a designation — by definition. + assert_eq!( + TriggerCondition::QuantityComparison { + lhs: QuantityExpr::Ref { + qty: QuantityRef::LifeTotal { + player: PlayerScope::DefendingPlayer + } + }, + comparator: Comparator::GT, + rhs: QuantityExpr::Fixed { value: 0 }, + } + .designation_player_anchor(), + None + ); + } + + /// CR 109.4: the mutable player-axis accessor must reach every reference + /// that carries one, and must leave object-axis references alone. + #[test] + fn quantity_ref_player_scope_mut_reaches_the_player_axis() { + let mut life = QuantityRef::LifeTotal { + player: PlayerScope::ScopedPlayer, + }; + *life.player_scope_mut().unwrap() = PlayerScope::DefendingPlayer; + assert_eq!( + life, + QuantityRef::LifeTotal { + player: PlayerScope::DefendingPlayer + } + ); + + let mut hand = QuantityRef::HandSize { + player: PlayerScope::ScopedPlayer, + }; + assert!(hand.player_scope_mut().is_some()); + + let mut object_axis = QuantityRef::SelfManaValue; + assert!(object_axis.player_scope_mut().is_none()); + } +} From 888fc050d5851af763649dd01f75f829182835a9 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 20:49:06 -0700 Subject: [PATCH 05/11] fix(PR-7332): retain intervening-if trigger cleanup --- crates/engine/src/game/ability_scan.rs | 2 +- crates/engine/src/game/engine.rs | 17 ++++++++--------- crates/engine/src/game/triggers.rs | 13 ++++++++++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/game/ability_scan.rs b/crates/engine/src/game/ability_scan.rs index 89881d55aa..53f5063b8d 100644 --- a/crates/engine/src/game/ability_scan.rs +++ b/crates/engine/src/game/ability_scan.rs @@ -264,7 +264,7 @@ fn resolved_ability_axes(a: &ResolvedAbility, mode: ScanMode) -> Axes { replacement_applied: _, // replacement provenance set, no dynamic read sub_link: _, // SubAbilityLink kind tag sibling_condition: _, // SiblingCondition replication marker, no dynamic read - distribute: _, // announcement unit tag/string, no resolution-time dynamic read + distribute: _, // announcement unit tag/string, no resolution-time dynamic read parent_target_missing_reason: _, // seam flag } = a; diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 8cac679b8c..510f8278d8 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -18163,7 +18163,7 @@ mod stage2_injector_tests { assert_eq!( producers.len() + readers.len() + in_test, - 40, + 42, "CR 603.5 prompt census drifted. A new PRODUCER must have its recipient bound \ somewhere — the mint's conjunct (a) covers exactly ONE of them. A new READER is \ the benign case (U4's own consumption arm was one): adjudicate it in this doc and \ @@ -18192,10 +18192,9 @@ mod stage2_injector_tests { // partition assert below before this total could absorb it. assert_eq!( (producers.len(), readers.len(), in_test), - (5, 7, 28), - "the partition, not just the total: five PRODUCTION producers, seven PRODUCTION \ - readers (they read `state.waiting_for` and never write it — the seventh is U4's \ - `inject_pinned_answer` arm), 28 `#[cfg(test)]` lines.\nproducers={producers:#?}\n\ + (5, 8, 29), + "the partition, not just the total: five PRODUCTION producers, eight PRODUCTION \ + readers (they read `state.waiting_for` and never write it), 29 `#[cfg(test)]` lines.\nproducers={producers:#?}\n\ readers={readers:#?}" ); assert_eq!( @@ -18674,9 +18673,9 @@ mod stage2_injector_tests { // trust looks like — and it is why the two prose entries are BOTH kept // rather than one overwriting the other: they are separate witnesses, not // duplicates. - "game/effects/mod.rs:6774".to_string(), - "game/effects/mod.rs:6851".to_string(), - "game/effects/mod.rs:10089".to_string(), + "game/effects/mod.rs:6922".to_string(), + "game/effects/mod.rs:6999".to_string(), + "game/effects/mod.rs:10237".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. @@ -19362,7 +19361,7 @@ mod stage2_injector_tests { // `origin/main:crates/engine/src/game/engine.rs:12773`, and its offset from // `begin_pending_trigger_target_selection` (`:12662`) is STILL 134 — the // control that caught this row's one historical SILENT drift. - "game/engine.rs:12796".to_string(), + "game/engine.rs:12851".to_string(), ], "the five production producers, NAMED: the CR 603.5 gate in `resolve_chain_body` \ plus the two repeated-optional-payment drivers, the per-player acceptance cursor \ diff --git a/crates/engine/src/game/triggers.rs b/crates/engine/src/game/triggers.rs index 9dd2d3de11..f015eab976 100644 --- a/crates/engine/src/game/triggers.rs +++ b/crates/engine/src/game/triggers.rs @@ -10596,6 +10596,7 @@ fn collect_matching_delayed_triggers( // One-shot triggers are removed; multi-fire triggers are cloned and left in place. let mut to_fire: Vec<(DelayedTrigger, usize, GameEvent, bool)> = Vec::new(); let mut to_remove: Vec<(usize, usize, GameEvent)> = Vec::new(); + let mut to_discard: Vec<(usize, super::lifecycle::DelayedTerminalDisposition)> = Vec::new(); for (idx, delayed) in state.delayed_triggers.iter().enumerate() { if let Some((event_index, trigger_event)) = delayed_trigger_event_with_index( @@ -10708,12 +10709,22 @@ fn collect_matching_delayed_triggers( .iter() .map(|(idx, event_index, event)| (*idx, (*event_index, event.clone()))) .collect(); - let mut combined: Vec = to_remove.iter().map(|(idx, _, _)| *idx).collect(); + let mut unfired_dispositions: std::collections::HashMap< + usize, + super::lifecycle::DelayedTerminalDisposition, + > = to_discard.iter().copied().collect(); + let mut combined: Vec = to_remove + .iter() + .map(|(idx, _, _)| *idx) + .chain(to_discard.iter().map(|(idx, _)| *idx)) + .collect(); combined.sort_unstable(); for idx in combined.into_iter().rev() { let trigger = state.delayed_triggers.remove(idx); if let Some((event_index, trigger_event)) = fired_events.remove(&idx) { to_fire.push((trigger, event_index, trigger_event, true)); + } else if let Some(disposition) = unfired_dispositions.remove(&idx) { + super::lifecycle::record_delayed_terminal(trigger.provenance.firing(), disposition); } } From 774a350ed5781daeb4e313be04a4d23452ae920d Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 20:58:31 -0700 Subject: [PATCH 06/11] fix(PR-7332): box triggered mana readiness wait --- crates/engine/src/game/engine_payment_choices.rs | 2 ++ crates/engine/src/game/triggers.rs | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/game/engine_payment_choices.rs b/crates/engine/src/game/engine_payment_choices.rs index a67ec696dd..d0ad8a2df8 100644 --- a/crates/engine/src/game/engine_payment_choices.rs +++ b/crates/engine/src/game/engine_payment_choices.rs @@ -50,6 +50,7 @@ pub(super) fn handle_optional_effect_choice( { // This arm falls through to the reducer's ordinary epilogue, which owns // release for the resumed frame; no settled-Priority convergence here. + let wait = *wait; state.waiting_for = wait.clone(); return Ok(wait); } @@ -219,6 +220,7 @@ pub(super) fn handle_opponent_may_choice( // its own frame's settled batch would sit undrained until some later // action — that is exactly the gap the settled wrapper closes. Every // other owner keeps its queue and returns the frame's wait unchanged. + let wait = *wait; let wait = if settled_direct_priority_root { engine_priority::run_post_action_pipeline_from_settled_priority( state, diff --git a/crates/engine/src/game/triggers.rs b/crates/engine/src/game/triggers.rs index f015eab976..34932b2878 100644 --- a/crates/engine/src/game/triggers.rs +++ b/crates/engine/src/game/triggers.rs @@ -8624,7 +8624,7 @@ pub(crate) fn finish_accepted_triggered_mana_action( // A tail member paused and installed its own carrier naming the same // outer continuation; that carrier is the authority now. return Ok(TriggeredManaReadiness::Resumed { - wait: state.waiting_for.clone(), + wait: Box::new(state.waiting_for.clone()), settled_direct_priority_root: false, }); } @@ -8650,7 +8650,7 @@ pub(crate) fn finish_accepted_triggered_mana_action( && !super::casting::mana_ability_cost_payment_is_paused(state) && resolution_completion_can_settle(state); Ok(TriggeredManaReadiness::Resumed { - wait, + wait: Box::new(wait), settled_direct_priority_root, }) } @@ -8670,7 +8670,7 @@ pub(crate) enum TriggeredManaReadiness { /// `run_post_action_pipeline_from_settled_priority`; every other owner /// returns `wait` unchanged. Resumed { - wait: WaitingFor, + wait: Box, settled_direct_priority_root: bool, }, } From d4122c8b707de935bee07dbf00f09b08f0443a90 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 21:04:56 -0700 Subject: [PATCH 07/11] fix(PR-7332): keep reflexive timestamps loop-stable --- crates/engine/src/game/effects/mod.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 25f49b6e07..6afaca3558 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -2307,8 +2307,6 @@ fn build_reflexive_pending_trigger( .description .clone() .or_else(|| parent.and_then(|parent| parent.description.clone())); - let timestamp = u32::try_from(state.next_timestamp()).unwrap_or(u32::MAX); - crate::game::triggers::PendingTrigger { source_id, controller, @@ -2324,7 +2322,10 @@ fn build_reflexive_pending_trigger( die_result: state.die_result_this_resolution, provenance: None, ability: Box::new(ability), - timestamp, + // CR 603.3b: reflexives from one collection share a turn key; the + // stable APNAP sort preserves their collection order without consuming + // the global timestamp allocator. + timestamp: state.turn_number, } } @@ -13998,6 +13999,8 @@ mod tests { #[test] fn reflexive_constructor_preserves_unassigned_distribution_metadata() { let mut state = GameState::new_two_player(42); + state.turn_number = 7; + state.next_timestamp = 41; let mut reflexive = reflexive_counter_ability(ObjectId(100)); reflexive.distribute = Some(crate::types::game_state::DistributionUnit::Counters( "+1/+1".to_string(), @@ -14008,6 +14011,8 @@ mod tests { assert_eq!(pending.distribute, reflexive.distribute); assert_eq!(pending.ability.distribute, reflexive.distribute); assert!(pending.ability.condition.is_none()); + assert_eq!(pending.timestamp, state.turn_number); + assert_eq!(state.next_timestamp, 41); } // CR 608.2h (#6486): Volcanic Vision — "Return target instant or sorcery card From 209688c929d489bc1b7f37732eb6886b1b105486 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 21:09:44 -0700 Subject: [PATCH 08/11] fix(PR-7332): normalize deferred trigger timestamps for loops Keep live CR 603.3b timestamp allocation for APNAP ordering and canonicalize PendingTrigger timestamps only in CR 104.4b loop snapshots. Add loop-equality and same-controller ordering regressions. --- crates/engine/src/game/effects/mod.rs | 27 ++++++++++--- crates/engine/src/types/game_state.rs | 55 ++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 6afaca3558..a3b006412d 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -2307,6 +2307,9 @@ fn build_reflexive_pending_trigger( .description .clone() .or_else(|| parent.and_then(|parent| parent.description.clone())); + // CR 603.3b: a reflexive joins the same APNAP ordering machinery as every + // other pending trigger, so it needs its own live ordering timestamp. + let timestamp = u32::try_from(state.next_timestamp()).unwrap_or(u32::MAX); crate::game::triggers::PendingTrigger { source_id, controller, @@ -2322,10 +2325,7 @@ fn build_reflexive_pending_trigger( die_result: state.die_result_this_resolution, provenance: None, ability: Box::new(ability), - // CR 603.3b: reflexives from one collection share a turn key; the - // stable APNAP sort preserves their collection order without consuming - // the global timestamp allocator. - timestamp: state.turn_number, + timestamp, } } @@ -14011,8 +14011,23 @@ mod tests { assert_eq!(pending.distribute, reflexive.distribute); assert_eq!(pending.ability.distribute, reflexive.distribute); assert!(pending.ability.condition.is_none()); - assert_eq!(pending.timestamp, state.turn_number); - assert_eq!(state.next_timestamp, 41); + assert_eq!(pending.timestamp, 41); + assert_eq!(state.next_timestamp, 42); + } + + #[test] + fn reflexive_construction_preserves_same_controller_ordering_timestamps() { + let mut state = GameState::new_two_player(42); + state.next_timestamp = 41; + let reflexive = reflexive_counter_ability(ObjectId(100)); + + let first = build_reflexive_pending_trigger(&mut state, &reflexive, None); + let second = build_reflexive_pending_trigger(&mut state, &reflexive, None); + + assert!( + first.timestamp < second.timestamp, + "CR 603.3b same-controller ordering must retain the distinct live timestamps the trigger sorter consumes" + ); } // CR 608.2h (#6486): Volcanic Vision — "Return target instant or sorcery card diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 7afe54ea7a..f4e199d2d3 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -21784,16 +21784,25 @@ impl GameState { ability.clear_trigger_identity_recursive(); } } + // `PendingTrigger::timestamp` is a live CR 603.3b ordering key. It is + // monotonic allocation history, not a difference in the recurring game + // position, so normalize it only in this CR 104.4b snapshot. Keeping + // the live values preserves APNAP/same-controller ordering in + // `game::triggers`. + let normalize_pending_trigger = |pending: &mut crate::game::triggers::PendingTrigger| { + pending.timestamp = 0; + pending.ability.clear_trigger_identity_recursive(); + }; if let Some(pt) = clone.pending_trigger.as_mut() { - pt.ability.clear_trigger_identity_recursive(); + normalize_pending_trigger(pt); } for ctx in clone.deferred_triggers.iter_mut() { - ctx.pending.ability.clear_trigger_identity_recursive(); + normalize_pending_trigger(&mut ctx.pending); } if let Some(order) = clone.pending_trigger_order.as_mut() { for group in order.groups.iter_mut() { for ctx in group.triggers.iter_mut() { - ctx.pending.ability.clear_trigger_identity_recursive(); + normalize_pending_trigger(&mut ctx.pending); } } } @@ -21808,17 +21817,13 @@ impl GameState { // normalization keeps that a non-load-bearing coincidence rather than a // hidden precondition of CR 104.4b detection. if let Some(resume) = clone.pending_triggered_mana_resume.as_mut() { - resume - .current - .pending - .ability - .clear_trigger_identity_recursive(); + normalize_pending_trigger(&mut resume.current.pending); for ctx in resume.accepted_tail.iter_mut() { - ctx.pending.ability.clear_trigger_identity_recursive(); + normalize_pending_trigger(&mut ctx.pending); } for batch in resume.collected_batches.iter_mut() { for ctx in batch.contexts.iter_mut() { - ctx.pending.ability.clear_trigger_identity_recursive(); + normalize_pending_trigger(&mut ctx.pending); } } resume.rules_execution_node = @@ -27775,6 +27780,36 @@ mod tests { ); } + /// CR 104.4b: deferred-trigger timestamps are CR 603.3b scheduling history, + /// not a changed recurring position. Their live values must remain distinct + /// for ordering, while loop snapshots compare the same pending trigger + /// regardless of the allocator position that produced it. + #[test] + fn normalize_for_loop_canonicalizes_deferred_trigger_timestamps() { + let mut first = GameState::new_two_player(7); + let source = ObjectId(90_001); + first + .deferred_triggers + .push(PendingTriggerContext::single(ordinary_pending_trigger( + source, 11, + ))); + let mut later = first.clone(); + later.deferred_triggers[0].pending.timestamp = 97; + + assert_ne!( + first, later, + "fixture must differ in the Eq-compared deferred scheduling timestamp" + ); + let first_normalized = first.normalize_for_loop(); + let later_normalized = later.normalize_for_loop(); + assert_eq!(first_normalized.deferred_triggers[0].pending.timestamp, 0); + assert_eq!(later_normalized.deferred_triggers[0].pending.timestamp, 0); + assert!( + loop_states_equal(&first_normalized, &later_normalized), + "the volatile deferred ordering timestamp must not hide a recurring state" + ); + } + #[test] fn loop_states_equal_distinguishes_stack_trigger_firing() { let mut ordinary = GameState::new_two_player(7); From 60b4f81dedf82b1eb57854f5c4a4b70a9a977956 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 21:12:57 -0700 Subject: [PATCH 09/11] fix(PR-7332): update named-choice fixture --- crates/engine/src/game/triggers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/engine/src/game/triggers.rs b/crates/engine/src/game/triggers.rs index 34932b2878..a7ba7da9dd 100644 --- a/crates/engine/src/game/triggers.rs +++ b/crates/engine/src/game/triggers.rs @@ -34252,6 +34252,7 @@ pub mod tests { options: vec!["P2".to_string()], source: None, persist_player: None, + free_entry: None, }, WaitingFor::OptionalEffectChoice { player: PlayerId(0), From 1d46ad8f352452b10306c5fd00495525d61262f9 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 22:09:16 -0700 Subject: [PATCH 10/11] test(PR-7332): correct prompt census pin --- crates/engine/src/game/engine.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 510f8278d8..9db0e1e1a1 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -18163,7 +18163,7 @@ mod stage2_injector_tests { assert_eq!( producers.len() + readers.len() + in_test, - 42, + 41, "CR 603.5 prompt census drifted. A new PRODUCER must have its recipient bound \ somewhere — the mint's conjunct (a) covers exactly ONE of them. A new READER is \ the benign case (U4's own consumption arm was one): adjudicate it in this doc and \ @@ -18192,9 +18192,9 @@ mod stage2_injector_tests { // partition assert below before this total could absorb it. assert_eq!( (producers.len(), readers.len(), in_test), - (5, 8, 29), + (5, 8, 28), "the partition, not just the total: five PRODUCTION producers, eight PRODUCTION \ - readers (they read `state.waiting_for` and never write it), 29 `#[cfg(test)]` lines.\nproducers={producers:#?}\n\ + readers (they read `state.waiting_for` and never write it), 28 `#[cfg(test)]` lines.\nproducers={producers:#?}\n\ readers={readers:#?}" ); assert_eq!( From e140b252c6b8b48c0c2c44919d5c211a27a8fe27 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Sat, 15 Aug 2026 23:05:07 -0700 Subject: [PATCH 11/11] test(PR-7332): refresh prompt census pins --- crates/engine/src/game/engine.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 9db0e1e1a1..6617467c17 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -18673,9 +18673,9 @@ mod stage2_injector_tests { // trust looks like — and it is why the two prose entries are BOTH kept // rather than one overwriting the other: they are separate witnesses, not // duplicates. - "game/effects/mod.rs:6922".to_string(), - "game/effects/mod.rs:6999".to_string(), - "game/effects/mod.rs:10237".to_string(), + "game/effects/mod.rs:6923".to_string(), + "game/effects/mod.rs:7000".to_string(), + "game/effects/mod.rs:10238".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.