Skip to content

fix(engine): require distinct players for a multi-target player requirement#6572

Open
rsnetworkinginc wants to merge 2 commits into
phase-rs:mainfrom
rsnetworkinginc:fix-scheming-symmetry-6459
Open

fix(engine): require distinct players for a multi-target player requirement#6572
rsnetworkinginc wants to merge 2 commits into
phase-rs:mainfrom
rsnetworkinginc:fix-scheming-symmetry-6459

Conversation

@rsnetworkinginc

@rsnetworkinginc rsnetworkinginc commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Choose two target players. (Scheming Symmetry) allowed choosing the same player for both slots. CR 115.1b + CR 601.2c: a single instruction that chooses two or more target players requires them to be different — the same player can't fill more than one of that instruction's target slots.

Root cause

Modal cards get distinctness from the sentence "each mode must target a different player" (ModalSelectionConstraint::DifferentTargetPlayersTargetSelectionConstraint::DifferentTargetPlayers). A plain multi-target player requirement (a multi_target over TargetFilter::Player/Opponent) had no constraint attached, so the runtime distinctness authority validate_target_constraints — which already knows how to reject duplicate player targets — never received a constraint to enforce.

Fix

A post-lowering pass (ensure_distinct_player_multi_targets, next to the existing reconcile_host_bound_phase_outs pass) attaches TargetSelectionConstraint::DifferentTargetPlayers to every non-modal ability/trigger whose multi_target requirement targets Player/Opponent and doesn't already carry it. No new enforcement code — it reuses the existing shared authority. The constraint is a no-op for a selection of fewer than two players, so attaching it whenever the requirement can select multiple players is always safe. Object multi-targets ("two target creatures") are unaffected (their distinctness is handled separately).

Tests

  • scheming_symmetry_parses_distinct_player_constraint — the lowered ability carries DifferentTargetPlayers.
  • scheming_symmetry_rejects_choosing_the_same_player_twice — end-to-end in a 3-player game: after choosing P1 for slot 1, ChooseTarget(P1) for slot 2 is rejected and the slot stays open, while choosing a different player (P2) is accepted and the spell leaves target selection.
  • RED→GREEN stash-verified: both fail without the pass (parser: target_constraints == []; runtime: the same player is accepted), pass with it.

Parity (isolated CARGO_TARGET_DIR, on 9613bcc22)

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --exclude phase-tauri --all-targets --features engine/proptest -- -D warnings: clean
  • cargo test -p engine --lib: 17590 passed / 0 failed
  • cargo test -p engine --test integration: 3878 passed / 0 failed

Closes #6459

Summary by CodeRabbit

  • Bug Fixes
    • Enforced “different players” targeting for abilities and triggers that select multiple players, ensuring the constraint is present in the lowered rules output.
    • Prevented selecting the same player in more than one target slot during multi-player targeting.
  • Tests
    • Added an integration test for “Scheming Symmetry” to validate the parsing constraint and reject duplicate-player selections in gameplay.

…rement

CR 115.1b + CR 601.2c: a single instruction that chooses two or more target
players requires them to be different — the same player can't fill more than
one of the instruction's target slots. Scheming Symmetry ("Choose two target
players.", issue phase-rs#6459) let the same player be chosen for both slots.

Modal cards attach DifferentTargetPlayers from "each mode must target a
different player"; a plain multi-target player requirement had no equivalent,
so the runtime distinctness authority (validate_target_constraints) never saw a
constraint to enforce. A post-lowering pass now attaches
TargetSelectionConstraint::DifferentTargetPlayers to every non-modal
multi-target Player/Opponent requirement, reusing the existing enforcement.

Closes phase-rs#6459
@coderabbitai

coderabbitai Bot commented Jul 23, 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: 1243befe-3991-44fa-89e2-8025945c9eaa

📥 Commits

Reviewing files that changed from the base of the PR and between 0e87a2d and b1bd781.

📒 Files selected for processing (1)
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/tests/integration/main.rs

📝 Walkthrough

Walkthrough

Oracle lowering now applies DifferentTargetPlayers to eligible multi-target player abilities and triggers. New integration tests verify Scheming Symmetry parsing and runtime enforcement of distinct player selections.

Changes

Distinct player targeting

