Skip to content

fix(engine): repeated Choose(Opponent)/Choose(Player) picks are independent by default (#6381) - #6747

Merged
matthewevans merged 4 commits into
phase-rs:mainfrom
jsdevninja:fix/6381-benevolent-offering-repeat-opponent
Jul 29, 2026
Merged

fix(engine): repeated Choose(Opponent)/Choose(Player) picks are independent by default (#6381)#6747
matthewevans merged 4 commits into
phase-rs:mainfrom
jsdevninja:fix/6381-benevolent-offering-repeat-opponent

Conversation

@jsdevninja

@jsdevninja jsdevninja commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #6381. Benevolent Offering's two independent "Choose an opponent." instructions unconditionally excluded a player already chosen earlier in the same resolution — correct for Gluntch, the Bestower's ordinal-cued "choose a second/third player", but wrong here. The official ruling for the whole "Offering" cycle (Benevolent/Infernal/Intellectual/Sylvan Offering) is explicit:

You may choose the same opponent for each of the effects, or you may choose different opponents.

In a two-player game the old behavior made the second "Choose an opponent." impossible (CR 609.3 no-op), silently dropping the chosen opponent's life gain — the observable "wrong game result" behind the bug report.

  • Adds PlayerChoiceDistinctness (mirrors the existing NumberRange.distinctness axis) to ChoiceType::Player/ChoiceType::Opponent. Default Independent never excludes prior choices; DistinctFromPriorChoices preserves Gluntch's "three distinct players" behavior. The parser sets the latter from the "second"/"third" ordinal it already scans for (previously discarded as a "consistency hint only").
  • game/effects/choose.rs::compute_options only consults chosen_players (the exclusion set) when distinctness == DistinctFromPriorChoices.

While building a runtime regression test for the actual card, found and fixed two related recipient-binding gaps that also made Benevolent Offering resolve incorrectly (both are class-level fixes, not single-card patches):

  • The "you and that player each ..." compound-subject splitter bound "that player" to the vote/fan-out ScopedPlayer axis instead of the resolution-scoped chosen player from a preceding Choose(Opponent), so both Spirit-token creation effects silently went to the caster.
  • GainLife's subject-injection pass never rebound "that player gains N life" (plain or "for each") away from its no-subject Controller default, so the second life-gain clause also credited the caster instead of the chosen opponent.

Parse-diff: two other cards' signatures change, both accounted for

CI's parse-diff artifact (run 30401472552 / artifact 8705074336) flags two more cards whose parsed shape changes. Both are direct, intended consequences of the two class-level fixes above — not incidental blast radius:

  • Intellectual OfferingChoose an opponent. You and that player each draw three cards. / Choose an opponent. Untap all nonland permanents you control and all nonland permanents that player controls. Same "Choose an opponent. You and that player each ..." shape as Benevolent Offering's first clause, so it goes through the same try_parse_compound_subject_each fix: the second Draw.target now binds to ChosenPlayer{index: 0} instead of ScopedPlayer. Intended — it's the same "Offering" cycle bug, on the same code path. Covered by the new test intellectual_offering_second_draw_binds_to_chosen_opponent.
  • Angel of DestinyWhenever a creature you control deals combat damage to a player, you and that player each gain that much life. This also hits the compound-subject splitter, but GainLife isn't wired into rewrite_recipient_on_link, so the split fails and the clause falls through to a single GainLife node with an unresolved "you and that player" subject. The first version of this PR's inject_subject_target GainLife arm blindly accepted that fallback subject, corrupting GainLife.player from the no-subject Controller default to TargetFilter::Any — a real regression, not an intended change. Fixed: the arm is now gated on target_filter_can_target_player (mirroring the existing thread_for_each_subject GainLife arm), so an unresolvable compound subject leaves the safe Controller default alone. The damaged-player life gain is a separate, pre-existing gap (unrelated to Benevolent offering allows a player to select themselves as the opponent. — [[Benevolent offering]] #6381) — not fixed here, just no longer made worse. Covered by the new regression test angel_of_destiny_combat_damage_gain_life_keeps_well_defined_recipient.

No other cards changed shape.

Test plan

  • New unit tests in game/effects/choose.rs covering Independent (repeat allowed) vs DistinctFromPriorChoices (Gluntch-style exclusion) for both ChoiceType::Player and ChoiceType::Opponent.
  • Updated gluntch_choose_player_chain_parses_with_chosen_player_scopes snapshot test to assert the ordinal-cued picks parse as DistinctFromPriorChoices and the plain first pick as Independent.
  • New end-to-end integration test issue_6381_benevolent_offering_repeat_opponent.rs: casts the real Oracle text, drives both Choose(Opponent) prompts to the same opponent, and asserts the token split (3/3) and life gain (+6/+6) are both correct.
  • Same file: intellectual_offering_second_draw_binds_to_chosen_opponent (names and locks in the intended parse-diff card) and angel_of_destiny_combat_damage_gain_life_keeps_well_defined_recipient (regression guard for the fixed-in-review GainLife/Any bug).
  • cargo fmt --all, cargo clippy -p engine --tests -- -D warnings clean.
  • Full cargo test -p engine --lib (17918 passed) and cargo test -p engine --test integration (4166 passed) green.
  • cargo check -p mtgish-import -p phase-ai --tests clean (both crates construct ChoiceType::Opponent/ChoiceType::Player via the new helper constructors); ChooseAPlayer CR citation corrected (CR 800.4a → CR 102.1-102.3 + CR 608.2d) and CodeRabbit thread r3669445472 resolved.

Summary by CodeRabbit

  • New Features

    • Added distinctness-aware “choose a player/opponent” behavior, including ordinal “choose a second/third player” prompts that can require selections to be distinct from prior picks.
  • Bug Fixes

    • Fixed “Independent” choice repeat handling so repeating a previously chosen opponent is allowed when the rules permit.
    • Improved resolution and anaphor targeting for chosen-player interactions (including related life/damage behaviors) to keep persisted and random outcomes consistent.
  • Tests

    • Updated and expanded regression and snapshot coverage for distinctness, offering repeat behavior, and affected rule interactions.

…endent by default (phase-rs#6381)

Benevolent Offering's two "Choose an opponent." instructions unconditionally
excluded a player already chosen earlier in the resolution, which is only
correct for Gluntch, the Bestower's ordinal-cued "choose a second/third
player." Per the "Offering" cycle ruling ("You may choose the same opponent
for each of the effects, or you may choose different opponents"), a bare
repeated choose must allow repeating an earlier pick. In a two-player game
the old behavior made the second choice impossible, silently dropping the
chosen opponent's life gain.

Adds `PlayerChoiceDistinctness` (mirroring `NumberRange`'s `distinctness`
axis) so `ChoiceType::Player`/`ChoiceType::Opponent` only exclude prior
choices when explicitly ordinal-cued. The parser sets it from the "second"/
"third" ordinal it already scans for.

Also fixes two related recipient-binding gaps this card exposed: the
"you and that player each ..." compound-subject splitter bound "that
player" to the unrelated vote-fanout `ScopedPlayer` axis instead of the
resolution-scoped chosen player, and `GainLife`'s subject-injection pass
never rebound "that player gains N life" (for-each and plain) away from
its no-subject `Controller` default.
@jsdevninja
jsdevninja requested a review from matthewevans as a code owner July 28, 2026 21:38
@coderabbitai

coderabbitai Bot commented Jul 28, 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: 7937a93e-2fb4-45ad-8d09-a564b1c487c0

📥 Commits

Reviewing files that changed from the base of the PR and between 23d568e and ce62f4e.

📒 Files selected for processing (4)
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_replacement.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/engine/src/parser/oracle_replacement.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/types/ability.rs

📝 Walkthrough

Walkthrough

The PR adds distinctness metadata to player and opponent choices, updates parsing and resolution behavior, preserves legacy serialization, and adds regression coverage for repeated independent opponent choices and chosen-player target binding.

Changes

Player choice distinctness

Layer / File(s) Summary
ChoiceType contract and serialization
crates/engine/src/types/ability.rs
Adds distinctness metadata, constructors, compatibility mappings, and legacy-preserving serde behavior.
Oracle parsing and chosen-player binding
crates/engine/src/parser/oracle*.rs
Maps ordinal choices to distinctness and updates chosen-player and gain-life target binding.
Choice resolution and compatibility wiring
crates/engine/src/game/..., crates/mtgish-import/src/convert/*, crates/engine/src/database/synthesis.rs
Allows repeated independent selections, excludes prior selections when required, and updates structured variant matching and construction.
Regression and integration validation
crates/engine/src/parser/oracle_effect/*tests.rs, crates/engine/tests/integration/*
Verifies distinctness parsing, serialization shapes, repeated opponent selection, and recipient binding.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OracleText
  participant OracleParser
  participant ChoiceType
  participant ChooseResolver
  participant GameState
  OracleText->>OracleParser: Parse player or opponent choice
  OracleParser->>ChoiceType: Set distinctness from ordinal
  ChoiceType->>ChooseResolver: Provide structured choice type
  ChooseResolver->>GameState: Filter prior choices or allow repeats
  GameState-->>ChooseResolver: Return legal options
Loading

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 clearly and accurately summarizes the main change: default-independent repeated Choose(Opponent)/Choose(Player) handling.
Linked Issues check ✅ Passed The changes address the reported Benevolent Offering bug by allowing repeated opponent/player choices independently and covering the card in tests.
Out of Scope Changes check ✅ Passed The additional parser, binding, and test updates all support the same choice-distinctness fix and related regressions.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@matthewevans

matthewevans commented Jul 28, 2026

Copy link
Copy Markdown
Member

Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main 309d7c49f28f)

🟡 Modified fields (1 signature)

  • 1 card · 🔄 ability/Draw · changed field target: scoped playerchosen player
    • Affected (first 3): Intellectual Offering

2 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@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: 1

🧹 Nitpick comments (1)
crates/engine/src/types/ability.rs (1)

738-770: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a symmetric opponent_distinct_from_prior() builder.

ChoiceType::Opponent.distinctness is documented to participate in the same "exclude players already chosen by an earlier Opponent/Player choice" semantics as Player.distinctness (see the doc comment on the Opponent variant, lines 615-619), but only Player gets a player_distinct_from_prior() builder + round-trip test. If another layer (parser/synthesis) ever needs an ordinal-cued distinct opponent pick, it will have to hand-construct the struct literal instead of using a documented builder, and this leaf stays untested here.

♻️ Suggested addition
     pub fn player_distinct_from_prior() -> Self {
         Self::Player {
             distinctness: PlayerChoiceDistinctness::DistinctFromPriorChoices,
         }
     }
+
+    /// Ordinal-cued "choose a second/third opponent": must exclude players
+    /// already chosen earlier in this resolution.
+    pub fn opponent_distinct_from_prior() -> Self {
+        Self::Opponent {
+            restriction: None,
+            distinctness: PlayerChoiceDistinctness::DistinctFromPriorChoices,
+        }
+    }
🤖 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/types/ability.rs` around lines 738 - 770, Add an
opponent_distinct_from_prior() builder alongside player_distinct_from_prior(),
returning ChoiceType::Opponent with no restriction and
PlayerChoiceDistinctness::DistinctFromPriorChoices. Add the corresponding
round-trip test using the existing test pattern for
player_distinct_from_prior().
🤖 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/mtgish-import/src/convert/replacement.rs`:
- Around line 1871-1875: Update the surrounding documentation for the
ChooseAPlayer conversion near players_to_controller and ChoiceType to replace
the incorrect CR 800.4a citation with the verified player/opponent choice
references: CR 102.1, CR 102.2, and CR 608.2d.

---

Nitpick comments:
In `@crates/engine/src/types/ability.rs`:
- Around line 738-770: Add an opponent_distinct_from_prior() builder alongside
player_distinct_from_prior(), returning ChoiceType::Opponent with no restriction
and PlayerChoiceDistinctness::DistinctFromPriorChoices. Add the corresponding
round-trip test using the existing test pattern for
player_distinct_from_prior().
🪄 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: f0291d07-526a-4d11-aefb-7313b4adbcaf

📥 Commits

Reviewing files that changed from the base of the PR and between 8a034e7 and 1fe12d8.

📒 Files selected for processing (22)
  • crates/engine/src/database/synthesis.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/effects/choose.rs
  • crates/engine/src/game/engine_resolution_choices.rs
  • crates/engine/src/game/triggers_ordering_parity_tests.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/snapshot_tests.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_replacement.rs
  • crates/engine/src/parser/oracle_vote.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/tests/integration/baleful_mastery_regression.rs
  • crates/engine/tests/integration/gollum_scheming_guide_card_predicate_guess.rs
  • crates/engine/tests/integration/issue_564_wishclaw_talisman_control.rs
  • crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/rules/tribute.rs
  • crates/engine/tests/integration/the_who_opponent_guess_resolution.rs
  • crates/mtgish-import/src/convert/action.rs
  • crates/mtgish-import/src/convert/replacement.rs

Comment thread crates/mtgish-import/src/convert/replacement.rs

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

[MED] Reconcile the full current-head parse-diff with the claimed scope. Evidence: CI run 30401472552, artifact 8705074336, reports two modified signatures: Intellectual Offering's Draw.target changed from scoped player to chosen player, and Angel of Destiny's GainLife.player changed from to any target; the PR description does not name either card or give an exact matching count/card set. Why it matters: these are behavior-affecting parser changes outside the stated Benevolent Offering/choice-distinctness narrative, so the review cannot distinguish intended class coverage from unintended parser blast radius. Suggested fix: explicitly name and justify both card-level changes as intended (including why they belong to this PR), or narrow the implementation so the artifact matches the stated scope.

[MED] Correct the CR annotation on ChooseAPlayer and resolve the existing CodeRabbit thread. Evidence: crates/mtgish-import/src/convert/replacement.rs:1867 annotates the player/opponent choice conversion with CR 800.4a; the locally verified Comprehensive Rules text defines 800.4a as the procedure when a player leaves the game. CR 102.1–102.3 define players, opponents, and teammates, while CR 608.2d governs choices made while resolving an effect. Why it matters: an incorrect CR reference gives false rules provenance to engine conversion logic and leaves a confirmed external review finding unresolved. Suggested fix: replace the annotation with the verified, scope-appropriate references (for example, CR 102.1–102.3 + CR 608.2d) or another directly applicable verified citation, then resolve CodeRabbit discussion r3669445472.

The required CI checks and the parse-diff technical gate are green on this head, but I cannot approve while these two substantive review items remain open.

Review of the parse-diff artifact on PR phase-rs#6747 found two card-level
signature changes beyond the stated scope:

- Intellectual Offering's second Draw now binds to ChosenPlayer{index: 0}
  instead of ScopedPlayer — intended: it shares Benevolent Offering's
  "Choose an opponent. You and that player each <body>." shape, so it
  exercises the same try_parse_compound_subject_each fix. Locked in with
  a new test.

- Angel of Destiny's GainLife.player changed from the no-subject
  Controller default to TargetFilter::Any — a real regression. "You and
  that player each gain that much life" is a compound subject that
  GainLife isn't wired into in rewrite_recipient_on_link (Token/Draw/
  Discard/Mill/Pump/GenericEffect only), so it falls through to a
  non-player-denoting subject filter; the new inject_subject_target arm
  blindly accepted it. Gated the arm on target_filter_can_target_player
  (mirroring the existing thread_for_each_subject GainLife arm) so an
  unresolved compound subject leaves the safer Controller default alone.
  Locked in with a regression test.

Also corrects a pre-existing wrong CR citation (CR 800.4a, which governs
a player leaving a multiplayer game) on the ChooseAPlayer replacement
conversion touched by this PR, to CR 102.1-102.3 + CR 608.2d — the
player/opponent + in-resolution-choice rules that actually apply.
Resolves CodeRabbit thread r3669445472.

@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: 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/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs`:
- Around line 166-217: Replace the AST-only assertions in
crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs:166-217
with a production-pipeline integration test that casts and resolves Intellectual
Offering, chooses an opponent, and verifies that opponent draws exactly three
cards; update the Angel of Destiny regression at
crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs:231-267
to deal combat damage and assert both the Angel’s controller and damaged player
gain that amount. Ensure both tests exercise runtime resolution rather than
inspecting parsed Effect trees.
🪄 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: 8fc4ac2e-b6c6-45da-adb8-a05d333571a1

📥 Commits

Reviewing files that changed from the base of the PR and between 1fe12d8 and ecf9daa.

📒 Files selected for processing (3)
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs
  • crates/mtgish-import/src/convert/replacement.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/mtgish-import/src/convert/replacement.rs
  • crates/engine/src/parser/oracle_effect/mod.rs

Comment on lines +166 to +217
fn intellectual_offering_second_draw_binds_to_chosen_opponent() {
let mut scenario = GameScenario::new();
scenario.at_phase(Phase::PreCombatMain);
let spell = scenario
.add_spell_to_hand_from_oracle(
P0,
"Intellectual Offering",
true,
"Choose an opponent. You and that player each draw three cards.\nChoose an opponent. Untap all nonland permanents you control and all nonland permanents that player controls.",
)
.id();
let runner = scenario.build();
let ability = &runner.state().objects.get(&spell).unwrap().abilities[0];
assert!(
matches!(
ability.effect.as_ref(),
Effect::Choose {
choice_type: ChoiceType::Opponent { .. },
..
}
),
"head must be Choose(Opponent), got {:?}",
ability.effect
);
let first_draw = ability.sub_ability.as_ref().expect("first Draw node");
assert!(
matches!(
first_draw.effect.as_ref(),
Effect::Draw {
target: TargetFilter::OriginalController,
..
}
),
"the caster's draw must target OriginalController, got {:?}",
first_draw.effect
);
let second_draw = first_draw.sub_ability.as_ref().expect("second Draw node");
assert!(
matches!(
second_draw.effect.as_ref(),
Effect::Draw {
target: TargetFilter::Typed(TypedFilter {
controller: Some(ControllerRef::ChosenPlayer { index: 0 }),
..
}),
..
}
),
"the chosen opponent's draw must bind to ChosenPlayer{{index: 0}}, not ScopedPlayer, got {:?}",
second_draw.effect
);
}

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Drive these recipient-binding regressions through resolution. Both tests only inspect parsed Effect trees, so they can pass while runtime target evaluation ignores ChosenPlayer or while Angel of Destiny still grants life solely to its controller—the latter is explicitly accepted by this test despite the card requiring both players to gain life.

  • crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs#L166-L217: cast Intellectual Offering, choose an opponent, resolve it, and assert that opponent draws exactly three cards.
  • crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs#L231-L267: produce combat damage to a player and assert both the Angel’s controller and the damaged player gain the damage amount.

As per path instructions, parser AST tests do not establish runtime semantics; integration regressions must use the production resolution pipeline.

📍 Affects 1 file
  • crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs#L166-L217 (this comment)
  • crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs#L231-L267
🤖 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_6381_benevolent_offering_repeat_opponent.rs`
around lines 166 - 217, Replace the AST-only assertions in
crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs:166-217
with a production-pipeline integration test that casts and resolves Intellectual
Offering, chooses an opponent, and verifies that opponent draws exactly three
cards; update the Angel of Destiny regression at
crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs:231-267
to deal combat damage and assert both the Angel’s controller and damaged player
gain that amount. Ensure both tests exercise runtime resolution rather than
inspecting parsed Effect trees.

Source: Path instructions

@matthewevans matthewevans self-assigned this Jul 29, 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.

1 remaining blocker on the current head.

🔴 Blocker

[MED] Add a production-pipeline regression for Intellectual Offering’s newly changed draw recipient. Evidence: crates/engine/tests/integration/issue_6381_benevolent_offering_repeat_opponent.rs:166-217 only inspects the parsed Effect::Draw shape, while the behavior enters game/effects/draw.rs:139-194 and resolves ChosenPlayer at game/effects/mod.rs:6143-6154. The existing Benevolent Offering scenario proves repeated choice plus token/life handling, but it does not execute the Draw resolver. Why it matters: this PR’s current parse-diff intentionally changes Intellectual Offering from ScopedPlayer to ChosenPlayer; an AST-only assertion would remain green if runtime resolution drew for the wrong player. Suggested fix: cast and resolve the real Intellectual Offering text with seeded libraries, choose the opponent, and assert that opponent draws three cards.

✅ Resolved

The current parse-diff now contains only Intellectual Offering, matching the updated scope. The previous Angel of Destiny signature is gone, and CR 102.1-102.3 + CR 608.2d is verified against the local Comprehensive Rules; the prior CR 800.4a citation was unrelated and has been corrected.

Recommendation: request changes for the focused Intellectual Offering runtime regression; no Angel of Destiny behavior expansion is requested in this PR.

@matthewevans matthewevans removed their assignment Jul 29, 2026
The existing intellectual_offering_second_draw_binds_to_chosen_opponent
test only inspects the parsed Effect::Draw shape; it stays green even if
runtime resolution drew for the wrong player, since it never executes
game/effects/draw.rs's ChosenPlayer resolution.

Adds intellectual_offering_draws_three_for_caster_and_chosen_opponent,
which casts the real Oracle text with seeded libraries and mana, drives
both Choose(Opponent) prompts to the same opponent (proving the repeated-
choice fix along the way), and asserts both the caster and the chosen
opponent actually draw three cards through the production cast/resolve
pipeline. Keeps the original AST-shape test as a companion SHAPE
assertion per the card-test skill.
@jsdevninja

Copy link
Copy Markdown
Contributor Author

Added a runtime regression in 23d568e: `intellectual_offering_draws_three_for_caster_and_chosen_opponent` casts the real Oracle text with seeded libraries (`{4}{U}`, 5 library cards per side), drives both `Choose(Opponent)` prompts to the same opponent through the production cast/resolve pipeline, and asserts both the caster and the chosen opponent actually draw three cards — exercising `game/effects/draw.rs`'s `ChosenPlayer` resolution end to end, not just the parsed AST. The prior AST-only test is kept alongside it as a SHAPE assertion (per the `card-test` skill) pinning the exact parser output.

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

Copy link
Copy Markdown
Member

Review update — hold on current head

The requested Intellectual Offering regression is now satisfied on 23d568e71: the test casts the real spell, drives both opponent choices through GameAction::ChooseOption, and asserts each player's three-card draw after resolution. It reaches the ChosenPlayer recipient path in game/effects/draw.rs, so it would fail if the parser binding were reverted.

The remaining unresolved CodeRabbit request to expand Angel of Destiny is not a blocker for this PR: the current parse-diff contains only Intellectual Offering's Draw.target change, and the Angel behavior is explicitly a pre-existing, unmodified gap. It needs a separately scoped engine/rules change rather than silently expanding this fix.

Holding rather than approving while the current-head Rust gate is still running and the branch is behind main. I will re-evaluate the exact current head after those merge-readiness signals settle.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer update — current-head hold

The branch was cleanly updated through current main at ce62f4e712; the reviewed contributor diff remains unchanged and has no textual conflict. Required checks restarted for this new merge head. Holding approval only until those current-head checks settle; the next action is approval/enqueue if they remain green.

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

Reviewed current head ce62f4e. The choice-distinctness model and targeted Intellectual Offering coverage are sound; the current-head CI and CodeRabbit checks are successful. Approving and enabling auto-merge.

@matthewevans
matthewevans added this pull request to the merge queue Jul 29, 2026
@matthewevans matthewevans removed their assignment Jul 29, 2026
Merged via the queue into phase-rs:main with commit 4f2ff6e Jul 29, 2026
15 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.

Benevolent offering allows a player to select themselves as the opponent. — [[Benevolent offering]]

2 participants