Skip to content

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

Open
rsnetworkinginc wants to merge 3 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 3 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 601.2c + CR 115.3: the same target — object or player — can't be chosen multiple times for any one instance of the word "target", so 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).

Update (post-review)

Per review, the fix now lives at the instance-aware distinctness authority in game/ability_utils.rs (legal_targets_for_selected_slot): already_in_instance is widened from HashSet<ObjectId> to HashSet<TargetRef>, so within one instance of "target" both objects and players stay distinct (CR 601.2c + CR 115.3), while separate instances may still reuse the same object or player. The parser-lowering pass and its parse-level test are dropped, restoring lower_oracle_ir untouched — the parse-diff for this head is empty. The relocated fix covers the ~21-card multi-target-player class enumerated in review with zero card-data churn, and both the selection and validation paths route through this helper.

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

    • Prevented the same player from being selected more than once when a spell requires distinct player targets.
    • Target selection now correctly remains active after an invalid duplicate choice and completes once distinct players are selected.
  • Documentation

    • Clarified target-selection behavior and ownership constraint descriptions.

…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

📝 Walkthrough

Walkthrough

The change extends per-target-instance distinctness checks from objects to all target references, updates related documentation, and adds an integration test verifying Scheming Symmetry rejects duplicate player targets while accepting distinct players.

Changes

Distinct player targeting

Layer / File(s) Summary
Target selection semantics
crates/engine/src/game/ability_utils.rs, crates/engine/src/types/game_state.rs
Target selection filters duplicate TargetRef values within an instance, and related target constraint documentation is updated without changing the data shape.
Scheming Symmetry regression coverage
crates/engine/tests/integration/issue_6459_scheming_symmetry.rs, crates/engine/tests/integration/main.rs
The integration harness registers a three-player test that rejects selecting the same player twice and completes after selecting two distinct players.

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

Suggested labels: bug

Suggested reviewers: matthewevans, lgray

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the core fix: enforcing distinct player targets for a multi-target player requirement in the engine.
✨ 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 thread crates/engine/src/parser/oracle.rs Outdated
Comment thread crates/engine/src/parser/oracle.rs Outdated
@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.

Relocate the issue phase-rs#6459 fix from a parser-lowering pass to the existing
per-instance distinctness authority in legal_targets_for_selected_slot
(CR 601.2c + CR 115.3): `already_in_instance` was narrowed to
HashSet<ObjectId>, so TargetRef::Player fell through and the same player
could fill two slots of one instance of "target" (Scheming Symmetry,
"Choose two target players."). Widening it to HashSet<TargetRef> keeps
objects AND players distinct within an instance while still permitting
reuse across separate instances, and covers both the selection and
validation paths, which route through the same helper.

- drop the ensure_distinct_player_multi_targets parser pass and its
  parse-level test: the runtime authority now enforces the rule with no
  card-data churn, so the parse-diff is empty
- correct the CR citation from CR 115.1b (the Aura rule) to the repo's
  standard CR 601.2c + CR 115.3 pair at every site
- fix the TargetInstanceId doc, which restated CR 115.3 as "mutually
  distinct objects" — the rule covers objects or players
- document TargetSelectionConstraint::DifferentTargetPlayers (modal
  path) with its CR reference

The runtime regression test (real CastSpell -> ChooseTarget in a
3-player game: same player rejected, slot stays open, different player
completes) is unchanged and passes against the relocated fix.
@rsnetworkinginc

Copy link
Copy Markdown
Contributor Author

Done as recommended — the fix is relocated to the instance-aware distinctness authority, and thanks for the correction on both the seam and the rule.

  • legal_targets_for_selected_slot: already_in_instance widened from HashSet<ObjectId> to HashSet<TargetRef>, so prior slots of the same instance now exclude players as well as objects. Cross-instance reuse (the CR 601.2c Example) is untouched, and both the selection and validation paths pick it up since they route through this helper.
  • The parser-lowering pass (ensure_distinct_player_multi_targets) and its parse-level test are dropped; parser/oracle.rs is restored to base, so lower_oracle_ir keeps its doc comment and the parse-diff for this head is empty (git diff origin/main...HEAD -- crates/engine/src/parser/oracle.rs is zero lines). No card-data churn — the widening is live for the whole ~21-card class you enumerated.
  • Citations corrected to the repo-standard CR 601.2c + CR 115.3 at every site: the widened filter, the test's module doc and both in-test sites, and the PR body. The TargetInstanceId doc no longer restates the rule as objects-only, and TargetSelectionConstraint::DifferentTargetPlayers now carries a doc with its CR reference.
  • The runtime test is kept exactly as written and passes unchanged against the relocated fix. RED→GREEN re-verified at this head: with only the widening stashed, ChooseTarget(P1) for the second slot is accepted and the test fails at the rejection assert; with it applied, rejection, slot-stays-open, and P2-completes all hold.
  • Full cargo test -p engine --test integration: 3909 passed / 0 failed; cargo test -p engine --lib: 17604 passed / 0 failed. cargo fmt --all -- --check clean (fmt collapsed the neighbouring SameZoneOwner variant when the enum gained the doc); check-engine-authorities.sh and check-parser-combinators.sh both pass on the new head.