Layer / File(s) Summary
Normalize multi-player targets
crates/engine/src/parser/oracle.rs
Oracle lowering detects multi-target abilities and triggers targeting players or opponents, adds DifferentTargetPlayers when absent, and invokes the normalization before later audits.
Validate Scheming Symmetry targeting
crates/engine/tests/integration/issue_6459_scheming_symmetry.rs, crates/engine/tests/integration/main.rs
Parser and runtime tests verify the constraint, reject duplicate second-player selections, accept distinct selections, and register the new integration module.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bug

Suggested reviewers: matthewevans

🚥 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 accurately summarizes the main change: enforcing distinct player targets for multi-target player requirements.
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/engine/src/parser/oracle.rs (1)

2884-2901: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Placement-pin ordering comment not updated for the new pass.

The preceding "PLACEMENT PIN" comment (lines 2884-2891) explicitly enumerates the ordered sequence of passes and states "Order is behavior-load-bearing"-style guarantees, but it doesn't mention ensure_distinct_player_multi_targets, inserted at line 2901 between reconcile_host_bound_phase_outs and apply_linked_choice_persisted_player. Given how heavily this file relies on this comment as the single source of truth for safe reordering, a future refactor could move this call unaware of any ordering constraint it has (or doesn't have).

📝 Suggested doc update
     // PLACEMENT PIN: the two enters-choice relations run first, then the
     // within-item `reconcile_host_bound_phase_outs` chain repair (NOT a document
-    // relation — it belongs to unit 7), then the persisted-player relation, then
-    // the swallow audit, then the two enters/attack relations — reproducing the
+    // relation — it belongs to unit 7), then `ensure_distinct_player_multi_targets`
+    // (a post-lowering normalization, not a document relation), then the
+    // persisted-player relation, then the swallow audit, then the two
+    // enters/attack relations — reproducing the
     // exact order the five standalone passes ran in (choose-counter → self-chosen
     // type → host-bound → persisted-player → swallow → etb-exile → punisher).
🤖 Prompt for 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.

