From 71b4e86b5548f54b873c8f3d891394cc825a18a1 Mon Sep 17 00:00:00 2001 From: lgray Date: Sat, 15 Aug 2026 21:10:28 -0500 Subject: [PATCH 1/7] fix(engine): publish the population an event-less producer froze (#6857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Effect::PumpAll`, `Effect::GoadAll` and `Effect::GiveControl` affect objects without moving them and without emitting any per-object event, so the chain publish site fell through to the `ZoneChanged` harvest and published an EMPTY tracked set. CR 611.2c makes that the WRONG set rather than merely an unhelpful one — the set of objects a resolution-generated continuous effect modifies is determined when the effect begins — so a following "Untap those creatures" (CR 701.26b) bound nothing. Jeskai Ascendancy's loot-and-untap did not untap. Engine half: three new arms in `affected_objects_from_events`, each publishing through the producing resolver's OWN enumeration function rather than a second hand-written scan at the publish site — `pump::pump_all_affected_objects` (extracted from `resolve_all`), `goad::goad_targets`, `gain_control::give_control_object_targets`. Be precise about what that buys: the pump helper is handed the head filter and re-runs the scan against a later `state`, so the two enumerations agree because they are the same code over an unchanged board. The precondition is that nothing flushes layers between resolution and publish — documented at the helper, along with why its unit test (a controller filter) would not catch a future flush. The event stream is deliberately NOT the authority: a `GiveControl` target the recipient already controls emits no `ControllerChanged` yet is still one of "those creatures" (CR 608.2c). Each arm is gated on `is_sole_chain_producer`: no EARLIER producer contributed (test the set's CONTENTS, not the id) and no LATER node is itself in publisher position. CR 608.2c's nearest-antecedent binding — in Outlaws' Fury the anaphor names the later exile, not the pumped creatures. A declined arm falls through to the `_ =>` harvest, which is empty for this class, i.e. byte-identical to before. Parser half: `patch_population_head_tap_anaphor` widens from `PutCounterAll` to every head that freezes a broadcast population (`PumpAll`, `GenericEffect`), and rebinds `TriggeringSource` alongside `SelfRef`/`ParentTarget`, so an implicit "Untap them." lowers to the published set whichever resolver produced the placeholder. 16 cards fixed, measured by env-toggled revert arms from one binary: disabling the engine arms flips 15 rows, disabling the parser rewrite flips 7, 5 rows need both, and the 17th flipping row (Trystan's Command) changes only the tracked set's shape and not the board. Each guard leg has a named revert witness that turns red when the leg is deleted: Surge to Victory and Trystan's Command for leg 1, Outlaws' Fury for leg 2. Three bounds documented in code rather than left for the next reader to find: leg 2 is chain-wide, not nearest-antecedent, so `head -> consumer -> consumer2` declines entirely (zero rows today; Motivated Pony's `Unimplemented { "they" }` tail is the loaded gun, named at the guard); a `player_scope` fan-out hides a later producer from leg 2 because the tail is already detached (0 of 627 event-less heads carry one); and a future head whose SOLE consumer is a `CreateDelayedTrigger` would regress the delayed contextual bind — that row set is empty corpus-wide, so the guard for it was measured unexercisable and removed rather than shipped inert. Also updates the CR 603.5 prompt-producer census pins in `game/engine.rs` (`:6923/:7000/:10238` -> `:7044/:7121/:10359`), a uniform +121 equal to this branch's net insertion into `effects/mod.rs` (30,851 -> 30,972 lines). The new coordinates were located by DIGEST SEARCH rather than arithmetic: each upstream pin's 10-line producer block was hashed at `upstream/main`, then that digest was searched for in this tree -- `19cb8354`/`1e74c6f1`/`980120c2`, all three found, all three at +121. Those digests are unchanged from the pre-rebase measurement, so upstream modified none of the three producers. This branch writes `waiting_for` nowhere. Assisted-by: ClaudeCode:claude-opus-5 --- .../engine/src/game/effects/gain_control.rs | 13 +- crates/engine/src/game/effects/goad.rs | 11 +- crates/engine/src/game/effects/mod.rs | 121 ++ crates/engine/src/game/effects/pump.rs | 143 ++- .../engine/src/parser/oracle_effect/lower.rs | 88 +- ...kai_ascendancy_pump_untap_anaphora_6857.rs | 1141 +++++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 7 files changed, 1478 insertions(+), 40 deletions(-) create mode 100644 crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs diff --git a/crates/engine/src/game/effects/gain_control.rs b/crates/engine/src/game/effects/gain_control.rs index a06bb59790..955efd6427 100644 --- a/crates/engine/src/game/effects/gain_control.rs +++ b/crates/engine/src/game/effects/gain_control.rs @@ -311,7 +311,18 @@ pub fn resolve_give( Ok(()) } -fn give_control_object_targets( +/// CR 611.2c: the objects whose controller this effect changes, fixed when the +/// control-change continuous effect begins. +/// +/// SINGLE AUTHORITY: `resolve_give` hands control over exactly this list, and +/// `effects::affected_objects_from_events` publishes exactly this list as the +/// chain tracked set. The `ControllerChanged` event is deliberately NOT the +/// authority: `resolve_give` emits it only when the controller actually changed, +/// while CR 608.2c makes "those creatures" name the objects the earlier text +/// named — Domineering Will's "up to three target nonattacking creatures … Untap +/// those creatures" must untap a target the recipient already controlled, which +/// produces no event. +pub(crate) fn give_control_object_targets( state: &GameState, ability: &ResolvedAbility, filter: &TargetFilter, diff --git a/crates/engine/src/game/effects/goad.rs b/crates/engine/src/game/effects/goad.rs index 3697ae1e58..1c6e9c172b 100644 --- a/crates/engine/src/game/effects/goad.rs +++ b/crates/engine/src/game/effects/goad.rs @@ -43,7 +43,16 @@ pub fn resolve( Ok(()) } -fn goad_targets(state: &GameState, ability: &ResolvedAbility) -> Vec { +/// CR 701.15a: the creatures this effect goads. +/// +/// SINGLE AUTHORITY: `resolve` marks exactly this list, and +/// `effects::affected_objects_from_events` publishes exactly this list as the +/// chain tracked set, so a downstream "those creatures can't block" or "for each +/// creature goaded this way" binds the creatures actually goaded. Goading emits +/// no per-object event, so the publish site has nothing to harvest — and +/// re-enumerating the head filter there would be a second authority rather than +/// the producer's own. +pub(crate) fn goad_targets(state: &GameState, ability: &ResolvedAbility) -> Vec { if let Effect::GoadAll { target } = &ability.effect { let effective_filter = crate::game::effects::resolved_object_filter(ability, target); let ctx = FilterContext::from_ability(ability); diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 87ada1ad68..196b30d649 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -5348,6 +5348,81 @@ pub(crate) fn chain_references_tracked_set(ability: &ResolvedAbility) -> bool { ability_or_branch_references_tracked_set(ability) } +/// CR 608.2c + CR 611.2c: An event-less producer publishes the population its +/// continuous effect froze ONLY when it is the resolution chain's sole +/// EFFECTIVE producer. +/// +/// [`publish_tracked_set`] unifies every publisher in a chain into one set, +/// which is right for same-verb compounds (Suspend Aggression's two exiles) and +/// wrong for a mixed chain: in "Creatures you control get +2/+0 … exile the top +/// card of your library. … you may play that card" (Outlaws' Fury) the anaphor +/// names ONLY the exile. CR 608.2c's "apply the rules of English" is +/// nearest-antecedent binding: a head that is followed by another producer in +/// the same chain is not the antecedent. +/// +/// Two conditions, both cheap and exact: +/// * no EARLIER producer contributed — an ancestor that published an EMPTY set +/// (an `Unimplemented` root) is not a producer, so test contents, not the id; +/// * no LATER node in this chain is itself in publisher position — the same +/// `next_sub_needs_tracked_set` predicate the publish site is gated on. +/// +/// When the guard declines, the arm falls through to the `_ =>` `ZoneChanged` +/// harvest, which yields `[]` for every head in this class (they emit no +/// `ZoneChanged`) — i.e. byte-identical to the pre-#6857 engine. +/// +/// KNOWN GAP, unexercised today: under a `player_scope` fan-out the publish +/// site hands `affected_objects_with_causes` the `scoped_template`, whose tail +/// `split_player_scope_chain` has already DETACHED, while the surrounding gate +/// reads the full `ability`. Leg 2 therefore cannot see a later producer that +/// lives in the detached tail, and would let the head publish where the +/// undetached chain would have declined. Measured unreachable at the time of +/// writing: 0 of the 627 event-less heads in the corpus carry a `player_scope`. +/// If one ever does, leg 2 needs the pre-split ability, not the template. +fn is_sole_chain_producer(state: &GameState, ability: &ResolvedAbility) -> bool { + let no_earlier_producer = state.chain_tracked_set_id.is_none_or(|id| { + state + .tracked_object_sets + .get(&id) + .is_none_or(|set| set.is_empty()) + }); + no_earlier_producer && !later_node_is_publisher_position(ability) +} + +/// Any strictly-later node of this chain that the publish site would itself +/// gate on. Walks `sub_ability` AND `else_ability` (conservative: only one +/// branch executes, so counting both can only DECLINE a publish, never cause a +/// wrong one). +/// +/// CHAIN-WIDE, NOT NEAREST-ANTECEDENT — and that is the sharp edge. Any later +/// publisher position anywhere below this node declines the head, including the +/// shape `head -> consumer{TrackedSet} -> consumer2{TrackedSet}`, where BOTH +/// consumers wanted this head's population and both get nothing. No corpus row +/// hits it today, but the loaded gun is named: **Motivated Pony**'s third node +/// is `Unimplemented { name: "they" }` ("and they get an additional +2/+2"). +/// The day that clause parses into any tracked-set consumer, this leg declines +/// the head, the untap regresses, and +/// `motivated_pony_untaps_only_the_attacking_creatures_it_pumped` goes red. The +/// fix at that point is to scope the leg to the nearest antecedent (CR 608.2c's +/// actual rule) rather than to relax the test. +fn later_node_is_publisher_position(ability: &ResolvedAbility) -> bool { + fn walk(node: Option<&ResolvedAbility>) -> bool { + node.is_some_and(|n| { + // CR 603.7: production's own predicate, unmodified — a node whose + // consumer merely DEFERS (a `CreateDelayedTrigger + // { uses_tracked_set: true }`, which acts at a later time) still + // counts as a publisher position here. Excluding deferring consumers + // from this leg would let a head publish across a `CopyTokenOf` + + // delayed-exile chain (Twinflame, Myra the Magnificent), putting the + // ORIGINAL creature into the set the delayed "exile those tokens" + // then binds — measured, not predicted. + next_sub_needs_tracked_set(n) + || walk(n.sub_ability.as_deref()) + || walk(n.else_ability.as_deref()) + }) + } + walk(ability.sub_ability.as_deref()) +} + fn ability_or_branch_references_tracked_set(ability: &ResolvedAbility) -> bool { let consumes = matches!( &ability.effect, @@ -5887,6 +5962,52 @@ fn affected_objects_from_events( .copied() .collect() } + // CR 611.2c (issue #6857): the set of objects a resolution-generated + // continuous effect modifies is determined when that effect BEGINS and + // never changes afterwards, so the population these heads froze is the + // antecedent a following "those creatures" names (CR 608.2c). Unlike + // every other producer here they move nothing and emit no per-object + // event, so without their own arm the `_ =>` `ZoneChanged` harvest + // publishes an EMPTY set — the WRONG set, not merely an unhelpful one — + // and "Untap those creatures" (CR 701.26b) binds nothing. + // + // The published population is the PRODUCING RESOLVER'S OWN enumeration, + // never a re-enumeration of the head filter and never the emitted + // events. A re-enumeration is a second authority that can disagree with + // the first (a mass pump whose head filter is `Any` would name the whole + // battlefield), and the event stream is incomplete by construction (a + // `GiveControl` target the recipient already controls emits no + // `ControllerChanged`). + // + // IMPLEMENTATION NOTE — why reading the post-resolution board here is + // still the resolution-time population, and why no CR is cited for it: + // CR 613.1 says continuous effects apply in layers CONTINUOUSLY, which + // would predict that a filter reading CURRENT power sees the pumped + // values. It does not, for a purely mechanical reason — + // `GameState::add_transient_continuous_effect` only INSTALLS the effect + // and marks the layer cache dirty; `layers::flush_layers` materialises, + // and nothing flushes between this node's resolve and this publish. The + // same holds for a controller change (`ContinuousModification:: + // ChangeController` goes through the identical install path), so this is + // uniform rather than a P/T special case. + // + // CR 704.4 + CR 704.3 cover the SEPARATE point that no state-based + // action and no priority intervene between the resolver and this + // publish. + Effect::PumpAll { target, .. } if is_sole_chain_producer(state, ability) => { + pump::pump_all_affected_objects(state, ability, target) + } + // CR 701.15a: the creatures actually goaded. + Effect::GoadAll { .. } if is_sole_chain_producer(state, ability) => { + goad::goad_targets(state, ability) + } + // CR 611.2c covers a controller change in the same sentence it covers a + // characteristic change, so `GiveControl` publishes on the identical + // rule — Domineering Will's "up to three target nonattacking creatures + // … Untap those creatures" (CR 608.2c) names the declared targets. + Effect::GiveControl { target, .. } if is_sole_chain_producer(state, ability) => { + gain_control::give_control_object_targets(state, ability, target) + } Effect::GainControl { .. } => fallback_targets .iter() .filter_map(|target| match target { diff --git a/crates/engine/src/game/effects/pump.rs b/crates/engine/src/game/effects/pump.rs index 4044be561b..1b1674be71 100644 --- a/crates/engine/src/game/effects/pump.rs +++ b/crates/engine/src/game/effects/pump.rs @@ -96,14 +96,6 @@ pub fn resolve_all( _ => return Ok(()), }; - // CR 608.2c: Concretize contextual filters against this ability's inherited - // target before the mass scan — e.g. `Not{ParentTarget}` from a "target X and - // all other X with the same name get -N/-M" chain (Bile Blight) becomes - // `Not{SpecificObject{target}}`, excluding the already-pumped target so it is - // not shrunk twice. No-op for filters without contextual refs, mirroring the - // single `resolve` (above) and `destroy`/`bounce`. - let target_filter = crate::game::effects::resolved_object_filter(ability, &target_filter); - let dur = ability.duration.clone().unwrap_or(Duration::UntilEndOfTurn); // CR 608.2h + CR 613.4c: same recipient-relative parity as `resolve` — an @@ -115,14 +107,7 @@ pub fn resolve_all( let shared = (!per_recipient).then(|| pt_modifications(power, toughness, state, ability, None)); // Collect matching object IDs first to avoid borrow conflicts. - // CR 107.3a + CR 601.2b: ability-context filter evaluation. - let ctx = filter::FilterContext::from_ability(ability); - let matching: Vec = state - .battlefield - .iter() - .filter(|id| filter::matches_target_filter(state, **id, &target_filter, &ctx)) - .copied() - .collect(); + let matching = pump_all_affected_objects(state, ability, &target_filter); for obj_id in matching { let modifications = match &shared { @@ -148,6 +133,69 @@ pub fn resolve_all( Ok(()) } +/// CR 611.2c: the population `Effect::PumpAll` affects, fixed at the moment its +/// continuous effect begins and never changing afterwards. +/// +/// SINGLE AUTHORITY: `resolve_all` installs the transient effects over exactly +/// this list, and `effects::affected_objects_from_events` publishes exactly this +/// list as the chain tracked set, so a later "Untap those creatures" +/// (CR 701.26b) binds the creatures actually pumped. Both call sites go through +/// this ONE enumeration function — never a second, hand-written scan at the +/// publish site. Be precise about what that does and does not buy: this +/// function IS handed the head filter and DOES re-run the scan against a later +/// `state`, so the two enumerations agree because they are the same code over +/// an unchanged board, not because the list was memoised. +/// +/// FLUSH HAZARD — the load-bearing precondition. The board must not change +/// between `resolve_all` and the publish, and in particular the pump's own +/// modifications must not have MATERIALISED. They do not today: +/// `GameState::add_transient_continuous_effect` installs the effect and marks +/// the layer cache dirty, while materialisation happens in +/// `layers::flush_layers`. The precise claim — the falsifiable one is weaker +/// than it looks — is NOT that `flush_layers` is never called during +/// resolution: several effect resolvers call it (`amass`, `attach`, +/// `become_copy`, among others), and a reader who greps for it will find them. +/// It is that nothing flushes on the SPAN between this node's own +/// `resolve_effect` and this node's publish, because the publish site runs +/// immediately after that call with no other resolver in between. Introduce +/// a flush on that span and a filter that reads a modified characteristic +/// (power, toughness, controller, a granted type) would enumerate DIFFERENTLY +/// at the publish site than at resolution — the published set would silently +/// stop being the frozen population. If you add such a flush, snapshot this +/// list at resolution instead of re-scanning. +/// +/// The unit test in this module pins the identity for a CONTROLLER filter, +/// which no pump modification can move, so it would NOT catch that regression. +/// A discriminating test for the flush hazard needs a filter reading a pumped +/// characteristic (e.g. "creatures with power 2 or less"). +/// +/// NOTE for a future zone-aware `resolve_all`: this scans `state.battlefield`, +/// so Elvish Elegy's `InZone: Graveyard` filter returns `[]` today. That is NOT +/// what keeps its milled tracked set intact — the `is_sole_chain_producer` +/// guard at the publish site does, because the preceding `Mill` already +/// published. Making this zone-aware is therefore safe. +pub(crate) fn pump_all_affected_objects( + state: &GameState, + ability: &ResolvedAbility, + target: &TargetFilter, +) -> Vec { + // CR 608.2c: concretize contextual filters against this ability's inherited + // target before the mass scan — e.g. `Not{ParentTarget}` from a "target X and + // all other X with the same name get -N/-M" chain (Bile Blight) becomes + // `Not{SpecificObject{target}}`, excluding the already-pumped target so it is + // not shrunk twice. No-op for filters without contextual refs, mirroring the + // single `resolve` and `destroy`/`bounce`. + let target_filter = crate::game::effects::resolved_object_filter(ability, target); + // CR 107.3a + CR 601.2b: ability-context filter evaluation. + let ctx = filter::FilterContext::from_ability(ability); + state + .battlefield + .iter() + .filter(|id| filter::matches_target_filter(state, **id, &target_filter, &ctx)) + .copied() + .collect() +} + /// CR 701.10a: "Doubling a creature's power and/or toughness creates a continuous effect." /// CR 701.10b: "To double a creature's power, that creature gets +X/+0, /// where X is that creature's power as the spell or ability resolves." @@ -511,6 +559,69 @@ mod tests { assert_eq!(state.objects[&opp].toughness, Some(3)); } + /// CR 611.2c (issue #6857): `pump_all_affected_objects` is the SINGLE + /// AUTHORITY for the population `resolve_all` freezes — the publish site + /// republishes this exact list rather than re-deriving it. + /// + /// Two properties, both parameter-level rather than card-level: + /// * the helper's list IS the pumped set, for whatever filter it is handed + /// (identity between publisher and producer); + /// * the filter is honoured, and a BROADER filter genuinely widens it — so + /// a `target: Any` head (the mass-pump shape whose head filter names the + /// whole battlefield) proves why the publish site may not substitute its + /// own re-enumeration for the resolver's. + #[test] + fn pump_all_affected_objects_is_the_filtered_population_that_resolve_all_pumps() { + let mut state = GameState::new_two_player(42); + let mine_a = make_creature(&mut state, "Mine A", 2, 2, PlayerId(0)); + let mine_b = make_creature(&mut state, "Mine B", 1, 1, PlayerId(0)); + let theirs = make_creature(&mut state, "Theirs", 3, 3, PlayerId(1)); + + let yours: TargetFilter = TypedFilter::creature() + .controller(ControllerRef::You) + .into(); + let ability = ResolvedAbility::new( + Effect::PumpAll { + power: PtValue::Fixed(1), + toughness: PtValue::Fixed(1), + target: yours.clone(), + }, + vec![], + ObjectId(100), + PlayerId(0), + ); + + let mut affected = pump_all_affected_objects(&state, &ability, &yours); + affected.sort(); + let mut expected = vec![mine_a, mine_b]; + expected.sort(); + assert_eq!( + affected, expected, + "the controller filter must survive: the opponent's creature is not in the frozen population" + ); + + // The same filter under a broader head names strictly more — the + // enumeration is parameterized, not a fixed battlefield scan. + let mut broad = pump_all_affected_objects(&state, &ability, &TargetFilter::Any); + broad.sort(); + let mut all = vec![mine_a, mine_b, theirs]; + all.sort(); + assert_eq!(broad, all); + assert_ne!( + affected, broad, + "if a broad filter returned the same list, the filter argument would be inert \ + and this test could not detect a publish-site re-enumeration" + ); + + // Identity with the producer: exactly the helper's list gets pumped. + let mut events = Vec::new(); + resolve_all(&mut state, &ability, &mut events).unwrap(); + evaluate_layers(&mut state); + assert_eq!(state.objects[&mine_a].power, Some(3)); + assert_eq!(state.objects[&mine_b].power, Some(2)); + assert_eq!(state.objects[&theirs].power, Some(3), "not pumped"); + } + /// Issue #4727 (CR 611.2a): "Target creature and all other creatures with the /// same name as that creature get -N/-M" (Bile Blight). The mass /// `PumpAll{ And[ SameNameAsParentTarget, Not{ParentTarget} ] }` sub-ability diff --git a/crates/engine/src/parser/oracle_effect/lower.rs b/crates/engine/src/parser/oracle_effect/lower.rs index 39fc9309f5..6863b97e91 100644 --- a/crates/engine/src/parser/oracle_effect/lower.rs +++ b/crates/engine/src/parser/oracle_effect/lower.rs @@ -51,11 +51,12 @@ use crate::types::zones::{EtbTapState, Zone}; // These are private to oracle_effect but accessible here as a descendant module. use super::subject; use super::{ - each_target_filter_mut, has_typed_target, parse_effect_clause, + each_target_filter_mut, has_typed_target, is_broadcast_population_filter, parse_effect_clause, parse_event_context_ref_with_ctx, parse_for_each_object_copy_parts, refine_damage_target_remainder, replace_player_anaphor_with_parent_target, scan_contains_phrase, target_filter_controller_ref, }; +use crate::game::effects::effect::generic_effect_application_filter; pub(super) fn rewrite_player_anaphor_targets_in_definition(def: &mut AbilityDefinition) { replace_player_anaphor_with_parent_target(def.effect.as_mut()); @@ -232,31 +233,57 @@ pub(super) fn patch_self_ref_head_tap_anaphor(def: &mut AbilityDefinition) { walk(def, false); } -/// CR 608.2c + CR 122.1: After a mass counter placement (`PutCounterAll`), a -/// chained "then untap them" continuation refers to the set of objects that -/// received counters (Lulu, Loyal Hollyphant). Phase-trigger bodies carry +/// CR 608.2c: After a head that FREEZES a broadcast population, a chained +/// "then untap them" continuation refers to that population (Lulu, Loyal +/// Hollyphant; Jeskai Ascendancy, issue #6857). Phase-trigger bodies carry /// `ctx.subject = Any`, so `resolve_it_pronoun` wrongly binds "them" to -/// `SelfRef` (the trigger source). Rewrite to `TrackedSet(0)` so the runtime -/// binds the published counter set via `affected_objects_from_events`. Sibling -/// of [`patch_self_ref_head_tap_anaphor`] for the population-head / plural- -/// anaphor polarity. +/// `SelfRef` (the trigger source), and a spell body defaults it to +/// `ParentTarget`. Rewrite to `TrackedSet(0)` so the runtime binds the +/// published population via `affected_objects_from_events`. Sibling of +/// [`patch_self_ref_head_tap_anaphor`] for the population-head / plural-anaphor +/// polarity. pub(super) fn patch_population_head_tap_anaphor(def: &mut AbilityDefinition) { - fn is_population_counter_publisher(effect: &Effect) -> bool { - matches!( - effect, - Effect::PutCounterAll { target, .. } - if !matches!( - target, - TargetFilter::SelfRef - | TargetFilter::ParentTarget - | TargetFilter::TriggeringSource - | TargetFilter::CostPaidObject - ) - ) + /// CR 608.2c + CR 611.2c: heads that freeze a broadcast population at + /// resolution and publish it as the chain tracked set. Mirrors the + /// publishers in `game/effects/mod.rs::affected_objects_from_events`: + /// * `PutCounterAll` -> `CounterAdded` events (CR 122.1 — counters are + /// the signal for THIS leg only) (Lulu, The Fifth Doctor) + /// * `PumpAll` -> `pump::pump_all_affected_objects` (issue #6857) + /// * `GenericEffect` -> filter re-enumeration (issue #6682) + /// + /// The broadcast test is `is_broadcast_population_filter`, NOT the + /// runtime's `generic_effect_affected_uses_inherited_targets`: the latter + /// does not exclude `SelfRef`, and using it here would rewrite self-scoped + /// grants whose `SelfRef`/`ParentTarget` tail is already correct. + fn is_population_publisher(effect: &Effect) -> bool { + match effect { + Effect::PutCounterAll { target, .. } | Effect::PumpAll { target, .. } => { + is_broadcast_population_filter(target) + } + Effect::GenericEffect { + static_abilities, + target, + .. + } => static_abilities + .iter() + .find(|sd| { + matches!( + sd.mode, + StaticMode::Continuous + | StaticMode::MustAttack + | StaticMode::MustAttackDefender { .. } + ) + }) + .and_then(|sd| { + generic_effect_application_filter(target.as_ref(), sd.affected.as_ref()) + }) + .is_some_and(is_broadcast_population_filter), + _ => false, + } } fn walk(def: &mut AbilityDefinition, carried_population: bool) { - let active_population = if is_population_counter_publisher(&def.effect) { + let active_population = if is_population_publisher(&def.effect) { true } else { match def.effect.target_filter() { @@ -272,7 +299,24 @@ pub(super) fn patch_population_head_tap_anaphor(def: &mut AbilityDefinition) { .. } = sub.effect.as_mut() { - if matches!(target, TargetFilter::SelfRef | TargetFilter::ParentTarget) { + // CR 608.2c: under a broadcast-population head the anaphor's + // antecedent is that frozen population, whichever resolver + // produced the placeholder — the spell-body default + // `ParentTarget` (`oracle_target::resolve_pronoun_target`), + // the self-subject trigger default `SelfRef`, or the + // named-subject trigger default `TriggeringSource` + // (`oracle_effect::resolve_it_pronoun`). All three name a + // SINGLE referent, and a head that has just frozen a + // population is the only live antecedent, so all three + // rebind to the published set. `scope: Single` stays in the + // pattern above: a mass "untap all …" is a population filter + // in its own right, not an anaphor. + if matches!( + target, + TargetFilter::SelfRef + | TargetFilter::ParentTarget + | TargetFilter::TriggeringSource + ) { *target = TargetFilter::TrackedSet { id: crate::types::identifiers::TrackedSetId(0), }; diff --git a/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs new file mode 100644 index 0000000000..fd39c252b0 --- /dev/null +++ b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs @@ -0,0 +1,1141 @@ +//! Issue #6857 — event-less producers publish the population they froze. +//! +//! `Effect::PumpAll`, `Effect::GoadAll` and `Effect::GiveControl` affect objects +//! without moving them and without emitting any per-object event, so before this +//! change the chain publish site fell through to the `ZoneChanged` harvest and +//! published an EMPTY tracked set. CR 611.2c makes that the WRONG set, not just +//! an unhelpful one: the set of objects a resolution-generated continuous effect +//! modifies is fixed when the effect begins. A following "Untap those creatures" +//! (CR 701.26b) therefore bound nothing — Jeskai Ascendancy's loot-and-untap did +//! not untap. +//! +//! Every row here is measured on the shipped tree. The suite carries its own +//! anti-vacuity instruments: +//! +//! * `known_changed_control_*` — the row that MUST differ from the old +//! behaviour. If it ever passes trivially the whole file is meaningless. +//! * `negative_control_*` — a chain with no consumer at all: the arms must +//! invent no publish. +//! * `leg1_witness_*` / `leg2_witness_*` — each pins one leg of +//! `is_sole_chain_producer`. Deleting that leg from the engine must turn the +//! named test RED; a leg whose deletion changes nothing is vacuous. +//! * the PRESERVED rows assert the publish did NOT widen a filter or reach a +//! grant that declares its own target. +//! +//! Oracle text is verbatim at the branch base unless a deviation is called out +//! in the test's doc comment. + +use engine::game::combat::AttackTarget; +use engine::game::layers::evaluate_layers; +use engine::game::scenario::{GameRunner, GameScenario, P0, P1}; +use engine::types::ability::TargetRef; +use engine::types::actions::GameAction; +use engine::types::card_type::CoreType; +use engine::types::game_state::{BattlefieldEntryRecord, CastPaymentMode, GameState, WaitingFor}; +use engine::types::identifiers::ObjectId; +use engine::types::mana::ManaCost; +use engine::types::phase::Phase; + +/// Every tracked set, id-ordered, each as a sorted list of raw object ids. +fn tracked_sets(state: &GameState) -> Vec> { + let mut sets: Vec<(u64, Vec)> = state + .tracked_object_sets + .iter() + .map(|(id, members)| { + let mut ids: Vec = members.iter().map(|o| o.0).collect(); + ids.sort_unstable(); + (id.0, ids) + }) + .collect(); + sets.sort(); + sets.into_iter().map(|(_, ids)| ids).collect() +} + +/// The single chain tracked set's contents. Panics if the resolution published +/// more than one set — every row in this file is a one-producer chain, and a +/// second set would mean the guard let two producers through. +fn published_set(state: &GameState) -> Vec { + let sets = tracked_sets(state); + assert!( + sets.len() <= 1, + "expected at most one tracked set in a single-producer chain, got {sets:?}" + ); + sets.into_iter().next().unwrap_or_default() +} + +fn ids(objects: &[ObjectId]) -> Vec { + let mut raw: Vec = objects.iter().map(|o| o.0).collect(); + raw.sort_unstable(); + raw +} + +fn tapped(state: &GameState, id: ObjectId) -> bool { + state.objects[&id].tapped +} + +/// Debug rendering of every transient continuous effect that applies to `id`. +/// The continuous-effect list is the observable for the `GenericEffect` grants +/// (MustAttack / CantBlock / keyword grants) a mass head feeds; a +/// tracked-set-only projection previously scored a non-fix as a fix. +fn effects_on(state: &GameState, id: ObjectId) -> Vec { + state + .transient_continuous_effects + .iter() + .filter(|tce| tce.affected == engine::types::ability::TargetFilter::SpecificObject { id }) + .map(|tce| format!("{:?}", tce.modifications)) + .collect() +} + +fn grant_lands_on(state: &GameState, id: ObjectId, needle: &str) -> bool { + effects_on(state, id).iter().any(|m| m.contains(needle)) +} + +// =========================================================================== +// CONTROLS +// =========================================================================== + +/// KNOWN-CHANGED CONTROL — issue #6857's own card, cast as the printed card. +/// Jeskai Ascendancy's first trigger is `PumpAll -> SetTapState +/// { target: TrackedSet, Untap }`. Before the fix the published set was empty +/// and the creature stayed TAPPED; it must now be published and untapped. +/// +/// The real enchantment is used deliberately rather than a synthesized trigger +/// body: this is the control for #6857, so it should exercise #6857's card, on +/// its real trigger path, with the second (loot) trigger present. If this row +/// ever reads the same as the pre-fix engine, every "identical" reading +/// elsewhere in this file is meaningless. +#[test] +fn known_changed_control_jeskai_ascendancy_untaps_the_creatures_it_pumped() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let mine = scenario.add_creature(P0, "Mine", 3, 3).id(); + scenario.add_enchantment_from_oracle( + P0, + "Jeskai Ascendancy", + "Whenever you cast a noncreature spell, creatures you control get +1/+1 until end of turn. Untap those creatures.\nWhenever you cast a noncreature spell, you may draw a card. If you do, discard a card.", + ); + let bolt = scenario.add_bolt_to_hand(P0); + let mut runner: GameRunner = scenario.build(); + runner.state_mut().objects.get_mut(&mine).unwrap().tapped = true; + // Both of the printed card's triggers fire on the same cast, so the engine + // parks an APNAP ordering prompt (CR 603.3b) that the one-shot cast driver + // does not handle. Drive the cast by hand rather than trimming the card to + // dodge the prompt: the point of this control is that it uses #6857's card. + let card_id = runner.state().objects[&bolt].card_id; + runner + .act(GameAction::CastSpell { + object_id: bolt, + card_id, + targets: vec![], + payment_mode: CastPaymentMode::Auto, + }) + .expect("casting the bolt should be legal"); + // Drain the prompts the printed card creates: the bolt's own target, the + // CR 603.3b ordering prompt for the two simultaneous cast triggers, and the + // second trigger's "you may draw a card" (declined — this row is about the + // first trigger). + for _ in 0..32 { + match runner.state().waiting_for.clone() { + WaitingFor::TargetSelection { .. } => { + runner + .act(GameAction::ChooseTarget { + target: Some(TargetRef::Player(P1)), + }) + .expect("the bolt targets a player"); + } + WaitingFor::OrderTriggers { triggers, .. } => { + runner + .act(GameAction::OrderTriggers { + order: (0..triggers.len()).collect(), + }) + .expect("CR 603.3b: order the two cast triggers"); + } + WaitingFor::OptionalEffectChoice { .. } => { + runner + .act(GameAction::DecideOptionalEffect { accept: false }) + .expect("CR 608.2d: decline the loot trigger"); + } + WaitingFor::Priority { .. } if !runner.state().stack.is_empty() => { + runner + .act(GameAction::PassPriority) + .expect("passing priority resolves the top of the stack"); + } + _ => break, + } + } + runner.advance_until_stack_empty(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[mine])); + assert!( + !tapped(runner.state(), mine), + "CR 701.26b: 'those creatures' names the pumped population, so it untaps" + ); + assert_eq!(runner.state().objects[&mine].power, Some(4), "pump applied"); +} + +/// NEGATIVE CONTROL — a mass pump with no anaphor at all. The publish gate never +/// fires, so the new arms must invent no set. +#[test] +fn negative_control_mass_pump_without_a_consumer_publishes_nothing() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let mine = scenario.add_creature(P0, "Mine", 3, 3).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Bare Pump", + true, + "Creatures you control get +1/+1 until end of turn.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert!(tracked_sets(runner.state()).is_empty()); + assert!(runner.state().chain_tracked_set_id.is_none()); + assert_eq!(runner.state().objects[&mine].power, Some(4), "pump applied"); +} + +// =========================================================================== +// `PumpAll` — FIX rows +// =========================================================================== + +/// War Flare's second sentence pair — the plainest `PumpAll -> SetTapState +/// { TrackedSet }` shape in the corpus. +#[test] +fn war_flare_untaps_the_creatures_it_pumped() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 2, 2).id(); + let b = scenario.add_creature(P0, "Mine B", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "War Flare", + true, + "Creatures you control get +2/+1 until end of turn. Untap those creatures.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [a, b] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a, b])); + assert!(!tapped(runner.state(), a) && !tapped(runner.state(), b)); +} + +/// Gleam of Resistance — the REAL card, including its basic landcycling line, so +/// the fixture cannot be a simplified proxy of the shape under test (the +/// `Typecycling` keyword on the built object is the discriminator). +/// +/// The opponent's creature staying tapped is the load-bearing half: it proves +/// the published population kept the head filter's `controller: You` rather than +/// being widened to the whole battlefield. +#[test] +fn gleam_of_resistance_untaps_only_the_creatures_its_controller_filter_named() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 2, 2).id(); + let b = scenario.add_creature(P0, "Mine B", 2, 2).id(); + let theirs = scenario.add_creature(P1, "Theirs", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Gleam of Resistance", + true, + "Creatures you control get +1/+2 until end of turn. Untap those creatures.\nBasic landcycling {1}{W} ({1}{W}, Discard this card: Search your library for a basic land card, reveal it, put it into your hand, then shuffle.)", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [a, b, theirs] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + assert!( + format!("{:?}", runner.state().objects[&spell].keywords).contains("Typecycling"), + "fixture guard: the full printed card was built, not a pump-only proxy" + ); + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a, b])); + assert!(!tapped(runner.state(), a) && !tapped(runner.state(), b)); + assert!( + tapped(runner.state(), theirs), + "CR 611.2c: the frozen population is the head filter's, and it says 'you control'" + ); +} + +/// Zealous Display's untap carries `condition: Not(IsYourTurn)`, so it is cast on +/// the OPPONENT's turn. Cast on your own turn the sub never executes and the row +/// is vacuously identical to the pre-fix engine. +#[test] +fn zealous_display_untaps_on_the_opponents_turn() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 2, 2).id(); + let b = scenario.add_creature(P0, "Mine B", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Zealous Display", + true, + "Creatures you control get +2/+0 until end of turn. If it's not your turn, untap those creatures.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + // Fixture setup: hand the turn to the opponent so `Not(IsYourTurn)` holds. + runner.state_mut().active_player = P1; + runner.state_mut().priority_player = P0; + runner.state_mut().waiting_for = engine::types::game_state::WaitingFor::Priority { player: P0 }; + for id in [a, b] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + assert_ne!( + runner.state().active_player, + P0, + "fixture guard: on your own turn the untap sub never runs and this row is vacuous" + ); + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a, b])); + assert!(!tapped(runner.state(), a) && !tapped(runner.state(), b)); +} + +/// Motivated Pony's attack trigger. Its untap is gated on +/// `BattlefieldEntriesThisTurn { Food } >= 1`, so a Food entry is stamped into +/// the ledger — without it the branch never executes and the row is vacuous. +/// Only ATTACKING creatures may enter the published set, which is what +/// keeps the `Attacking` property in the head filter honest. +#[test] +fn motivated_pony_untaps_only_the_attacking_creatures_it_pumped() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let pony = scenario + .add_creature_from_oracle( + P0, + "Motivated Pony", + 3, + 3, + "Trample, haste\nWhenever this creature attacks, attacking creatures get +1/+1 until end of turn. If a Food entered the battlefield under your control this turn, untap those creatures and they get an additional +2/+2 until end of turn.", + ) + .id(); + let buddy = scenario.add_creature(P0, "Buddy", 2, 2).id(); + let home = scenario.add_creature(P0, "Stays Home", 2, 2).id(); + let mut runner: GameRunner = scenario.build(); + runner.state_mut().objects.get_mut(&buddy).unwrap().keywords = + vec![engine::types::keywords::Keyword::Haste]; + // Fixture setup: a Food entered the battlefield this turn, so the + // intervening-if holds and the untap branch actually runs. + runner + .state_mut() + .battlefield_entries_this_turn + .push(BattlefieldEntryRecord { + object_id: ObjectId(9_999), + name: "Food".to_string(), + core_types: vec![CoreType::Artifact], + subtypes: vec!["Food".to_string()], + supertypes: vec![], + colors: vec![], + keywords: vec![], + controller: P0, + }); + runner.advance_to_combat(); + runner + .declare_attackers(&[ + (pony, AttackTarget::Player(P1)), + (buddy, AttackTarget::Player(P1)), + ]) + .expect("fixture guard: the attack trigger must actually fire"); + runner.advance_until_stack_empty(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[pony, buddy])); + assert!(!tapped(runner.state(), pony) && !tapped(runner.state(), buddy)); + assert!( + !published_set(runner.state()).contains(&home.0), + "CR 611.2c: the non-attacker was never in the frozen population" + ); +} + +/// Suicidal Charge — the mass head feeds a `GenericEffect { affected: +/// ParentTarget, MustAttack }` coercion instead of an untap. Before the fix the +/// opponent's creatures were shrunk but not coerced: half the card did nothing. +#[test] +fn suicidal_charge_coerces_the_creatures_it_shrank() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P1, "Theirs A", 3, 3).id(); + let b = scenario.add_creature(P1, "Theirs B", 2, 2).id(); + let src = scenario + .add_enchantment_from_oracle( + P0, + "Suicidal Charge", + "Sacrifice this enchantment: Creatures your opponents control get -1/-1 until end of turn. Those creatures attack this turn if able.", + ) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.activate(src, 0).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a, b])); + for id in [a, b] { + assert!( + grant_lands_on(runner.state(), id, "MustAttack"), + "CR 608.2c: 'those creatures' names the shrunk population" + ); + } + assert_eq!(runner.state().objects[&a].power, Some(2), "shrink applied"); +} + +// =========================================================================== +// `PumpAll` — PRESERVED rows +// =========================================================================== + +/// Elvish Elegy: `Mill -> PumpAll -> ChangeZoneAll { TrackedSetFiltered }`. +/// +/// LEG-1 ROW. The `Mill` already published the milled cards, so the mass pump is +/// not the antecedent of "from among the milled cards" and must not join the +/// set. (`leg1_witness_surge_to_victory_*` is the sharper revert probe for the +/// same leg; this row covers the same leg on a `PumpAll` whose own enumeration +/// happens to be empty.) +#[test] +fn elvish_elegy_keeps_the_milled_set_free_of_the_graveyard_pump() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + scenario.with_library_top(P0, &["Lib Elf", "Lib Land", "Lib Bear"]); + scenario.with_graveyard(P0, &["Yard Creature"]); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Elvish Elegy", + false, + "Mill three cards, then each creature card in your graveyard perpetually gets +1/+1. You may put an Elf or land card from among the milled cards into your hand.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.cast(spell).resolve(); + + let set = published_set(runner.state()); + assert_eq!( + set.len(), + 3, + "the milled cards, and only those: got {set:?}" + ); +} + +/// Heroic Charge, cast UNKICKED. Its trample grant sits behind the kicked +/// condition, so publishing the pumped population must not make it execute. +#[test] +fn heroic_charge_unkicked_publishes_without_granting_trample() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 3, 3).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Heroic Charge", + false, + "Kicker {1}{R} (You may pay an additional {1}{R} as you cast this spell.)\nCreatures you control get +2/+1 until end of turn. If this spell was kicked, those creatures also gain trample until end of turn.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + runner.state().objects[&a].power, + Some(5), + "non-vacuity: the pump ran, so the chain really resolved" + ); + assert!( + !grant_lands_on(runner.state(), a, "Trample"), + "the kicked-only grant must not fire on an unkicked cast" + ); +} + +/// Valley Rally with its condition removed, so the targeted grant actually +/// executes. The head is a population and the grant DECLARES its own target: the +/// grant node's own targets must win over the published set. +/// +/// DISCLOSED DEVIATION: the printed card gates the grant on `AdditionalCostPaid` +/// (the gift). That branch never runs in this harness, which would make the row +/// vacuous, so the condition is dropped and everything else kept. +#[test] +fn valley_rally_grant_binds_its_own_target_not_the_published_population() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 3, 3).id(); + let b = scenario.add_creature(P0, "Mine B", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Valley Rally Grant Path", + true, + "Creatures you control get +2/+0 until end of turn. Target creature you control gains first strike until end of turn.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.cast(spell).target_objects(&[a]).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + runner.state().objects[&b].power, + Some(4), + "non-vacuity: the mass pump reached the non-targeted creature" + ); + assert!(grant_lands_on(runner.state(), a, "FirstStrike")); + assert!( + !grant_lands_on(runner.state(), b, "FirstStrike"), + "CR 608.2c: a grant with its own declared target does not read the frozen population" + ); +} + +// =========================================================================== +// `GoadAll` +// =========================================================================== + +/// Kaima, the Fractured Calm — the consumer is a COUNT +/// (`FilteredTrackedSetSize`), not an anaphor, so the observable is Kaima's +/// counter total. Only the ENCHANTED opponent creature may be counted, which is +/// what proves the head filter's `HasAttachment { Aura }` property survived into +/// the published population. +/// +/// DISCLOSED DEVIATION: given as an activated ability so `SelfRef` denotes the +/// permanent and the chain runs without waiting for the printed trigger. +#[test] +fn kaima_counts_only_the_enchanted_creature_it_goaded() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let victim = scenario.add_creature(P1, "Enchanted Victim", 3, 3).id(); + let plain = scenario.add_creature(P1, "Plain Victim", 2, 2).id(); + let aura = scenario + .add_enchantment_from_oracle(P0, "Kaima Aura", "Enchant creature") + .id(); + let kaima = scenario + .add_creature_from_oracle( + P0, + "Kaima Body", + 3, + 3, + "{T}: Goad each creature your opponents control that's enchanted by an Aura you control. Put a +1/+1 counter on Kaima Body for each creature goaded this way.", + ) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.attach_as_bestowed_aura(aura, victim); + runner.activate(kaima, 0).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[victim])); + assert!( + !published_set(runner.state()).contains(&plain.0), + "CR 611.2c: the unenchanted creature was never in the frozen population" + ); + assert_eq!( + runner.state().objects[&kaima] + .counters + .get(&engine::types::counter::CounterType::Plus1Plus1) + .copied(), + Some(1), + "one creature goaded this way" + ); +} + +/// Taunt from the Rampart — `GoadAll` feeding a `GenericEffect { affected: +/// ParentTarget, CantBlock }`. +#[test] +fn taunt_from_the_rampart_stops_the_creatures_it_goaded_from_blocking() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let theirs = scenario.add_creature(P1, "Theirs A", 3, 3).id(); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Taunt from the Rampart", + true, + "Goad all creatures your opponents control. Until your next turn, those creatures can't block. (Until your next turn, those creatures attack each combat if able and attack a player other than you if able.)", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[theirs])); + assert!(grant_lands_on(runner.state(), theirs, "CantBlock")); + assert!( + !grant_lands_on(runner.state(), mine, "CantBlock"), + "CR 701.15a: only the goaded creatures are named" + ); +} + +// =========================================================================== +// `GiveControl` +// =========================================================================== + +/// Domineering Will — the authority test for `GiveControl`. "Those creatures" +/// names the DECLARED TARGETS (CR 608.2c), and a target the recipient already +/// controls emits no `ControllerChanged`, so an event-harvest authority would +/// leave it tapped. Here the recipient is P0 and one target is already P0's, so +/// the two candidate authorities disagree and the event one fails. +#[test] +fn domineering_will_untaps_a_target_the_recipient_already_controlled() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let theirs = scenario.add_creature(P1, "Theirs", 2, 2).id(); + let already_mine = scenario.add_creature(P0, "Already Mine", 1, 1).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Domineering Will", + true, + "Target player gains control of up to three target nonattacking creatures until end of turn. Untap those creatures. They block this turn if able.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [theirs, already_mine] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + runner + .cast(spell) + .target_player(P0) + .target_objects(&[theirs, already_mine]) + .resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[theirs, already_mine])); + assert!(!tapped(runner.state(), theirs)); + assert!( + !tapped(runner.state(), already_mine), + "CR 608.2c: a declared target that changed no controller is still one of 'those creatures'" + ); +} + +/// Coveted Falcon's turn-face-up trigger body: `GiveControl -> Draw +/// { TrackedSetSize }`. The observable is cards drawn, which was 0 before the +/// fix. +#[test] +fn coveted_falcon_draws_for_each_permanent_handed_over() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Give A", 1, 1).id(); + scenario.add_card_to_library_top(P0, "Library Card A"); + scenario.add_card_to_library_top(P0, "Library Card B"); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Falcon Trigger Body", + true, + "Target opponent gains control of any number of target permanents you control. Draw a card for each one they gained control of this way.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + let before = runner.state().players[0].hand.len(); + let outcome = runner.cast(spell).target_objects(&[a]).resolve(); + + assert_eq!(published_set(outcome.state()), ids(&[a])); + assert_eq!( + outcome.state().players[0].hand.len(), + before, + "one card drawn, and the spell itself left the hand" + ); + assert_eq!( + outcome.state().objects[&a].controller, + P1, + "non-vacuity: control actually changed" + ); +} + +// =========================================================================== +// LEG WITNESSES — each pins one leg of `is_sole_chain_producer` +// =========================================================================== + +/// LEG-1 WITNESS (the sharper of the two: it flips a set's CONTENTS, not just a +/// boolean). Surge to Victory exiles a card and then mass-pumps; "the exiled +/// card" names the exile, not the creatures. The `ChangeZone` ancestor already +/// published, so the mass pump must decline. +/// +/// Deleting `no_earlier_producer` makes the pumped creature join the set. +#[test] +fn leg1_witness_surge_to_victory_binds_the_exiled_card_not_the_pumped_creatures() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + scenario.add_creature(P0, "Alpha", 2, 2); + let graveyard_card = scenario.add_spell_to_graveyard(P0, "Shock", true).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Surge to Victory", + false, + "Exile target instant or sorcery card from your graveyard. Creatures you control get +X/+0 until end of turn, where X is that card's mana value. Whenever a creature you control deals combat damage to a player this turn, copy the exiled card. You may cast the copy without paying its mana cost.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner + .cast(spell) + .target_objects(&[graveyard_card]) + .resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + published_set(runner.state()), + ids(&[graveyard_card]), + "CR 608.2c: the anaphor names the exile, so the pumped creature must stay out" + ); + assert_eq!( + runner.state().objects[&graveyard_card].zone, + engine::types::zones::Zone::Exile, + "non-vacuity: the exile really happened" + ); +} + +/// LEG-2 WITNESS. Outlaws' Fury pumps FIRST and exiles afterwards, so the later +/// exile is the antecedent of "you may play that card" and the mass pump must +/// decline even though nothing published before it. +/// +/// Deleting `!later_node_is_publisher_position` makes the pumped creatures join +/// the exiled card's set, and the play permission would then cover creatures. +#[test] +fn leg2_witness_outlaws_fury_binds_the_later_exile_not_the_pumped_creatures() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let alpha = scenario.add_creature(P0, "Alpha", 2, 2).id(); + scenario + .add_creature(P0, "Rogue Pal", 1, 1) + .with_subtypes(vec!["Rogue"]); + scenario.with_library_top(P0, &["Lib A", "Lib B"]); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Outlaws' Fury", + false, + "Creatures you control get +2/+0 until end of turn. If you control an outlaw, exile the top card of your library. Until the end of your next turn, you may play that card. (Assassins, Mercenaries, Pirates, Rogues, and Warlocks are outlaws.)", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + let set = published_set(runner.state()); + assert_eq!(set.len(), 1, "exactly the exiled card: got {set:?}"); + assert!( + !set.contains(&alpha.0), + "CR 608.2c: a head followed by another producer is not the antecedent" + ); + assert_eq!( + runner.state().objects[&alpha].power, + Some(4), + "non-vacuity: the mass pump ran, it simply did not publish" + ); +} + +// =========================================================================== +// PARSER HALF — the implicit-pronoun anaphor ("Untap them.") +// =========================================================================== + +/// PARSER KNOWN-CHANGED CONTROL — Rallying Roar. Verbatim. Its untap is an +/// implicit pronoun, which the spell-body default lowers to `ParentTarget`; only +/// the parser rewrite turns it into `TrackedSet(0)`. If this passes without the +/// rewrite, every other parser row here is meaningless. +#[test] +fn parser_control_rallying_roar_untaps_the_creatures_it_pumped() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 2, 2).id(); + let b = scenario.add_creature(P0, "Mine B", 2, 2).id(); + let theirs = scenario.add_creature(P1, "Theirs", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Rallying Roar", + true, + "Creatures you control get +1/+1 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [a, b, theirs] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a, b])); + assert!(!tapped(runner.state(), a) && !tapped(runner.state(), b)); + assert!(tapped(runner.state(), theirs), "controller filter survives"); +} + +/// Rally to Battle — same shape, different numbers; kept as its own row because +/// the roster is per-card. +#[test] +fn rally_to_battle_untaps_the_creatures_it_pumped() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 2, 2).id(); + let b = scenario.add_creature(P0, "Mine B", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Rally to Battle", + true, + "Creatures you control get +1/+3 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [a, b] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + runner.cast(spell).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a, b])); + assert!(!tapped(runner.state(), a) && !tapped(runner.state(), b)); + assert_eq!(runner.state().objects[&a].toughness, Some(5)); +} + +/// Great Oak Guardian's ETB trigger — the population is `target player`'s +/// creatures, so targeting the OPPONENT makes the anaphor's scope observable: +/// their creatures untap, mine do not. +/// Great Oak Guardian's ETB trigger — the population is `target player`'s +/// creatures, so targeting the OPPONENT makes the anaphor's scope observable: +/// their creatures untap and mine do not. A rewrite that bound "them" to the +/// source or to the parent target could not produce this split. +#[test] +fn great_oak_guardian_untaps_the_targeted_players_creatures_only() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let theirs = scenario.add_creature(P1, "Theirs", 2, 2).id(); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + let spell = scenario + .add_creature_to_hand_from_oracle( + P0, + "Great Oak Guardian", + 4, + 5, + "Flash (You may cast this spell any time you could cast an instant.)\nReach\nWhen this creature enters, creatures target player controls get +2/+2 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [theirs, mine] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + runner.cast(spell).target_player(P1).resolve(); + runner.advance_until_stack_empty(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[theirs])); + assert!(!tapped(runner.state(), theirs)); + assert_eq!(runner.state().objects[&theirs].power, Some(4), "pumped"); + assert!( + tapped(runner.state(), mine), + "CR 611.2c: the frozen population is the TARGETED player's creatures" + ); +} + +/// The General — the same anaphor under an activated ability with a +/// self-exile cost, i.e. the population head is not the ability source. +#[test] +fn the_general_untaps_the_creatures_it_pumped() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 2, 2).id(); + let src = scenario + .add_enchantment_from_oracle( + P0, + "The General", + "Exile The General: Creatures you control get +1/+1 until end of turn. Untap them.", + ) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.state_mut().objects.get_mut(&a).unwrap().tapped = true; + runner.activate(src, 0).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a])); + assert!(!tapped(runner.state(), a)); + assert_eq!(runner.state().objects[&a].power, Some(3), "pumped"); +} + +/// Essence of Antiquity — a `GenericEffect` head (a keyword grant, not a pump) +/// feeding the same implicit-pronoun untap. This is the third publisher class in +/// the parser predicate, and the one with no `PumpAll` involved at all. +/// +/// DISCLOSED DEVIATION: the printed card fires this off a Disguise +/// turn-face-up trigger, which this harness cannot drive. The body is given as a +/// `{T}` activated ability on a creature, which keeps every element the row +/// turns on — the same broadcast `affected` filter, the same implicit-pronoun +/// untap, and a real permanent source. `{T}` also taps the source, so the source +/// joining the untapped population ("creatures you control" includes it) is +/// directly observable. +#[test] +fn essence_of_antiquity_untaps_the_creatures_it_granted_hexproof() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let a = scenario.add_creature(P0, "Mine A", 2, 2).id(); + let src = scenario + .add_creature_from_oracle( + P0, + "Essence Body", + 1, + 10, + "{T}: Creatures you control gain hexproof until end of turn. Untap them.", + ) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.state_mut().objects.get_mut(&a).unwrap().tapped = true; + runner.activate(src, 0).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!(published_set(runner.state()), ids(&[a, src])); + assert!(!tapped(runner.state(), a)); + assert!( + !tapped(runner.state(), src), + "the source is one of 'creatures you control', so its own {{T}} tap is undone" + ); + assert!(grant_lands_on(runner.state(), a, "Hexproof")); +} + +/// Valley Floodcaller's cast trigger. +/// +/// KNOWN, BOUNDED GAP (issue #7451): the grant's four-subtype filter +/// ("Birds, Frogs, Otters, and Rats") is misparsed upstream of this change — +/// only the last subtype survives into the pumped population. This row therefore +/// asserts the INVARIANT this PR owns, which holds regardless of that bug: +/// **the untapped set is exactly the pumped set is exactly the published set.** +/// Before the fix nothing untapped at all, so the row is strictly closer to +/// correct; when #7451 is fixed the pumped set widens and this test follows it +/// without needing to change, because it asserts the identity and not a +/// hard-coded population. +#[test] +fn valley_floodcaller_untaps_exactly_the_creatures_it_pumped() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let bird = scenario + .add_creature(P0, "Birdy", 1, 1) + .with_subtypes(vec!["Bird"]) + .id(); + let frog = scenario + .add_creature(P0, "Froggy", 1, 1) + .with_subtypes(vec!["Frog"]) + .id(); + let otter = scenario + .add_creature(P0, "Ottery", 1, 1) + .with_subtypes(vec!["Otter"]) + .id(); + let rat = scenario + .add_creature(P0, "Ratty", 1, 1) + .with_subtypes(vec!["Rat"]) + .id(); + let bear = scenario.add_creature(P0, "Beary", 2, 2).id(); + scenario.add_creature_from_oracle( + P0, + "Valley Floodcaller", + 2, + 2, + "Flash\nYou may cast noncreature spells as though they had flash.\nWhenever you cast a noncreature spell, Birds, Frogs, Otters, and Rats you control get +1/+1 until end of turn. Untap them.", + ); + let bolt = scenario.add_bolt_to_hand(P0); + let subjects = [bird, frog, otter, rat, bear]; + let mut runner: GameRunner = scenario.build(); + for id in subjects { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + let base_power: Vec> = subjects + .iter() + .map(|id| runner.state().objects[id].power) + .collect(); + runner.cast(bolt).target_player(P1).resolve(); + runner.advance_until_stack_empty(); + evaluate_layers(runner.state_mut()); + + let pumped: Vec = subjects + .iter() + .zip(&base_power) + .filter(|(id, before)| runner.state().objects[*id].power != **before) + .map(|(id, _)| id.0) + .collect(); + let untapped: Vec = subjects + .iter() + .filter(|id| !tapped(runner.state(), **id)) + .map(|id| id.0) + .collect(); + + assert!( + !pumped.is_empty(), + "non-vacuity: the trigger must have pumped something, or the identity below is trivial" + ); + assert_eq!(pumped, untapped, "untapped set == pumped set"); + assert_eq!(published_set(runner.state()), pumped, "== published set"); + assert!( + !untapped.contains(&bear.0), + "the plain creature is outside the grant's population under any reading of it" + ); +} + +/// Trystan's Command — PRESERVED, and a regression sentinel for the two-regime +/// law rather than a fix. +/// +/// The card is MODAL (choose two of four sibling abilities), not a chain — the +/// engine resolves the chosen modes in sequence, so the publish gate sees the +/// later mode's consumer. With the destroy mode chosen alongside the pump mode, +/// the destroy publishes first, `is_sole_chain_producer`'s leg 1 declines the +/// mass pump, and the anaphor resolves against the destroyed creature — which +/// untaps nothing. That is a KNOWN, BOUNDED gap that predates this PR: before +/// the parser rewrite the implicit pronoun bound elsewhere and also untapped +/// nothing. The row exists to prove the behaviour did not get WORSE, so do not +/// "simplify" it away on the grounds that it asserts a non-untap. +/// +/// STRUCTURAL CONSEQUENCE — this is not "one unmeasured mode pair". MEASURED: +/// the card is `min_choices: 2, max_choices: 2` over `mode_count: 4`, so a +/// companion mode is ALWAYS chosen; and two of the three possible companions +/// publish before mode 4 resolves — destroy (this row) and token copy (the row +/// below, `[0, 3]`, published set = the created token). INFERRED, not measured: +/// the graveyard-return companion publishes too, because it moves cards to hand +/// and the `_ =>` arm of the publish switch harvests `ZoneChanged`. If that +/// inference is wrong, mode 4 is fixable for exactly one of three pairs. +/// **On the two measured pairs, Trystan's Command mode 4 cannot be fixed while +/// the publish gate is chain-wide rather than mode-scoped.** +/// CR 700.2 is the lever: modes are separate instructions, so a sibling mode's +/// `Destroy` arguably should not count as an "earlier producer" for mode 4's +/// anaphor at all. Fixing that means scoping the gate to the mode, not weakening +/// this test. +/// +/// Two measured side-facts, recorded because they are easy to misread: +/// * the TRACKED SET does change (empty -> `[victim]`). The parser rewrite +/// creates a `TrackedSet` consumer where there was none, so the pre-existing +/// `Destroy` publish arm now fires. The BOARD is unaffected, because the only +/// consumer is an untap aimed at a creature that is already in the graveyard. +/// * this row is a SECOND leg-1 witness: with `no_earlier_producer` deleted the +/// set becomes `[victim, mine]`, i.e. the mass pump joins the destroy's set. +/// +/// The `tapped` assertion below therefore pins a rules-INCORRECT outcome on +/// purpose, as a no-regression sentinel. When the mode-scoping fix lands it must +/// be flipped to `!tapped`, not deleted. +#[test] +fn trystans_command_pump_mode_is_unchanged_when_an_earlier_mode_publishes() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let victim = scenario.add_creature(P1, "Victim", 2, 2).id(); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Trystan's Command", + false, + "Choose two —\n• Create a token that's a copy of target Elf you control.\n• Return one or two target permanent cards from your graveyard to your hand.\n• Destroy target creature or enchantment.\n• Creatures target player controls get +3/+3 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [victim, mine] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + runner + .cast(spell) + .modes(&[2, 3]) + .target_objects(&[victim]) + .target_player(P0) + .resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + published_set(runner.state()), + ids(&[victim]), + "CR 608.2c: the earlier mode's destroy is the live antecedent" + ); + assert_eq!( + runner.state().objects[&victim].zone, + engine::types::zones::Zone::Graveyard, + "non-vacuity: the destroy mode really executed" + ); + assert_eq!( + runner.state().objects[&mine].power, + Some(5), + "non-vacuity: the pump mode really executed too" + ); + assert!( + tapped(runner.state(), mine), + "unchanged from before this PR — see the doc comment" + ); +} + +/// The token-copy companion mode, measured: the second half of the "any pair +/// preempts mode 4" claim in the row above. +/// +/// Modes 1 and 4 (`[0, 3]`). The copy token's creation publishes first, leg 1 +/// declines the mass pump, and the anaphor binds the TOKEN — so the pumped +/// creatures stay tapped even though the pump itself ran. Same bounded gap as +/// the destroy pair, reached through a different publishing arm, which is the +/// point: the gate is chain-wide, so WHICH earlier mode published is irrelevant. +#[test] +fn trystans_command_token_copy_mode_also_preempts_the_pump_anaphor() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let elf = scenario + .add_creature(P0, "Elf Pal", 1, 1) + .with_subtypes(vec!["Elf"]) + .id(); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Trystan's Command", + false, + "Choose two —\n• Create a token that's a copy of target Elf you control.\n• Return one or two target permanent cards from your graveyard to your hand.\n• Destroy target creature or enchantment.\n• Creatures target player controls get +3/+3 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [elf, mine] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + runner + .cast(spell) + .modes(&[0, 3]) + .target_objects(&[elf]) + .target_player(P0) + .resolve(); + evaluate_layers(runner.state_mut()); + + let token: Vec = runner + .state() + .battlefield + .iter() + .filter(|id| **id != elf && **id != mine) + .map(|id| id.0) + .collect(); + assert_eq!(token.len(), 1, "non-vacuity: the copy mode really ran"); + assert_eq!( + published_set(runner.state()), + token, + "CR 608.2c: the earlier mode's token is the live antecedent" + ); + assert_eq!( + runner.state().objects[&mine].power, + Some(5), + "non-vacuity: the pump mode really executed too" + ); + assert!( + tapped(runner.state(), mine), + "unchanged from before this PR — see the doc comment above" + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 267db59c7e..995a72c627 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -771,6 +771,7 @@ mod ivory_gargoyle_temporal_and_skip_tail; mod jace_wielder_empty_library_win; mod jagged_lightning_each_of_two_targets; mod jaws_of_defeat; +mod jeskai_ascendancy_pump_untap_anaphora_6857; mod json_smoke_test; mod judgment_bolt_where_x_damage_runtime; mod kaito_integration; From d897d8ae0f6bd623bf35b0378a5ff66ac52781be Mon Sep 17 00:00:00 2001 From: lgray Date: Sun, 16 Aug 2026 09:50:28 -0500 Subject: [PATCH 2/7] docs(test): drop a duplicated Great Oak Guardian doc paragraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doc comment above `great_oak_guardian_untaps_the_targeted_players_creatures_only` carried a truncated earlier draft of itself immediately before the complete paragraph. Keeps the complete one, which states the discriminator ("a rewrite that bound "them" to the source or to the parent target could not produce this split"); the truncated copy stopped before it. Comment-only; no behavioural change. Swept the rest of the file for the same recipe — only bare `///` separators repeat. Raised by CodeRabbit on #7484. Assisted-by: ClaudeCode:claude-opus-5 --- .../integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs index fd39c252b0..09fa7119ea 100644 --- a/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs +++ b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs @@ -811,9 +811,6 @@ fn rally_to_battle_untaps_the_creatures_it_pumped() { assert_eq!(runner.state().objects[&a].toughness, Some(5)); } -/// Great Oak Guardian's ETB trigger — the population is `target player`'s -/// creatures, so targeting the OPPONENT makes the anaphor's scope observable: -/// their creatures untap, mine do not. /// Great Oak Guardian's ETB trigger — the population is `target player`'s /// creatures, so targeting the OPPONENT makes the anaphor's scope observable: /// their creatures untap and mine do not. A rewrite that bound "them" to the From c2407443168a895b72ea9f511a25c91d2d9ff857 Mon Sep 17 00:00:00 2001 From: lgray Date: Sun, 16 Aug 2026 13:38:43 -0500 Subject: [PATCH 3/7] feat(engine): mark modal mode roots with their CR 700.2d occurrence ordinal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build_chained_resolved` linearizes every selected mode of a modal spell or ability into ONE `sub_ability` chain, which erases the mode boundary from the chain's shape. CR 700.2 makes each option a separate mode, and CR 608.2c makes an anaphor ("those cards", "it") bind to its nearest antecedent — never to a sibling mode's population. Nothing in the resolved chain could express that boundary; this commit adds the marker that later commits key on. `ResolvedAbility::modal_instruction_ordinal: Option` is `Some(n)` on a mode root and `None` everywhere else. The value is the OCCURRENCE ordinal within the ordered selection, taken from `ordered.iter().enumerate().rev()`, not the printed mode index: CR 700.2d says a mode chosen twice is treated as appearing twice in sequence, so Eldrazi Confluence's `[1, 1]` must yield two distinct instructions at one printed index. Zero behaviour change. `build_chained_resolved` is the sole writer, pinned by a source census with a positive control. All eight exhaustive `ResolvedAbility` destructures are classified with a stated rationale: * `resolved_ability_axes`, `walk_ability`, `chain_offers_choice` — read-free / write-free / choice-free position marker, bound `_`; * the three `*_ability_is_batch_candidate` gates — a mode root is not the vanilla batchable shape, since a batch collapses N stack entries into one chain entry and would fire a per-instruction boundary once instead of N times; declining only costs the optimization; * `inert_trigger_abilities_eq_ignoring_provenance` (both sides) — bound `_`, provably never non-`None` there because that function is reached only through the three batch-candidate gates above. Serde: `#[serde(default, skip_serializing_if = "Option::is_none")]`, so existing saved states load unchanged and new ones gain no bytes for non-modal abilities. Assisted-by: ClaudeCode:claude-opus-4.8 --- crates/engine/src/game/ability_rw.rs | 9 +- crates/engine/src/game/ability_scan.rs | 49 +++-- crates/engine/src/game/ability_utils.rs | 190 +++++++++++++++++- .../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 + .../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 | 4 + crates/engine/src/game/resolution_prompt.rs | 17 +- crates/engine/src/game/stack.rs | 49 +++++ crates/engine/src/types/ability.rs | 27 +++ .../the_chain_veil_loyalty_grants.rs | 1 + 16 files changed, 323 insertions(+), 32 deletions(-) diff --git a/crates/engine/src/game/ability_rw.rs b/crates/engine/src/game/ability_rw.rs index 9f750b8ae3..0f3c85485f 100644 --- a/crates/engine/src/game/ability_rw.rs +++ b/crates/engine/src/game/ability_rw.rs @@ -3911,7 +3911,14 @@ fn walk_ability( target_choice_timing: _, description: _, selected_mode_labels: _, // display snapshots, no game-state read/write - min_x_value: _, // u32, no read + // CR 700.2: mode-root position marker — reads and writes NOTHING on any + // of the profiler's axes (kind+scope, `reads_member_bound`, + // `reads_event_live`, `writes_event_object`). It gates when the chain's + // tracked-set identity RESETS, which narrows what a later member-bound + // read can see; narrowing never adds a read, and the member-bound axis is + // already set by the `TrackedSet`-bearing effects themselves. + modal_instruction_ordinal: _, + min_x_value: _, // u32, no read cant_be_copied: _, copy_count_status: _, forward_result: _, diff --git a/crates/engine/src/game/ability_scan.rs b/crates/engine/src/game/ability_scan.rs index e67751d38f..f7798ff7aa 100644 --- a/crates/engine/src/game/ability_scan.rs +++ b/crates/engine/src/game/ability_scan.rs @@ -243,28 +243,33 @@ fn resolved_ability_axes(a: &ResolvedAbility, mode: ScanMode) -> Axes { optional_targeting: _, // bool optional: _, // bool optional_player, - optional_for: _, // OpponentMayScope: AnyOpponent/AnyPlayer, no read - target_choice_timing: _, // Stack/Resolution tag - description: _, // display string - selected_mode_labels: _, // display strings, no dynamic read - min_x_value: _, // u32 - cant_be_copied: _, // bool - copy_count_status: _, // status tag - forward_result: _, // bool - distribution: _, // concrete pre-assigned (TargetRef, u32) portions - chosen_x: _, // concrete cast-time X - cost_paid_object: _, // concrete captured-object snapshot - cost_paid_object_ids: _, // concrete captured-object ids (issue #4948) - effect_context_object: _, // concrete captured-object snapshot - amassed_army_object: _, // concrete captured-object snapshot - ability_index: _, // usize provenance - may_trigger_origin: _, // provenance tag - target_selection_mode: _, // Chosen/Random tag - chosen_players: _, // concrete chosen player ids - 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 + optional_for: _, // OpponentMayScope: AnyOpponent/AnyPlayer, no read + target_choice_timing: _, // Stack/Resolution tag + description: _, // display string + selected_mode_labels: _, // display strings, no dynamic read + // CR 700.2: mode-root position marker. Read-FREE on every scan axis: it + // selects nothing from game state, it only says "a new instruction starts + // here". The instructions themselves are `effect`/`sub_ability`, already + // scanned above, so the axes of a chain are identical with or without it. + modal_instruction_ordinal: _, + min_x_value: _, // u32 + cant_be_copied: _, // bool + copy_count_status: _, // status tag + forward_result: _, // bool + distribution: _, // concrete pre-assigned (TargetRef, u32) portions + chosen_x: _, // concrete cast-time X + cost_paid_object: _, // concrete captured-object snapshot + cost_paid_object_ids: _, // concrete captured-object ids (issue #4948) + effect_context_object: _, // concrete captured-object snapshot + amassed_army_object: _, // concrete captured-object snapshot + ability_index: _, // usize provenance + may_trigger_origin: _, // provenance tag + target_selection_mode: _, // Chosen/Random tag + chosen_players: _, // concrete chosen player ids + 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; diff --git a/crates/engine/src/game/ability_utils.rs b/crates/engine/src/game/ability_utils.rs index 427ad3214d..d22844d2ee 100644 --- a/crates/engine/src/game/ability_utils.rs +++ b/crates/engine/src/game/ability_utils.rs @@ -303,8 +303,12 @@ pub fn build_chained_resolved( controller: PlayerId, ) -> Result { if indices.is_empty() { - // CR 700.2a: "Choose up to one" permits choosing no modes. The ability - // still resolves, but it has no instructions to perform. + // CR 700.2: the modes are the bulleted options, chosen per "instructions + // for a player to choose A NUMBER of those options" — and under "choose up + // to one" that number may be zero. The ability still resolves; it just has + // no instructions to perform. (Not CR 700.2a, which is about WHEN modes are + // chosen and illegal modes; not CR 700.2i, whose "choose up to" is specific + // to pawprint {P} worth of modes.) return Ok(ResolvedAbility::new( Effect::GenericEffect { static_abilities: Vec::new(), @@ -321,11 +325,19 @@ pub fn build_chained_resolved( let ordered = ordered_selected_mode_indices(indices); let mut result: Option = None; - for &idx in ordered.iter().rev() { + for (ordinal, &idx) in ordered.iter().enumerate().rev() { let def = abilities .get(idx) .ok_or_else(|| EngineError::InvalidAction(format!("Mode index {idx} out of range")))?; let mut resolved = build_resolved_from_def(def, source_id, controller); + // CR 700.2 ("each of those options is a mode") + CR 700.2d: stamp this + // mode root with its OCCURRENCE ORDINAL within the ordered selection — + // taken from `enumerate()`, never from `idx`. `ordered_selected_mode_indices` + // preserves duplicates, so an `allow_repeat_modes` card (Eldrazi + // Confluence, `[1, 1]`) has two distinct instructions at one printed + // index; keying on `idx` would collapse them into one. This is the ONLY + // write site for the field (see its doc on `ResolvedAbility`). + resolved.modal_instruction_ordinal = Some(ordinal); // CR 700.2d: When chaining multiple modes, append subsequent modes after // the current mode's own sub_ability chain (e.g., Cathartic Pyre mode 2's // "discard, then draw that many" must preserve the draw sub_ability). @@ -9604,6 +9616,178 @@ mod tests { ); } + /// CR 700.2d: the mode-root stamp is the OCCURRENCE ORDINAL, not the printed + /// mode index. "If a particular mode is chosen multiple times, the spell is + /// treated as if that mode appeared that many times in sequence" — so a + /// repeated mode is two independent instructions and must carry two distinct + /// ordinals even though both live at the same printed index. + /// + /// DISCRIMINATION: key the stamp on `idx` instead of `enumerate()`'s counter + /// and the `[1, 1]` arm reads `Some(1), Some(1)` — the two occurrences + /// collapse into one instruction, which is exactly what a mode-boundary + /// consumer must not see. The `[0, 1, 2]` arm cannot distinguish the two + /// keyings (index == ordinal there), which is why the repeat arm is here. + #[test] + fn build_chained_resolved_stamps_occurrence_ordinals_not_printed_indices() { + let mode = |effect| AbilityDefinition::new(AbilityKind::Spell, effect); + let abilities = vec![ + mode(Effect::Destroy { + target: TargetFilter::Any, + cant_regenerate: false, + }), + mode(Effect::Draw { + count: QuantityExpr::Fixed { value: 1 }, + target: TargetFilter::Controller, + }), + mode(Effect::GainLife { + amount: QuantityExpr::Fixed { value: 1 }, + player: TargetFilter::Controller, + }), + ]; + + let distinct = + build_chained_resolved(&abilities, &[0, 1, 2], ObjectId(1), PlayerId(0)).unwrap(); + let second = distinct.sub_ability.as_deref().expect("mode 1 follows"); + let third = second.sub_ability.as_deref().expect("mode 2 follows"); + assert_eq!( + ( + distinct.modal_instruction_ordinal, + second.modal_instruction_ordinal, + third.modal_instruction_ordinal, + ), + (Some(0), Some(1), Some(2)), + "CR 700.2: every mode root is stamped, including the first" + ); + + // CR 700.2d: Eldrazi Confluence's `allow_repeat_modes` shape. + let repeated = + build_chained_resolved(&abilities, &[1, 1], ObjectId(1), PlayerId(0)).unwrap(); + let repeated_second = repeated + .sub_ability + .as_deref() + .expect("the repeated mode occurs twice in sequence"); + assert!( + matches!(repeated.effect, Effect::Draw { .. }) + && matches!(repeated_second.effect, Effect::Draw { .. }), + "reach-guard: both occurrences must really be printed mode 1, or the \ + distinct-ordinal assertion below is about the wrong nodes" + ); + assert_eq!( + ( + repeated.modal_instruction_ordinal, + repeated_second.modal_instruction_ordinal, + ), + (Some(0), Some(1)), + "CR 700.2d: two occurrences of ONE printed mode are two instructions. \ + Keying on the printed index would give (Some(1), Some(1))" + ); + + // CR 700.2: the modes are the bulleted options, so "choose up to one" + // with zero chosen has no instructions at all — it builds a bare + // `GenericEffect` root, which is not a mode root. + let none = build_chained_resolved(&abilities, &[], ObjectId(1), PlayerId(0)).unwrap(); + assert_eq!(none.modal_instruction_ordinal, None); + } + + /// PROVENANCE PIN for `ResolvedAbility::modal_instruction_ordinal`: exactly + /// ONE non-test writer in the whole engine crate. + /// + /// The field's meaning ("this node begins a new CR 700.2 instruction") is only + /// sound while `build_chained_resolved` — the one function that linearizes + /// selected modes into a chain — is its only author. A second writer would let + /// a non-mode-root claim a mode boundary and reset the chain-local tracked-set + /// identity mid-instruction. + /// + /// Classification is by WRITE, not by name occurrence: the identifier also + /// appears at every exhaustive `ResolvedAbility` literal as `: None` (a + /// default, not a write) and at each of the eight exhaustive destructures. + /// + /// Test regions are excluded by the `#[cfg(test)] mod` boundary, not by + /// filename — a filename-keyed scan of this crate has produced a wrong census + /// before (13 "src" sites that were all inside `#[cfg(test)] mod tests`). + #[test] + fn modal_instruction_ordinal_has_exactly_one_non_test_writer() { + // Assembled so this test's own source cannot be counted. + let needle = format!("modal_instruction_{}", "ordinal"); + let write_forms = [format!("{needle} = "), format!("{needle}: Some(")]; + // POSITIVE CONTROL: `build_chained_resolved`'s OWN other write, five lines + // from the one under census, in the same non-test region of the same file. + // If the walk or the `#[cfg(test)]` cut ever stops reaching that function, + // this reads 0 and the "exactly 1 writer" assertion below would be + // counterfeit. Counted per file rather than crate-wide: the needle is + // written 17 times across the crate, a number that drifts with unrelated + // work, and a crate-wide pin would be a maintenance tax that measures + // nothing this row cares about. + let control = format!("sub_link = SubAbilityLink::{}", "SequentialSibling"); + let control_file = "ability_utils.rs"; + + let src_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files: Vec = Vec::new(); + let mut stack = vec![src_root]; + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(&dir).unwrap_or_else(|e| panic!("read {dir:?}: {e}")) { + let path = entry.expect("dir entry").path(); + if path.is_dir() { + stack.push(path); + } else if path.extension().is_some_and(|e| e == "rs") { + files.push(path); + } + } + } + files.sort(); + assert!(files.len() > 100, "reach-guard: the walk found the crate"); + + let mut writers: Vec = Vec::new(); + let mut control_hits = 0usize; + for path in &files { + let text = std::fs::read_to_string(path).expect("read source"); + // Comment halves removed by the shared authority, so a needle written + // in prose is neither counted nor able to hide a deleted writer. + let code = crate::source_census::code_lines(&text); + let lines: Vec<&str> = code.lines().collect(); + // Cut at the `#[cfg(test)] mod ...` boundary. `#[cfg(test)]` also + // guards individual `use`/`fn` items in this crate; those are NOT the + // boundary, and treating them as one would hide real writers. + let end = lines + .iter() + .position(|line| line.trim_start().starts_with("#[cfg(test)]")) + .filter(|i| { + lines[i + 1..] + .iter() + .find(|l| !l.trim().is_empty()) + .is_some_and(|l| l.trim_start().starts_with("mod ")) + }) + .unwrap_or(lines.len()); + let rel = path.display().to_string(); + for line in &lines[..end] { + if write_forms.iter().any(|f| line.contains(f.as_str())) { + writers.push(format!("{rel}: {}", line.trim())); + } + if rel.ends_with(control_file) { + control_hits += line.matches(control.as_str()).count(); + } + } + } + + assert_eq!( + control_hits, 1, + "POSITIVE CONTROL: `build_chained_resolved`'s `SequentialSibling` write \ + must be visible to this scan, or a zero writer count is counterfeit. \ + control_hits={control_hits}" + ); + assert_eq!( + writers.len(), + 1, + "CR 700.2: `modal_instruction_ordinal` must have exactly one non-test \ + writer (`build_chained_resolved`). writers: {writers:#?}" + ); + assert!( + writers[0].contains("ability_utils.rs"), + "the one writer must be `build_chained_resolved`, got {:?}", + writers[0] + ); + } + #[test] fn selected_mode_labels_follow_printed_order_and_preserve_repeats() { let labels = selected_mode_labels( diff --git a/crates/engine/src/game/effects/additional_phase.rs b/crates/engine/src/game/effects/additional_phase.rs index 70db43ea8e..8a404f0fae 100644 --- a/crates/engine/src/game/effects/additional_phase.rs +++ b/crates/engine/src/game/effects/additional_phase.rs @@ -289,6 +289,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, diff --git a/crates/engine/src/game/effects/double.rs b/crates/engine/src/game/effects/double.rs index debaedbdf6..e510455661 100644 --- a/crates/engine/src/game/effects/double.rs +++ b/crates/engine/src/game/effects/double.rs @@ -359,6 +359,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, repeat_for: None, min_x_value: 0, announced_x: None, diff --git a/crates/engine/src/game/effects/extra_turn.rs b/crates/engine/src/game/effects/extra_turn.rs index 69c33aec85..8c134a536e 100644 --- a/crates/engine/src/game/effects/extra_turn.rs +++ b/crates/engine/src/game/effects/extra_turn.rs @@ -94,6 +94,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: 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 fd90c87dad..af4be6e501 100644 --- a/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs +++ b/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs @@ -112,6 +112,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, diff --git a/crates/engine/src/game/effects/player_counter.rs b/crates/engine/src/game/effects/player_counter.rs index 6a1c903cf7..acc45062e3 100644 --- a/crates/engine/src/game/effects/player_counter.rs +++ b/crates/engine/src/game/effects/player_counter.rs @@ -462,6 +462,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, @@ -661,6 +662,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, diff --git a/crates/engine/src/game/effects/reverse_turn_order.rs b/crates/engine/src/game/effects/reverse_turn_order.rs index c93a8df6d6..8b290b4112 100644 --- a/crates/engine/src/game/effects/reverse_turn_order.rs +++ b/crates/engine/src/game/effects/reverse_turn_order.rs @@ -67,6 +67,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, diff --git a/crates/engine/src/game/effects/skip_next_step.rs b/crates/engine/src/game/effects/skip_next_step.rs index 4789783f5e..a0bb25c94f 100644 --- a/crates/engine/src/game/effects/skip_next_step.rs +++ b/crates/engine/src/game/effects/skip_next_step.rs @@ -130,6 +130,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, diff --git a/crates/engine/src/game/effects/skip_next_turn.rs b/crates/engine/src/game/effects/skip_next_turn.rs index a6f1f3a23c..186f5ba8a1 100644 --- a/crates/engine/src/game/effects/skip_next_turn.rs +++ b/crates/engine/src/game/effects/skip_next_turn.rs @@ -108,6 +108,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, diff --git a/crates/engine/src/game/effects/vote.rs b/crates/engine/src/game/effects/vote.rs index c372bcda49..3c3cdfafb5 100644 --- a/crates/engine/src/game/effects/vote.rs +++ b/crates/engine/src/game/effects/vote.rs @@ -808,6 +808,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, repeat_for: None, min_x_value: 0, announced_x: None, @@ -921,6 +922,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, repeat_for: None, min_x_value: 0, announced_x: None, @@ -1359,6 +1361,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, repeat_for: None, min_x_value: 0, announced_x: None, @@ -1529,6 +1532,7 @@ mod tests { target_choice_timing: crate::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, repeat_for: None, min_x_value: 0, announced_x: None, diff --git a/crates/engine/src/game/resolution_prompt.rs b/crates/engine/src/game/resolution_prompt.rs index 27ab6c09e9..a00ed0edcd 100644 --- a/crates/engine/src/game/resolution_prompt.rs +++ b/crates/engine/src/game/resolution_prompt.rs @@ -571,11 +571,16 @@ pub(crate) fn chain_offers_choice(a: &ResolvedAbility) -> bool { context: _, // SpellContext: cast-time fact snapshot, not a live choice description: _, // display string selected_mode_labels: _, // display strings, no resolution-time choice - min_x_value: _, // u32 - cant_be_copied: _, // bool - copy_count_status: _, // status tag - forward_result: _, // bool - chosen_x: _, // concrete cast-time X (chosen at announcement, not resolution) + // CR 700.2 + CR 700.2a: mode-root position marker. The modes were CHOSEN + // at announcement (`modal` / `mode_abilities`, folded into the verdict + // above); this records only where each chosen mode's instructions begin + // in the linearized chain. It raises no `WaitingFor` and gates no prompt. + modal_instruction_ordinal: _, + min_x_value: _, // u32 + cant_be_copied: _, // bool + copy_count_status: _, // status tag + forward_result: _, // bool + chosen_x: _, // concrete cast-time X (chosen at announcement, not resolution) cost_paid_object: _, // concrete captured-object snapshot cost_paid_object_ids: _, // concrete captured-object ids (issue #4948) effect_context_object: _, // concrete captured-object snapshot @@ -585,7 +590,7 @@ pub(crate) fn chain_offers_choice(a: &ResolvedAbility) -> bool { target_selection_mode: _, // Chosen/Random tag (announce-time) chosen_players: _, // concrete chosen player ids (already selected) replacement_applied: _, // replacement provenance set, no prompt - sub_link: _, // SubAbilityLink kind tag + sub_link: _, // SubAbilityLink kind tag sibling_condition: _, // SiblingCondition replication marker, no resolution-time choice parent_target_missing_reason: _, // seam flag } = a; diff --git a/crates/engine/src/game/stack.rs b/crates/engine/src/game/stack.rs index a83830bfe6..d635d3be85 100644 --- a/crates/engine/src/game/stack.rs +++ b/crates/engine/src/game/stack.rs @@ -3144,6 +3144,7 @@ fn self_counter_ability_is_batch_candidate(ability: &ResolvedAbility) -> bool { target_choice_timing, description, selected_mode_labels, + modal_instruction_ordinal, repeat_for, min_x_value, announced_x, @@ -3206,6 +3207,14 @@ fn self_counter_ability_is_batch_candidate(ability: &ResolvedAbility) -> bool { && *target_choice_timing == TargetChoiceTiming::Stack && description.is_none() && selected_mode_labels.is_empty() + // CR 700.2 + CR 700.2d: a mode root is the head of ONE selected + // instruction of a modal ability, and its ordinal gates the + // per-mode reset of the chain-local tracked-set identity in + // `resolve_ability_chain`. A batch collapses N stack entries into a + // SINGLE chain entry, so it would fire that per-instruction boundary + // once instead of N times. That is outside what this batch proof + // covers, so decline — declining only costs the optimization. + && modal_instruction_ordinal.is_none() && repeat_for.is_none() && *min_x_value == 0 // CR 601.2b: an announce-locked X makes this ability's X board-dependent; @@ -3361,6 +3370,7 @@ fn fixed_controller_gain_life_ability_is_batch_candidate(ability: &ResolvedAbili target_choice_timing, description: _, selected_mode_labels, + modal_instruction_ordinal, repeat_for, min_x_value, announced_x, @@ -3417,6 +3427,14 @@ fn fixed_controller_gain_life_ability_is_batch_candidate(ability: &ResolvedAbili && target_constraints.is_empty() && *target_choice_timing == TargetChoiceTiming::Stack && selected_mode_labels.is_empty() + // CR 700.2 + CR 700.2d: a mode root is the head of ONE selected + // instruction of a modal ability, and its ordinal gates the + // per-mode reset of the chain-local tracked-set identity in + // `resolve_ability_chain`. A batch collapses N stack entries into a + // SINGLE chain entry, so it would fire that per-instruction boundary + // once instead of N times. That is outside what this batch proof + // covers, so decline — declining only costs the optimization. + && modal_instruction_ordinal.is_none() && repeat_for.is_none() && *min_x_value == 0 && announced_x.is_none() @@ -3558,6 +3576,7 @@ fn fixed_opponent_lose_life_ability_is_batch_candidate(ability: &ResolvedAbility target_choice_timing, description: _, selected_mode_labels, + modal_instruction_ordinal, repeat_for, min_x_value, announced_x, @@ -3614,6 +3633,14 @@ fn fixed_opponent_lose_life_ability_is_batch_candidate(ability: &ResolvedAbility && target_constraints.is_empty() && *target_choice_timing == TargetChoiceTiming::Stack && selected_mode_labels.is_empty() + // CR 700.2 + CR 700.2d: a mode root is the head of ONE selected + // instruction of a modal ability, and its ordinal gates the + // per-mode reset of the chain-local tracked-set identity in + // `resolve_ability_chain`. A batch collapses N stack entries into a + // SINGLE chain entry, so it would fire that per-instruction boundary + // once instead of N times. That is outside what this batch proof + // covers, so decline — declining only costs the optimization. + && modal_instruction_ordinal.is_none() && repeat_for.is_none() && *min_x_value == 0 && announced_x.is_none() @@ -4202,6 +4229,17 @@ fn inert_trigger_abilities_eq_ignoring_provenance( target_choice_timing: a_target_choice_timing, description: _, selected_mode_labels: a_selected_mode_labels, + // CR 700.2: deliberately NOT part of run identity. At the ROOT it is + // provably `None` — this function is entered ONLY through the three + // `*_ability_is_batch_candidate` gates, each of which now requires + // `modal_instruction_ordinal.is_none()`. That guarantee is ONE HOP + // deep: the `sub_ability`/`else_ability` recursions below re-enter + // this function directly, without re-checking a gate, so a deeper node + // could in principle carry an ordinal. Ignoring it is still right — + // this equality is issue #5946's `SourceIndependent` inert-trigger RUN + // IDENTITY, not a modal check, and two runs that differ only in which + // mode produced them are still the same run. + modal_instruction_ordinal: _, repeat_for: a_repeat_for, min_x_value: a_min_x_value, announced_x: a_announced_x, @@ -4260,6 +4298,17 @@ fn inert_trigger_abilities_eq_ignoring_provenance( target_choice_timing: b_target_choice_timing, description: _, selected_mode_labels: b_selected_mode_labels, + // CR 700.2: deliberately NOT part of run identity. At the ROOT it is + // provably `None` — this function is entered ONLY through the three + // `*_ability_is_batch_candidate` gates, each of which now requires + // `modal_instruction_ordinal.is_none()`. That guarantee is ONE HOP + // deep: the `sub_ability`/`else_ability` recursions below re-enter + // this function directly, without re-checking a gate, so a deeper node + // could in principle carry an ordinal. Ignoring it is still right — + // this equality is issue #5946's `SourceIndependent` inert-trigger RUN + // IDENTITY, not a modal check, and two runs that differ only in which + // mode produced them are still the same run. + modal_instruction_ordinal: _, repeat_for: b_repeat_for, min_x_value: b_min_x_value, announced_x: b_announced_x, diff --git a/crates/engine/src/types/ability.rs b/crates/engine/src/types/ability.rs index 565f641081..6a26a5d026 100644 --- a/crates/engine/src/types/ability.rs +++ b/crates/engine/src/types/ability.rs @@ -26002,6 +26002,32 @@ pub struct ResolvedAbility { /// individual instructions selected from those modes. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub selected_mode_labels: Vec, + /// CR 700.2 + CR 608.2c: Marks this node as the ROOT of one selected mode's + /// instructions, and gives its position in the resolution order. + /// + /// CR 700.2: "Each of those options is a mode" — distinct modes are distinct + /// instructions, not continuations of each other. CR 608.2c ("follows its + /// instructions in the order written … apply the rules of English") makes an + /// anaphor like "those cards" / "it" bind to its NEAREST antecedent, which + /// can never be a sibling mode's population. `build_chained_resolved` + /// linearizes every selected mode into ONE `sub_ability` chain, erasing that + /// boundary from the chain shape; this field restores it. + /// + /// CR 700.2d: the value is the OCCURRENCE ORDINAL (0-based position within + /// the ordered selection), NOT the printed mode index — "if a particular + /// mode is chosen multiple times, the spell is treated as if that mode + /// appeared that many times in sequence", so Eldrazi Confluence's `[1, 1]` + /// yields ordinals `0` and `1` at the same printed index. Index-keying would + /// collide the two occurrences into one instruction. + /// + /// `None` on every non-mode-root node, including within-mode continuation + /// steps and every ability of a non-modal spell. + /// + /// SOLE WRITER: `game::ability_utils::build_chained_resolved`. Do not stamp + /// it anywhere else — consumers rely on "is a mode root" being decidable + /// from this field alone. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub modal_instruction_ordinal: Option, /// CR 608.2c: Repeat this ability N times (from "for each [X], [effect]"). #[serde(default, skip_serializing_if = "Option::is_none")] pub repeat_for: Option, @@ -26228,6 +26254,7 @@ impl ResolvedAbility { target_choice_timing: TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, repeat_for: None, min_x_value: 0, announced_x: None, 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 08d8248fe6..6ad2160df7 100644 --- a/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs +++ b/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs @@ -175,6 +175,7 @@ fn make_grant_ability(controller: PlayerId, source: ObjectId) -> ResolvedAbility target_choice_timing: engine::types::ability::TargetChoiceTiming::Stack, description: None, selected_mode_labels: Vec::new(), + modal_instruction_ordinal: None, player_scope: None, starting_with: None, chosen_x: None, From 709757b1b0fd86b248754aa894cd4223bd1693f7 Mon Sep 17 00:00:00 2001 From: lgray Date: Sun, 16 Aug 2026 14:46:19 -0500 Subject: [PATCH 4/7] fix(engine): stop the tracked-set publish walk at a CR 700.2 mode boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build_chained_resolved` linearizes a modal spell's selected modes into ONE resolution chain. The publish gate and its two supporting walks are chain-wide, so an earlier mode's producer saw a LATER mode's "those cards" / "them" anaphor as its own consumer, published for it, and made the later mode's own publish decline. Trystan's Command mode 4 ("Creatures target player controls get +3/+3 until end of turn. Untap them.") was board-wrong on 3 of 3 legal mode pairs: the untap bound the companion mode's destroyed creature, created token, or returned card, and untapped nothing. CR 700.2 makes each bulleted option a mode — a separate instruction. CR 608.2c ("follows its instructions in the order written … apply the rules of English") makes an anaphor bind to its nearest antecedent, which is never a sibling mode's population. `crosses_modal_boundary` reads the mode-root marker added in the previous commit, and one shared descent wrapper applies it at all four places a walk can enter another instruction: 1. `next_sub_needs_tracked_set`'s entry hop; 2. both recursive descents in `ability_or_branch_references_tracked_set`: `append_to_sub_chain` hangs the next mode's root off the TAIL of the current mode's sub-chain, so for any mode with more than one node the entry hop lands on a within-mode node and the recursion is what reaches the next mode. NO CORPUS CARD exercises this today — a scan of `data/card-data.json` funnels 179 modal cards -> 69 with a multi-node mode -> 10 with a tracked-set-consuming mode -> 0 with a multi-node mode ordered BEFORE a consuming one, since `ordered_selected_mode_indices` sorts and every multi-node mode found sits at its card's highest index. The discriminating row is therefore SYNTHESIZED and disclosed as such, and the corpus is a generated artifact whose consumer side may be undercounted relative to this branch's parser; 3. `later_node_is_publisher_position`'s walk, so a later MODE's producer cannot veto this mode's publish; 4. `chain_references_tracked_set`'s ARGUMENT — its two callers hold the producer and pass the parked continuation, so for them entering the argument is itself a crossing. It is deliberately keyed on the mode ordinal, never on `sub_link`: a sentence boundary and a mode boundary are different things. That was RUN, not reasoned about — keying the reset on `sub_link == SequentialSibling` instead reddens 8 integration rows of 5131. Among the two non-modal rows added here, RANDOM ENCOUNTER is the witness: its chain fragments into two tracked sets. EPIC EXPERIMENT, which has the deeper cross-sibling consumer and looks like the stronger guard, stays GREEN under the same mis-key, so it is kept as a derivation-only no-regression row with no guard status claimed. Both chains are dumped from `parse_oracle_text` rather than inferred from punctuation; two successive readings of these rows predicted the opposite pairing before the probe was run. Tests: the two deliberately-inverted Trystan canaries are flipped as their own doc comments mandated; Settle Beyond Reality pins the set contents; a synthesized two-node-mode row discriminates crossing 2 specifically; Expose the Culprit is the positive control for the already-correct fresh-publish class. Assisted-by: ClaudeCode:claude-opus-4.8 --- crates/engine/src/game/effects/mod.rs | 101 ++- ...kai_ascendancy_pump_untap_anaphora_6857.rs | 679 ++++++++++++++++-- 2 files changed, 716 insertions(+), 64 deletions(-) diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 196b30d649..38a7302781 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -5330,11 +5330,43 @@ pub fn is_known_effect(effect: &Effect) -> bool { /// what makes compound exile (Suspend Aggression's /// "Exile target nonland permanent and the top card of your library ... /// for each of those cards") expose both exiled objects to the grant. +/// +/// The walk STOPS at a mode boundary — see [`crosses_modal_boundary`]. pub(crate) fn next_sub_needs_tracked_set(ability: &ResolvedAbility) -> bool { - ability - .sub_ability - .as_deref() - .is_some_and(ability_or_branch_references_tracked_set) + branch_references_tracked_set(ability.sub_ability.as_deref()) +} + +/// CR 700.2 + CR 608.2c: does ENTERING `node` cross into a different modal +/// instruction? +/// +/// CR 700.2 makes each bulleted option a mode, and `build_chained_resolved` +/// linearizes the selected modes into one `sub_ability` chain — so "is the next +/// node part of my instruction or the start of the next one?" is not answerable +/// from the chain's shape. It is answerable from `modal_instruction_ordinal`, +/// which is stamped on exactly the mode roots. +/// +/// CR 608.2c ("apply the rules of English") is why this matters: a walk asking +/// "does anything after me consume the set I would publish?" is looking for an +/// ANTECEDENT relationship, and a later mode's "those cards" never names an +/// earlier mode's population. A node's OWN consumption is never a crossing — +/// only entering a child, or being handed a parked continuation, is. +/// +/// Deliberately NOT keyed on `sub_link`: a `SequentialSibling` marks a sentence +/// boundary, which is a different thing from a mode boundary (measured — Random +/// Encounter's sentence boundary parses to `ContinuationStep`, while Epic +/// Experiment's non-modal chain puts a live tracked-set consumer two +/// `SequentialSibling` hops below its producer). See the `SubAbilityLink` doc: +/// "Do not add a consumer that infers a sentence boundary from this field." +fn crosses_modal_boundary(node: &ResolvedAbility) -> bool { + node.modal_instruction_ordinal.is_some() +} + +/// [`ability_or_branch_references_tracked_set`] applied to a branch that the +/// caller is about to ENTER, with the mode-boundary stop of +/// [`crosses_modal_boundary`]. Every descent in this family goes through here, +/// so the stop cannot be applied at some entry points and forgotten at others. +fn branch_references_tracked_set(node: Option<&ResolvedAbility>) -> bool { + node.is_some_and(|n| !crosses_modal_boundary(n) && ability_or_branch_references_tracked_set(n)) } /// CR 608.2c: Does `ability` (or any of its continuation branches) consume the @@ -5344,8 +5376,16 @@ pub(crate) fn next_sub_needs_tracked_set(ability: &ResolvedAbility) -> bool { /// whether the chosen cards must be published as the fresh tracked set the /// continuation reads (End-Blaze Epiphany: "choose a card exiled this way … /// you may play that card"). +/// +/// CR 700.2: the mode-boundary stop applies to THE ARGUMENT ITSELF here, unlike +/// [`next_sub_needs_tracked_set`] where the caller IS the node. Both live +/// callers hold the producer and pass the PARKED CONTINUATION, so for them +/// entering the argument is already a crossing: an interactive choose made in +/// mode N must not publish for mode N+1's anaphor. Within-mode continuations +/// carry no ordinal and are unaffected — that is every corpus row these two +/// sites serve today. pub(crate) fn chain_references_tracked_set(ability: &ResolvedAbility) -> bool { - ability_or_branch_references_tracked_set(ability) + branch_references_tracked_set(Some(ability)) } /// CR 608.2c + CR 611.2c: An event-less producer publishes the population its @@ -5366,6 +5406,12 @@ pub(crate) fn chain_references_tracked_set(ability: &ResolvedAbility) -> bool { /// * no LATER node in this chain is itself in publisher position — the same /// `next_sub_needs_tracked_set` predicate the publish site is gated on. /// +/// CR 700.2 + CR 608.2c: leg 2 stops at a mode boundary +/// ([`crosses_modal_boundary`]). A later MODE's producer is a different +/// instruction, not a competing antecedent for this one, so it must not veto +/// this mode's publish. Leg 1 is the mode's own responsibility and is made true +/// at every mode root by the boundary reset in `resolve_ability_chain`. +/// /// When the guard declines, the arm falls through to the `_ =>` `ZoneChanged` /// harvest, which yields `[]` for every head in this class (they emit no /// `ZoneChanged`) — i.e. byte-identical to the pre-#6857 engine. @@ -5378,6 +5424,15 @@ pub(crate) fn chain_references_tracked_set(ability: &ResolvedAbility) -> bool { /// undetached chain would have declined. Measured unreachable at the time of /// writing: 0 of the 627 event-less heads in the corpus carry a `player_scope`. /// If one ever does, leg 2 needs the pre-split ability, not the template. +/// The mode-boundary stop is neither wider nor narrower than that gap: it is the +/// same walk over the same pre-split ability. On the fan-out path itself the stop +/// is REDUNDANT rather than load-bearing — `split_player_scope_chain` has already +/// detached the tail that would hold the next mode's root — so it can only narrow +/// the gate/leg-2 disagreement above, never widen it. (A mode root CAN be the +/// fan-out head: `build_resolved_from_def` copies `player_scope` onto every mode +/// root, pinned by `build_resolved_from_def_preserves_player_scope`, and 17 +/// corpus cards carry a mode-level `player_scope` — Rankle's Prank on all three +/// modes.) fn is_sole_chain_producer(state: &GameState, ability: &ResolvedAbility) -> bool { let no_earlier_producer = state.chain_tracked_set_id.is_none_or(|id| { state @@ -5404,9 +5459,18 @@ fn is_sole_chain_producer(state: &GameState, ability: &ResolvedAbility) -> bool /// `motivated_pony_untaps_only_the_attacking_creatures_it_pumped` goes red. The /// fix at that point is to scope the leg to the nearest antecedent (CR 608.2c's /// actual rule) rather than to relax the test. +/// +/// CR 700.2: chain-wide, but NOT past a mode boundary. Without that stop, two +/// publishing modes A → B make leg 2 walk past A's own consumer into B's root, +/// find B's consumer, and DECLINE A's publish — so A's within-mode consumer +/// binds nothing. The stop is applied inside `walk`, which covers the seed and +/// both recursions uniformly. fn later_node_is_publisher_position(ability: &ResolvedAbility) -> bool { fn walk(node: Option<&ResolvedAbility>) -> bool { node.is_some_and(|n| { + if crosses_modal_boundary(n) { + return false; + } // CR 603.7: production's own predicate, unmodified — a node whose // consumer merely DEFERS (a `CreateDelayedTrigger // { uses_tracked_set: true }`, which acts at a later time) still @@ -5442,15 +5506,26 @@ fn ability_or_branch_references_tracked_set(ability: &ResolvedAbility) -> bool { .as_ref() .is_some_and(quantity_expr_references_tracked_set); + // CR 700.2 + CR 608.2c: both descents stop at a mode boundary. Guarding only + // the entry hop in `next_sub_needs_tracked_set` is INSUFFICIENT whenever a + // mode has more than one node: `append_to_sub_chain` hangs the next mode's + // root off the TAIL of the current mode's own sub-chain, so the entry hop + // lands on a within-mode node (no ordinal, so it passes) and an unguarded + // recursion then descends into the next mode's root and finds ITS consumer. + // + // NO CORPUS CARRIER — disclosed, and the discriminating row + // (`modal_two_node_mode_does_not_publish_for_a_later_modes_anaphor`) is + // SYNTHESIZED from two shapes this engine already parses separately. A scan + // of `data/card-data.json` funnels 179 modal cards with >= 2 selectable modes + // -> 69 with a multi-node mode -> 10 with a tracked-set-consuming mode -> 0 + // with a multi-node mode ORDERED BEFORE a consuming one, because + // `ordered_selected_mode_indices` sorts and every multi-node mode found sits + // at the highest index of its card. That corpus is a generated artifact and + // its consumer side may be undercounted relative to this branch's parser; + // regenerate `card-data.json` to close it. consumes - || ability - .sub_ability - .as_deref() - .is_some_and(ability_or_branch_references_tracked_set) - || ability - .else_ability - .as_deref() - .is_some_and(ability_or_branch_references_tracked_set) + || branch_references_tracked_set(ability.sub_ability.as_deref()) + || branch_references_tracked_set(ability.else_ability.as_deref()) } /// Returns true if the effect references the most recent tracked set through diff --git a/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs index 09fa7119ea..2877c17c79 100644 --- a/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs +++ b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs @@ -988,47 +988,41 @@ fn valley_floodcaller_untaps_exactly_the_creatures_it_pumped() { ); } -/// Trystan's Command — PRESERVED, and a regression sentinel for the two-regime -/// law rather than a fix. +/// Trystan's Command — THE BOARD-VISIBLE HEADLINE for the mode-scoping fix. +/// This row was committed INVERTED, pinning the rules-wrong outcome with a doc +/// comment mandating this flip; the flip is what that comment asked for. /// /// The card is MODAL (choose two of four sibling abilities), not a chain — the -/// engine resolves the chosen modes in sequence, so the publish gate sees the -/// later mode's consumer. With the destroy mode chosen alongside the pump mode, -/// the destroy publishes first, `is_sole_chain_producer`'s leg 1 declines the -/// mass pump, and the anaphor resolves against the destroyed creature — which -/// untaps nothing. That is a KNOWN, BOUNDED gap that predates this PR: before -/// the parser rewrite the implicit pronoun bound elsewhere and also untapped -/// nothing. The row exists to prove the behaviour did not get WORSE, so do not -/// "simplify" it away on the grounds that it asserts a non-untap. +/// engine linearizes the chosen modes into ONE resolution chain, so the publish +/// gate used to see a later mode's consumer as if it were its own. With the +/// destroy mode chosen alongside the pump mode, the destroy published first, +/// `is_sole_chain_producer`'s leg 1 declined the mass pump, and mode 4's anaphor +/// resolved against the DESTROYED creature — untapping nothing. /// -/// STRUCTURAL CONSEQUENCE — this is not "one unmeasured mode pair". MEASURED: -/// the card is `min_choices: 2, max_choices: 2` over `mode_count: 4`, so a -/// companion mode is ALWAYS chosen; and two of the three possible companions -/// publish before mode 4 resolves — destroy (this row) and token copy (the row -/// below, `[0, 3]`, published set = the created token). INFERRED, not measured: -/// the graveyard-return companion publishes too, because it moves cards to hand -/// and the `_ =>` arm of the publish switch harvests `ZoneChanged`. If that -/// inference is wrong, mode 4 is fixable for exactly one of three pairs. -/// **On the two measured pairs, Trystan's Command mode 4 cannot be fixed while -/// the publish gate is chain-wide rather than mode-scoped.** -/// CR 700.2 is the lever: modes are separate instructions, so a sibling mode's -/// `Destroy` arguably should not count as an "earlier producer" for mode 4's -/// anaphor at all. Fixing that means scoping the gate to the mode, not weakening -/// this test. +/// CR 700.2: "each of those options is a mode", i.e. a separate instruction. +/// CR 608.2c ("apply the rules of English"): "Untap them" names the creatures +/// THIS mode just pumped; a sibling mode's `Destroy` is not its antecedent. +/// `next_sub_needs_tracked_set` now stops at the mode boundary, so mode 3's +/// destroy no longer publishes for mode 4's consumer and mode 4 publishes its +/// own pumped population. /// -/// Two measured side-facts, recorded because they are easy to misread: -/// * the TRACKED SET does change (empty -> `[victim]`). The parser rewrite -/// creates a `TrackedSet` consumer where there was none, so the pre-existing -/// `Destroy` publish arm now fires. The BOARD is unaffected, because the only -/// consumer is an untap aimed at a creature that is already in the graveyard. -/// * this row is a SECOND leg-1 witness: with `no_earlier_producer` deleted the -/// set becomes `[victim, mine]`, i.e. the mass pump joins the destroy's set. +/// STRUCTURAL SCOPE — this is not one mode pair. MEASURED: the card is +/// `min_choices: 2, max_choices: 2` over `mode_count: 4`, so a companion mode is +/// ALWAYS chosen, and all three possible companions published before mode 4 +/// resolved: destroy (this row), token copy (the row below, `[0, 3]`), and +/// graveyard-return (`[1, 3]`, measured `tracked_sets = [(1, [grave_card])]`). +/// Mode 4 was board-wrong on 3 of 3 legal pairs. /// -/// The `tapped` assertion below therefore pins a rules-INCORRECT outcome on -/// purpose, as a no-regression sentinel. When the mode-scoping fix lands it must -/// be flipped to `!tapped`, not deleted. +/// DISCRIMINATION: the two flipping values are `published_set` (`[victim]` -> +/// `[mine]`) and `tapped(mine)` (`true` -> `false`). Revert the mode-boundary +/// stop in `next_sub_needs_tracked_set` and both go back. `power == Some(5)` and +/// the graveyard assertion are the paired NON-VACUITY witnesses: they prove both +/// modes really executed, so a `!tapped` reading cannot come from the pump +/// simply never running. `published_set` panics on a second set, so it doubles +/// as a "the fix did not fragment the chain into two sets" assertion — the count +/// stays 1. #[test] -fn trystans_command_pump_mode_is_unchanged_when_an_earlier_mode_publishes() { +fn trystans_command_pump_mode_untaps_the_population_its_own_mode_pumped() { let mut scenario = GameScenario::new(); scenario.at_phase(Phase::PreCombatMain); let victim = scenario.add_creature(P1, "Victim", 2, 2).id(); @@ -1056,8 +1050,9 @@ fn trystans_command_pump_mode_is_unchanged_when_an_earlier_mode_publishes() { assert_eq!( published_set(runner.state()), - ids(&[victim]), - "CR 608.2c: the earlier mode's destroy is the live antecedent" + ids(&[mine]), + "CR 608.2c: mode 4's \"them\" names the creatures mode 4 pumped, not the \ + sibling mode's destroy victim" ); assert_eq!( runner.state().objects[&victim].zone, @@ -1070,21 +1065,24 @@ fn trystans_command_pump_mode_is_unchanged_when_an_earlier_mode_publishes() { "non-vacuity: the pump mode really executed too" ); assert!( - tapped(runner.state(), mine), - "unchanged from before this PR — see the doc comment" + !tapped(runner.state(), mine), + "CR 701.26b: the pumped creature untaps — the defect this PR fixes" ); } -/// The token-copy companion mode, measured: the second half of the "any pair -/// preempts mode 4" claim in the row above. +/// The token-copy companion mode: the same fix reached through a DIFFERENT +/// publishing arm. Also committed inverted, also flipped here. /// -/// Modes 1 and 4 (`[0, 3]`). The copy token's creation publishes first, leg 1 -/// declines the mass pump, and the anaphor binds the TOKEN — so the pumped -/// creatures stay tapped even though the pump itself ran. Same bounded gap as -/// the destroy pair, reached through a different publishing arm, which is the -/// point: the gate is chain-wide, so WHICH earlier mode published is irrelevant. +/// Modes 1 and 4 (`[0, 3]`). The copy token's creation used to publish first, +/// leg 1 declined the mass pump, and mode 4's anaphor bound the TOKEN — so the +/// pumped creatures stayed tapped even though the pump itself ran. The +/// mode-boundary stop is arm-agnostic: it is not "the destroy arm was special", +/// it is that no earlier MODE publishes for a later mode's anaphor. +/// +/// REDUNDANT BY MECHANISM with the row above — same crossing, same predicate. +/// Kept for card/arm coverage; do NOT cite it as a second independent bar. #[test] -fn trystans_command_token_copy_mode_also_preempts_the_pump_anaphor() { +fn trystans_command_token_copy_mode_no_longer_preempts_the_pump_anaphor() { let mut scenario = GameScenario::new(); scenario.at_phase(Phase::PreCombatMain); let elf = scenario @@ -1121,10 +1119,19 @@ fn trystans_command_token_copy_mode_also_preempts_the_pump_anaphor() { .map(|id| id.0) .collect(); assert_eq!(token.len(), 1, "non-vacuity: the copy mode really ran"); + // Mode 4 pumps "creatures target player controls", and by the time it + // resolves that is the elf, `mine`, AND the token mode 1 just created — CR + // 611.2c fixes the affected set when the continuous effect begins, and the + // token already exists. So the token's presence here is the PUMP's own + // population, not a leak: pre-fix this set was the token ALONE. + let mut expected = ids(&[elf, mine]); + expected.extend_from_slice(&token); + expected.sort_unstable(); assert_eq!( published_set(runner.state()), - token, - "CR 608.2c: the earlier mode's token is the live antecedent" + expected, + "CR 608.2c: mode 4's \"them\" is the population mode 4 pumped, not the \ + sibling mode's token alone" ); assert_eq!( runner.state().objects[&mine].power, @@ -1132,7 +1139,577 @@ fn trystans_command_token_copy_mode_also_preempts_the_pump_anaphor() { "non-vacuity: the pump mode really executed too" ); assert!( - tapped(runner.state(), mine), - "unchanged from before this PR — see the doc comment above" + !tapped(runner.state(), mine), + "CR 701.26b: the pumped creature untaps — the defect this PR fixes" + ); +} + +/// Settle Beyond Reality (2X2, `{4}{W}` sorcery) — a tracked-set-CONTENTS row +/// plus a shadowing canary. It is NOT board-visible: an earlier draft claimed it +/// was, and running it falsified that (see the shadowing paragraph below). Oracle +/// text verbatim from Scryfall; both modes chosen ("Choose one or both —", +/// `min_choices: 1, max_choices: 2`). +/// +/// MEASURED parse shape at `77e686cae` (probe over `parse_oracle_text`): +/// * mode 0 — `ChangeZone { destination: Exile, target: Typed { Creature, +/// controller: Opponent } }`, no sub-ability; +/// * mode 1 — `ChangeZone { destination: Exile, target: Typed { Creature, +/// controller: You } }` -> `ChangeZone { destination: Battlefield, target: +/// TrackedSet { id: 0 } }`. +/// +/// `TrackedSetId(0)` is the "most recent set" sentinel: it binds to the +/// HIGHEST-id tracked set, whatever produced it. The publish gate +/// (`next_sub_needs_tracked_set`) is chain-wide, and the modes are linearized +/// into ONE resolution chain, so mode 0's exile sees mode 1's `TrackedSet` +/// consumer below it and publishes `[theirs]`. Mode 1's exile then EXTENDS that +/// same set to `[theirs, mine]`. **This row asserts the SET, not the board.** +/// +/// MEASURED at `77e686cae`: the board is already correct here, and an earlier +/// draft of this comment claiming otherwise was falsified by running it. A +/// singular `ChangeZone` calls `targeting::resolved_targets` then +/// `effects::effect_object_targets`, where `TargetFilter::TrackedSet` falls into +/// the `_ =>` arm that returns `ability.targets` — this mode's OWN inherited +/// chosen target, `[mine]`. `targeted_objects` is then non-empty, so the +/// untargeted zone-scan path (which would have used `matches_target_filter` and +/// moved every set member) is never reached. **Chosen-target inheritance is +/// already mode-scoped and SHADOWS the sentinel for this consumer**, which is +/// why the leaked `[theirs, mine]` is inert on the board here. +/// +/// So this is a tracked-set-CONTENTS row. The two "theirs stays exiled" +/// assertions below PASS pre-fix: they are a no-regression **canary** that would +/// flip if anyone ever unshadowed `ChangeZone`, NOT discriminators. Only the +/// `published_set` assertion discriminates. Any later claim that the zone +/// assertions discriminate is a re-justification, not a restatement. +/// +/// CR 700.2 + CR 700.2a: each bullet is a separate mode, chosen independently. +/// CR 608.2c ("apply the rules of English"): "it" in mode 1 is a +/// nearest-antecedent pronoun bound to mode 1's own exile — a sibling mode's +/// exile is not its antecedent. The opponent's creature must stay in exile. +/// +/// The three assertions are load-bearing in this order: +/// 1. non-vacuity — mode 0's exile really executed (a `ZoneChanged` to Exile for +/// the opponent's creature). Without it, "the opponent's creature is not on +/// the battlefield" would also pass if mode 0 had silently never run, and +/// "it is on the battlefield" could not be distinguished from "it was never +/// exiled"; +/// 2. THE DEFECT — the opponent's creature is still in exile; +/// 3. the published set contains only mode 1's own exile. `published_set` +/// panics on a second set, so it doubles as a "the fix did not fragment the +/// chain" assertion. +#[test] +fn settle_beyond_reality_return_mode_returns_only_the_creature_its_own_mode_exiled() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let theirs = scenario.add_creature(P1, "Their Bear", 2, 2).id(); + let mine = scenario.add_creature(P0, "My Bear", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Settle Beyond Reality", + false, + "Choose one or both —\n• Exile target creature you don't control.\n• Exile target creature you control, then return it to the battlefield under its owner's control.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + let outcome = runner + .cast(spell) + .modes(&[0, 1]) + .target_objects(&[theirs, mine]) + .resolve(); + + let exiled_theirs = outcome + .events() + .iter() + .filter(|e| { + matches!( + e, + engine::types::events::GameEvent::ZoneChanged { + object_id, + to: engine::types::zones::Zone::Exile, + .. + } if *object_id == theirs + ) + }) + .count(); + assert_eq!( + exiled_theirs, 1, + "non-vacuity: mode 0 must actually exile the opponent's creature, \ + otherwise the exile assertion below is vacuous" + ); + + assert_eq!( + runner.state().objects[&theirs].zone, + engine::types::zones::Zone::Exile, + "CR 608.2c: mode 1's \"return it\" names the creature MODE 1 exiled; \ + the opponent's creature must stay exiled" + ); + assert!( + !runner.state().battlefield.contains(&theirs), + "CR 700.2: a sibling mode's exile is not mode 1's antecedent — the \ + opponent's creature must not come back" + ); + + assert_eq!( + runner.state().objects[&mine].zone, + engine::types::zones::Zone::Battlefield, + "CR 400.7j: an effect that moves an object to a public zone can still \ + find it, so mode 1 returns its own exiled creature to the battlefield" + ); + assert!( + runner.state().battlefield.contains(&mine), + "mode 1's own creature is the whole population of \"return it\"" + ); + + assert_eq!( + published_set(runner.state()), + ids(&[mine]), + "CR 608.2c: the tracked set mode 1's anaphor binds holds only mode 1's \ + own exile" + ); +} + +/// SYNTHESIZED, disclosed (precedent: this file's "Bare Pump" rows) — the +/// discriminator for the SECOND mode-boundary crossing: the recursive descents +/// inside `ability_or_branch_references_tracked_set`. +/// +/// Guarding only the ENTRY HOP in `next_sub_needs_tracked_set` is insufficient +/// whenever a mode has MORE THAN ONE node, because `append_to_sub_chain` hangs +/// the next mode's root off the TAIL of the current mode's own sub-chain. Here +/// mode 1 is `Destroy -> Draw`, so the entry hop lands on the `Draw` — which +/// carries no ordinal (it is within-mode) and does not consume, so it passes — +/// and the recursion below it reaches mode 2's root. Without the stop on that +/// recursion, mode 1's `Destroy` publishes `[victim]` for mode 2's "Untap +/// them", `is_sole_chain_producer`'s leg 1 then declines mode 2's own publish, +/// and the untap binds a creature that is already in the graveyard. +/// +/// DISCRIMINATION: delete the stop from the `sub_ability` descent in +/// `ability_or_branch_references_tracked_set` and `published_set` goes back to +/// `[victim]` while `tapped(mine)` goes back to `true`. The single-node modes on +/// the real cards above cannot flip this — their entry hop already lands on the +/// next mode's root, so crossing #1 alone covers them. +/// +/// The `else_ability` descent of the same function has NO discriminating row and +/// no coverage is claimed for it: reverting it (together with crossing #4, the +/// other `else_ability` guard) leaves the integration suite at 5131 passed / 0 +/// failed. It cannot currently be reached — `append_to_sub_chain` walks only +/// `sub_ability`, and `build_chained_resolved` is the sole writer of +/// `modal_instruction_ordinal`, so no mode root can sit in an `else_ability` +/// slot. It is kept for uniformity across the descents, not for a demonstrated +/// defect. +/// +/// WHY SYNTHESIZED: the plan named Grub's Command as the real carrier. MEASURED +/// against `parse_oracle_text` in this worktree, it is not one — its pump bullet +/// lowers to a SINGLE `GenericEffect` node with no rider, and its mill bullet's +/// consumer lowers to a singular `ChangeZone { destination: Hand, target: +/// TrackedSetFiltered { id: 0, filter: Any } }` whose Goblin restriction the +/// parser drops and which is inert at runtime. It could not have discriminated +/// anything. The two bullets glued here are the verbatim shapes this file +/// already exercises separately. +/// +/// CR 700.2 (each bullet is a mode) + CR 608.2c ("Untap them" names the +/// creatures its OWN mode pumped) + CR 701.26b (untap). +#[test] +fn modal_two_node_mode_does_not_publish_for_a_later_modes_anaphor() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let victim = scenario.add_creature(P1, "Victim", 2, 2).id(); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + scenario.with_library_top(P0, &["Lib A"]); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Two Node Command", + false, + "Choose two —\n• Destroy target creature. Draw a card.\n• Creatures target player controls get +3/+3 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + let hand_before = runner.state().players[0].hand.len(); + for id in [victim, mine] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + + runner + .cast(spell) + .modes(&[0, 1]) + .target_objects(&[victim]) + .target_player(P0) + .resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + runner.state().objects[&victim].zone, + engine::types::zones::Zone::Graveyard, + "non-vacuity: mode 1's destroy really executed, so it really was in \ + publisher position when the descent ran" + ); + assert_eq!( + runner.state().players[0].hand.len(), + hand_before, + "non-vacuity: mode 1's SECOND node really executed too (the cast spent \ + the spell from hand and the draw replaced it), so the entry hop really \ + landed on a within-mode node rather than on mode 2's root" + ); + assert_eq!( + runner.state().objects[&mine].power, + Some(5), + "non-vacuity: mode 2's pump really executed" + ); + assert_eq!( + published_set(runner.state()), + ids(&[mine]), + "CR 608.2c: mode 2's \"them\" is mode 2's own pumped population — mode \ + 1's destroy must not have published across the boundary" + ); + assert!( + !tapped(runner.state(), mine), + "CR 701.26b: the pumped creature untaps" + ); +} + +/// Expose the Culprit, modes `[0, 1]` — POSITIVE CONTROL for the class of +/// producers that already allocate a fresh, strictly-greater set id. +/// +/// NO-REGRESSION. Mode 2 (index 1) heads with `ChooseObjectsIntoTrackedSet`, +/// which publishes through `publish_fresh_tracked_set` — an UNGATED site that +/// never consults `next_sub_needs_tracked_set` at all. The mode-boundary work +/// must leave it exactly where it was: the pile it cloaks is the pile the player +/// chose, no more and no less, with mode 1's unrelated turn-face-up in front of +/// it in the same resolution chain. +/// +/// CR 701.58a (cloak) + CR 700.2 (modes are separate instructions). +#[test] +fn expose_the_culprit_cloaks_only_the_pile_it_chose() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let hidden = scenario.add_creature(P0, "Hidden", 2, 2).id(); + let pile: Vec = ["Dis A", "Dis B"] + .iter() + .map(|n| { + scenario + .add_creature(P0, n, 2, 2) + .with_keyword(engine::types::keywords::Keyword::Disguise( + ManaCost::generic(3).into(), + )) + .id() + }) + .collect(); + let bystander = scenario + .add_creature(P0, "Dis C", 2, 2) + .with_keyword(engine::types::keywords::Keyword::Disguise( + ManaCost::generic(3).into(), + )) + .id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Expose the Culprit", + false, + "Choose one or both —\n• Turn target face-down creature face up.\n• Exile any number of face-up creatures you control with disguise in a face-down pile, shuffle that pile, then cloak them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner + .state_mut() + .objects + .get_mut(&hidden) + .unwrap() + .face_down = true; + + runner + .cast(spell) + .modes(&[0, 1]) + .target_objects(&[hidden]) + .commit(); + runner.advance_until_stack_empty(); + assert!( + matches!( + runner.state().waiting_for, + WaitingFor::ChooseObjectsSelection { .. } + ), + "reach-guard: mode 2's pile selection must be reached, or nothing below \ + is about the fresh-publish class. got {:?}", + runner.state().waiting_for ); + runner + .act(GameAction::SelectTargets { + targets: pile.iter().map(|&id| TargetRef::Object(id)).collect(), + }) + .expect("pile selection accepted"); + + assert!( + !runner.state().objects[&hidden].face_down, + "non-vacuity: mode 1 really turned its own target face up, so both modes \ + ran in one chain" + ); + for &id in &pile { + assert!( + runner.state().objects[&id].face_down, + "CR 701.58a: every chosen creature is cloaked" + ); + } + assert!( + !runner.state().objects[&bystander].face_down, + "an unchosen disguise creature is outside the pile under any reading" + ); + // `ChooseObjectsIntoTrackedSet` publishes a fresh EMPTY set at its head and + // then the chosen pile as a strictly-greater id, so this chain legitimately + // holds two sets and `published_set`'s single-set helper does not apply. + let sets = tracked_sets(runner.state()); + assert_eq!( + sets.last().map(Vec::as_slice), + Some(ids(&pile).as_slice()), + "the fresh-published pile is the highest-id set, unchanged by the \ + mode-boundary work: got {sets:?}" + ); +} + +/// Drive the game forward until every delayed trigger has resolved, answering +/// the turn-based actions that would otherwise stall a bare priority pass. +fn advance_until_delayed_triggers_resolve(runner: &mut GameRunner) { + for guard in 0..256 { + if runner.state().delayed_triggers.is_empty() && runner.state().stack.is_empty() { + return; + } + let action = match &runner.state().waiting_for { + WaitingFor::DeclareAttackers { .. } => GameAction::DeclareAttackers { + attacks: vec![], + bands: vec![], + }, + WaitingFor::DeclareBlockers { .. } => GameAction::DeclareBlockers { + assignments: vec![], + }, + WaitingFor::DiscardToHandSize { count, cards, .. } => GameAction::SelectCards { + cards: cards.iter().take(*count).copied().collect(), + }, + _ => GameAction::PassPriority, + }; + assert!( + runner.act(action).is_ok(), + "stalled at guard {guard}: phase={:?} waiting={:?}", + runner.state().phase, + runner.state().waiting_for + ); + } + panic!("delayed trigger never resolved"); +} + +// =========================================================================== +// CROSS-`SequentialSibling` NO-REGRESSION GATES +// +// Both cards are NON-MODAL, so `modal_instruction_ordinal` is `None` on every +// node and the mode-boundary machinery is provably inert for them. That is the +// point: they hold the mode-keyed design to its claim that a sentence boundary +// is not a mode boundary. +// +// WHICH OF THE TWO ACTUALLY GUARDS THAT — MEASURED, not derived. Probe: key the +// reset on `sub_link == SubAbilityLink::SequentialSibling` instead of the mode +// ordinal, run the whole integration suite (8 rows red of 5131): +// +// * RANDOM ENCOUNTER goes RED — it is the mis-key guard. Its chain fragments +// into two tracked sets (`[[1, 1, 2, 2, 3, 4], []]`) and the single-set +// precondition in `published_set` fails. NOTE this is NOT its delayed +// `Bounce`, which is a separately measured runtime no-op: the guard value is +// the fragmentation, not the bounce. +// * EPIC EXPERIMENT stays GREEN — so despite having the deeper cross-sibling +// consumer, it does NOT discriminate a `sub_link` mis-key and no guard status +// is claimed for it. It is derivation-only here. +// +// Two successive readings of these rows (mine, then a reviewer's) predicted the +// opposite pairing. Both were wrong. Do not re-derive this from the chain shapes +// below — re-run the probe. +// +// Chains dumped from `parse_oracle_text` in this worktree (not inferred from +// the Oracle text's punctuation — that inference has been measured wrong here): +// +// Epic Experiment: ExileTop(ContinuationStep) +// -> CastFromZone(SequentialSibling) +// -> ChangeZoneAll{TrackedSetFiltered{0}}(SequentialSibling) +// +// Random Encounter: Shuffle(ContinuationStep) +// -> Mill(ContinuationStep) +// -> ChangeZoneAll{TrackedSetFiltered{0,Creature}}(ContinuationStep) +// -> haste GenericEffect(SequentialSibling) +// -> CreateDelayedTrigger{Bounce{TrackedSet{0}}}(SequentialSibling) +// =========================================================================== + +/// Epic Experiment — a tracked-set consumer TWO `SequentialSibling` hops below +/// its producer, with an INTERACTIVE cast step inside the window. +/// +/// The `ExileTop` publishes; `CastFromZone` (a `SequentialSibling`, and a real +/// pause) sits between; and `ChangeZoneAll { TrackedSetFiltered { 0 } }` — "put +/// all cards exiled this way that weren't cast into your graveyard" — is a +/// second `SequentialSibling` below that. +/// +/// NO GUARD STATUS IS CLAIMED. The obvious derivation — "a reset keyed on +/// `sub_link` would fire twice inside one instruction and orphan the anaphor, so +/// the exiled cards would stay in exile forever" — was MEASURED FALSE: under that +/// exact mis-key this row stays GREEN (see the section header). WHY it survives +/// is not measured — the standing candidate, that `TrackedSetFiltered(0)` binds +/// through `targeting::resolve_tracked_set_id` whose later rungs still find the +/// `ExileTop` set after a chain-id clear, is a reading and is recorded as one. +/// Keep this as a derivation-only no-regression row; the measured mis-key +/// witness is Random Encounter. +/// +/// CR 608.2c: this is ONE instruction sequence, not two. The card is not modal, +/// so `crosses_modal_boundary` is false at every node and the binding must +/// survive byte-identically. +#[test] +fn epic_experiment_binds_its_exiled_set_across_two_sequential_siblings() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + // Mana value 5 each, so `X = 2` makes NONE of them castable: the + // `CastFromZone` step is reached and offers nothing, and every exiled card + // must fall through to the graveyard step below it. + let lib: Vec = ["Lib A", "Lib B"] + .iter() + .map(|n| { + scenario + .add_spell_to_library_top(P0, n, false) + .with_mana_cost(ManaCost::generic(5)) + .from_oracle_text("You gain 1 life.") + .id() + }) + .collect(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Epic Experiment", + false, + "Exile the top X cards of your library. You may cast instant and sorcery spells with mana value X or less from among them without paying their mana costs. Then put all cards exiled this way that weren't cast into your graveyard.", + ) + .with_mana_cost(ManaCost::Cost { + shards: vec![engine::types::mana::ManaCostShard::X], + generic: 0, + }) + .id(); + scenario.with_mana_pool( + P0, + (0..2) + .map(|_| { + engine::types::mana::ManaUnit::new( + engine::types::mana::ManaType::Colorless, + ObjectId(0), + false, + vec![], + ) + }) + .collect(), + ); + let mut runner: GameRunner = scenario.build(); + runner.cast(spell).x(2).resolve(); + + assert_eq!( + published_set(runner.state()), + ids(&lib), + "CR 608.2c: the exiled cards remain the anaphor's antecedent across both \ + SequentialSibling hops" + ); + for &id in &lib { + assert_eq!( + runner.state().objects[&id].zone, + engine::types::zones::Zone::Graveyard, + "the uncast exiled cards reach the graveyard — the observable that \ + an orphaned anaphor would leave stranded in exile" + ); + } +} + +/// Random Encounter — the harder cross-`SequentialSibling` case: a DELAYED +/// consumer, two `SequentialSibling` hops deep, that reads slot 0 at the next +/// end step rather than during the resolution that published it. +/// +/// `Mill` publishes; `ChangeZoneAll { TrackedSetFiltered { 0, Creature } }` puts +/// the milled creatures onto the battlefield; then, past a haste rider and a +/// `CreateDelayedTrigger`, both `SequentialSibling`, the delayed "return those +/// creatures to their owner's hand" binds `TrackedSet { 0 }`. +/// +/// THIS ROW IS THE MIS-KEY GUARD, measured (see the section header): with the +/// reset keyed on `sub_link` instead of the mode ordinal, this chain fragments +/// into two tracked sets (`[[1, 1, 2, 2, 3, 4], []]`) and the single-set +/// precondition in `published_set` fails. The guard value is the FRAGMENTATION, +/// not the delayed bounce — the bounce is a separately measured runtime no-op +/// (see the KNOWN GAP note at the end of this test), so no claim rides on it. +/// +/// CR 603.7 (delayed triggered ability) + CR 608.2c. Non-modal, so the +/// mode-boundary machinery is inert here by construction. +#[test] +fn random_encounter_delayed_bounce_binds_across_two_sequential_siblings() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let lib: Vec = ["Lib A", "Lib B", "Lib C", "Lib D"] + .iter() + .map(|n| scenario.add_card_to_library_top(P0, n)) + .collect(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Random Encounter", + false, + "Shuffle your library, then mill four cards. Put each creature card milled this way onto the battlefield. They gain haste. At the beginning of the next end step, return those creatures to their owner's hand.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + // Two of the four milled cards are creatures, so the reanimated population + // is a proper subset of the milled set. + let creatures = [lib[0], lib[1]]; + for &id in &creatures { + let obj = runner.state_mut().objects.get_mut(&id).unwrap(); + obj.card_types.core_types = vec![CoreType::Creature]; + obj.base_card_types.core_types = vec![CoreType::Creature]; + obj.power = Some(2); + obj.toughness = Some(2); + obj.base_power = Some(2); + obj.base_toughness = Some(2); + } + runner.cast(spell).resolve(); + + for &id in &creatures { + assert_eq!( + runner.state().objects[&id].zone, + engine::types::zones::Zone::Battlefield, + "non-vacuity: the milled creatures really entered, or the delayed \ + bounce below has nothing to bind" + ); + } + assert!( + !runner.state().delayed_triggers.is_empty(), + "reach-guard: the delayed 'return those creatures' trigger was created" + ); + + let set = published_set(runner.state()); + for &id in &creatures { + assert!( + set.contains(&id.0), + "CR 603.7 + CR 608.2c: the set the delayed trigger will read still \ + holds the creatures this resolution put onto the battlefield, two \ + SequentialSibling hops above it. got {set:?}" + ); + } + + advance_until_delayed_triggers_resolve(&mut runner); + + // KNOWN GAP, MEASURED at this tree and NOT caused by this PR: the end-step + // return is a no-op. The delayed ability is created carrying an UNBOUND + // `Bounce { target: TrackedSet { id: 0 } }` with an empty `targets` list + // (dumped), and draining it to the end step produces ZERO `ZoneChanged` + // events. Every node of this non-modal card carries + // `modal_instruction_ordinal: None` (also dumped), so all four mode-boundary + // crossings are inert here by construction and this reading is byte-identical + // to the pre-PR engine. + // + // Pinned rather than omitted, in this file's established canary style: when + // the delayed `TrackedSet(0)` binding is fixed, these two assertions go RED + // and must be flipped to `Zone::Hand`, not deleted. + for &id in &creatures { + assert_eq!( + runner.state().objects[&id].zone, + engine::types::zones::Zone::Battlefield, + "unchanged from before this PR — see the KNOWN GAP note above" + ); + } } From e6d7f42185f35ae6414d9b82a3c33f59c19c3056 Mon Sep 17 00:00:00 2001 From: lgray Date: Sun, 16 Aug 2026 15:31:32 -0500 Subject: [PATCH 5/7] fix(engine): reset the chain tracked set at each CR 700.2 mode boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four crossings in the previous commit stop an earlier mode publishing FOR a later mode's anaphor. They cannot reach the other half of the defect: a mode that publishes legitimately, for its OWN within-mode consumer, and thereby leaves `chain_tracked_set_id` pointing at a non-empty set when the NEXT mode begins. `is_sole_chain_producer`'s leg 1 is `chain_tracked_set_id.is_none_or(|id| set.is_empty())`, so the next mode's event-less producer declines to publish, falls through to the `ZoneChanged` harvest (empty by construction for that class), and `publish_tracked_set([])` EXTENDS the previous mode's set instead of allocating a new one. Its own "those cards" then binds the previous mode's population. `GameState::resolving_modal_instruction` makes `resolve_ability_chain` clear `chain_tracked_set_id` at each mode root, so leg 1 is true at every mode boundary by construction and `publish_tracked_set`'s else-branch allocates a strictly greater id. That is what makes "the highest tracked-set id" mean "the set the currently-resolving instruction published" — CR 608.2c's nearest antecedent — for the eight readers that bind the sentinel with a raw `max_by_key`. The ordering argument is written once, on `publish_tracked_set`, and cross-referenced from each of the eight; they stay deliberately un-unified because not skipping empty sets is the CORRECT semantics under mode scoping and two of them are pinned against unification by their own regression tests. The reset is EDGE-triggered on the ordinal and cleared in the same prelude line group as `chain_tracked_set_id` — adjacency is load-bearing. A level trigger would re-fire once per fanned-out player, because `split_player_scope_chain`'s per-player clone retains the ordinal and re-enters at depth + 1. A new unit test is the only instrument with a nameable flip for that. Also collapses `FilterProp::InTrackedSet`, which open-coded `targeting::resolve_tracked_set_id`'s body verbatim, into a call. Tests: reverting this commit reddens TWO integration rows — `modal_pump_mode_untaps_its_own_population_when_an_earlier_mode_published_for_itself` and the arm-D row below — plus the edge-trigger unit test, which is the only instrument with a nameable flip for edge-vs-level. Neither integration row is commit-exclusive, so a red there localises the defect to "the reset or the crossing", not to one of them; a mode-alone row is the anti-vacuity pair. Also lands the arm-D gate for the PREVIOUS commit's third crossing (the stop inside `later_node_is_publisher_position`'s walk), which had no falsifying row. The plan's named carrier, Plunge into Darkness, was measured incapable of it: `is_sole_chain_producer` is consulted from exactly three match guards (`PumpAll`, `GoadAll`, `GiveControl`), all event-less producers, and Plunge's first mode heads with an event-emitting `Sacrifice`. The row is therefore a synthesized two-bullet card, disclosed in its doc comment, whose bullets are verbatim shapes this suite already exercises. Reverting the crossing at tip takes mode 1's published set from `[mine, flickered]` to `[]` and leaves `tapped(mine)` true; the set COUNT does not move, so the row asserts contents. Two always-on censuses moved and were re-derived with evidence rather than re-pinned: the CR 603.5 prompt census (uniform line drift, each producer sha256-identical at its new coordinate) and the CR 733 authority matrix (a new write-family field needs a row). The new field's own provenance census also found a real bug in its `#[cfg(test)] mod` region cut and now carries a negative control for it. Assisted-by: ClaudeCode:claude-opus-4.8 --- crates/engine/src/game/ability_utils.rs | 44 +++- crates/engine/src/game/effects/counters.rs | 11 + .../src/game/effects/grant_permission.rs | 5 + crates/engine/src/game/effects/mod.rs | 137 +++++++++++ crates/engine/src/game/effects/tap_untap.rs | 5 + crates/engine/src/game/engine.rs | 29 ++- crates/engine/src/game/filter.rs | 8 +- crates/engine/src/game/quantity.rs | 25 ++ crates/engine/src/types/game_state.rs | 28 ++- .../fixtures/cr733/authority_matrix.json.gz | Bin 42027 -> 42170 bytes ...kai_ascendancy_pump_untap_anaphora_6857.rs | 231 ++++++++++++++++++ 11 files changed, 502 insertions(+), 21 deletions(-) diff --git a/crates/engine/src/game/ability_utils.rs b/crates/engine/src/game/ability_utils.rs index d22844d2ee..3d3fd04ef4 100644 --- a/crates/engine/src/game/ability_utils.rs +++ b/crates/engine/src/game/ability_utils.rs @@ -9738,6 +9738,7 @@ mod tests { assert!(files.len() > 100, "reach-guard: the walk found the crate"); let mut writers: Vec = Vec::new(); + let mut uncut_writers = 0usize; let mut control_hits = 0usize; for path in &files { let text = std::fs::read_to_string(path).expect("read source"); @@ -9745,30 +9746,49 @@ mod tests { // in prose is neither counted nor able to hide a deleted writer. let code = crate::source_census::code_lines(&text); let lines: Vec<&str> = code.lines().collect(); - // Cut at the `#[cfg(test)] mod ...` boundary. `#[cfg(test)]` also - // guards individual `use`/`fn` items in this crate; those are NOT the - // boundary, and treating them as one would hide real writers. + // Cut at the FIRST `#[cfg(test)]` THAT IS FOLLOWED BY `mod`, not at + // the first `#[cfg(test)]` full stop. This crate also `#[cfg(test)]`- + // guards individual `use` and `fn` items (`ability_utils.rs` has four + // before its test module), and an earlier draft of this scan located + // the first marker and then merely CHECKED whether it introduced a + // module — which made the cut silently degrade to "no cut at all" in + // exactly the files that need it. Measured: it counted this PR's own + // `effects/mod.rs` unit-test writers as production writers. let end = lines .iter() - .position(|line| line.trim_start().starts_with("#[cfg(test)]")) - .filter(|i| { - lines[i + 1..] - .iter() - .find(|l| !l.trim().is_empty()) - .is_some_and(|l| l.trim_start().starts_with("mod ")) + .enumerate() + .position(|(i, line)| { + line.trim_start().starts_with("#[cfg(test)]") + && lines[i + 1..] + .iter() + .find(|l| !l.trim().is_empty()) + .is_some_and(|l| l.trim_start().starts_with("mod ")) }) .unwrap_or(lines.len()); let rel = path.display().to_string(); - for line in &lines[..end] { + for (i, line) in lines.iter().enumerate() { if write_forms.iter().any(|f| line.contains(f.as_str())) { - writers.push(format!("{rel}: {}", line.trim())); + uncut_writers += 1; + if i < end { + writers.push(format!("{rel}: {}", line.trim())); + } } - if rel.ends_with(control_file) { + if i < end && rel.ends_with(control_file) { control_hits += line.matches(control.as_str()).count(); } } } + // NEGATIVE CONTROL for the region cut itself: the same scan WITHOUT the + // `#[cfg(test)] mod` cut must find strictly more writers. Without this + // arm a broken cut is invisible whenever no test happens to write the + // field — and then the day one does, this row reds for the wrong reason. + assert!( + uncut_writers > writers.len(), + "NEGATIVE CONTROL: the `#[cfg(test)] mod` cut must actually be \ + excluding test-module writers. uncut={uncut_writers} cut={}", + writers.len() + ); assert_eq!( control_hits, 1, "POSITIVE CONTROL: `build_chained_resolved`'s `SequentialSibling` write \ diff --git a/crates/engine/src/game/effects/counters.rs b/crates/engine/src/game/effects/counters.rs index 4103db1b11..534a80339e 100644 --- a/crates/engine/src/game/effects/counters.rs +++ b/crates/engine/src/game/effects/counters.rs @@ -1709,6 +1709,17 @@ pub fn resolve_add_all( // effect refers to the preceding effect's set even when it affected no // objects. Preserve that counter-specific fallback while supporting the // filtered "each of those " intersection. + // CR 700.2 + CR 608.2c: both sentinel arms below are ladders whose FIRST rung + // is `chain_tracked_set_id`, and that rung is what mode scoping acts on — the + // boundary reset in `resolve_ability_chain` clears it at each mode root, so + // the chain rung either holds the currently-resolving mode's own set or is + // absent. The trailing raw `max_by_key` rung is the fallback, and it stays + // mode-correct for the same reason the other sentinel readers do: the + // ordering argument written once on `effects::publish_tracked_set`. + // Deliberately not routed through `targeting::resolve_tracked_set_id`: that + // authority SKIPS empty sets, and here not skipping is the correct semantics + // (a chained counter effect refers to the preceding effect's set even when it + // affected no objects). let target_filter = match crate::game::effects::resolved_object_filter(ability, &target_filter) { TargetFilter::TrackedSet { diff --git a/crates/engine/src/game/effects/grant_permission.rs b/crates/engine/src/game/effects/grant_permission.rs index e86c09a8d6..c0a2382e78 100644 --- a/crates/engine/src/game/effects/grant_permission.rs +++ b/crates/engine/src/game/effects/grant_permission.rs @@ -55,6 +55,11 @@ pub fn resolve( // (which skips empties for inline "from among the milled cards" // continuations) — see the regression test // `tracked_set_sentinel_does_not_reuse_prior_non_empty_set_when_current_move_is_empty`. + // CR 700.2 + CR 608.2c: "highest id" == "the set the currently-resolving + // instruction published" — the ordering argument is written once, on + // `effects::publish_tracked_set`. Deliberately not routed through + // `targeting::resolve_tracked_set_id`: that authority SKIPS empty sets, and + // under mode scoping not skipping is the correct semantics here. TargetFilter::TrackedSet { id: TrackedSetId(0), } => state diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index 38a7302781..d25d400bbf 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -6456,6 +6456,32 @@ fn mandatory_parent_effect_performed(effect: &Effect, events: &[GameEvent]) -> b } } +/// THE ORDERING ARGUMENT for the global-max sentinel readers. +/// +/// Eight consumers bind `TrackedSetId(0)` with a raw +/// `state.tracked_object_sets.iter().max_by_key(|(id, _)| id.0)` instead of the +/// documented id authority `targeting::resolve_tracked_set_id`, and they are +/// DELIBERATELY not unified — their empty-set semantics are individually +/// load-bearing and two are pinned by name in their own doc comments. What makes +/// "the highest tracked-set id" the right answer under CR 608.2c is ordering, +/// not selection: +/// +/// > [`publish_tracked_set`]'s else-branch allocates +/// > `TrackedSetId(next_tracked_set_id)` and increments; `next_tracked_set_id` is +/// > monotone (seeded at 1, never decremented). `resolve_ability_chain` sets +/// > `chain_tracked_set_id = None` at every CR 700.2 mode boundary, so the first +/// > publish inside each mode takes that else-branch and allocates a STRICTLY +/// > GREATER id than any preceding mode's set. "The highest tracked-set id" is +/// > therefore "the set the currently-resolving instruction published" — exactly +/// > CR 608.2c's nearest antecedent. +/// +/// The eight readers cross-reference this paragraph rather than restating it. +/// NOTE the polarity difference that keeps them un-unified: the authority skips +/// EMPTY sets (`latest_tracked_set_id`), while these readers do not. Under mode +/// scoping, not skipping is the CORRECT behaviour — a mode whose producer +/// affected nothing publishes a fresh EMPTY set at the highest id, and its own +/// consumer must bind that empty set rather than fall back to a preceding mode's +/// non-empty one. pub(crate) fn publish_tracked_set(state: &mut GameState, affected_ids: Vec) { // CR 603.7 + CR 608.2c: Chain unification. If an ancestor in this // resolution chain already published a tracked set, extend that set with @@ -9343,12 +9369,54 @@ pub fn resolve_ability_chain( // coalesce into a single tracked set, while unrelated resolutions // stay isolated. state.chain_tracked_set_id = None; + // CR 700.2: the edge latch for the mode boundary below. It is cleared + // HERE, in the same line group as `chain_tracked_set_id`, and that + // ADJACENCY IS LOAD-BEARING: the latch means "the chain set has already + // been cleared for this mode", so a prelude that cleared one without the + // other would either suppress the first mode's reset (stale `Some(0)` + // from a previous resolution) or fire it against a set the previous + // resolution owned. Keep them together. + state.resolving_modal_instruction = None; // CR 608.2c + CR 109.5: Player-action accumulator resets per // top-level chain so "each opponent who searched this way" only sees // players who acted in the current resolution. state.player_actions_this_way.clear(); } + // CR 700.2 ("each of those options is a mode") + CR 608.2c (instructions in + // the order written; apply the rules of English): a preceding mode's + // published population is not this mode's antecedent, so a mode root starts + // with no inherited chain tracked set. This is the FOURTH narrowing of + // `chain_tracked_set_id`, and its closest analogue is the + // `RepeatContinuation::WhileCondition` arm below — "each repeated process is + // a FRESH execution of the instructions, so its 'that card'/'those cards' + // tracked set must not extend the prior iteration's". Substitute "mode" for + // "iteration" and that is this reset. + // + // EDGE-triggered on the ORDINAL, never on `sub_link`: a sentence boundary + // and a mode boundary are different things (see the `SubAbilityLink` doc: + // "Do not add a consumer that infers a sentence boundary from this field"). + // MEASURED, not derived: keying this reset on + // `sub_link == SubAbilityLink::SequentialSibling` instead reddens 8 + // integration rows of 5131 — Random Encounter, Suicidal Charge, Taunt from + // the Rampart, both Witness rows, Emperor of Bones (#1515), Sanar Vivid + // (#4253) and Winding Way (#2931). Which rows those are was predicted wrong + // twice before the probe was run; re-run it rather than re-deriving it. + // + // The edge is what keeps a `player_scope` fan-out — which re-enters this + // function once per player with a clone that RETAINS the ordinal — from + // resetting once per player and fragmenting the mode's population. + // + // Deliberately NOT mirrored into `resolve_chain_body`, the second entry + // point (`drive_repeat_for_outermost` calls it directly): a `repeat_for` + // iteration of ONE mode must not re-fire its own mode boundary. + if ability.modal_instruction_ordinal.is_some() + && ability.modal_instruction_ordinal != state.resolving_modal_instruction + { + state.resolving_modal_instruction = ability.modal_instruction_ordinal; + state.chain_tracked_set_id = None; + } + // BeginGame abilities are handled by mulligan setup, not normal stack resolution. // CR 103.5b: Mulligan-time abilities (Serum Powder, No-Regrets Egret) likewise never // resolve through the stack — their runtime path lives in `mulligan.rs`. @@ -16546,6 +16614,75 @@ mod tests { } } + /// CR 700.2: the mode boundary is EDGE-triggered, and this is the only + /// instrument in the suite with a nameable flip for "it must not re-fire on + /// re-entry into the SAME mode". + /// + /// The hazard is real and not hypothetical: `split_player_scope_chain` does + /// `let mut scoped = ability.clone()`, so the per-player clone RETAINS + /// `modal_instruction_ordinal`, and the fan-out loop re-enters + /// `resolve_ability_chain` once per matching player at `depth + 1` — past the + /// depth-0 prelude. A level trigger (reset whenever an ordinal is present) + /// would clear `chain_tracked_set_id` on every one of those entries and + /// fragment ONE mode's population into one set per player. The paused-chain + /// resume takes the same shape: its remaining scoped nodes also carry the + /// ordinal and also re-enter at depth 1. + /// + /// DISCRIMINATION: drop the `!= state.resolving_modal_instruction` conjunct + /// (the edge → level change) and the seeded id is gone after the second + /// entry, so `assert_eq!(.., Some(seeded))` fails. + #[test] + fn mode_boundary_reset_is_edge_triggered_and_survives_re_entry() { + let mut state = GameState::new_two_player(42); + let mut mode_root = ResolvedAbility::new( + Effect::GenericEffect { + static_abilities: Vec::new(), + duration: None, + target: None, + end_cost: None, + }, + vec![], + ObjectId(100), + PlayerId(0), + ); + mode_root.modal_instruction_ordinal = Some(0); + let mut events = Vec::new(); + + // First entry: the depth-0 prelude clears both fields, then the edge + // fires because `Some(0) != None`. + resolve_ability_chain(&mut state, &mode_root, &mut events, 0).expect("first entry"); + assert_eq!( + state.resolving_modal_instruction, + Some(0), + "reach-guard: the edge must have fired on the first entry, or the \ + second entry below is not testing re-entry at all" + ); + + // This mode then publishes. `split_player_scope_chain`'s loop re-enters + // with the SAME ordinal-bearing node at depth 1, which skips the prelude. + let seeded = TrackedSetId(7); + state.chain_tracked_set_id = Some(seeded); + resolve_ability_chain(&mut state, &mode_root, &mut events, 1).expect("re-entry"); + + assert_eq!( + state.chain_tracked_set_id, + Some(seeded), + "CR 700.2: re-entering the SAME modal instruction is not a new \ + instruction, so its published set must survive" + ); + + // And the edge DOES fire for the next mode, which is what keeps this row + // from passing by simply never resetting anything. + let mut next_mode = mode_root.clone(); + next_mode.modal_instruction_ordinal = Some(1); + resolve_ability_chain(&mut state, &next_mode, &mut events, 1).expect("next mode"); + assert_eq!( + state.chain_tracked_set_id, None, + "CR 608.2c: a DIFFERENT mode is a new instruction and does start \ + with no inherited antecedent" + ); + } + #[test] fn resolve_ability_chain_single_effect() { let mut state = GameState::new_two_player(42); diff --git a/crates/engine/src/game/effects/tap_untap.rs b/crates/engine/src/game/effects/tap_untap.rs index 0f29559cff..734efac1cd 100644 --- a/crates/engine/src/game/effects/tap_untap.rs +++ b/crates/engine/src/game/effects/tap_untap.rs @@ -41,6 +41,11 @@ fn tap_untap_target_ids( ) -> Vec { match effect_target { TargetFilter::SelfRef => vec![ability.source_id], + // CR 700.2 + CR 608.2c: "highest id" == "the set the currently-resolving + // instruction published" — the ordering argument is written once, on + // `effects::publish_tracked_set`. Deliberately not routed through + // `targeting::resolve_tracked_set_id`: that authority SKIPS empty sets, and + // under mode scoping not skipping is the correct semantics here. TargetFilter::TrackedSet { id: TrackedSetId(0), } => state diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 8b369025e2..575169cd0b 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -19012,11 +19012,30 @@ mod stage2_injector_tests { // Identity re-established, not assumed: `9869a19f28c791ee`, // `2bc316e3aa0297f8`, `8df98486627bfe15` at the new coordinates — the same // three digests this log has carried since the first merge. - // `PreviousEffectCount` classification adds one line above all three producers, - // so they move uniformly to `:7003/:7080/:10318`; no prompt site changes. - "game/effects/mod.rs:7003".to_string(), - "game/effects/mod.rs:7080".to_string(), - "game/effects/mod.rs:10318".to_string(), + // `PreviousEffectCount` classification adds one line above all three + // producers, so main's move uniformly to `:7003/:7080/:10318`; no prompt + // site changes. + // + // #6857 (this branch, re-measured after the rebase onto `4c987f92a`): + // main's `:7003/:7080/:10318` => `:7225/:7302/:10582`. LOCATED BY DIGEST, + // not by arithmetic: each upstream pin's 10-line producer block was hashed + // at `upstream/main` and that digest searched for in this tree -- + // `817ae852`/`43c05331`/`37d51f60`, each found at exactly ONE coordinate. + // Those three digests measure IDENTICALLY at `a8244e734` and at + // `4c987f92a`, so #7503 moved the producers without modifying them, and + // this branch displaced none of them. + // + // The deltas are AGAIN NOT uniform: `+222`/`+222`/`+264`. The additivity + // argument used above does not apply to this branch, because its + // insertions are not all above the first producer -- the mode-boundary + // reset lands in `resolve_ability_chain`, between the second and third. + // Uniformity is therefore the WRONG evidence here; digest identity is the + // right one, and it is what establishes the set was preserved. This + // branch writes `state.waiting_for` nowhere: the publish arms return + // `Vec` and prompt for nothing, so it adds no producer here. + "game/effects/mod.rs:7225".to_string(), + "game/effects/mod.rs:7302".to_string(), + "game/effects/mod.rs:10582".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/game/filter.rs b/crates/engine/src/game/filter.rs index 93c09c59ec..7d9c4eb0ec 100644 --- a/crates/engine/src/game/filter.rs +++ b/crates/engine/src/game/filter.rs @@ -5895,11 +5895,13 @@ fn matches_filter_prop( // legs; the combat-damage-source leg of that authority injects a source // constraint rather than a set id and does not apply to a set-membership // predicate. Composes with `FilterProp::Not` for "all other ". + // + // The two-rung ladder is `targeting::resolve_tracked_set_id`'s body + // verbatim, so it is a CALL rather than a copy — an open-coded duplicate + // of a documented single authority is a divergence waiting to happen. FilterProp::InTrackedSet { id } => { let resolved = if id.0 == 0 { - state - .chain_tracked_set_id - .or_else(|| crate::game::targeting::latest_tracked_set_id(state)) + crate::game::targeting::resolve_tracked_set_id(state) } else { Some(*id) }; diff --git a/crates/engine/src/game/quantity.rs b/crates/engine/src/game/quantity.rs index acfaaa8b70..cf104fddca 100644 --- a/crates/engine/src/game/quantity.rs +++ b/crates/engine/src/game/quantity.rs @@ -276,6 +276,11 @@ fn visit_characteristic_leaf<'s>( // equals the bound cause; drawn members are unstamped and excluded. // `None` admits every member. Mirrors `FilteredTrackedSetSize`'s set // selection (highest set id) and cause filter. + // CR 700.2 + CR 608.2c: "highest id" == "the set the currently-resolving + // instruction published" — the ordering argument is written once, on + // `effects::publish_tracked_set`. Deliberately not routed through + // `targeting::resolve_tracked_set_id`: that authority SKIPS empty sets, and + // under mode scoping not skipping is the correct semantics here. CardTypeSetSource::TrackedSet { caused_by } => { if let Some((set_id, ids)) = state.tracked_object_sets.iter().max_by_key(|(id, _)| id.0) { @@ -3998,6 +4003,11 @@ fn resolve_ref( // An unavailable count resolves to zero. QuantityRef::PreviousEffectCount => state.last_effect_count.unwrap_or(0), // CR 608.2c: "for each [thing] this way" — read the most recent tracked set size. + // CR 700.2 + CR 608.2c: "highest id" == "the set the currently-resolving + // instruction published" — the ordering argument is written once, on + // `effects::publish_tracked_set`. Deliberately not routed through + // `targeting::resolve_tracked_set_id`: that authority SKIPS empty sets, and + // under mode scoping not skipping is the correct semantics here. QuantityRef::TrackedSetSize => state .tracked_object_sets .iter() @@ -4008,6 +4018,11 @@ fn resolve_ref( // set that also satisfy the inner filter. Used for "for each nontoken // creature you controlled that was destroyed this way" — the tracked set // holds all destroyed creatures; the filter narrows to controlled nontokens. + // CR 700.2 + CR 608.2c: "highest id" == "the set the currently-resolving + // instruction published" — the ordering argument is written once, on + // `effects::publish_tracked_set`. Deliberately not routed through + // `targeting::resolve_tracked_set_id`: that authority SKIPS empty sets, and + // under mode scoping not skipping is the correct semantics here. QuantityRef::FilteredTrackedSetSize { filter, caused_by } => { let Some((set_id, ids)) = state.tracked_object_sets.iter().max_by_key(|(id, _)| id.0) else { @@ -4075,6 +4090,11 @@ fn resolve_ref( let ids: Vec = match source { // Chain-published tracked set ("those exiled cards"): the set the // immediately-preceding chain effect published (highest id). + // CR 700.2 + CR 608.2c: "highest id" == "the set the currently-resolving + // instruction published" — the ordering argument is written once, on + // `effects::publish_tracked_set`. Deliberately not routed through + // `targeting::resolve_tracked_set_id`: that authority SKIPS empty sets, and + // under mode scoping not skipping is the correct semantics here. TrackedAnaphorSource::ChainSet => state .tracked_object_sets .iter() @@ -7172,6 +7192,11 @@ pub(crate) fn possessed_tracked_set_member( controller: PlayerId, source_id: ObjectId, ) -> bool { + // CR 700.2 + CR 608.2c: "highest id" == "the set the currently-resolving + // instruction published" — the ordering argument is written once, on + // `effects::publish_tracked_set`. Deliberately not routed through + // `targeting::resolve_tracked_set_id`: that authority SKIPS empty sets, and + // under mode scoping not skipping is the correct semantics here. let Some((set_id, ids)) = state.tracked_object_sets.iter().max_by_key(|(id, _)| id.0) else { return false; }; diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 84cf0b5d64..caa0c5f8bc 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -15638,10 +15638,30 @@ declare_game_state! { /// effects (e.g., "Exile target permanent and the top card of your library /// ... For each of those cards") merge their results into a single set /// before downstream "those cards" references resolve. Cleared at the - /// top-level chain entry (depth == 0) in `resolve_ability_chain`. + /// top-level chain entry (depth == 0) in `resolve_ability_chain`, and — when + /// the chain is modal — again at each CR 700.2 mode boundary, keyed on + /// [`Self::resolving_modal_instruction`]. #[serde(default, skip_serializing_if = "Option::is_none")] pub chain_tracked_set_id: Option, + /// CR 700.2 + CR 608.2c: The `modal_instruction_ordinal` of the modal + /// instruction currently resolving. It EDGE-TRIGGERS the mode boundary in + /// `resolve_ability_chain`; `None` outside a modal resolution. + /// + /// Paired with [`Self::chain_tracked_set_id`] and cleared in the SAME depth-0 + /// prelude block: this field's only job is to record whether + /// `chain_tracked_set_id` has already been cleared for the mode now entering, + /// so the two must never be reset at different times. + /// + /// EDGE, not level. A level trigger (reset whenever an ordinal is present) + /// would re-fire on every re-entry into the SAME mode: + /// `split_player_scope_chain` clones the ordinal-bearing node once per + /// fanned-out player and re-enters `resolve_ability_chain` with each, and a + /// paused chain resumes its remaining scoped nodes at depth 1. Either would + /// fragment one mode's population into one set per player. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub resolving_modal_instruction: Option, + /// CR 608.2c + CR 614.6: Per-member producer-action provenance for tracked /// sets. When a producer publishes (or extends) a chain tracked set, each /// affected object is additionally stamped here with the ACTION that made it @@ -21169,6 +21189,7 @@ impl GameState { tracked_object_sets: HashMap::new(), next_tracked_set_id: 1, chain_tracked_set_id: None, + resolving_modal_instruction: None, tracked_set_member_causes: HashMap::new(), commander_cast_count: HashMap::new(), commander_cast_owners: HashMap::new(), @@ -23005,6 +23026,10 @@ fn _gamestate_partition_is_total(s: &GameState) { tracked_object_sets: _, next_tracked_set_id: _, chain_tracked_set_id: _, + // CR 700.2: mode-boundary edge latch, cleared in the same depth-0 prelude + // block as `chain_tracked_set_id` above and meaningful only inside one + // resolution — the same reason that field is projected out here. + resolving_modal_instruction: _, tracked_set_member_causes: _, commander_cast_count: _, commander_cast_owners: _, @@ -23332,6 +23357,7 @@ impl PartialEq for GameState { && self.tracked_object_sets == other.tracked_object_sets && self.next_tracked_set_id == other.next_tracked_set_id && self.chain_tracked_set_id == other.chain_tracked_set_id + && self.resolving_modal_instruction == other.resolving_modal_instruction && self.tracked_set_member_causes == other.tracked_set_member_causes && self.commander_cast_count == other.commander_cast_count && self.commander_cast_owners == other.commander_cast_owners diff --git a/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz b/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz index bf1aae1c260a399471896bbe1be6a33bc0908de3..70d39522fedbd94e6473a55be784c03cd4a0129c 100644 GIT binary patch literal 42170 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

jVLN46kRfoa%W18$-IJV}?u$W|>2`&~ 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 Smw)@$zy1F)SN(8RlLi3mZrRBI literal 42027 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 diff --git a/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs index 2877c17c79..fb4bf600bb 100644 --- a/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs +++ b/crates/engine/tests/integration/jeskai_ascendancy_pump_untap_anaphora_6857.rs @@ -1713,3 +1713,234 @@ fn random_encounter_delayed_bounce_binds_across_two_sequential_siblings() { ); } } + +/// SYNTHESIZED, disclosed (precedent: this file's "Bare Pump" rows) — the ONLY +/// row that reverts on the boundary RESET alone, and therefore the only evidence +/// that the reset earns its place beside the four crossings. +/// +/// Both bullets are verbatim shapes this file/PR already exercise separately: +/// bullet 1 is Settle Beyond Reality's return mode, bullet 2 is Trystan's +/// Command mode 4. Glued into one card, they produce the shape the crossings +/// CANNOT reach: mode 1 publishes for its OWN within-mode consumer, so the +/// mode-boundary stop in `next_sub_needs_tracked_set` correctly does NOT fire — +/// that publish is legitimate. +/// +/// What happens next is the defect the reset closes. Mode 1 leaves +/// `chain_tracked_set_id` pointing at a NON-EMPTY set. Mode 2's `PumpAll` then +/// consults `is_sole_chain_producer`, whose leg 1 is +/// `chain_tracked_set_id.is_none_or(|id| set.is_empty())` — false — so the pump +/// declines to publish, falls through to the `_ =>` `ZoneChanged` harvest (empty +/// for an event-less producer), and `publish_tracked_set([])` EXTENDS mode 1's +/// set instead of allocating a new one. "Untap them" then binds mode 1's +/// flickered creature and the pumped creature stays tapped. +/// +/// The reset makes leg 1 true at every mode root by construction. +/// +/// DISCRIMINATION: `tapped(mine)` `true` -> `false`, and `tracked_sets().len()` +/// `1` -> `2`. Revert the reset in `resolve_ability_chain` and both go back; +/// reverting the four crossings instead leaves this row GREEN, which is what +/// makes it commit-specific rather than a duplicate of the rows above. +/// +/// CR 700.2 + CR 608.2c + CR 701.26b. +#[test] +fn modal_pump_mode_untaps_its_own_population_when_an_earlier_mode_published_for_itself() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let flickered = scenario.add_creature(P0, "Flickered", 2, 2).id(); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Self Publishing Command", + false, + "Choose one or both —\n• Exile target creature you control, then return it to the battlefield under its owner's control.\n• Creatures target player controls get +3/+3 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.state_mut().objects.get_mut(&mine).unwrap().tapped = true; + + runner + .cast(spell) + .modes(&[0, 1]) + .target_objects(&[flickered]) + .target_player(P0) + .resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + runner.state().objects[&flickered].zone, + engine::types::zones::Zone::Battlefield, + "non-vacuity: mode 1's exile-and-return really executed, so it really \ + published a non-empty set of its own" + ); + assert_eq!( + runner.state().objects[&mine].power, + Some(5), + "non-vacuity: mode 2's pump really executed" + ); + let sets = tracked_sets(runner.state()); + assert_eq!( + sets.len(), + 2, + "CR 700.2: two modes, two instructions, two sets — mode 2 must ALLOCATE \ + rather than extend mode 1's. got {sets:?}" + ); + // Mode 2 pumps "creatures target player controls", and the flickered + // creature is back on the battlefield by then (CR 611.2c fixes the affected + // set when the continuous effect begins), so BOTH are mode 2's own + // population. Pre-reset there is only ONE set and it holds `[flickered]` + // alone — mode 2's harvest was empty and merely extended mode 1's set. + assert_eq!( + sets.last().map(Vec::as_slice), + Some(ids(&[flickered, mine]).as_slice()), + "CR 608.2c: mode 2's own pumped population is the highest-id set" + ); + assert!( + !tapped(runner.state(), mine), + "CR 701.26b: the pumped creature untaps" + ); +} + +/// ANTI-VACUITY INSTRUMENT for the row above, and a no-regression row in its own +/// right: the SAME synthesized card with only its pump bullet chosen. +/// +/// If this reads RED, the pump bullet does not lower to `PumpAll -> +/// SetTapState { target: TrackedSet }` at all, the row above is vacuous in BOTH +/// directions, and its verdict is void. It must pass before AND after the reset. +#[test] +fn modal_pump_mode_untaps_when_chosen_alone() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + scenario.add_creature(P0, "Flickered", 2, 2); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Self Publishing Command", + false, + "Choose one or both —\n• Exile target creature you control, then return it to the battlefield under its owner's control.\n• Creatures target player controls get +3/+3 until end of turn. Untap them.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + runner.state_mut().objects.get_mut(&mine).unwrap().tapped = true; + + runner.cast(spell).modes(&[1]).target_player(P0).resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + runner.state().objects[&mine].power, + Some(5), + "non-vacuity: the pump really executed" + ); + assert!( + !tapped(runner.state(), mine), + "the pump bullet really does lower to a TrackedSet untap — unchanged \ + before and after the reset" + ); +} + +/// THE ARM-D GATE, and the discriminator for the THIRD mode-boundary crossing — +/// the stop inside `later_node_is_publisher_position`'s walk. +/// +/// SYNTHESIZED, disclosed (precedent: this file's "Bare Pump" rows). Both +/// bullets are verbatim shapes this suite already exercises: bullet 1 is +/// Trystan's Command mode 4, bullet 2 is Settle Beyond Reality's return mode. +/// +/// WHY NOT PLUNGE INTO DARKNESS, the plan's named carrier: MEASURED — its first +/// mode heads with `Sacrifice`, and `is_sole_chain_producer` (the function +/// crossing #3 lives in) is consulted from exactly three match guards, all of +/// them event-less producers: `Effect::PumpAll`, `Effect::GoadAll`, +/// `Effect::GiveControl`. An event-emitting producer never reaches it, so Plunge +/// could not have discriminated this crossing on any mode pair. The shape that +/// CAN is "an event-less producer with its own within-mode consumer, followed by +/// another publishing mode", which is what this card is. +/// +/// THE DEFECT: `is_sole_chain_producer`'s leg 2 asks "is any LATER node in this +/// chain itself in publisher position?" and answers by walking the whole linear +/// chain. Mode 1 publishes for its own "Untap them" — legitimate, so crossing #1 +/// does not fire. But the walk then continues past that untap into MODE 2's root, +/// finds mode 2's own `TrackedSet` consumer, and reports "a later publisher +/// exists" — so mode 1's `PumpAll` DECLINES to publish, its harvest is empty, and +/// its own untap binds an empty set. A later mode's producer is a different +/// instruction (CR 700.2), not a competing antecedent (CR 608.2c), so leg 2 must +/// stop at the boundary. +/// +/// DISCRIMINATION, MEASURED at tip by deleting the `crosses_modal_boundary` +/// early return from `later_node_is_publisher_position::walk`: the first +/// published set goes `[mine, flickered]` -> `[]`; with that assertion relaxed +/// to a print, the untap assertion then fails too, i.e. `tapped(mine)` goes back +/// to `true`. The COUNT does not flip — both readings publish two sets — which +/// is why this row asserts contents and not `sets.len()` alone. +/// +/// ARM D: two modes both publishing, each consumer resolving inside its own +/// mode. RED here means the ordering argument in `publish_tracked_set`'s doc is +/// unsound, not merely that this crossing regressed. +/// +/// NOT COMMIT-EXCLUSIVE: this row also reverts on the mode-boundary reset in the +/// following commit (measured — reverting the reset reddens both this row and +/// `modal_pump_mode_untaps_its_own_population_when_an_earlier_mode_published_for_itself`), +/// so a red here localises the defect to "one of the two", not to this crossing. +/// The crossing-#3 attribution is the probe recorded above, not this row alone. +/// +/// CR 700.2 + CR 608.2c + CR 701.26b. +#[test] +fn two_publishing_modes_each_bind_their_own_population() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let mine = scenario.add_creature(P0, "Mine", 2, 2).id(); + let flickered = scenario.add_creature(P0, "Flickered", 2, 2).id(); + let spell = scenario + .add_spell_to_hand_from_oracle( + P0, + "Two Publisher Command", + false, + "Choose one or both —\n• Creatures target player controls get +3/+3 until end of turn. Untap them.\n• Exile target creature you control, then return it to the battlefield under its owner's control.", + ) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner: GameRunner = scenario.build(); + for id in [mine, flickered] { + runner.state_mut().objects.get_mut(&id).unwrap().tapped = true; + } + + runner + .cast(spell) + .modes(&[0, 1]) + .target_player(P0) + .target_objects(&[flickered]) + .resolve(); + evaluate_layers(runner.state_mut()); + + assert_eq!( + runner.state().objects[&mine].power, + Some(5), + "non-vacuity: mode 1's pump really executed, so its publish decision was \ + really taken" + ); + assert_eq!( + runner.state().objects[&flickered].zone, + engine::types::zones::Zone::Battlefield, + "non-vacuity: mode 2's exile-and-return really executed — that is what \ + puts mode 2 in PUBLISHER POSITION behind mode 1, which is the whole \ + precondition for leg 2 to have anything to find" + ); + + let sets = tracked_sets(runner.state()); + assert_eq!( + sets.len(), + 2, + "CR 700.2: two publishing modes, two instructions, two sets. got {sets:?}" + ); + assert_eq!( + sets.first().map(Vec::as_slice), + Some(ids(&[mine, flickered]).as_slice()), + "CR 608.2c: mode 1's set holds the population MODE 1 pumped. Empty here \ + means leg 2 walked into mode 2 and declined mode 1's publish" + ); + assert!( + !tapped(runner.state(), mine), + "CR 701.26b: mode 1's own \"Untap them\" binds mode 1's own population" + ); +} From ec2a139ef62869a62a61c1c09bc9c5c747ab0534 Mon Sep 17 00:00:00 2001 From: lgray Date: Sun, 16 Aug 2026 20:55:00 -0500 Subject: [PATCH 6/7] fix(engine): clear the CR 700.2 mode latch in normalize_for_loop `resolving_modal_instruction` is a resolution-scoped edge latch cleared only at depth-0 chain entry, so between resolutions it holds the last resolved mode's ordinal as pure residue. The field is eq-compared, so two otherwise-identical positions reached via different last-resolved modes differed here alone and never confirmed a CR 104.4b repeated position. Normalized at the loop-detection site rather than excluded from `PartialEq`. The `last_loop_action_sequence` precedent in this file shows that PartialEq exclusion means "compare it analysis-locally instead", which is not the intent, and AI-search dedup legitimately reads the field. The test is revert-probed rather than merely added: red with the clear line deleted (failing on the loop-equality assertion, not the guard), green with it restored. It carries a paired non-vacuity assertion proving the two states genuinely differ before normalization, so it cannot pass by both sides being trivially identical. Its lockstep partner `chain_tracked_set_id` carries the same residue on main today and is deliberately untouched here: pre-existing behavior, queued for filing separately rather than widened into this diff. Reported by CodeRabbit on the previous head. Assisted-by: ClaudeCode:claude-opus-4.8 --- crates/engine/src/types/game_state.rs | 44 ++++++++++++++++++ .../fixtures/cr733/authority_matrix.json.gz | Bin 42170 -> 42239 bytes 2 files changed, 44 insertions(+) diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index caa0c5f8bc..200c366e82 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -22069,6 +22069,17 @@ impl GameState { // Private shortcut capabilities are live interaction state, never part // of a CR 104.4b position sample. clone.precast_shortcut_runtime = PrecastShortcutRuntime::default(); + // CR 700.2 + CR 104.4b: the mode-boundary edge latch is resolution-scoped + // and is cleared only at depth-0 chain ENTRY, so between resolutions it + // holds the LAST resolved mode's ordinal as pure residue. Two otherwise + // identical positions reached via different last-resolved modes would + // then differ here alone and never confirm a repeated position. It is + // eq-compared (AI-search dedup legitimately reads it), so it is + // normalized away HERE rather than excluded from `PartialEq`. + // NOTE: its lockstep partner `chain_tracked_set_id` carries the same + // residue and is deliberately NOT cleared here — that is pre-existing + // behavior with its own follow-up, not something this line may widen. + clone.resolving_modal_instruction = None; // CR 104.4b + CR 400.7: the all-zone incarnation bump advances a source's // epoch on every zone change, so a mandatory loop that cycles its source's // zones would otherwise carry a growing `TriggerSourceContext` into loop @@ -28403,6 +28414,39 @@ mod tests { ); } + /// CR 700.2 + CR 104.4b: the mode-boundary edge latch + /// (`resolving_modal_instruction`) is resolution-scoped and is cleared only at + /// depth-0 chain ENTRY, so between resolutions it holds the last resolved + /// mode's ordinal as pure residue. Two identical positions reached via + /// different last-resolved modes must still confirm as a repeated position. + /// + /// DISCRIMINATION: delete `clone.resolving_modal_instruction = None;` from + /// `normalize_for_loop` and this test FAILS — the field is eq-compared, so the + /// residue alone defeats the CR 104.4b repeat. The `a != b` assertion is the + /// paired non-vacuity witness: it proves the two inputs really do differ + /// BEFORE normalization, so the positive assertion cannot pass by the two + /// sides being trivially identical. + #[test] + fn normalize_for_loop_clears_the_modal_boundary_latch_residue() { + let mut first = GameState::new_two_player(7); + let mut second = first.clone(); + // The ONLY difference: which mode resolved last. Same board, same stack. + first.resolving_modal_instruction = Some(0); + second.resolving_modal_instruction = Some(2); + + assert!( + first != second, + "non-vacuity: the two states must differ before normalization, else the \ + equality assertion below proves nothing" + ); + + assert!( + loop_states_equal(&first.normalize_for_loop(), &second.normalize_for_loop()), + "CR 104.4b: positions differing only in the resolution-scoped mode-boundary \ + latch must confirm as a repeat" + ); + } + /// 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 diff --git a/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz b/crates/engine/tests/fixtures/cr733/authority_matrix.json.gz index 70d39522fedbd94e6473a55be784c03cd4a0129c..f0916909f0f338271e2e61fe9809c99d342ee3ee 100644 GIT binary patch delta 35380 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ྫྷsC5TVDudAump7fm6;0=G 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 delta 35310 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 Date: Mon, 17 Aug 2026 00:42:40 -0500 Subject: [PATCH 7/7] fix(engine): one publication authority for every event-less producer Maintainer review round 4 on #7484. Three confirmed findings, plus the measured refutation of a fourth. The sole-producer gate this branch introduced was applied to three of the four event-less publisher arms. `GenericEffect` (#6682's mass coercion / broadcast Continuous grant) was left ungated, so in a mixed chain it could seed the tracked set before a later producer owned the antecedent, and that later producer's consumer read the wrong population. It is squarely the class the gate's own doc describes -- it moves nothing and emits no object-affecting event -- so it is now gated identically. When the gate declines, the arm falls through to the `ZoneChanged` harvest, which is empty for this head, leaving the later producer to publish. Which static names the frozen population was decided twice, independently, by the parser's routing predicate and the runtime's publish arm -- and both picked the FIRST eligible static and only then asked whether it carried an application filter. An earlier filterless `Continuous` static therefore suppressed a later broadcast one, so neither routing nor publishing saw the real population. Both now call one `find_map`-shaped authority, `generic_effect_population_filter`, which is also what makes it impossible for lowering to mark a head a publisher that resolution declines. The pre-existing sibling `is_mass_coerce_static` is `any`-quantified and never had the bug; the new predicate had diverged from it. The player-scope fan-out fix carries a detached remainder's publisher-position verdict onto the scoped template so the gate judges the PRE-SPLIT chain while per-iteration resolution keeps its template. Modelled as a typed field rather than a `SubAbilityLink` variant deliberately: a new field fails CLOSED -- the compiler flagged all 8 destructures and 14 literals -- where a new variant would have been silently fail-open at ~40 equality and `matches!` sites. The reported phased-out leak in `PumpAll`'s population helper is NOT reachable. `matches_target_filter` routes into `filter_inner`, which already excludes phased-out objects at its own CR 702.26b choke point; probed directly, the filter returns false for the phased-out object, so the raw `state.battlefield` scan could not leak one. The enumeration change is kept -- it holds the invariant locally instead of inheriting it from a matcher whose comment reserves the right to be bypassed, and it makes this producer match `goad_targets` -- but both comments are rewritten, because the versions first pushed asserted a leak that measurement refutes. The regression test records the full 2x2 in its doc: two independent guards, neither necessary, each sufficient, red only when both are removed. `pump.rs` consequently joins the mass-battlefield-scan idiom, which is independent evidence that `battlefield_phased_in_ids` is the canonical enumeration here. Classified as Census with a reason and tied to the `effect_census_role` oracle, where `PumpAll` was already a census member. Every new test in this round is revert-probed, and each is reported red- without / green-with. The one that does not flip is disclosed as not flipping rather than presented as a demonstration. CR 608.2c, CR 611.2c, CR 700.2, CR 702.26b/e, CR 508.1a/d. Assisted-by: ClaudeCode:claude-opus-4.8 --- crates/engine/src/game/ability_rw.rs | 4 + crates/engine/src/game/ability_scan.rs | 15 +- .../src/game/effects/additional_phase.rs | 1 + crates/engine/src/game/effects/double.rs | 1 + crates/engine/src/game/effects/effect.rs | 35 ++ crates/engine/src/game/effects/extra_turn.rs | 1 + .../grant_extra_loyalty_activations.rs | 1 + crates/engine/src/game/effects/mod.rs | 362 +++++++++++++++--- .../engine/src/game/effects/player_counter.rs | 2 + crates/engine/src/game/effects/pump.rs | 101 ++++- .../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 | 4 + crates/engine/src/game/engine.rs | 18 +- crates/engine/src/game/resolution_prompt.rs | 3 + crates/engine/src/game/stack.rs | 20 +- .../engine/src/parser/oracle_effect/lower.rs | 18 +- crates/engine/src/types/ability.rs | 29 ++ .../the_chain_veil_loyalty_grants.rs | 1 + 20 files changed, 540 insertions(+), 79 deletions(-) diff --git a/crates/engine/src/game/ability_rw.rs b/crates/engine/src/game/ability_rw.rs index 0f3c85485f..0b67346e68 100644 --- a/crates/engine/src/game/ability_rw.rs +++ b/crates/engine/src/game/ability_rw.rs @@ -3918,6 +3918,10 @@ fn walk_ability( // read can see; narrowing never adds a read, and the member-bound axis is // already set by the `TrackedSet`-bearing effects themselves. modal_instruction_ordinal: _, + // CR 608.2c: structural record of what a chain SPLIT detached. Read-FREE: + // it selects nothing from game state and gates only whether a producer + // may publish its population, which can narrow but never widen. + detached_remainder: _, min_x_value: _, // u32, no read cant_be_copied: _, copy_count_status: _, diff --git a/crates/engine/src/game/ability_scan.rs b/crates/engine/src/game/ability_scan.rs index f7798ff7aa..c7253d5a24 100644 --- a/crates/engine/src/game/ability_scan.rs +++ b/crates/engine/src/game/ability_scan.rs @@ -252,6 +252,10 @@ fn resolved_ability_axes(a: &ResolvedAbility, mode: ScanMode) -> Axes { // here". The instructions themselves are `effect`/`sub_ability`, already // scanned above, so the axes of a chain are identical with or without it. modal_instruction_ordinal: _, + // CR 608.2c: structural record of what a chain SPLIT detached. Read-FREE: + // it selects nothing from game state and gates only whether a producer + // may publish its population, which can narrow but never widen. + detached_remainder: _, min_x_value: _, // u32 cant_be_copied: _, // bool copy_count_status: _, // status tag @@ -7264,7 +7268,7 @@ mod tests { /// helper-enumerator mass reads on existing relaxed variants; raw-iteration mass /// reads rely on the oracle's no-wildcard forcing. /// - BOUNDED raw-iter / O(1) reads are deliberately kept OUT of `CLASSIFIED` (so the - /// set-equality stays over the 14 idiom-matched files — no allowlist pollution): + /// set-equality stays over the 15 idiom-matched files — no allowlist pollution): /// `vote.rs` (`votes_per_session_for` = 1 + count of `GrantsExtraVote` statics, /// snapshotted at session start — bounded single outcome) and `switch_pt.rs` /// (O(1) `state.battlefield.contains()` over the effect's own `ids` — bounded @@ -7328,6 +7332,14 @@ mod tests { "PhaseOut/PhaseIn: targets-empty -> battlefield_phased_in_ids / \ state.battlefield mass scan (CR 702.26)", ), + ( + "pump.rs", + true, + "PumpAll (pump_all_affected_objects): battlefield_phased_in_ids mass pump, \ + a read that scales with the board; single Pump path also present in-file. \ + Joined the idiom in #7484, when the producer moved off a raw \ + state.battlefield scan onto the same enumeration goad.rs uses", + ), ( "turn_face_up.rs", true, @@ -7426,6 +7438,7 @@ mod tests { ("counters.rs", "PutCounterAll"), ("goad.rs", "GoadAll"), ("phase_out.rs", "PhaseOut"), + ("pump.rs", "PumpAll"), ("turn_face_up.rs", "TurnFaceUp"), ("turn_face_down.rs", "TurnFaceDown"), ]; diff --git a/crates/engine/src/game/effects/additional_phase.rs b/crates/engine/src/game/effects/additional_phase.rs index 8a404f0fae..39199a4a61 100644 --- a/crates/engine/src/game/effects/additional_phase.rs +++ b/crates/engine/src/game/effects/additional_phase.rs @@ -254,6 +254,7 @@ mod tests { count: QuantityExpr, ) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::AdditionalPhase { target, phase, diff --git a/crates/engine/src/game/effects/double.rs b/crates/engine/src/game/effects/double.rs index e510455661..8a130d1947 100644 --- a/crates/engine/src/game/effects/double.rs +++ b/crates/engine/src/game/effects/double.rs @@ -318,6 +318,7 @@ mod tests { targets: Vec, ) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::Double { target_kind, target, diff --git a/crates/engine/src/game/effects/effect.rs b/crates/engine/src/game/effects/effect.rs index e68aa8d439..5da5193586 100644 --- a/crates/engine/src/game/effects/effect.rs +++ b/crates/engine/src/game/effects/effect.rs @@ -811,6 +811,41 @@ pub fn generic_effect_application_filter<'a>( } } +/// CR 508.1a + CR 608.2c + CR 611.2c: SINGLE AUTHORITY for "which static on this +/// `GenericEffect` names the population `those creatures` freezes" — shared by the +/// parser's population-publisher routing (`oracle_effect::lower`) and the runtime's +/// publish arm (`effects::affected_objects_from_events`) so lowering can never mark +/// a head a publisher that resolution then declines to publish. +/// +/// Eligible modes are the ones whose population is FROZEN at resolution rather than +/// re-evaluated live at each future check: a coercion requirement (`MustAttack` / +/// `MustAttackDefender`, CR 508.1a/d) or a `Continuous` grant. +/// +/// Selection is `find_map`, NOT `find`-then-ask: a chain may carry an earlier +/// eligible static with no application filter (a `Continuous` coercion with neither +/// an outer `target` nor an `affected`), and taking it would suppress a later +/// application-bearing broadcast static — so neither routing nor publishing would +/// see the actual population. Shape mirrors the `any`-quantified sibling +/// `is_mass_coerce_static` (`oracle_effect/mod.rs`) rather than a first-wins scan. +pub fn generic_effect_population_filter<'a>( + target_filter: Option<&'a TargetFilter>, + static_abilities: &'a [StaticDefinition], +) -> Option<&'a TargetFilter> { + static_abilities + .iter() + .filter(|static_def| { + matches!( + static_def.mode, + crate::types::statics::StaticMode::MustAttack + | crate::types::statics::StaticMode::MustAttackDefender { .. } + | crate::types::statics::StaticMode::Continuous + ) + }) + .find_map(|static_def| { + generic_effect_application_filter(target_filter, static_def.affected.as_ref()) + }) +} + fn snapshot_transient_modifications( state: &GameState, ability: &ResolvedAbility, diff --git a/crates/engine/src/game/effects/extra_turn.rs b/crates/engine/src/game/effects/extra_turn.rs index 8c134a536e..aca9a0becc 100644 --- a/crates/engine/src/game/effects/extra_turn.rs +++ b/crates/engine/src/game/effects/extra_turn.rs @@ -66,6 +66,7 @@ mod tests { fn make_ability(target: TargetFilter, controller: PlayerId) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::ExtraTurn { target }, controller, original_controller: 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 af4be6e501..33615f6457 100644 --- a/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs +++ b/crates/engine/src/game/effects/grant_extra_loyalty_activations.rs @@ -81,6 +81,7 @@ mod tests { fn make_ability(amount: QuantityExpr, controller: PlayerId) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::GrantExtraLoyaltyActivations { amount, target: TargetFilter::Controller, diff --git a/crates/engine/src/game/effects/mod.rs b/crates/engine/src/game/effects/mod.rs index d25d400bbf..9dd6dd5ac7 100644 --- a/crates/engine/src/game/effects/mod.rs +++ b/crates/engine/src/game/effects/mod.rs @@ -11,12 +11,12 @@ use crate::game::speed::has_max_speed; use crate::types::ability::{ AbilityCondition, AbilityCost, AbilityDefinition, AbilityKind, CardPlayMode, CardTypeSetSource, ChosenAttribute, CommanderOwnership, ControllerRef, CopyRetargetPermission, - CostPaidObjectSnapshot, EachDamageRecipient, Effect, EffectError, EffectKind, - EffectOutcomeSignal, EffectResolutionResult, EffectScope, FilterProp, ManaProduction, - OpponentMayScope, PlayerFilter, PlayerScope, QuantityExpr, QuantityRef, RepeatContinuation, - ResolvedAbility, RevealUntilDisposition, SacrificeCost, SacrificeRequirement, SharedQuality, - SharedQualityRelation, SiblingCondition, SubAbilityLink, TapStateChange, TargetChoiceTiming, - TargetFilter, TargetRef, ThisWayCause, + CostPaidObjectSnapshot, DetachedRemainder, EachDamageRecipient, Effect, EffectError, + EffectKind, EffectOutcomeSignal, EffectResolutionResult, EffectScope, FilterProp, + ManaProduction, OpponentMayScope, PlayerFilter, PlayerScope, QuantityExpr, QuantityRef, + RepeatContinuation, ResolvedAbility, RevealUntilDisposition, SacrificeCost, + SacrificeRequirement, SharedQuality, SharedQualityRelation, SiblingCondition, SubAbilityLink, + TapStateChange, TargetChoiceTiming, TargetFilter, TargetRef, ThisWayCause, }; #[cfg(test)] use crate::types::ability::{AttackScope, AttackSubject}; @@ -4405,9 +4405,21 @@ fn split_player_scope_chain( let mut scoped = ability.clone(); scoped.player_scope = None; let tail = detach_after_player_scope_local_chain(&mut scoped, scope, false); + scoped.detached_remainder = detached_remainder_verdict(tail.as_deref()); (scoped, tail) } +/// CR 608.2c: classify a detached chain remainder for the publish gate. The walk +/// is the same one leg 2 uses, applied to the detached node itself. +fn detached_remainder_verdict(tail: Option<&ResolvedAbility>) -> DetachedRemainder { + match tail { + Some(node) if node_or_later_is_publisher_position(node) => { + DetachedRemainder::HoldsPublisher + } + _ => DetachedRemainder::NoProducer, + } +} + /// CR 608.2c: A multi-target player subject owns only its same-sentence /// continuation chain. A `SequentialSibling` starts an independent instruction /// that resolves once after every selected player has completed the subject's @@ -4418,6 +4430,9 @@ fn split_multi_target_player_chain( ) -> (ResolvedAbility, Option>) { let mut per_target = ability.clone(); let tail = detach_after_multi_target_player_local_chain(&mut per_target); + // Same hazard, same primitive: the per-target template loses sight of a + // producer left in the detached remainder. + per_target.detached_remainder = detached_remainder_verdict(tail.as_deref()); (per_target, tail) } @@ -5416,14 +5431,17 @@ pub(crate) fn chain_references_tracked_set(ability: &ResolvedAbility) -> bool { /// harvest, which yields `[]` for every head in this class (they emit no /// `ZoneChanged`) — i.e. byte-identical to the pre-#6857 engine. /// -/// KNOWN GAP, unexercised today: under a `player_scope` fan-out the publish -/// site hands `affected_objects_with_causes` the `scoped_template`, whose tail -/// `split_player_scope_chain` has already DETACHED, while the surrounding gate -/// reads the full `ability`. Leg 2 therefore cannot see a later producer that -/// lives in the detached tail, and would let the head publish where the -/// undetached chain would have declined. Measured unreachable at the time of -/// writing: 0 of the 627 event-less heads in the corpus carry a `player_scope`. -/// If one ever does, leg 2 needs the pre-split ability, not the template. +/// CLOSED by leg 3 (was a documented gap): under a `player_scope` fan-out the +/// publish site hands `affected_objects_with_causes` the `scoped_template`, +/// whose tail `split_player_scope_chain` has already DETACHED, while the +/// surrounding gate reads the full `ability`. Leg 2 alone therefore cannot see a +/// later producer living in the detached tail, and would let the head publish +/// where the undetached chain declines. It was measured unreachable when first +/// written (0 of the 627 event-less heads in the corpus carried a +/// `player_scope`), but "unreachable today" is not a fix: the splitter now +/// records the remainder's publisher-position verdict on the template +/// (`DetachedRemainder`), and leg 3 reads it, so the gate judges the PRE-SPLIT +/// chain without the per-iteration resolution losing its scoped template. /// The mode-boundary stop is neither wider nor narrower than that gap: it is the /// same walk over the same pre-split ability. On the fan-out path itself the stop /// is REDUNDANT rather than load-bearing — `split_player_scope_chain` has already @@ -5440,7 +5458,14 @@ fn is_sole_chain_producer(state: &GameState, ability: &ResolvedAbility) -> bool .get(&id) .is_none_or(|set| set.is_empty()) }); - no_earlier_producer && !later_node_is_publisher_position(ability) + // CR 608.2c: leg 3 — a producer that survives in a remainder DETACHED by a + // chain split still competes for the anaphor, and the template handed to the + // per-iteration resolution cannot see it. The splitter records that verdict + // structurally; without this leg the head publishes where the undetached + // chain declines. Closes the fan-out gap the leg-2 doc describes. + no_earlier_producer + && !later_node_is_publisher_position(ability) + && ability.detached_remainder == DetachedRemainder::NoProducer } /// Any strictly-later node of this chain that the publish site would itself @@ -5466,25 +5491,39 @@ fn is_sole_chain_producer(state: &GameState, ability: &ResolvedAbility) -> bool /// binds nothing. The stop is applied inside `walk`, which covers the seed and /// both recursions uniformly. fn later_node_is_publisher_position(ability: &ResolvedAbility) -> bool { - fn walk(node: Option<&ResolvedAbility>) -> bool { - node.is_some_and(|n| { - if crosses_modal_boundary(n) { - return false; - } - // CR 603.7: production's own predicate, unmodified — a node whose - // consumer merely DEFERS (a `CreateDelayedTrigger - // { uses_tracked_set: true }`, which acts at a later time) still - // counts as a publisher position here. Excluding deferring consumers - // from this leg would let a head publish across a `CopyTokenOf` + - // delayed-exile chain (Twinflame, Myra the Magnificent), putting the - // ORIGINAL creature into the set the delayed "exile those tokens" - // then binds — measured, not predicted. - next_sub_needs_tracked_set(n) - || walk(n.sub_ability.as_deref()) - || walk(n.else_ability.as_deref()) - }) + ability + .sub_ability + .as_deref() + .is_some_and(node_or_later_is_publisher_position) +} + +/// CR 603.7 + CR 700.2: is THIS node, or any strictly-later node of its chain, +/// in publisher position? Stops at a mode boundary, exactly like its caller. +/// +/// Split out of [`later_node_is_publisher_position`] so that a chain remainder a +/// splitter DETACHED can be judged by the same predicate: there the detached +/// node ITSELF is a candidate, not merely its descendants. +fn node_or_later_is_publisher_position(node: &ResolvedAbility) -> bool { + if crosses_modal_boundary(node) { + return false; } - walk(ability.sub_ability.as_deref()) + // CR 603.7: production's own predicate, unmodified — a node whose + // consumer merely DEFERS (a `CreateDelayedTrigger + // { uses_tracked_set: true }`, which acts at a later time) still + // counts as a publisher position here. Excluding deferring consumers + // from this leg would let a head publish across a `CopyTokenOf` + + // delayed-exile chain (Twinflame, Myra the Magnificent), putting the + // ORIGINAL creature into the set the delayed "exile those tokens" + // then binds — measured, not predicted. + next_sub_needs_tracked_set(node) + || node + .sub_ability + .as_deref() + .is_some_and(node_or_later_is_publisher_position) + || node + .else_ability + .as_deref() + .is_some_and(node_or_later_is_publisher_position) } fn ability_or_branch_references_tracked_set(ability: &ResolvedAbility) -> bool { @@ -5995,28 +6034,25 @@ fn affected_objects_from_events( // (oracle_effect/mod.rs), which still gates only the MustAttack/ // MustAttackDefender coercion pair for its own (unrelated) // ParentTarget-rewrite purpose. + // CR 608.2c: gated on `is_sole_chain_producer` exactly as the three + // event-less sibling arms below (`PumpAll` / `GoadAll` / `GiveControl`) + // are. This head is the same class the gate's doc describes — it moves + // nothing and emits no object-affecting event — so a mixed chain whose + // LATER node is the real antecedent must not have its anaphor bound by + // this broadcast coercion/grant. When the gate declines, the arm falls + // through to the `_ =>` `ZoneChanged` harvest, which is `[]` for this + // head (it emits none), leaving the later producer to publish. Effect::GenericEffect { static_abilities, target, .. - } => { - // Select the first static whose population is meant to be frozen - // at resolution rather than re-evaluated live at each future - // check — a coercion requirement or a Continuous grant. - let Some(static_def) = static_abilities.iter().find(|sd| { - matches!( - sd.mode, - crate::types::statics::StaticMode::MustAttack - | crate::types::statics::StaticMode::MustAttackDefender { .. } - | crate::types::statics::StaticMode::Continuous - ) - }) else { - return Vec::new(); - }; - let Some(governing) = effect::generic_effect_application_filter( - target.as_ref(), - static_def.affected.as_ref(), - ) else { + } if is_sole_chain_producer(state, ability) => { + // Which static names the frozen population is decided by ONE + // authority shared with the parser's routing predicate, so lowering + // cannot mark a head a publisher that this arm then declines. + let Some(governing) = + effect::generic_effect_population_filter(target.as_ref(), static_abilities) + else { return Vec::new(); }; // CR 608.2c: an inherited-reference affected filter (ParentTarget / @@ -23307,6 +23343,230 @@ mod tests { ); } + /// CR 608.2c (maintainer review, #7484): the publish gate must judge the + /// PRE-SPLIT chain. A `player_scope` fan-out hands the per-player resolution + /// a template whose remainder has been DETACHED, so a walk over that + /// template alone cannot see a producer surviving in the tail — and the head + /// would publish where the undetached chain declines, binding a later + /// `TrackedSet` consumer to the wrong population. + /// + /// MATCHED PAIR, so the assertion is discriminating rather than decorative: + /// the two chains differ ONLY in whether the detached tail is in publisher + /// position. Delete the `detached_remainder` leg from + /// `is_sole_chain_producer` and the first case flips to `true` (wrongly + /// publishing) while the second stays `true` — i.e. the leg is what + /// separates them. + #[test] + fn player_scope_split_carries_a_detached_publisher_into_the_gate() { + let state = GameState::new_two_player(7); + let pump = |scope: Option| { + let mut a = ResolvedAbility::new( + Effect::PumpAll { + power: PtValue::Fixed(1), + toughness: PtValue::Fixed(1), + target: TargetFilter::Any, + }, + vec![], + ObjectId(1), + PlayerId(0), + ); + a.player_scope = scope; + a + }; + + // Tail that IS a publisher position: its own sub consumes the set. + let mut publishing_tail = pump(None); + publishing_tail.sub_link = SubAbilityLink::SequentialSibling; + publishing_tail.sub_ability = Some(Box::new(ResolvedAbility::new( + Effect::SetTapState { + target: TargetFilter::TrackedSet { + id: crate::types::identifiers::TrackedSetId(0), + }, + scope: EffectScope::Single, + state: TapStateChange::Untap, + }, + vec![], + ObjectId(1), + PlayerId(0), + ))); + let mut head = pump(Some(PlayerFilter::All)); + head.sub_ability = Some(Box::new(publishing_tail)); + + let (scoped, tail) = split_player_scope_chain(&head, &PlayerFilter::All); + assert!( + tail.is_some(), + "precondition: the sibling tail must actually be detached, else this \ + test proves nothing about the detached case" + ); + assert_eq!( + scoped.detached_remainder, + DetachedRemainder::HoldsPublisher, + "the splitter must record that the detached remainder still holds a producer" + ); + assert!( + !is_sole_chain_producer(&state, &scoped), + "CR 608.2c: the head is NOT the sole producer of its pre-split chain, so it \ + must not publish — the detached tail's consumer owns that population" + ); + + // Same shape, tail NOT a publisher position (its sub consumes nothing). + let mut inert_tail = pump(None); + inert_tail.sub_link = SubAbilityLink::SequentialSibling; + let mut head2 = pump(Some(PlayerFilter::All)); + head2.sub_ability = Some(Box::new(inert_tail)); + + let (scoped2, tail2) = split_player_scope_chain(&head2, &PlayerFilter::All); + assert!( + tail2.is_some(), + "precondition: same detachment as the case above" + ); + assert_eq!( + scoped2.detached_remainder, + DetachedRemainder::NoProducer, + "a detached remainder with no consumer must not veto the head" + ); + assert!( + is_sole_chain_producer(&state, &scoped2), + "non-vacuity: with nothing consuming downstream the head DOES publish, so the \ + veto above is caused by the publisher position and not by the split itself" + ); + } + + /// CR 608.2c (maintainer review, #7484): the event-less `GenericEffect` + /// broadcast head is gated on `is_sole_chain_producer` exactly as its three + /// sibling arms are. Before this gate the head published unconditionally, so + /// a mixed chain whose LATER node owns the anaphor had its `TrackedSet` + /// consumer bound to the coercion/grant population instead. + /// + /// MATCHED PAIR through the production publish function + /// (`affected_objects_from_events`), not the predicate: the two chains differ + /// ONLY in whether a later producer occupies publisher position. Remove the + /// `if is_sole_chain_producer(state, ability)` guard from the `GenericEffect` + /// arm and the first case flips from `[]` to `[creature]`; the second case is + /// the paired non-vacuity witness that the empty result is caused by the gate + /// and not by the head failing to enumerate at all. + #[test] + fn generic_effect_publish_defers_to_a_later_producer_in_the_chain() { + let mut state = GameState::new_two_player(7); + let creature = reflexive_test_creature(&mut state, PlayerId(0), "Bear"); + + let broadcast_head = || { + ResolvedAbility::new( + Effect::GenericEffect { + static_abilities: vec![ + StaticDefinition::continuous().affected(TargetFilter::Any) + ], + duration: None, + target: None, + end_cost: None, + }, + vec![], + ObjectId(1), + PlayerId(0), + ) + }; + + // Later node in publisher position: a producer whose own sub consumes the + // published set, i.e. the real antecedent of "those creatures". + let mut later_producer = ResolvedAbility::new( + Effect::PumpAll { + power: PtValue::Fixed(1), + toughness: PtValue::Fixed(1), + target: TargetFilter::Any, + }, + vec![], + ObjectId(1), + PlayerId(0), + ); + later_producer.sub_link = SubAbilityLink::SequentialSibling; + later_producer.sub_ability = Some(Box::new(ResolvedAbility::new( + Effect::SetTapState { + target: TargetFilter::TrackedSet { + id: crate::types::identifiers::TrackedSetId(0), + }, + scope: EffectScope::Single, + state: TapStateChange::Untap, + }, + vec![], + ObjectId(1), + PlayerId(0), + ))); + + let mut mixed = broadcast_head(); + mixed.sub_ability = Some(Box::new(later_producer)); + assert_eq!( + affected_objects_from_events(&state, &mixed, &mixed.effect, &[]), + Vec::::new(), + "CR 608.2c: a later producer in the same chain owns the anaphor, so this \ + broadcast head must not publish its own population" + ); + + let alone = broadcast_head(); + assert_eq!( + affected_objects_from_events(&state, &alone, &alone.effect, &[]), + vec![creature], + "non-vacuity: as the chain's sole producer the same head DOES publish, so \ + the empty result above is the gate and not a failure to enumerate" + ); + } + + /// CR 608.2c (maintainer review, #7484): which static names the frozen + /// population is decided by `generic_effect_population_filter`, the ONE + /// authority the parser's routing predicate and this publish arm now share. + /// + /// The selection must be `find_map`, not `find`-then-ask: an earlier eligible + /// static carrying no application filter (a bare `Continuous` with neither an + /// outer `target` nor an `affected`) would otherwise be taken and returned as + /// `None`, suppressing the later broadcast static that actually names the + /// population — so neither routing nor publishing would see it. + /// + /// DISCRIMINATING: restore `.find(eligible).and_then(application_filter)` and + /// BOTH assertions fail (`None` / `[]`). The single-static case is the paired + /// witness that the multi-static result is not an artifact of the fixture. + #[test] + fn generic_effect_population_filter_skips_an_earlier_static_with_no_application_filter() { + let mut state = GameState::new_two_player(7); + let creature = reflexive_test_creature(&mut state, PlayerId(0), "Bear"); + + let statics = || { + vec![ + // Earlier, eligible, but names no population. + StaticDefinition::continuous(), + // Later, and the one that actually broadcasts. + StaticDefinition::continuous().affected(TargetFilter::Any), + ] + }; + + assert_eq!( + effect::generic_effect_population_filter(None, &statics()), + Some(&TargetFilter::Any), + "selection must skip the filterless earlier static and reach the broadcast one" + ); + assert_eq!( + effect::generic_effect_population_filter(None, &[StaticDefinition::continuous()]), + None, + "non-vacuity: a chain of ONLY filterless statics still names no population" + ); + + // The runtime arm must honour that selection end-to-end. + let ability = ResolvedAbility::new( + Effect::GenericEffect { + static_abilities: statics(), + duration: None, + target: None, + end_cost: None, + }, + vec![], + ObjectId(1), + PlayerId(0), + ); + assert_eq!( + affected_objects_from_events(&state, &ability, &ability.effect, &[]), + vec![creature], + "CR 611.2c: the published population is the later static's broadcast set" + ); + } + /// CR 608.2c — building-block discriminator for the per-player reveal-anaphora /// chain (issue #1534, Duskmantle Seer). `split_player_scope_chain` must keep a /// co-scoped sub-clause that consumes the reveal's per-player object referent diff --git a/crates/engine/src/game/effects/player_counter.rs b/crates/engine/src/game/effects/player_counter.rs index acc45062e3..ea6da2e662 100644 --- a/crates/engine/src/game/effects/player_counter.rs +++ b/crates/engine/src/game/effects/player_counter.rs @@ -430,6 +430,7 @@ mod tests { controller: PlayerId, ) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::GivePlayerCounter { counter_kind, count, @@ -634,6 +635,7 @@ mod tests { fn make_lose_all(target: TargetFilter, controller: PlayerId) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::LoseAllPlayerCounters { target }, controller, original_controller: None, diff --git a/crates/engine/src/game/effects/pump.rs b/crates/engine/src/game/effects/pump.rs index 1b1674be71..4dc0d12915 100644 --- a/crates/engine/src/game/effects/pump.rs +++ b/crates/engine/src/game/effects/pump.rs @@ -169,8 +169,8 @@ pub fn resolve_all( /// A discriminating test for the flush hazard needs a filter reading a pumped /// characteristic (e.g. "creatures with power 2 or less"). /// -/// NOTE for a future zone-aware `resolve_all`: this scans `state.battlefield`, -/// so Elvish Elegy's `InZone: Graveyard` filter returns `[]` today. That is NOT +/// NOTE for a future zone-aware `resolve_all`: this scans the battlefield, so +/// Elvish Elegy's `InZone: Graveyard` filter returns `[]` today. That is NOT /// what keeps its milled tracked set intact — the `is_sole_chain_producer` /// guard at the publish site does, because the preceding `Mill` already /// published. Making this zone-aware is therefore safe. @@ -188,11 +188,23 @@ pub(crate) fn pump_all_affected_objects( let target_filter = crate::game::effects::resolved_object_filter(ability, target); // CR 107.3a + CR 601.2b: ability-context filter evaluation. let ctx = filter::FilterContext::from_ability(ability); + // CR 702.26b: a phased-out permanent "is treated as though it does not + // exist". CR 702.26e is the specific rule for this producer: a continuous + // effect from a resolving spell/ability that modifies characteristics does + // NOT include phased-out permanents in its set of affected objects. + // + // Deliberately redundant with `filter_inner`'s own CR 702.26b choke point in + // `filter.rs`, which already excludes phased-out objects from every + // `matches_target_filter` call — measured (see the test's DISCRIMINATION + // table), not assumed. Kept because this function is the single authority for + // BOTH the pump and the published tracked set, so it holds the invariant + // locally rather than inheriting it from a matcher whose own comment reserves + // the right to be bypassed by "targeted callers". Enumerates exactly as the + // sibling `goad_targets` authority does. state - .battlefield - .iter() - .filter(|id| filter::matches_target_filter(state, **id, &target_filter, &ctx)) - .copied() + .battlefield_phased_in_ids() + .into_iter() + .filter(|id| filter::matches_target_filter(state, *id, &target_filter, &ctx)) .collect() } @@ -424,6 +436,7 @@ fn resolve_variable_pt(value: &str, ability: &ResolvedAbility) -> Option { #[cfg(test)] mod tests { use super::*; + use crate::game::game_object::{PhaseOutCause, PhaseStatus}; use crate::game::layers::evaluate_layers; use crate::game::zones::create_object; use crate::types::ability::{ @@ -559,6 +572,82 @@ mod tests { assert_eq!(state.objects[&opp].toughness, Some(3)); } + /// CR 702.26b + CR 702.26e: a phased-out permanent "is treated as though it + /// does not exist", and a continuous effect from a resolving spell/ability + /// does NOT include it in its set of affected objects. + /// + /// Covers BOTH halves the review asked for with one assertion each, because + /// `pump_all_affected_objects` is the single authority for the pump AND for + /// the published tracked set (that identity is pinned by the sibling test + /// below): excluding the phased-out creature from this population excludes + /// it from the set a later "those creatures" consumer reads. + /// + /// DISCRIMINATION, measured as a 2x2 rather than claimed — two independent + /// guards enforce this invariant, so reverting either ALONE leaves the test + /// green, and only the conjunction is load-bearing: + /// + /// | `filter_inner` CR 702.26b choke point | this fn's `battlefield_phased_in_ids()` | result | + /// |---|---|---| + /// | on | on | pass | + /// | on | off | pass | + /// | off | on | pass | + /// | off | off | **FAIL** — population comes back `[phased_in, phased_out]` | + /// + /// So this test does not prove the local enumeration is *necessary* today; it + /// pins that the producer keeps excluding phased-out permanents if EITHER + /// guard is later removed or routed around. The phased-IN creature's + /// assertions are the paired non-vacuity witness: without them a helper that + /// returned `[]` for everything would pass. + #[test] + fn pump_all_excludes_a_phased_out_creature_from_the_population_and_the_pump() { + let mut state = GameState::new_two_player(7); + let phased_in = make_creature(&mut state, "Phased In", 2, 2, PlayerId(0)); + let phased_out = make_creature(&mut state, "Phased Out", 2, 2, PlayerId(0)); + if let Some(obj) = state.objects.get_mut(&phased_out) { + obj.phase_status = PhaseStatus::PhasedOut { + cause: PhaseOutCause::Directly, + }; + } + + let yours: TargetFilter = TypedFilter::creature() + .controller(ControllerRef::You) + .into(); + let ability = ResolvedAbility::new( + Effect::PumpAll { + power: PtValue::Fixed(1), + toughness: PtValue::Fixed(1), + target: yours.clone(), + }, + vec![], + ObjectId(100), + PlayerId(0), + ); + + assert_eq!( + pump_all_affected_objects(&state, &ability, &yours), + vec![phased_in], + "CR 702.26e: a phased-out permanent must not enter the frozen population, \ + which is also the set the publish site republishes" + ); + + let mut events = Vec::new(); + resolve_all(&mut state, &ability, &mut events).unwrap(); + // The pump installs a transient continuous effect; P/T only materializes + // once the layer system evaluates (see this module's FLUSH HAZARD note). + evaluate_layers(&mut state); + assert_eq!( + state.objects[&phased_in].power, + Some(3), + "non-vacuity: the phased-IN creature must actually be pumped, else the \ + exclusion above could pass on an empty population" + ); + assert_eq!( + state.objects[&phased_out].power, + Some(2), + "CR 702.26b: the phased-out creature must be untouched by the pump" + ); + } + /// CR 611.2c (issue #6857): `pump_all_affected_objects` is the SINGLE /// AUTHORITY for the population `resolve_all` freezes — the publish site /// republishes this exact list rather than re-deriving it. diff --git a/crates/engine/src/game/effects/reverse_turn_order.rs b/crates/engine/src/game/effects/reverse_turn_order.rs index 8b290b4112..bd7bfb151b 100644 --- a/crates/engine/src/game/effects/reverse_turn_order.rs +++ b/crates/engine/src/game/effects/reverse_turn_order.rs @@ -39,6 +39,7 @@ mod tests { fn make_ability() -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::ReverseTurnOrder, controller: PlayerId(0), original_controller: None, diff --git a/crates/engine/src/game/effects/skip_next_step.rs b/crates/engine/src/game/effects/skip_next_step.rs index a0bb25c94f..feb90fca01 100644 --- a/crates/engine/src/game/effects/skip_next_step.rs +++ b/crates/engine/src/game/effects/skip_next_step.rs @@ -97,6 +97,7 @@ mod tests { scope: SkipScope, ) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::SkipNextStep { target: TargetFilter::Controller, step, diff --git a/crates/engine/src/game/effects/skip_next_turn.rs b/crates/engine/src/game/effects/skip_next_turn.rs index 186f5ba8a1..cbd91df997 100644 --- a/crates/engine/src/game/effects/skip_next_turn.rs +++ b/crates/engine/src/game/effects/skip_next_turn.rs @@ -80,6 +80,7 @@ mod tests { count: QuantityExpr, ) -> ResolvedAbility { ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::SkipNextTurn { target, count }, controller, original_controller: None, diff --git a/crates/engine/src/game/effects/vote.rs b/crates/engine/src/game/effects/vote.rs index 3c3cdfafb5..23073aaccc 100644 --- a/crates/engine/src/game/effects/vote.rs +++ b/crates/engine/src/game/effects/vote.rs @@ -772,6 +772,7 @@ mod tests { let token_def = AbilityDefinition::new(AbilityKind::Spell, Effect::Investigate); // simple stand-in let ability = ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::Vote { choices: vec!["evidence".to_string(), "bribery".to_string()], per_choice_effect: vec![Box::new(inv_def), Box::new(token_def)], @@ -886,6 +887,7 @@ mod tests { }) .collect(); ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::Vote { choices, per_choice_effect, @@ -1333,6 +1335,7 @@ mod tests { // Build a ResolvedAbility from the parsed AbilityDefinition. let ability = ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: (*parsed_def.effect).clone(), targets: vec![], source_id: ObjectId(1), @@ -1496,6 +1499,7 @@ mod tests { }) .collect(); let ability = ResolvedAbility { + detached_remainder: crate::types::ability::DetachedRemainder::NoProducer, effect: Effect::Vote { choices: vec!["friend".to_string(), "foe".to_string()], per_choice_effect, diff --git a/crates/engine/src/game/engine.rs b/crates/engine/src/game/engine.rs index 575169cd0b..f2f718cfa1 100644 --- a/crates/engine/src/game/engine.rs +++ b/crates/engine/src/game/engine.rs @@ -19033,9 +19033,21 @@ mod stage2_injector_tests { // right one, and it is what establishes the set was preserved. This // branch writes `state.waiting_for` nowhere: the publish arms return // `Vec` and prompt for nothing, so it adds no producer here. - "game/effects/mod.rs:7225".to_string(), - "game/effects/mod.rs:7302".to_string(), - "game/effects/mod.rs:10582".to_string(), + // + // #7484 maintainer review round 4 (same branch, no rebase): + // `:7225/:7302/:10582` => `:7261/:7338/:10618`, UNIFORM `+36`, and this + // time uniformity IS available as corroboration because every insertion + // sits above all three: the `GenericEffect` publish arm's + // `is_sole_chain_producer` gate at `:6037` and its comment block. Still + // located by digest rather than by arithmetic — each producer's 9-line + // block was hashed at this branch's committed tip and re-found at exactly + // one coordinate in the working tree (`cffb4348`/`0c3bdd6d`/`393bb75a`, + // all MATCH). The round's other edits are the two new `#[cfg(test)]` + // tests, which are below all three and mint no prompt, so the `in_test` + // total is unchanged. + "game/effects/mod.rs:7261".to_string(), + "game/effects/mod.rs:7338".to_string(), + "game/effects/mod.rs:10618".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/game/resolution_prompt.rs b/crates/engine/src/game/resolution_prompt.rs index a00ed0edcd..c19eb48152 100644 --- a/crates/engine/src/game/resolution_prompt.rs +++ b/crates/engine/src/game/resolution_prompt.rs @@ -576,6 +576,9 @@ pub(crate) fn chain_offers_choice(a: &ResolvedAbility) -> bool { // above); this records only where each chosen mode's instructions begin // in the linearized chain. It raises no `WaitingFor` and gates no prompt. modal_instruction_ordinal: _, + // CR 608.2c: split-remainder marker. Raises no `WaitingFor` and gates no + // prompt; it only decides whether a producer publishes its tracked set. + detached_remainder: _, min_x_value: _, // u32 cant_be_copied: _, // bool copy_count_status: _, // status tag diff --git a/crates/engine/src/game/stack.rs b/crates/engine/src/game/stack.rs index d635d3be85..4ba136e1b1 100644 --- a/crates/engine/src/game/stack.rs +++ b/crates/engine/src/game/stack.rs @@ -1,8 +1,8 @@ use crate::types::ability::{ - AbilityKind, ContinuousModification, CopyCountStatus, Duration, Effect, EffectKind, FilterProp, - KeywordAction, ObjectScope, PlayerFilter, QuantityExpr, QuantityRef, ResolvedAbility, - SiblingCondition, SpellContext, SubAbilityLink, TargetChoiceTiming, TargetFilter, TargetRef, - TargetSelectionMode, TriggerCondition, + AbilityKind, ContinuousModification, CopyCountStatus, DetachedRemainder, Duration, Effect, + EffectKind, FilterProp, KeywordAction, ObjectScope, PlayerFilter, QuantityExpr, QuantityRef, + ResolvedAbility, SiblingCondition, SpellContext, SubAbilityLink, TargetChoiceTiming, + TargetFilter, TargetRef, TargetSelectionMode, TriggerCondition, }; use crate::types::card_type::CoreType; use crate::types::counter::CounterType; @@ -3145,6 +3145,7 @@ fn self_counter_ability_is_batch_candidate(ability: &ResolvedAbility) -> bool { description, selected_mode_labels, modal_instruction_ordinal, + detached_remainder, repeat_for, min_x_value, announced_x, @@ -3215,6 +3216,7 @@ fn self_counter_ability_is_batch_candidate(ability: &ResolvedAbility) -> bool { // once instead of N times. That is outside what this batch proof // covers, so decline — declining only costs the optimization. && modal_instruction_ordinal.is_none() + && matches!(detached_remainder, DetachedRemainder::NoProducer) && repeat_for.is_none() && *min_x_value == 0 // CR 601.2b: an announce-locked X makes this ability's X board-dependent; @@ -3371,6 +3373,7 @@ fn fixed_controller_gain_life_ability_is_batch_candidate(ability: &ResolvedAbili description: _, selected_mode_labels, modal_instruction_ordinal, + detached_remainder, repeat_for, min_x_value, announced_x, @@ -3435,6 +3438,7 @@ fn fixed_controller_gain_life_ability_is_batch_candidate(ability: &ResolvedAbili // once instead of N times. That is outside what this batch proof // covers, so decline — declining only costs the optimization. && modal_instruction_ordinal.is_none() + && matches!(detached_remainder, DetachedRemainder::NoProducer) && repeat_for.is_none() && *min_x_value == 0 && announced_x.is_none() @@ -3577,6 +3581,7 @@ fn fixed_opponent_lose_life_ability_is_batch_candidate(ability: &ResolvedAbility description: _, selected_mode_labels, modal_instruction_ordinal, + detached_remainder, repeat_for, min_x_value, announced_x, @@ -3641,6 +3646,7 @@ fn fixed_opponent_lose_life_ability_is_batch_candidate(ability: &ResolvedAbility // once instead of N times. That is outside what this batch proof // covers, so decline — declining only costs the optimization. && modal_instruction_ordinal.is_none() + && matches!(detached_remainder, DetachedRemainder::NoProducer) && repeat_for.is_none() && *min_x_value == 0 && announced_x.is_none() @@ -4240,6 +4246,9 @@ fn inert_trigger_abilities_eq_ignoring_provenance( // IDENTITY, not a modal check, and two runs that differ only in which // mode produced them are still the same run. modal_instruction_ordinal: _, + // CR 608.2c: split-remainder marker. Guaranteed `NoProducer` ONE HOP + // upstream by the batch-candidate checks, same as the modal ordinal. + detached_remainder: _, repeat_for: a_repeat_for, min_x_value: a_min_x_value, announced_x: a_announced_x, @@ -4309,6 +4318,9 @@ fn inert_trigger_abilities_eq_ignoring_provenance( // IDENTITY, not a modal check, and two runs that differ only in which // mode produced them are still the same run. modal_instruction_ordinal: _, + // CR 608.2c: split-remainder marker. Guaranteed `NoProducer` ONE HOP + // upstream by the batch-candidate checks, same as the modal ordinal. + detached_remainder: _, repeat_for: b_repeat_for, min_x_value: b_min_x_value, announced_x: b_announced_x, diff --git a/crates/engine/src/parser/oracle_effect/lower.rs b/crates/engine/src/parser/oracle_effect/lower.rs index 6863b97e91..a3e84ac7f0 100644 --- a/crates/engine/src/parser/oracle_effect/lower.rs +++ b/crates/engine/src/parser/oracle_effect/lower.rs @@ -56,7 +56,7 @@ use super::{ refine_damage_target_remainder, replace_player_anaphor_with_parent_target, scan_contains_phrase, target_filter_controller_ref, }; -use crate::game::effects::effect::generic_effect_application_filter; +use crate::game::effects::effect::generic_effect_population_filter; pub(super) fn rewrite_player_anaphor_targets_in_definition(def: &mut AbilityDefinition) { replace_player_anaphor_with_parent_target(def.effect.as_mut()); @@ -260,23 +260,13 @@ pub(super) fn patch_population_head_tap_anaphor(def: &mut AbilityDefinition) { Effect::PutCounterAll { target, .. } | Effect::PumpAll { target, .. } => { is_broadcast_population_filter(target) } + // Same authority the runtime publish arm selects with, so a head can + // never be routed here and then declined there (or vice versa). Effect::GenericEffect { static_abilities, target, .. - } => static_abilities - .iter() - .find(|sd| { - matches!( - sd.mode, - StaticMode::Continuous - | StaticMode::MustAttack - | StaticMode::MustAttackDefender { .. } - ) - }) - .and_then(|sd| { - generic_effect_application_filter(target.as_ref(), sd.affected.as_ref()) - }) + } => generic_effect_population_filter(target.as_ref(), static_abilities) .is_some_and(is_broadcast_population_filter), _ => false, } diff --git a/crates/engine/src/types/ability.rs b/crates/engine/src/types/ability.rs index 6a26a5d026..1dac0e6d0b 100644 --- a/crates/engine/src/types/ability.rs +++ b/crates/engine/src/types/ability.rs @@ -25884,6 +25884,29 @@ pub enum ParentTargetMissingReason { RevealHandChoice, } +/// CR 608.2c: what a chain split — a `player_scope` fan-out, or a multi-target +/// player subject — DETACHED from this node's chain. +/// +/// Why this exists: the publish gate for a tracked-set producer must judge the +/// PRE-SPLIT chain. A splitter hands the per-iteration resolution a template +/// whose remainder has been detached, so a structural walk over that template +/// cannot see a producer surviving in the detached tail — the head would publish +/// where the undetached chain declines, binding a later `TrackedSet` consumer to +/// the wrong population. The verdict is purely structural, so it is computed +/// once at split time and carried on the template rather than re-derived. +/// +/// SOLE WRITERS: `split_player_scope_chain`, `split_multi_target_player_chain`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +pub enum DetachedRemainder { + /// Nothing was detached, or the detached remainder holds no producer in + /// publisher position. The template's own walk is the whole truth. + #[default] + NoProducer, + /// The detached remainder holds a node in publisher position, so this node + /// is NOT the sole producer of its pre-split chain and must not publish. + HoldsPublisher, +} + /// Runtime ability data passed to effect handlers at resolution time. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ResolvedAbility { @@ -26028,6 +26051,11 @@ pub struct ResolvedAbility { /// from this field alone. #[serde(default, skip_serializing_if = "Option::is_none")] pub modal_instruction_ordinal: Option, + /// CR 608.2c: the publisher-position verdict of the chain remainder a + /// splitter detached from this node. See [`DetachedRemainder`]. Default + /// (`NoProducer`) on every node that was never split. + #[serde(default)] + pub detached_remainder: DetachedRemainder, /// CR 608.2c: Repeat this ability N times (from "for each [X], [effect]"). #[serde(default, skip_serializing_if = "Option::is_none")] pub repeat_for: Option, @@ -26255,6 +26283,7 @@ impl ResolvedAbility { description: None, selected_mode_labels: Vec::new(), modal_instruction_ordinal: None, + detached_remainder: DetachedRemainder::NoProducer, repeat_for: None, min_x_value: 0, announced_x: None, 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 6ad2160df7..c8ea7687bf 100644 --- a/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs +++ b/crates/engine/tests/integration/the_chain_veil_loyalty_grants.rs @@ -143,6 +143,7 @@ fn install_competing_counter_addition_replacements(state: &mut GameState) { fn make_grant_ability(controller: PlayerId, source: ObjectId) -> ResolvedAbility { ResolvedAbility { + detached_remainder: engine::types::ability::DetachedRemainder::NoProducer, effect: Effect::GrantExtraLoyaltyActivations { amount: QuantityExpr::Fixed { value: 1 }, target: TargetFilter::Controller,