refactor(engine): unify tap effects into Effect::SetTapState (#2777) - #2811
Conversation
…s#2777) Phase 1 of phase-rs#2777 (parameterize Effect sibling-clusters). Collapse the four tap-state siblings — Tap, Untap, TapAll, UntapAll — into one parameterized variant: Effect::SetTapState { target: TargetFilter, scope: EffectScope, state: TapStateChange } with new typed axes EffectScope { Single, All } (a reusable scope primitive for the remaining phase-rs#2777 clusters) and TapStateChange { Tap, Untap } (CR 701.26a tap / 701.26b untap). Mapping: Tap->{Single,Tap}, Untap->{Single,Untap}, TapAll->{All,Tap}, UntapAll->{All,Untap}. The Single and All resolver paths genuinely diverge (Single resolves chosen/SelfRef/TrackedSet subjects with a resolution-timed prompt; All iterates the battlefield by filter), so scope is a load-bearing field, not a TargetFilter fold. Behavior is preserved at every site: - target_filter(): Single->Some(target), All->None (was Tap/Untap-has-target, *All-no-target). - EffectKind kept whole (4 variants) and effect_variant_name() still maps to the 4 legacy strings, so coverage classification and EffectResolved tags are byte-stable. - Every match arm that distinguished the four variants branches on (scope, state) — no behavior-erasing catch-all (AI polarity Single+Tap=Harmful / Single+Untap=Beneficial; coverage swallowed-clause guards; etc.). Also updates the Forge importer constructors, the build-unbreaking mtgish-import destructures (mechanical rename only), and the committed snapshots/fixtures whose serde "type" tag flips. The other ~13 phase-rs#2777 clusters (Destroy/Bounce/Counter/Pump/Goad/...) are deferred to follow-up PRs that reuse EffectScope. Verified: cargo build (full workspace incl mtgish-import), clippy -D warnings, cargo test -p engine (11233 lib + 807 integration), cargo test -p phase-ai — all green; snapshots blessed. The card-data swallowed-clause gate is coverage-neutral by construction (pure variant rename; the coverage.rs swallow detectors are byte-identical) — CI's card-data job confirms. CR 701.26a / CR 701.26b. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request refactors the game engine's tap and untap effects by collapsing the legacy Tap, Untap, TapAll, and UntapAll variants of Effect into a single parameterized SetTapState variant. This variant uses EffectScope (Single or All) and TapStateChange (Tap or Untap) to determine its behavior, simplifying the codebase and reducing variant proliferation in accordance with style guide rule R3. The changes are propagated across the engine, parser, AI policies, and tests. Feedback on the pull request identifies a minor violation of style guide rule R6 in overload.rs, where a comment incorrectly references CR 702.96e instead of CR 702.96b for Overload rules.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // CR 702.96e + CR 701.26a/b: overload promotes single-target tap/untap to | ||
| // its mass scope, carrying the tap/untap polarity through. (Only the Tap | ||
| // polarity appears in the current overload corpus; Untap promotion is | ||
| // type-supported for parity.) |
There was a problem hiding this comment.
[MEDIUM] Hallucinated CR rule number in comment.
The comment cites CR 702.96e, but the rules for Overload text-changing effects are defined under CR 702.96b. Please update the reference to CR 702.96b to ensure strict fidelity and compliance with the repository's style guide (R6).
| // CR 702.96e + CR 701.26a/b: overload promotes single-target tap/untap to | |
| // its mass scope, carrying the tap/untap polarity through. (Only the Tap | |
| // polarity appears in the current overload corpus; Untap promotion is | |
| // type-supported for parity.) | |
| // CR 702.96b + CR 701.26a/b: overload promotes single-target tap/untap to | |
| // its mass scope, carrying the tap/untap polarity through. (Only the Tap | |
| // polarity appears in the current overload corpus; Untap promotion is | |
| // type-supported for parity.) |
References
- Every rules-touching line of engine code must carry a comment of the form CR : . The number must be verified against docs/MagicCompRules.txt before writing. (link)
Review:
|
|
Both addressed:
The non-blocking |
|
Re-review (since 10:42Z): ✅ Required fix landed — clean approve. Both follow-up commits address my prior findings exactly:
No other changes; the behavior-preserving tap-cluster collapse ( |
|
Brought current with |
|
Review (review-impl lenses): Reviewed the ground-truth API diff (head Seam/idiom gates: pass. The collapse lives at the right layer ( Verification performed:
One non-blocking observation:
VERDICT: approve |
|
Thanks for the thorough second pass. On the [LOW] serde-default note ( |
matthewevans
left a comment
There was a problem hiding this comment.
Maintainer sign-off: 4 sibling tap variants collapsed into parameterized Effect::SetTapState (#2777 Phase 1) — parameterize-don't-proliferate at the prescribed seam; behavior preservation traced by two reviewers; hallucinated CR 702.96e corrected to 702.96a; brought current with main by author.
…date to SetTapState
The merge queue bisected this PR out (E0599): three identical
ReplacementDefinition test fixtures using Effect::Tap landed on main
(attractions.rs, keywords.rs, morph.rs) after this branch converted the
variant away. Ported to SetTapState { Single, Tap } per this PR's own
convention (CR 701.26a).
|
Maintainer note: the merge queue bisected this PR out (E0599 in the speculative group) — three enters-tapped test fixtures using |
# Conflicts: # crates/engine/tests/fixtures/integration_cards.json
…cts to SetTapState The phase-rs#2811 collapse removed Effect::{Tap,Untap,TapAll,UntapAll} in favor of Effect::SetTapState { target, scope, state }. A prior merge-resolve took origin/main's fixture wholesale, reintroducing the legacy variants and breaking CardDatabase::from_export (serde unknown-variant). Remap the 50 tap-family effect entries to SetTapState: single-scope (Tap/Untap) -> scope Single, mass-scope (TapAll/UntapAll) -> scope All, preserving each target as the selectable/population filter (CR 701.26a/b).
Phase 1 of #2777 (parameterize
Effectsibling-clusters).What
Collapse the four tap-state siblings —
Tap,Untap,TapAll,UntapAll— into one parameterized variant:with new typed axes
EffectScope { Single, All }(a reusable scope primitive for the remaining #2777 clusters) andTapStateChange { Tap, Untap }. Mapping:Tap→{Single,Tap},Untap→{Single,Untap},TapAll→{All,Tap},UntapAll→{All,Untap}.Why scope is a real field (not a
TargetFilterfold)Unlike the
*Allcollapse in #1693/#1736 (where Destroy behaves identically once you have the object set), the tap Single and All resolvers genuinely diverge: Single resolves chosen /SelfRef/TrackedSetsubjects and runs a resolution-timed multi-target prompt; All iterates the battlefield by filter. Soscopedispatches between two real code paths.Behavior preserved at every site
target_filter():Single→Some(target),All→None(was Tap/Untap-has-target,*All-no-target).EffectKindkept whole (4 variants) andeffect_variant_name()still maps(scope,state)→ the 4 legacy strings, so coverage classification andEffectResolvedtags are byte-stable.(scope, state)— no behavior-erasing catch-all (AI polaritySingle+Tap=Harmful /Single+Untap=Beneficial; coverage swallowed-clause guards; the resolver; the "did-anything" / event-context arms).mtgish-importdestructures updated (mechanical rename only;ReplacementEvent::Tap/Untap— a different enum — left untouched)."type"tag flips were regenerated.Scope
The other ~13 #2777 clusters (Destroy/Bounce/Counter/Pump/Goad/DoublePT/…) are deferred to follow-up PRs that reuse
EffectScope. Shipping one cluster with full verification (the prior all-at-once attempt #2778 was closed for failing CI).Verification
cargo build(full workspace incl. mtgish-import),clippy -D warnings,cargo test -p engine(11233 lib + 807 integration),cargo test -p phase-ai— all green; snapshots blessed; a building-blockset_tap_state_routes_all_four_quadrantstest added. The card-data swallowed-clause gate is coverage-neutral by construction (pure variant rename; thecoverage.rsswallow detectors are byte-identical) — CI’s card-data job confirms.CR 701.26a / CR 701.26b.
🤖 Generated with Claude Code