In `@crates/engine/src/parser/oracle.rs` around lines 2884 - 2901, Update the
PLACEMENT PIN comment above the linked-choice passes to include
ensure_distinct_player_multi_targets in its enumerated execution order between
reconcile_host_bound_phase_outs and the persisted-player relation. Clarify its
ordering constraint or lack thereof consistently with the existing
behavior-load-bearing guidance, without changing the pass implementations.
🤖 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/parser/oracle.rs`:
- Around line 2769-2791: Expand test coverage for
ensure_distinct_player_multi_target beyond the existing fixed-count Player case:
add cases for TargetFilter::Opponent and variable/“any number of target players”
multi-target definitions, verifying distinct-target enforcement in each. Keep
the existing card scenario, and exercise the helper’s supported parameter range
directly or through representative parser fixtures.
- Around line 2743-2746: Update the rule citation in the comment above the
target-player validation to reference CR 601.2c only, removing the incorrect CR
115.1b reference while preserving the explanation of the no-duplicate-target
restriction.

---

Outside diff comments:
In `@crates/engine/src/parser/oracle.rs`:
- Around line 2884-2901: Update the PLACEMENT PIN comment above the
linked-choice passes to include ensure_distinct_player_multi_targets in its
enumerated execution order between reconcile_host_bound_phase_outs and the
persisted-player relation. Clarify its ordering constraint or lack thereof
consistently with the existing behavior-load-bearing guidance, without changing
the pass implementations.
🪄 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: 8aca0558-46f7-4a7d-8472-f3d85ebce795

📥 Commits

Reviewing files that changed from the base of the PR and between 21a53d5 and 0e87a2d.

📒 Files selected for processing (3)
  • crates/engine/src/parser/oracle.rs
  • crates/engine/tests/integration/issue_6459_scheming_symmetry.rs
  • crates/engine/tests/integration/main.rs

Comment on lines +2743 to +2746
/// CR 115.1b + CR 601.2c: a single instruction that chooses two or more target
/// PLAYERS requires them to be different — the same player can't be chosen for
/// more than one of that instruction's target slots (Scheming Symmetry, issue
/// #6459: "Choose two target players." allowed picking the same player twice).

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant source and any CR references in the repo docs.
sed -n '2730,2765p' crates/engine/src/parser/oracle.rs
printf '\n---\n'
rg -n '115\.1b|601\.2c|same target can.t be chosen multiple times|DifferentTargetPlayers' docs/MagicCompRules.txt crates/engine/tests/integration/issue_6459_scheming_symmetry.rs crates/engine/src/parser/oracle.rs

Repository: phase-rs/phase

Length of output: 3973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '[oracle.rs excerpt]'
sed -n '2738,2758p' crates/engine/src/parser/oracle.rs
echo
echo '[integration test excerpt]'
sed -n '1,140p' crates/engine/tests/integration/issue_6459_scheming_symmetry.rs
echo
echo '[repo CR references]'
rg -n 'CR 115\.1b|CR 601\.2c|DifferentTargetPlayers|same target can.t be chosen multiple times' crates/engine/src/parser/oracle.rs crates/engine/tests/integration/issue_6459_scheming_symmetry.rs docs/MagicCompRules.txt

Repository: phase-rs/phase

Length of output: 8377


🌐 Web query:

Magic Comprehensive Rules 115.1b same target can't be chosen multiple times 601.2c choose targets

💡 Result:

Under the Magic: The Gathering Comprehensive Rules, the ability to choose the same object or player as a target multiple times depends on how the word "target" is used in the card's text. The primary rule governing this is that the same target cannot be chosen multiple times for any single instance of the word "target" [1][2]. This is explicitly addressed during the process of casting a spell (or activating/triggering an ability) when targets are chosen [1]. Key takeaways from the rules: 1. Single instance of "target": If a spell or ability uses the word "target" followed by a quantity (e.g., "target two creatures"), you must choose that many distinct objects or players [1][2]. You cannot choose the same object more than once for that specific instance [1][2]. 2. Multiple instances of "target": If a spell or ability uses the word "target" in multiple separate places, you may choose the same object or player once for each instance of the word, provided it meets the targeting criteria for each [1][2]. For example, if a card says "Target creature gains flying. Target creature gains haste," you may choose the same creature for both instances [3][2]. 3. Rule References: Rule 601.2c covers the process of choosing targets when casting a spell, establishing that the same target cannot be chosen multiple times for one instance of the word "target" [1]. Rule 115 (specifically 115.3 in current rulesets) provides the broader definitions for targeting and reinforces that while you cannot repeat a target for a single instance of the word, you can do so across multiple instances [2][4]. Note that some spells may also include additional restrictions, such as the word "another," which would further prohibit choosing the same object even if multiple instances of "target" are present [4].

Citations:


Fix the rule citation Use CR 601.2c for the no-duplicate-target restriction; CR 115.1b is the wrong reference here.

🤖 Prompt for 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.

In `@crates/engine/src/parser/oracle.rs` around lines 2743 - 2746, Update the rule
citation in the comment above the target-player validation to reference CR
601.2c only, removing the incorrect CR 115.1b reference while preserving the
explanation of the no-duplicate-target restriction.

Source: Path instructions

Comment on lines +2769 to +2791
fn ensure_distinct_player_multi_target(def: &mut AbilityDefinition) {
if def.multi_target.is_none() {
return;
}
// Only player-target requirements are governed by this constraint; an
// object multi-target ("two target creatures") is kept distinct by the
// per-slot legal-set exclusion instead.
if !matches!(
def.effect.target_filter(),
Some(TargetFilter::Player | TargetFilter::Opponent)
) {
return;
}
if def
.target_constraints
.iter()
.any(|c| matches!(c, TargetSelectionConstraint::DifferentTargetPlayers))
{
return;
}
def.target_constraints
.push(TargetSelectionConstraint::DifferentTargetPlayers);
}

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Test coverage limited to one card's shape.

The pass explicitly targets both TargetFilter::Player and TargetFilter::Opponent, and the comment (lines 2755-2756) claims it's also safe for "the 'any number of target players' class", but the only added test (issue_6459_scheming_symmetry.rs) exercises a single "Player, exactly two" instance. Per this repo's engine testing guidance, a building block should be tested across its parameter range rather than through a single card case — an Opponent-filtered multi-target or a variable/"any number" multi-target case is untested here.

As per path instructions: "test the building block and its parameter range rather than a single card case."

🤖 Prompt for 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.

In `@crates/engine/src/parser/oracle.rs` around lines 2769 - 2791, Expand test
coverage for ensure_distinct_player_multi_target beyond the existing fixed-count
Player case: add cases for TargetFilter::Opponent and variable/“any number of
target players” multi-target definitions, verifying distinct-target enforcement
in each. Keep the existing card scenario, and exercise the helper’s supported
parameter range directly or through representative parser fixtures.

Source: Path instructions

@matthewevans

Copy link
Copy Markdown
Member

Right seam, right class, and a genuinely discriminating runtime test — this reuses the existing distinctness authority instead of adding enforcement, which is exactly the shape I want. Two things to fix, one of them a wrong CR citation.

Reviewed at head 0e87a2d0.

🔴 Blocker

CR 115.1b is the wrong rule, and it is cited in the load-bearing doc comment. docs/MagicCompRules.txt:843 — CR 115.1b is "Aura spells are always targeted. An Aura's target is specified by its enchant keyword ability…" That has nothing to do with player-target distinctness, and it appears both in the PR body and in the ensure_distinct_player_multi_targets doc comment, which is where a future reader will go to understand why this pass exists.

CR 601.2c is correct and is the only citation you need. Verified verbatim at :2461:

The same target can't be chosen multiple times for any one instance of the word "target" on the spell. However, if the spell uses the word "target" in multiple places, the same object or player can be chosen once for each instance of the word "target" (as long as it fits the targeting criteria).

If you want a companion citation for "what a target is" generally, that's CR 115.1 (:832), not 115.1b. Please fix the annotation in both places — a wrong CR number is worse than none, because it creates false confidence that the code was checked against a rule it was never checked against.

Worth noting the second sentence of 601.2c is also the precise justification for why this pass is scoped to a single multi_target requirement rather than across an ability's separate target slots: distinctness binds per instance of the word "target", so two separate "target player" phrases in one ability may legitimately name the same player. Your scoping already gets this right — it's just currently justified by the wrong rule.

🟡 Non-blocking

1. The gate reads only the primary effect's filter, so the pass can silently no-op. ensure_distinct_player_multi_target gates on def.effect.target_filter() matching Player | Opponent. target_filter() returns the filter of def.effect alone — there is no chained-effect traversal in it. If an ability carries a player multi_target while its primary effect's own filter is an object filter or None, the constraint is never attached and the bug reappears for that card, with no test failing. Since the doc comment claims "the 'any number of target players' class", could you confirm the gate actually fires across that class rather than just for Scheming Symmetry's effect shape?

2. One card is not a class. The doc comment claims two classes (Scheming Symmetry and "any number of target players"), but both tests are Scheming Symmetry. Given this is a blanket post-lowering pass applied to every non-modal ability and trigger, a second card from the claimed class — ideally one with a variable target count, where the "no-op below two players" reasoning actually gets exercised — would turn the class claim into evidence. Right now the "constraint is a no-op for a selection of fewer than two players" assertion in the doc is reasoning, not a tested property.

✅ Clean

  • No new enforcement code, and no new enum variant. The pass attaches the existing TargetSelectionConstraint::DifferentTargetPlayers and lets the existing runtime authority validate_target_constraints do the work. That is the correct answer to "modal cards already have distinctness, plain multi-targets don't" — extend the shared authority's reach rather than write a second checker that can drift from it.
  • Correctly scoped away from object targets. The matches!(..., Player | Opponent) gate leaves "two target creatures" to the per-slot legal-set exclusion, and the comment says so. Conflating the two would have been the easy mistake here.
  • Idempotent. The pre-existing-constraint check means re-running the pass, or an ability that already carries the constraint from the modal path, won't double-attach.
  • Placed with its peers. Running next to reconcile_host_bound_phase_outs in lower_oracle_ir puts it in the established post-lowering reconciliation slot rather than threading a special case through the parser.
  • The runtime test discriminates. scheming_symmetry_rejects_choosing_the_same_player_twice drives real GameAction::ChooseTarget calls in a 3-player game and asserts both directions — that re-selecting P1 is rejected and the slot stays open, and that P2 is accepted and selection completes. Asserting the slot stays open is the detail that makes this a real test rather than a "returns Err" check. Registered at tests/integration/main.rs, so it isn't inert.
  • RED→GREEN verification was actually performed and reported, with the failing symptom named for each test (target_constraints == [] for the parse test, same-player accepted for the runtime one). That is the evidence standard I want on every PR — thank you for including it rather than asserting "tests pass".
  • Tauri compile check red is ours, not yours — the v0.35.2 release left client/src-tauri/Cargo.lock stale against --locked. Fixed on main in fix(ci): sync client/src-tauri/Cargo.lock with the v0.35.2 version bump #6568 and permanently prevented in fix(release): keep client/src-tauri/Cargo.lock in sync on version bumps #6574; it will clear on rebase. It is the only failing job here.

Recommendation: fix the CR citation (blocker), answer #1, and this is ready to land. The architecture is the part that's hard to get right and you got it right — I'd rather have this pattern with a corrected annotation than any amount of additional machinery.

@matthewevans matthewevans mentioned this pull request Jul 23, 2026
@matthewevans

Copy link
Copy Markdown
Member

Correcting my previous comment: I called this the right seam, and it isn't. There's a better fix twenty lines away in a file this PR doesn't touch — and my CR guidance was also wrong.

The CR citation — I sent you to the wrong rule too

I said CR 115.1b was wrong (it is — it's the Aura rule, docs/MagicCompRules.txt:845) but then told you CR 601.2c was "the only citation you need" and pointed at CR 115.1 as the companion. There is a dedicated rule for this and I missed it:

CR 115.3 (docs/MagicCompRules.txt:853):

The same target can't be chosen multiple times for any one instance of the word "target" on a spell or ability. If the spell or ability uses the word "target" in multiple places, the same object or player can be chosen once for each instance of the word "target"…

This repo already standardises on CR 601.2c + CR 115.3 for exactly this rule at roughly twenty sites in game/ability_utils.rs (:993, :1041, :1320, :4609, :4875, :5074, :6463), and reserves CR 115.1b for Aura contexts. Please use CR 601.2c + CR 115.3 at all five sites — parser/oracle.rs and tests/integration/issue_6459_scheming_symmetry.rs:2, 78, 184, 198.

The seam — the existing CR 115.3 authority drops players, and that is this bug

crates/engine/src/game/ability_utils.rs:5074-5088 already implements per-instance target distinctness, and its own comment cites the rule:

// CR 601.2c + CR 115.3: within one instance of "target", the same object
// can't be chosen twice. …
let already_in_instance: HashSet<ObjectId> = prior_specs.iter().zip(selected_slots)
    .filter(|(prior, _)| prior.instance == spec.instance)
    .filter_map(|(_, sel)| match sel {
        Some(TargetRef::Object(id)) => Some(*id),
        _ => None,                                   // <-- TargetRef::Player discarded here
    })
    .collect();
legal.retain(|t| !matches!(t, TargetRef::Object(id) if already_in_instance.contains(id)));

It is narrowed to ObjectId, and TargetRef::Player falls straight through. That is the Scheming Symmetry defect, at the authority, and the TargetInstanceId doc at :993-997 compounds it by restating CR 115.3 as "must be mutually distinct objects" — contradicting the rule text it cites. Widening already_in_instance to HashSet<TargetRef> fixes all 21 affected cards there.

Four reasons that beats the parser-lowering pass:

  1. DifferentTargetPlayers is instance-blind. Its arm at ability_utils.rs:6478-6497 collapses every TargetRef::Player on the ability into one HashSet with no TargetInstanceId filtering — so an ability with a multi-target player instance plus a separate "target player" instance would reject a choice CR 115.3 expressly permits. Latent rather than live today, but you'd be attaching a correct rule to a checker that can't express it, when the instance-aware one is right there.
  2. Parser-only reach. The pass runs inside lower_oracle_ir; abilities from any other builder never receive the constraint. The runtime filter has no such hole.
  3. Inert until card-data regen. target_constraints is a serialized AbilityDefinition field shipped in card-data.json. A fix at :5088 is live immediately with zero card-data churn.
  4. The repo has already asked for this primitive by name. crates/mtgish-import/src/convert/action.rs:921-931 strict-fails an import shape with: "The engine currently models only the player-specific modal constraint (DifferentTargetPlayers)… Strict-fail until the engine has a general object/player distinctness primitive." Widening line 5088 is that primitive. This PR entrenches the special case the comment is waiting to retire.

There's also a live asymmetry worth knowing: the stateless validate_target_prefix family (:1510-1534) checks only static slot.legal_targets plus validate_target_constraints, never legal_targets_for_selected_slot. After this PR, players are distinct there but objects are not; inside legal_targets_for_selected_slot, objects are distinct but players are not. One CR rule, two mechanisms, two different path sets.

And it's not a selection-only fix. Both paths route through legal_targets_for_selected_slot — selection via build_target_selection_progress:5346legal_targets_for_slot:5480, validation via casting_targets.rs:306 / engine_stack.rs:161validate_selected_slots_with_specs:5768, which re-derives each slot's legal set at :5792-5806. One edit covers both.

Blast radius is 21 cards, not one

Measured against data/card-data.json using your pass's own gate — every ability/trigger.execute with multi_target.is_some() and a top-level Player/Opponent effect target — this changes 23 sites across 21 cards, all currently carrying target_constraints: []: Cabal Conditioning, Court of Cunning, Deceptive Divination, Endurance, Huddle Up, Jace Memory Adept, Kaboom!, Memory Vampire, Officious Interrogation, Priest of Forgotten Gods, Ral Zarek Guest Lecturer, Riverchurn Monument ×2, Scheming Symmetry, Stonespeaker Crystal, The Brothers' War, The Death of Gwen Stacy, Thraben Charm, Tinybones Joins Up ×2, Torgaar Famine Incarnate, Veteran Ice Climber, Your Plans Mean Nothing.

The class is correct — this is not build-for-the-card, and I want to be clear about that. It's just undeclared, and worth enumerating in the description.

Related: eleven of those 23 are multi_target { min: 0, max: None } ("any number of target players"), a different resolve_multi_target_bounds arm with optional trailing slots. Scheming Symmetry is { min: 2, max: Fixed(2) }, so no fixture reaches that arm.

Also non-blocking

  • The new functions hijack lower_oracle_ir's doc comment. parser/oracle.rs:2726-2741 is a 15-line block owned by lower_oracle_ir at :2742; your two functions are inserted between them, so that block now documents ensure_distinct_player_multi_targets and lower_oracle_ir is left undocumented. fmt and clippy can't see this. Move the new functions above :2726 or below lower_oracle_ir.
  • TargetSelectionConstraint::DifferentTargetPlayers (types/game_state.rs:6192) is the only variant in its enum with no doc and no CR reference. Whichever seam this lands at, that's worth adding.

What stands from my last comment

The runtime test is genuinely discriminating and I'd keep it exactly as written — real CastSpellChooseTarget in a 3-player game, asserting rejection, that the slot stays open, and that a different player completes selection. That last clause rules out the dead-slot false positive, which is the part most people miss. Against the relocated fix it should pass unchanged, cover the object case for free, and produce an empty parse-diff — which would be the strongest evidence available.

Oracle text is verbatim-correct against Scryfall, no new enum variant, no new bool, no parser string-matching. Tauri compile check red is ours.


Revised recommendation: move the fix to ability_utils.rs:5088 — widen already_in_instance from HashSet<ObjectId> to HashSet<TargetRef> and drop the parser pass — and cite CR 601.2c + CR 115.3. My earlier "right seam" call was wrong: you are reusing existing machinery, which is why it looked right to me, but it's the instance-blind checker rather than the instance-aware one that already claims this rule and fails to finish it.

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

Resolves the deterministic crates/engine/tests/integration/main.rs mod-line
conflict by keeping both test modules in rustfmt-sorted order. Maintainer-side
port: main advanced under this PR (phase-rs#6498 landed a sibling integration test).

@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 behavior is sound, but the new CR annotation is not.

🔴 Blocker

[MED] Correct the duplicate-target citation at crates/engine/src/parser/oracle.rs:2743. CR 115.1b concerns Aura targeting; CR 601.2c is the rule that says the same target cannot be chosen multiple times for one instance of “target.” Cite CR 601.2c alone here and in matching new comments/tests.

Recommendation: request changes; make the local CR-citation correction and re-request review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

scheming symmetry lets you target the same player both times. — [[scheming symmetry]] I feel like this has happened wit…

2 participants