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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions crates/engine/src/game/ability_rw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2825,7 +2825,8 @@ fn legacy_effect(x: &Effect) -> bool {
| Effect::ExileHaunting { target }
| Effect::HideawayConceal { target }
| Effect::ChooseCard { target, .. }
| Effect::Transform { target }
// CR 701.27a: both scopes write ObjectPt on the target/population filter.
| Effect::Transform { target, .. }
// CR 710.4: same single-target-slot shape as `Transform`.
| Effect::FlipPermanent { target }
| Effect::Shuffle { target }
Expand Down Expand Up @@ -4949,7 +4950,7 @@ fn rw_effect(
factor: _,
} => obj(StateKind::ObjectPt, target),
Effect::SwitchPT { target } => obj(StateKind::ObjectPt, target),
Effect::Transform { target } => obj(StateKind::ObjectPt, target),
Effect::Transform { target, .. } => obj(StateKind::ObjectPt, target),
// CR 710.1b: flipping replaces the permanent's power and toughness
// (along with its name, type line, and text box) — the same
// `ObjectPt` write axis `Transform` records.
Expand Down Expand Up @@ -7106,6 +7107,7 @@ mod tests {
let docent = cond(
ra(token(&["Creature", "Wizard"], qfix(1))).sub_ability(ra(Effect::Transform {
target: TargetFilter::SelfRef,
scope: crate::types::ability::EffectScope::Single,
})),
qcheck(obj_count(sub("Wizard")), 1),
);
Expand Down
81 changes: 69 additions & 12 deletions crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ fn scan_effect(x: &Effect, mode: ScanMode) -> Axes {
acc = acc.or(scan_target_filter(target, target_ctx, mode));
acc
}
Effect::Transform { target } => {
Effect::Transform { target, .. } => {
let mut acc = Axes::NONE;
acc = acc.or(scan_target_filter(target, target_ctx, mode));
acc
Expand Down Expand Up @@ -5198,6 +5198,16 @@ fn effect_target_ctx(e: &Effect, mode: ScanMode) -> FilterReadContext {
// (missed offer, never a false certificate).
| Effect::Suspect { scope: EffectScope::All, .. }
| Effect::Unsuspect { scope: EffectScope::All, .. }
// CR 701.27a + CR 115.10a: mass Transform ("Transform all Humans", scope:All)
// is a non-targeting battlefield-population read (`target_filter()`==None;
// `transform_effect::resolve_all` enumerates `state.battlefield`, like
// DestroyAll) ⇒ census — its read SCALES with the growing class. Unlike the
// state-convergent SetTapState exception below, Transform WRITES ObjectPt and
// swaps the object's abilities, so a grown token is NOT inert and the read can
// escalate: `LiveBoardCensus`, never the Snapshot exception. scope:Single is a
// single announced/anaphoric target (a2), relaxed in the single-object group
// below. Exhaustive over EffectScope = {Single, All}.
| Effect::Transform { scope: EffectScope::All, .. }
// ── F1-CLASS DUAL-MODE MASS-BATTLEFIELD RESOLVERS (P3-B round-2): each has a
// resolver mode that, when the ability carries NO explicit object target,
// enumerates the battlefield (or all phased-in/-out permanents) and applies the
Expand Down Expand Up @@ -5337,8 +5347,11 @@ fn effect_target_ctx(e: &Effect, mode: ScanMode) -> FilterReadContext {
| Effect::Mana { .. }
| Effect::Discard { .. }
| Effect::Shuffle { .. }
| Effect::Transform { .. }
// CR 710.4: same single-target read context as `Transform`.
// CR 701.27a: only the scope:Single Transform relaxes — a single announced or
// anaphoric target (a2). scope:All is the mass battlefield read, census-tagged
// above with the DestroyAll/Suspect{All} group.
| Effect::Transform { scope: EffectScope::Single, .. }
// CR 710.4: same single-target read context as `Transform` (always self-ref).
| Effect::FlipPermanent { .. }
| Effect::SearchLibrary { .. }
| Effect::SearchOutsideGame { .. }
Expand Down Expand Up @@ -5540,7 +5553,7 @@ enum CensusRole {
#[cfg(test)]
fn effect_census_role(e: &Effect) -> CensusRole {
match e {
// -- CENSUS (29): verbatim mirror of `effect_target_ctx`'s LiveBoardCensus
// -- CENSUS (30): verbatim mirror of `effect_target_ctx`'s LiveBoardCensus
// arm - mass battlefield population reads that scale with growth.
Effect::EachSourceDealsDamage { .. }
| Effect::EachDealsDamageEqualToPower { .. }
Expand Down Expand Up @@ -5592,7 +5605,16 @@ fn effect_census_role(e: &Effect) -> CensusRole {
// token copy per matching attacker over an UNCONDITIONAL battlefield scan (grows
// the board); unsound across CR 508.1 multi-combat loops. Mirror of the new
// effect_target_ctx census member.
| Effect::CopyTokenBlockingAttacker { .. } => CensusRole::Census,
| Effect::CopyTokenBlockingAttacker { .. }
// CR 701.27a + CR 115.10a: mass Transform (scope:All) enumerates
// `state.battlefield` (`transform_effect::resolve_all`) — a census read that
// GROWS with the class. It WRITES ObjectPt + swaps abilities (NOT state-
// convergent like SetTapState), so it is a true `Census`, never the SetTapState
// relax exception. Parity with the effect_target_ctx LiveBoardCensus member.
| Effect::Transform {
scope: EffectScope::All,
..
} => CensusRole::Census,

// -- SetTapState (scope-DESTRUCTURED, exhaustive over EffectScope): scope:All is
// the census-ROLE proven exception (TapAll/UntapAll - state-convergent/idempotent,
Expand Down Expand Up @@ -5725,9 +5747,14 @@ fn effect_census_role(e: &Effect) -> CensusRole {
| Effect::Mana { .. }
| Effect::Discard { .. }
| Effect::Shuffle { .. }
| Effect::Transform { .. }
// CR 701.27a: scope:Single Transform reads only its single announced/anaphoric
// target — not a board census. scope:All is census-tagged above.
| Effect::Transform {
scope: EffectScope::Single,
..
}
// CR 710.4: a flip reads only its own self-referential target — not a
// board census, mirroring `Transform`.
// board census, mirroring `Transform`'s single scope.
| Effect::FlipPermanent { .. }
| Effect::TargetOnly { .. }
| Effect::Choose { .. }
Expand Down Expand Up @@ -6951,7 +6978,7 @@ mod tests {
}

/// guard#3 (mitigation #3): the `LiveBoardCensus` tag set of `effect_target_ctx`
/// == EXACTLY the enumeration-derived MASS-POPULATION set (29). Source-scanned, not
/// == EXACTLY the enumeration-derived MASS-POPULATION set (30). Source-scanned, not
/// hand-counted (the hand-count is what produced the earlier "relax=4" miss). Under
/// B's SnapshotOrEvent default this is the primary false-certificate gate: only a
/// census tag vetoes a mass read that ESCALATES over inert token growth (which
Expand Down Expand Up @@ -7007,6 +7034,11 @@ mod tests {
// `EffectScope::All`; the scope:Single arms live in the relax group below and
// are NOT scanned here (they sit past the census terminator).
"Suspect",
// CR 701.27a + CR 115.10a: mass Transform (scope:All) enumerates
// `state.battlefield` (`transform_effect::resolve_all`). Scope-gated on
// `EffectScope::All` in the census `|`-chain; the scope:Single arm sits past
// the census terminator in the relax group and is NOT scanned here.
"Transform",
"UnattachAll",
"Unsuspect",
// P3-B round-2: F1-class dual-mode mass-battlefield resolvers (a resolver
Expand All @@ -7027,7 +7059,7 @@ mod tests {
got, want,
"census tag set drifted from the enumeration-derived mass-population set"
);
assert_eq!(got.len(), 29, "exactly 29 mass-population census tags");
assert_eq!(got.len(), 30, "exactly 30 mass-population census tags");
}

/// A7' (mitigation #4, replaces the void census-default A7): with SnapshotOrEvent the
Expand Down Expand Up @@ -7138,7 +7170,7 @@ mod tests {
/// with `effect_target_ctx` on the Census/Relax boundary, closing the F1 gap where a
/// census-ROLE slot silently in the generic relax `|`-chain (exactly R1's Suspect{All})
/// is invisible to the census-arm-only guards. Structural: both functions' `Census`
/// name-sets are source-scanned and asserted IDENTICAL (== the 29). Behavioral: the
/// name-sets are source-scanned and asserted IDENTICAL (== the 30). Behavioral: the
/// two oracles agree on every discriminator, incl. BOTH Suspect/Unsuspect scopes.
///
/// REVERT-PROBE (discrimination proof): moving `Suspect{All}` out of the census arm of
Expand All @@ -7153,7 +7185,7 @@ mod tests {
use crate::types::ability::{EffectScope, TapStateChange};
use ScanMode::LoopFirewall;

// -- Structural: the two census name-sets are byte-identical (and == 29).
// -- Structural: the two census name-sets are byte-identical (and == 30).
fn census_names(fnsrc: &str, terminator: &str) -> Vec<String> {
let end = fnsrc.find(terminator).expect("census terminator");
let block = &fnsrc[..end];
Expand Down Expand Up @@ -7182,7 +7214,7 @@ mod tests {
etc_census, ecr_census,
"effect_census_role Census set diverged from effect_target_ctx"
);
assert_eq!(ecr_census.len(), 29, "exactly 29 census members");
assert_eq!(ecr_census.len(), 30, "exactly 30 census members");

// -- Behavioral: the two oracles agree on the Census/Relax boundary for every
// discriminator. `census(e, true)` requires BOTH `effect_census_role == Census`
Expand Down Expand Up @@ -7237,6 +7269,31 @@ mod tests {
census(&settap, false);
census(&Effect::HeistExile, false);
census(&Effect::NoOp, false);
// CR 701.27a + CR 115.10a: mass Transform is a battlefield census in BOTH oracles
// (scope:All), and a bounded single-target read (scope:Single) that relaxes. It is
// a true Census, NOT the SetTapState relax exception (ObjectPt/ability write).
census(
&Effect::Transform {
target: f(),
scope: EffectScope::All,
},
true,
);
census(
&Effect::Transform {
target: f(),
scope: EffectScope::Single,
},
false,
);
assert_eq!(
effect_census_role(&Effect::Transform {
target: f(),
scope: EffectScope::All,
}),
CensusRole::Census,
"mass Transform must be a true Census, not the SetTapState relax exception"
);

// -- Reason sub-tags reachable and correct (documentation-grade, unenforced by the
// Census/Relax boundary but proving each `RelaxReason` arm is live).
Expand Down
32 changes: 30 additions & 2 deletions crates/engine/src/game/ability_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2995,6 +2995,13 @@ fn mass_all_target_filter(effect: &Effect) -> Option<&TargetFilter> {
| Effect::BounceAll { target, .. }
| Effect::CounterAll { target, .. }
| Effect::ChangeZoneAll { target, .. }
// CR 701.27a + CR 115.10a: mass Transform's `target` is a resolution-time
// population scan (`target_filter()`==None), exactly like `TapAll`/`DestroyAll`.
| Effect::Transform {
scope: EffectScope::All,
target,
..
}
| Effect::DoublePTAll { target, .. } => Some(target),
_ => None,
}
Expand Down Expand Up @@ -3703,6 +3710,13 @@ fn effect_references_parent_target_combat_relation(effect: &Effect) -> bool {
| Effect::ChangeZoneAll { target, .. }
| Effect::DoublePTAll { target, .. }
| Effect::DamageAll { target, .. }
// CR 701.27a + CR 115.10a: parity with the other mass-population `target`
// filters — mass Transform's population filter is walked here too.
| Effect::Transform {
scope: EffectScope::All,
target,
..
}
| Effect::PutCounterAll { target, .. } => {
filter_references_parent_target_combat_relation(target)
}
Expand Down Expand Up @@ -3779,6 +3793,13 @@ fn effect_target_slot_filter(effect: &Effect) -> Option<TargetFilter> {
| Effect::BounceAll { target, .. }
| Effect::CounterAll { target, .. }
| Effect::ChangeZoneAll { target, .. }
// CR 701.27a + CR 115.10a: mass Transform's population filter is a
// resolution-time scan, walked here like the other mass-`All` effects.
| Effect::Transform {
scope: EffectScope::All,
target,
..
}
| Effect::DoublePTAll { target, .. } => filter_target_slot_filter(target),
_ => None,
}
Expand Down Expand Up @@ -4857,8 +4878,15 @@ fn concretize_granting_object_in_effect(effect: &mut Effect, granter: ObjectId)
| Effect::DealDamage { target, .. }
| Effect::Pump { target, .. }
| Effect::Counter { target, .. }
| Effect::Transform { target, .. }
// CR 710.4: same single-target-slot shape as `Transform`.
// CR 701.27a: only single-scope Transform carries a targetable slot that
// can bind a GrantingObject anaphor; the mass (`All`) scope's `target` is a
// population filter (mirrors the SetTapState Single-gate above).
| Effect::Transform {
scope: EffectScope::Single,
target,
..
}
// CR 710.4: same single-target-slot shape as `Transform`'s single scope.
| Effect::FlipPermanent { target, .. }
| Effect::Connive { target, .. }
| Effect::PhaseOut { target }
Expand Down
15 changes: 14 additions & 1 deletion crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2367,7 +2367,13 @@ fn effect_details(effect: &Effect) -> Vec<(String, String)> {
| Effect::PhaseOut { target }
| Effect::PhaseIn { target }
| Effect::ForceAttack { target, .. }
| Effect::Transform { target }
// CR 701.27a: single-scope Transform reports its `target` like other
// single-target effects; mass Transform (scope:All) reports a `filter` below.
| Effect::Transform {
scope: EffectScope::Single,
target,
..
}
// CR 710.4: the flipping permanent is the effect's single reported target.
| Effect::FlipPermanent { target }
| Effect::Shuffle { target }
Expand Down Expand Up @@ -2406,6 +2412,13 @@ fn effect_details(effect: &Effect) -> Vec<(String, String)> {
target,
..
}
// CR 701.27a + CR 115.10a: mass Transform ("Transform all Humans") reports its
// non-targeting population `filter`, like the other mass effects.
| Effect::Transform {
scope: EffectScope::All,
target,
..
}
| Effect::BounceAll { target, .. }
| Effect::CounterAll { target, .. }
| Effect::DamageAll {
Expand Down
12 changes: 10 additions & 2 deletions crates/engine/src/game/effects/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6432,8 +6432,7 @@ fn extract_event_context_filter(effect: &Effect) -> Option<&TargetFilter> {
| Effect::Fight { target, .. }
| Effect::Attach { target, .. }
| Effect::UnattachAll { target, .. }
| Effect::Transform { target, .. }
// CR 710.4: same single-target-slot shape as `Transform`.
// CR 710.4: same single-target-slot shape as `Transform`'s single scope.
| Effect::FlipPermanent { target, .. }
| Effect::CopySpell { target, .. }
| Effect::CastCopyOfCard { target, .. }
Expand Down Expand Up @@ -6492,6 +6491,15 @@ fn extract_event_context_filter(effect: &Effect) -> Option<&TargetFilter> {
target,
..
} => target,
// CR 701.27a + CR 603.7c: only the single-scope Transform exposes an
// event-context target (e.g. an anaphoric trigger subject). The mass
// (`All`) scope's `target` is a population filter, not a per-event ref —
// it falls through to `None`, mirroring the SetTapState split above.
Effect::Transform {
scope: EffectScope::Single,
target,
..
} => target,
// CR 603.7c + CR 608.2c: `GenericEffect` carries an optional `target` that may
// be an event-context ref (e.g., `TriggeringSource` for "that land doesn't untap
// during its controller's next untap step" on a TapsForMana trigger). Routing it
Expand Down
4 changes: 3 additions & 1 deletion crates/engine/src/game/effects/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2714,6 +2714,7 @@ fn incubator_ability() -> AbilityDefinition {
AbilityKind::Activated,
Effect::Transform {
target: TargetFilter::SelfRef,
scope: crate::types::ability::EffectScope::Single,
},
)
.cost(AbilityCost::Mana {
Expand Down Expand Up @@ -3996,7 +3997,8 @@ mod tests {
assert!(matches!(
*abilities[0].effect,
Effect::Transform {
target: TargetFilter::SelfRef
target: TargetFilter::SelfRef,
..
}
));
assert!(matches!(
Expand Down
Loading
Loading