@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.

Caution

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

⚠️ Outside diff range comments (2)
crates/engine/tests/integration/issue_6459_scheming_symmetry.rs (2)

80-112: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Add coverage for reuse across separate target instances.

This test only proves distinctness within one target instance. An implementation that incorrectly carries the duplicate set into a later target instance would still pass. Add an end-to-end case where the same player is legally selected in two separate target instances and both selections complete.

As per path instructions, tests should exercise the building block’s parameter range; the PR objective also requires reuse across separate instances.

🤖 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/tests/integration/issue_6459_scheming_symmetry.rs` around lines
80 - 112, Extend the integration test around the existing target-selection flow
to create and complete a second target instance after the first resolves,
selecting the same player in both instances. Assert both selections succeed and
each instance completes, demonstrating duplicate tracking is scoped per target
instance rather than carried across instances.

Source: Path instructions


80-104: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the duplicate-rejection assertion prove it reaches the second slot.

The guard before reselect_same.is_err() only proves that target selection is active; it does not prove that the runner advanced to the second slot or that this is an otherwise valid player-target path. An unrelated validation failure could therefore make this regression pass. Assert the active slot and a valid alternative candidate such as P2 before attempting to reselect P1, then retain the existing post-error state assertion.

As per path instructions, negative assertions require a positive reach-guard proving the input reached the code under test.

🤖 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/tests/integration/issue_6459_scheming_symmetry.rs` around lines
80 - 104, Strengthen the precondition in the target-selection test before
reselect_same.is_err(): inspect WaitingFor::TargetSelection and assert it
represents the second target slot with P2 as a valid alternative candidate,
rather than only confirming target selection is active. Then keep the existing
duplicate-P1 rejection and post-error waiting-state assertions unchanged.

Source: Path instructions

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

Outside diff comments:
In `@crates/engine/tests/integration/issue_6459_scheming_symmetry.rs`:
- Around line 80-112: Extend the integration test around the existing
target-selection flow to create and complete a second target instance after the
first resolves, selecting the same player in both instances. Assert both
selections succeed and each instance completes, demonstrating duplicate tracking
is scoped per target instance rather than carried across instances.
- Around line 80-104: Strengthen the precondition in the target-selection test
before reselect_same.is_err(): inspect WaitingFor::TargetSelection and assert it
represents the second target slot with P2 as a valid alternative candidate,
rather than only confirming target selection is active. Then keep the existing
duplicate-P1 rejection and post-error waiting-state assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a382d493-9b7f-4318-92e4-5ba7e638fcdd

📥 Commits

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

📒 Files selected for processing (3)
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/tests/integration/issue_6459_scheming_symmetry.rs

@matthewevans matthewevans self-assigned this Jul 25, 2026
@matthewevans matthewevans added the bug Bug fix label Jul 25, 2026
@matthewevans

Copy link
Copy Markdown
Member

Maintainer acknowledgement: rechecked current head b2f8472 after the latest follow-up. This remains blocked on a maintainer merge of current main and fresh CI; the authorized fork-push GH_TOKEN needed to publish that merge is unavailable. No further author action is requested.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer handling update: current-head review confirmed the implementation seam and runtime behavior, but CodeRabbit correctly identified that the new negative assertion needs a second-slot/alternative-target reach guard. This is a maintainer-fixup-sized test-only edit; no contributor action is requested. The authorized handler cannot publish it because the fork-push credential (GH_TOKEN) is unavailable in this session. The PR remains held until a maintainer with that credential adds the guard, waits for fresh CI, and re-approves on the resulting head.

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.

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

2 participants