Skip to content

test(engine): Riptide per-opponent target slots are scoped to that opponent (#6565) - #6836

Merged
matthewevans merged 3 commits into
phase-rs:mainfrom
galuis116:test/riptide-per-opponent-controller-scope
Jul 31, 2026
Merged

test(engine): Riptide per-opponent target slots are scoped to that opponent (#6565)#6836
matthewevans merged 3 commits into
phase-rs:mainfrom
galuis116:test/riptide-per-opponent-controller-scope

Conversation

@galuis116

@galuis116 galuis116 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What this is

The per-opponent fanout targeting fix for Riptide Gearhulk (#6565 � "that player controls" bound to the iterated opponent) had no runtime regression covering the controller scope. riptide_gearhulk_5994.rs covers the optional-count behavior (#5994), but not that each opponent's slot targets that opponent's permanents rather than the caster's.

Tests

Two runtime tests driving Riptide's real ETB through the cast pipeline into WaitingFor::TriggerTargetSelection:

  • riptide_per_opponent_slot_excludes_the_casters_own_permanents â�� offers only the caster's own permanent as target intent. It is not a legal target for the opponent's per-opponent slot, so the slot is declined and the permanent stays on the battlefield. Non-vacuous: a broken scope that let the opponent slot target the caster's permanent would move it to the library and flip the assertion.
  • riptide_per_opponent_slot_targets_that_opponents_permanent â�� the positive half: the opponent's own permanent IS legal and moves to its owner's library third from the top, while the caster's permanent is untouched.

cargo fmt, clippy -p phase-engine --tests, and both tests are green.

Refs #6565

Summary by CodeRabbit

  • Bug Fixes

    • Corrected Riptide Gearhulk targeting so each opponent’s target must come from that opponent’s permanents.
    • Ensured valid abilities still resolve when the caster’s own permanent cannot fulfill an opponent’s target requirement.
    • Confirmed targeted opponent permanents are returned to their owner’s library while the caster’s permanents remain on the battlefield.
  • Tests

    • Added regression coverage for Riptide Gearhulk’s per-opponent targeting behavior.

@galuis116
galuis116 requested a review from matthewevans as a code owner July 31, 2026 13:28
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1c72551e-5a35-44ab-a14e-b597389de108

📥 Commits

Reviewing files that changed from the base of the PR and between 887a6ee and c25788f.

📒 Files selected for processing (3)
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/effects/put_on_top.rs
  • crates/engine/tests/integration/riptide_gearhulk_5994.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/tests/integration/riptide_gearhulk_5994.rs

📝 Walkthrough

Walkthrough

Updated per-opponent target fanout resolution to place all preselected targets. Added two integration tests for Riptide Gearhulk target scoping and resulting battlefield and library states.

Changes

Riptide Gearhulk targeting

Layer / File(s) Summary
Per-opponent placement resolution
crates/engine/src/game/ability_utils.rs, crates/engine/src/game/effects/put_on_top.rs
Made is_per_opponent_target_fanout crate-visible. The placement effect now uses the number of preselected targets for per-opponent fanout abilities.
Per-opponent targeting regression tests
crates/engine/tests/integration/riptide_gearhulk_5994.rs
Added tests that exclude the caster’s permanent and move selected opponent permanents to their owners’ libraries.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: matthewevans, kiannidev

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the engine tests and the Riptide per-opponent target-slot scoping change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/tests/integration/riptide_gearhulk_5994.rs`:
- Around line 141-142: Add a multiplayer runtime test case alongside the
existing target-selection cases, using the target players [P1, P2] and target
objects [p1_perm, p2_perm]. Assert that both selected permanents are moved into
their respective owners’ libraries, covering independent acceptance of P2’s
permanent.
- Around line 118-122: The CR reference CR 109.5 in the comments at both the
anchor site (lines 118-122) and the sibling site (lines 156-158) of
crates/engine/tests/integration/riptide_gearhulk_5994.rs does not properly cover
player binding semantics. Replace CR 109.5 with CR 102.2 + CR 608.2c in both
comment locations to accurately reference the rules governing opponent scoping
and target player binding, or remove the CR citations entirely if the more
specific rules are not needed for clarity.
🪄 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: c2258ff2-6043-45af-a214-5351f347d0b8

📥 Commits

Reviewing files that changed from the base of the PR and between ae0777f and 32f20bd.

📒 Files selected for processing (1)
  • crates/engine/tests/integration/riptide_gearhulk_5994.rs

Comment thread crates/engine/tests/integration/riptide_gearhulk_5994.rs Outdated
Comment thread crates/engine/tests/integration/riptide_gearhulk_5994.rs
@matthewevans matthewevans self-assigned this Jul 31, 2026
@matthewevans matthewevans added the test Add tests label Jul 31, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — the per-opponent controller-scope regression does not exercise the second opponent’s slot.

🔴 Blocker

[MED] The new tests use two-player GameScenario::new() and select only P1 at crates/engine/tests/integration/riptide_gearhulk_5994.rs:125-143 and :161-178. The production fanout creates a separate player/object slot pair for each opponent at crates/engine/src/game/ability_utils.rs:4906-4935; the existing three-player test at riptide_gearhulk_5994.rs:87-115 selects P1 and declines P2. A regression that binds the first opponent correctly but aliases or skips the second therefore passes every test here.

Please add a three-player runtime cast selecting [P1, P2] and [p1_perm, p2_perm], and assert both opponent permanents reach their respective libraries while the caster’s permanent remains on the battlefield. That drives the real pipeline through both independently bound fanout slots.

🟡 Non-blocking

[LOW] The new comments at crates/engine/tests/integration/riptide_gearhulk_5994.rs:118 and :156 cite CR 109.5 for the that player binding. CR 109.5 defines only “you” and “your”; the local rules text at docs/MagicCompRules.txt:2793 is the applicable general instruction to read the whole text and apply English. Please remove the false CR 109.5 citations, or replace them only with a verified, accurately worded CR 608.2c rationale.

Recommendation: request changes for the two-opponent runtime guard; correct the two CR comments in the same update.

@matthewevans matthewevans removed their assignment Jul 31, 2026
@matthewevans

matthewevans commented Jul 31, 2026

Copy link
Copy Markdown
Member

Maintainer update: the two review findings are addressed on the current head. Rust lint/tests and frontend CI are still pending; the fresh CodeRabbit review is clean. Once the remaining CI checks are green, I will resume with approval and merge-queue enqueue.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — the new three-player regression exposes an unresolved production defect, so this test-only PR cannot merge until the engine behavior is corrected.

[HIGH] Per-opponent library placement resolves only the first selected opponent target. Evidence: crates/engine/tests/integration/riptide_gearhulk_5994.rs:176-184 selects [P1, P2] and [p1_perm, p2_perm]; current-head CI fails with p2_perm still on the battlefield (Rust shard 1/2 log). The fanout validator preserves both legal object targets at crates/engine/src/game/ability_utils.rs:4988-5001, but PutAtLibraryPosition resolves its printed count as a single global limit at crates/engine/src/game/effects/put_on_top.rs:162 and truncates the selected targets at :315. Why it matters: “for each opponent, put up to one target … that player controls” must apply once for each selected opponent; in a three-player game the second opponent’s selected permanent is silently unaffected. Suggested fix: extend the engine’s per-opponent fanout execution semantics at the target/resolver seam so each selected player/object pair receives its own one-object action, with a runtime regression that covers both selected and declined slots. Do not weaken the new two-opponent regression.

galuis116 and others added 3 commits July 31, 2026 12:25
…ponent's permanents (phase-rs#6565)

The per-opponent fanout targeting fix (phase-rs#6565, "that player controls" bound to the
iterated opponent) had no runtime regression covering the controller scope. The
existing riptide_gearhulk_5994.rs tests cover the optional-count behavior (phase-rs#5994)
but not that each opponent's slot targets THAT opponent's permanents rather than
the caster's.

Add two runtime tests driving Riptide's real ETB through the cast pipeline:
- `riptide_per_opponent_slot_excludes_the_casters_own_permanents` offers only the
  caster's own permanent as target intent; it is not a legal target for the
  opponent's per-opponent slot, so the slot is declined and the permanent stays.
  Non-vacuous: a broken scope that let the opponent slot target the caster's
  permanent would move it to the library and flip the assertion.
- `riptide_per_opponent_slot_targets_that_opponents_permanent` is the positive
  half — the opponent's own permanent IS legal and moves to its owner's library,
  while the caster's permanent is untouched.

Refs phase-rs#6565
…sition (phase-rs#6565)

A per-opponent target fanout ("for each opponent, put up to one target
nonland permanent that player controls into its owner's library third from
the top") pre-selects one target PER opponent at stack time via
multi_target.max = PlayerCount { Opponent }. The PutAtLibraryPosition
effect's count field (Fixed(1)) is the PER-OPPONENT cap, not the total, so
put_on_top::resolve wrongly treated the two pre-chosen permanents as "more
candidates than count" and issued an interactive EffectZoneChoice { count: 1 }
over the already-targeted permanents. That prompt looped forever (selecting one
never advanced) and, at best, would have placed only one of the two.

CR 601.2c: the number of targets is fixed at targeting; each chosen target is
placed. CR 401.4: cards put at the same library position are arranged by their
owner. For a per-opponent fanout, expected placement count is the number of
pre-chosen targets, so each is routed to its own owner's library (CR 400.7).

Fixes the runtime regression covered by
riptide_per_opponent_slots_target_each_opponents_permanent (phase-rs#6836).
@galuis116
galuis116 force-pushed the test/riptide-per-opponent-controller-scope branch from 887a6ee to c25788f Compare July 31, 2026 15:26
@galuis116

Copy link
Copy Markdown
Contributor Author

The 2-opponent test surfaced a real engine bug, so I pushed an engine fix (c25788f) rather than adjusting the test.

Root cause: the per-opponent fanout targets correctly at stack time (4 scoped slots; each opponent's permanent chosen for that opponent's slot, per the #6565 fix). But at resolution, put_on_top::resolve compared the number of pre-chosen permanents (2) against the effect's count field, which for this shape is Fixed(1) � the PER-OPPONENT cap, not the total. That tripped the "more candidates than count" guard and issued an interactive EffectZoneChoice { count: 1 } over the already-targeted permanents. That prompt looped forever (selecting one never advanced) and would at best have placed only one of the two.

Fix: for a per-opponent target fanout the expected placement count is the number of pre-chosen targets, so each is placed into its own owner's library (routed by the move, CR 400.7). Reuses the existing is_per_opponent_target_fanout authority (now pub(crate)) that validate_targets_in_chain / flatten_targets_in_chain already special-case, so the resolver stays consistent with the rest of the fanout plumbing.

Verified locally: all 4 tests in riptide_gearhulk_5994.rs pass (incl. the previously-failing riptide_per_opponent_slots_target_each_opponents_permanent), the 22 put_on_top unit tests pass, and cargo fmt + clippy -p phase-engine --tests are clean. Rebased on latest main.

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans matthewevans self-assigned this Jul 31, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. The per-opponent fanout now treats the already-selected one-per-opponent targets as the resolved placement set, and the three-player cast-pipeline regression proves both opponent targets move to their respective owners' libraries. Current-head CI and the parser parse-diff check are green.

@matthewevans matthewevans added bug Bug fix and removed test Add tests labels Jul 31, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jul 31, 2026
@matthewevans matthewevans removed their assignment Jul 31, 2026
Merged via the queue into phase-rs:main with commit 5a3a42a Jul 31, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants