fix(engine): keep a "return to hand" card as the referent for "that card" (#6486)#6561
Conversation
…ard" (phase-rs#6486) Volcanic Vision ("Return target instant or sorcery card from your graveyard to your hand. Volcanic Vision deals damage equal to that card's mana value to each creature your opponents control. Exile Volcanic Vision.") returned the card but dealt no damage. The spell parses correctly — the damage sub-effect's amount is the Demonstrative "that card's mana value". That resolves against the earlier-instruction referent (`effect_context_object`), which `parent_referent_context_from_events` derives from the parent effect's `ZoneChanged` events via `moved_object_context_from_events`. That helper only captured moves to a PUBLIC zone, so the graveyard->HAND return bound no referent, "that card's mana value" resolved to 0, and every opponent creature took 0 damage. CR 608.2c: a later instruction may refer to an object an earlier instruction returned to hand — its identity was established in the public source zone (graveyard), and the reference reads the move-time last-known mana value, so the hidden destination is immaterial. Capture the moved referent for a move to hand when the SOURCE zone is public. A move from a hidden source (a draw, library -> hand) establishes no such referent and is excluded; library destinations stay excluded entirely (a shuffle loses identity). Verified: cargo test -p engine --lib -> 17591 passed, 0 failed. New card-level cast test drives Volcanic Vision through the real pipeline: with a mana-value-3 instant in the graveyard, each opponent creature takes 3 damage, the card is in hand, and Volcanic Vision is exiled.
|
🚨 Contributor flagged. Click here for more info: Superagent Dashboard |
📝 WalkthroughWalkthroughZoneChanged snapshot creation now covers cards moved from public zones to hidden hands. Volcanic Vision and CR 608.2h tests verify that qualifying moves preserve move-time referent data. ChangesZone-change snapshot tracking
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Parse changes introduced by this PR✓ No card-parse changes detected. |
|
Right seam, right shape, real bug — two small fixups before this enqueues: the CR annotation cites the wrong rule for the clause it implements, and the widened predicate has no building-block test pinning its boundaries. Reviewed at head Premise verified against Scryfall (
Mana cost 🔴 BlockerNone. 🟡 Non-blocking1. The CR annotation cites CR 608.2c, but the rule that authorizes this change is CR 608.2h — and the guard being relaxed encodes CR 400.7j.
} if is_public_zone(*to)
// CR 608.2c: a later instruction may refer to an object an earlier
// instruction returned to hand, provided its identity was established
// in a PUBLIC source zone — Volcanic Vision (...). The reference reads the
// move-time LKI snapshot, so the hidden destination is immaterial.
|| (*to == Zone::Hand && is_public_zone(*from_zone)) =>Grep-verified against
That is instruction ordering and later-text-modifies-earlier-text. It says nothing about source-zone publicity or last-known information, so it does not support the two propositions the comment actually makes. CR 608.2h (line 2806) states the implemented predicate almost verbatim:
And CR 400.7j (line 1970) is precisely why the original guard was public-only:
So the rules story for this fix is exact and clean: CR 400.7j governs findability and is public-only; CR 608.2h supplies the last-known-information path for the public→hidden move, which is the Volcanic Vision case word for word. This is the same argument this repo already litigated and landed for CR 608.2c is not a hallucinated number and it is the cascade's house citation for anaphora generally, so keep it if you like; the gap is that the one rule making this change defensible is missing. Fix: annotate 2. No building-block test pins the widened predicate's boundaries — most importantly the draw exclusion.
After this change, The repo bar is explicit here — "Test the building block, not the special case." The neighbouring helpers already have exactly these unit tests ( Fix: three cheap unit tests on
3. The blast radius is wider than the PR body describes: every bounce now binds a moved-referent.
The body frames this as the "return/move a card to hand from a public zone" class, which reads as graveyard/exile recursion. The dominant newly-captured population is actually Battlefield → Hand — ordinary bounce. Battlefield is public, so every Evidence this is benign in practice, and worth crediting: the existing Fix: name the bounce class in the code comment so the next reader sizes the surface correctly. A precedence test (bounce + tap in one span binds the bounced object) would pin it, though the existing coverage makes this optional. 4. Test fixture uses a real card name with a fabricated mana cost.
.add_spell_to_graveyard(P0, "Fire Blast", true)
.with_mana_cost(ManaCost::generic(3))Real Fire Blast is 5. Process note (not a code defect). The PR body is missing the required AI-contributor template sections ( ✅ Clean
Recommendation: two fixups, then enqueue. (1) Change the annotation at Note that the required |
The referent widening is rules-correct, but its annotation cited CR 608.2c
(instruction ordering / later-text-modifies-earlier-text), which does not
speak to source-zone publicity or last known information.
Cite the two rules that actually carry the clause, both grep-verified
against docs/MagicCompRules.txt:
CR 400.7j findability on a move to a PUBLIC zone (the pre-existing arm)
CR 608.2h "... or if the effect has moved it from a public zone to a
hidden zone, the effect uses the object's last known
information" (the new public -> Hand arm)
Add three helper-level tests on parent_referent_context_from_events so the
widened predicate's boundaries are pinned:
- Library -> Hand (a draw) binds no referent. After the widening,
is_public_zone's exclusion of Library is the only thing keeping every
draw in the game from binding one, and nothing tested it.
- Graveyard -> Hand binds the referent and carries the move-time mana
value (the Volcanic Vision case, at helper level).
- Two qualifying moves in one span bind nothing — the CR 608.2k singular
guard now has a wider surface to trip on.
Also name the dominant newly-captured population (ordinary Battlefield ->
Hand bounce) in the predicate comment so the next reader sizes the blast
radius correctly.
Co-authored-by: philluiz2323 <philluiz2323@gmail.com>
matthewevans
left a comment
There was a problem hiding this comment.
Maintainer sign-off. The referent widening is at the right seam (moved_object_context_from_events, the existing zone-change member of the anaphoric-referent cascade) and is rules-correct.
Two maintainer fixups pushed in 5a182ec (co-authored to you, @philluiz2323 — the engine logic is unchanged):
- CR citation corrected. The clause implements CR 608.2h ("...or if the effect has moved it from a public zone to a hidden zone, the effect uses the object's last known information") alongside CR 400.7j for the pre-existing public-destination arm. CR 608.2c is instruction ordering / later-text-modifies-earlier-text and does not carry source-zone publicity or LKI. Both replacements grep-verified against
docs/MagicCompRules.txt. - Three helper-level tests on
parent_referent_context_from_events, pinning the widened predicate's boundaries — most importantlyLibrary -> Hand(a draw) binding nothing, since after this changeis_public_zone's exclusion ofLibraryis the only thing standing between the engine and every draw binding a referent, and nothing tested it.
Also named the dominant newly-captured population (ordinary Battlefield -> Hand bounce) in the predicate comment.
Nice work on this one: correct root-cause trace, one predicate widened rather than a new fail-closed sibling, and a discriminating cast-pipeline test that resolves to 0 on revert.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/engine/src/game/effects/mod.rs`:
- Around line 11555-11569: Update the documentation comment above
multiple_qualifying_moves_bind_no_parent_referent to cite CR 608.2c and CR
400.7j, matching the basis used by parent_referent_context_from_events, or
remove the CR citation if neither applies cleanly; preserve the existing
explanation of singular referent resolution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: db0a2c98-2bb4-48cf-acd5-29e06d0a269f
📒 Files selected for processing (1)
crates/engine/src/game/effects/mod.rs
…mp (#6568) `release: v0.35.2` (21a53d5) bumped `client/src-tauri/Cargo.toml` to 0.35.2 via cargo-release's `pre-release-replacements`, but `client/src-tauri/` is a separate cargo workspace with its own lockfile that cargo-release does not manage. The lock still recorded `phase-tauri 0.35.1`, so the `tauri-check` CI job's `cargo check --locked --manifest-path client/src-tauri/Cargo.toml` refused to reconcile the mismatch and exited 101. That reds the required `Rust (fmt, clippy, test, coverage-gate)` aggregator (which `needs: [... tauri-check]`) on every open PR whose merge ref includes the release commit, blocking the merge queue repo-wide. Evidence: PR #6561 tauri-check PASSED at 20:43:00Z; the release commit landed at 20:49:54Z; PRs #6564 (20:56:21Z) and #6563 (21:15:11Z) both FAILED with the identical --locked error. None of the three touched any Cargo manifest. Follow-up (not in this change): cargo-release should keep the nested lockfile in sync so the next release does not re-break it. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
|
Temporarily dequeued by a maintainer — nothing is wrong with this PR, and it will be re-enqueued shortly.
That job is a The fix is #6568. It was queued behind the PRs it needs to unblock, which is a deadlock: the queue merges in order, so it could not land until those cleared, and they could not clear until it landed. Dequeuing the entries ahead of it is how that gets broken. No action needed from you. Your PR keeps its approval and its head is unchanged. Once #6568 merges, this will be re-enqueued and should go through on the same green it already had. Apologies for the churn — this was our breakage, not yours. |
|
Second infrastructure-caused delay on this PR, and again nothing to do with your changes — apologies for the churn. What happened this time: GitHub's hosted runners stalled repo-wide from roughly 00:18Z to 02:20Z, so no check could report back. The merge queue's ruleset has a 60-minute check-response timeout; this PR's merge group was created at 00:37:43Z, and Both required checks are green at the current head and the approval is on this commit. Re-enqueuing now. (The |
…mp (phase-rs#6568) `release: v0.35.2` (21a53d5) bumped `client/src-tauri/Cargo.toml` to 0.35.2 via cargo-release's `pre-release-replacements`, but `client/src-tauri/` is a separate cargo workspace with its own lockfile that cargo-release does not manage. The lock still recorded `phase-tauri 0.35.1`, so the `tauri-check` CI job's `cargo check --locked --manifest-path client/src-tauri/Cargo.toml` refused to reconcile the mismatch and exited 101. That reds the required `Rust (fmt, clippy, test, coverage-gate)` aggregator (which `needs: [... tauri-check]`) on every open PR whose merge ref includes the release commit, blocking the merge queue repo-wide. Evidence: PR phase-rs#6561 tauri-check PASSED at 20:43:00Z; the release commit landed at 20:49:54Z; PRs phase-rs#6564 (20:56:21Z) and phase-rs#6563 (21:15:11Z) both FAILED with the identical --locked error. None of the three touched any Cargo manifest. Follow-up (not in this change): cargo-release should keep the nested lockfile in sync so the next release does not re-break it. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…ard" (phase-rs#6486) (phase-rs#6561) * fix(engine): keep a "return to hand" card as the referent for "that card" (phase-rs#6486) Volcanic Vision ("Return target instant or sorcery card from your graveyard to your hand. Volcanic Vision deals damage equal to that card's mana value to each creature your opponents control. Exile Volcanic Vision.") returned the card but dealt no damage. The spell parses correctly — the damage sub-effect's amount is the Demonstrative "that card's mana value". That resolves against the earlier-instruction referent (`effect_context_object`), which `parent_referent_context_from_events` derives from the parent effect's `ZoneChanged` events via `moved_object_context_from_events`. That helper only captured moves to a PUBLIC zone, so the graveyard->HAND return bound no referent, "that card's mana value" resolved to 0, and every opponent creature took 0 damage. CR 608.2c: a later instruction may refer to an object an earlier instruction returned to hand — its identity was established in the public source zone (graveyard), and the reference reads the move-time last-known mana value, so the hidden destination is immaterial. Capture the moved referent for a move to hand when the SOURCE zone is public. A move from a hidden source (a draw, library -> hand) establishes no such referent and is excluded; library destinations stay excluded entirely (a shuffle loses identity). Verified: cargo test -p engine --lib -> 17591 passed, 0 failed. New card-level cast test drives Volcanic Vision through the real pipeline: with a mana-value-3 instant in the graveyard, each opponent creature takes 3 damage, the card is in hand, and Volcanic Vision is exiled. * fix(PR-6561): correct CR citation and pin the widened move predicate The referent widening is rules-correct, but its annotation cited CR 608.2c (instruction ordering / later-text-modifies-earlier-text), which does not speak to source-zone publicity or last known information. Cite the two rules that actually carry the clause, both grep-verified against docs/MagicCompRules.txt: CR 400.7j findability on a move to a PUBLIC zone (the pre-existing arm) CR 608.2h "... or if the effect has moved it from a public zone to a hidden zone, the effect uses the object's last known information" (the new public -> Hand arm) Add three helper-level tests on parent_referent_context_from_events so the widened predicate's boundaries are pinned: - Library -> Hand (a draw) binds no referent. After the widening, is_public_zone's exclusion of Library is the only thing keeping every draw in the game from binding one, and nothing tested it. - Graveyard -> Hand binds the referent and carries the move-time mana value (the Volcanic Vision case, at helper level). - Two qualifying moves in one span bind nothing — the CR 608.2k singular guard now has a wider surface to trip on. Also name the dominant newly-captured population (ordinary Battlefield -> Hand bounce) in the predicate comment so the next reader sizes the blast radius correctly. Co-authored-by: philluiz2323 <philluiz2323@gmail.com> --------- Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
Summary
Fixes #6486. Volcanic Vision — "Return target instant or sorcery card from your graveyard to your hand. Volcanic Vision deals damage equal to that card's mana value to each creature your opponents control. Exile Volcanic Vision." — returned the card but dealt no damage.
Implementation method (required)
/engine-implementerpipeline/engine-implementer— explain why belowRoot cause
The spell parses correctly — the damage sub-effect's amount is the Demonstrative
ObjectManaValue { scope: Demonstrative }("that card's mana value"). That resolves against the earlier-instruction referent (effect_context_object), whichparent_referent_context_from_eventsderives from the parent effect'sZoneChangedevents viamoved_object_context_from_events. That helper only captured moves to a public zone:Volcanic Vision returns the card to hand (hidden), so no referent was bound,
that card's mana valueresolved to0, and every opponent creature took0damage.Fix
CR 608.2c: a later instruction may refer to an object an earlier instruction returned to hand — its identity was established in the public source zone (graveyard), and the reference reads the move-time last-known mana value, so the hidden destination is immaterial. Capture the moved referent for a move to hand when the source zone is public:
This is a building-block fix to the shared referent helper, so it covers the whole "return/move a card to hand from a public zone, then reference that card" class rather than one card.
CR references
CR 608.2c— later instructions may refer to an object an earlier instruction acted on; the reference reads last-known information.Verification
cargo test -p engine --lib→ 17591 passed, 0 failed (the referent helper is shared across many cards).volcanic_vision_deals_returned_cards_mana_value_after_return_to_hand) drives the real pipeline: with a mana-value-3 instant in the graveyard, each opponent creature takes 3 damage, the returned card is in hand, and Volcanic Vision is exiled. Fails (0 damage) if the referent fix is reverted.cargo fmt --allclean.Summary by CodeRabbit