Skip to content

fix(engine): recognize the symmetric two-way control-swap idiom, and keep it simultaneous#6103

Open
tryeverything24 wants to merge 2 commits into
phase-rs:mainfrom
tryeverything24:fix/issue-4731
Open

fix(engine): recognize the symmetric two-way control-swap idiom, and keep it simultaneous#6103
tryeverything24 wants to merge 2 commits into
phase-rs:mainfrom
tryeverything24:fix/issue-4731

Conversation

@tryeverything24

@tryeverything24 tryeverything24 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #4731.

Reins of Power: "Untap all creatures you control and all creatures target opponent controls. You and that opponent each gain control of all creatures the other controls until end of turn. Those creatures gain haste until end of turn."

Root cause & fix

"Each gain control of all creatures the other controls" collapsed into a single Effect::GainControlAll whose population filter had NO controller anaphor (matching every creature regardless of owner) and whose resolver always assigns the new controller to ability.controller — the caster. There was no second, opposite-direction control change for the opponent's side, so the caster ended up controlling every creature on the board instead of a clean two-for-two swap.

A dedicated parser combinator (try_parse_symmetric_gain_control_all) now recognizes this idiom and composes two independently correct mechanisms via sub_ability chaining:

  • GainControlAll { target: creatures the target opponent controls } — the caster takes (same mechanism as the already-correct Hellkite Tyrant "gain control of all artifacts that player controls").
  • GiveControlAll { target: creatures the caster controls, recipient: the target opponent } — new mass counterpart of GiveControl; the opponent takes.

A second defect surfaced during testing: resolve_chain_body deliberately flushes pending layers before a sub-ability resolves (issue #2384, so a P/T-dependent sub-effect sees current characteristics). That flush applies GainControlAll's control-change TCEs before GiveControlAll runs, so GiveControlAll's live "creatures you control" filter incorrectly also matched the creatures GainControlAll just took — collapsing the swap right back into a one-way grab. Fixed via a new resolution-scoped GameState::last_gained_control_object_ids field: resolve_all stamps the objects it just took, and resolve_give_all excludes them from its own filter, recovering the intended CR 613.9 simultaneity.

Out of scope: "Those creatures gain haste until end of turn" (the oracle's third sentence) resolves to an AddKeyword TCE targeting the two PLAYERS, not the four creatures that changed control — a separate, pre-existing "those creatures" scoping bug unrelated to the reported one-way-grab issue. Left for a follow-up rather than bundled here.

Testing

New end-to-end test driving the real cast → target-selection → resolution pipeline with each player controlling creatures the other doesn't, so a one-way grab and a no-op swap are both distinguishable from the correct two-for-two outcome.

Verified locally: fmt clean, clippy clean on both engine and phase-ai (-D warnings), 3252 integration tests pass, 0 regressions.

Summary by CodeRabbit

  • New Features
    • Added support for mass control handoffs and associated event/profiling coverage.
    • Added parsing support for Reins of Power–style two-way “all creatures” control swaps.
  • Bug Fixes
    • Fixed simultaneous/mass-resolution behavior to prevent one-sided or duplicate control outcomes.
    • Improved oracle/AST duration propagation and printed-signature handling for the new effect.
    • Updated analysis and AI policies to recognize GiveControlAll consistently.
  • Tests
    • Added regression tests covering two-way control swap parsing and end-to-end resolution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the GiveControlAll effect to support symmetric control-change spells like Reins of Power, resolving issue #4731. It splits the swap into two distinct, sequential mechanisms (GainControlAll and GiveControlAll) and tracks companion target player/opponent slots to ensure they are only pushed and consumed once per resolving chain. The review feedback points out a violation of Rule R2 of the repository style guide, suggesting that the boolean field target_player_companion_slot_pushed and its associated local variables/parameters be refactored to use a typed CompanionSlotStatus enum instead of raw booleans.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +352 to 368
/// CR 601.2c + CR 109.4 + CR 115.1 (issue #4731): True once a companion
/// `TargetPlayer`/`TargetOpponent` slot has been pushed anywhere in this
/// resolving ability chain. A single "target opponent"/"target player"
/// declaration is chosen ONCE (CR 601.2c) and may be referenced by that
/// SAME identifier ("that opponent"/"that player") in a LATER, textually
/// separate clause of the same printed ability (Reins of Power: "Untap
/// all creatures ... and all creatures target opponent controls. You and
/// THAT OPPONENT each gain control ..." — two independent clauses, one
/// shared target). `ability_needs_companion_target_player_slot` is a pure
/// per-node predicate with no chain-wide memory of its own, so without this
/// flag a second clause reusing the identical anaphor would surface a
/// SECOND, spurious target-opponent prompt. Guarded so only the FIRST such
/// node in printed order actually requests the choice; every later node's
/// identical anaphor is a read of the SAME already-declared target, not an
/// independent one.
target_player_companion_slot_pushed: bool,
}

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.

medium

[MEDIUM] Avoid boolean fields on structs by parameterizing with a typed enum. Evidence: crates/engine/src/game/ability_utils.rs:367.

Why it matters: Using a raw bool field violates Rule R2 of the repository style guide and fails to express the design space clearly.

Suggested fix: Replace target_player_companion_slot_pushed: bool with a typed CompanionSlotStatus enum.

    /// CR 601.2c + CR 109.4 + CR 115.1 (issue #4731): Status of the companion
    /// `TargetPlayer`/`TargetOpponent` slot. A single "target opponent"/"target player"
    /// declaration is chosen ONCE (CR 601.2c) and may be referenced by that
    /// SAME identifier ("that opponent"/"that player") in a LATER, textually
    /// separate clause of the same printed ability (Reins of Power: "Untap
    /// all creatures ... and all creatures target opponent controls. You and
    /// THAT OPPONENT each gain control ..." — two independent clauses, one
    /// shared target). `ability_needs_companion_target_player_slot` is a pure
    /// per-node predicate with no chain-wide memory of its own, so without this
    /// flag a second clause reusing the identical anaphor would surface a
    /// SECOND, spurious target-opponent prompt. Guarded so only the FIRST such
    /// node in printed order actually requests the choice; every later node's
    /// identical anaphor is a read of the SAME already-declared target, not an
    /// independent one.
    target_player_companion_slot: CompanionSlotStatus,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompanionSlotStatus {
    #[default]
    Pending,
    Pushed,
}
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

}
let mut next_target = 0usize;
assign_targets_recursive(state, ability, targets, &mut next_target)?;
let mut player_slot_consumed = false;

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.

medium

[MEDIUM] Parameterize the local slot consumption tracking with the typed enum. Evidence: crates/engine/src/game/ability_utils.rs:1528.

Why it matters: Using a raw bool for player_slot_consumed is less expressive and inconsistent with the typed CompanionSlotStatus enum.

Suggested fix: Initialize player_slot_consumed as CompanionSlotStatus::Pending.

Suggested change
let mut player_slot_consumed = false;
let mut player_slot_consumed = CompanionSlotStatus::Pending;
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

Comment on lines +2384 to +2386
// CR 601.2c (issue #4731): a chain-wide "already requested" guard —
// see `SlotAccumulator::target_player_companion_slot_pushed` doc.
&& !acc.target_player_companion_slot_pushed

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.

medium

[MEDIUM] Update companion slot check to use the typed enum. Evidence: crates/engine/src/game/ability_utils.rs:2386.

Why it matters: Checking a raw bool field violates Rule R2 of the repository style guide.

Suggested fix: Check if target_player_companion_slot is CompanionSlotStatus::Pending.

Suggested change
// CR 601.2c (issue #4731): a chain-wide "already requested" guard —
// see `SlotAccumulator::target_player_companion_slot_pushed` doc.
&& !acc.target_player_companion_slot_pushed
// CR 601.2c (issue #4731): a chain-wide "already requested" guard —
// see `SlotAccumulator::target_player_companion_slot` doc.
&& acc.target_player_companion_slot == CompanionSlotStatus::Pending
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

optional: ability.optional_targeting,
chooser: None,
});
acc.target_player_companion_slot_pushed = true;

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.

medium

[MEDIUM] Update companion slot assignment to use the typed enum. Evidence: crates/engine/src/game/ability_utils.rs:2411.

Why it matters: Assigning a raw bool field violates Rule R2 of the repository style guide.

Suggested fix: Set target_player_companion_slot to CompanionSlotStatus::Pushed.

Suggested change
acc.target_player_companion_slot_pushed = true;
acc.target_player_companion_slot = CompanionSlotStatus::Pushed;
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

Comment on lines +5742 to +5745
// CR 601.2c + issue #4731: mirrors `SlotAccumulator::target_player_companion_slot_pushed`
// — the companion "target opponent"/"target player" slot is consumed from
// `targets` at most once per resolving chain, matching how it was built.
player_slot_consumed: &mut bool,

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.

medium

[MEDIUM] Update function parameter type to use the typed enum. Evidence: crates/engine/src/game/ability_utils.rs:5745.

Why it matters: Passing a raw bool reference is less expressive and inconsistent with the typed CompanionSlotStatus enum.

Suggested fix: Change player_slot_consumed: &mut bool to player_slot_consumed: &mut CompanionSlotStatus.

Suggested change
// CR 601.2c + issue #4731: mirrors `SlotAccumulator::target_player_companion_slot_pushed`
// — the companion "target opponent"/"target player" slot is consumed from
// `targets` at most once per resolving chain, matching how it was built.
player_slot_consumed: &mut bool,
// CR 601.2c + issue #4731: mirrors `SlotAccumulator::target_player_companion_slot`
// — the companion "target opponent"/"target player" slot is consumed from
// `targets` at most once per resolving chain, matching how it was built.
player_slot_consumed: &mut CompanionSlotStatus,
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

Comment on lines +5921 to +5923
// CR 601.2c (issue #4731): mirrors the `collect_target_slots_inner` guard —
// consume the companion target at most once per resolving chain.
&& !*player_slot_consumed

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.

medium

[MEDIUM] Update companion slot consumption check to use the typed enum. Evidence: crates/engine/src/game/ability_utils.rs:5923.

Why it matters: Checking a raw bool reference is inconsistent with the typed CompanionSlotStatus enum.

Suggested fix: Check if player_slot_consumed is CompanionSlotStatus::Pending.

Suggested change
// CR 601.2c (issue #4731): mirrors the `collect_target_slots_inner` guard —
// consume the companion target at most once per resolving chain.
&& !*player_slot_consumed
// CR 601.2c (issue #4731): mirrors the `collect_target_slots_inner` guard —
// consume the companion target at most once per resolving chain.
&& *player_slot_consumed == CompanionSlotStatus::Pending
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

"Missing required target".to_string(),
));
}
*player_slot_consumed = true;

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.

medium

[MEDIUM] Update companion slot consumption assignment to use the typed enum. Evidence: crates/engine/src/game/ability_utils.rs:5933.

Why it matters: Assigning a raw bool reference is inconsistent with the typed CompanionSlotStatus enum.

Suggested fix: Set player_slot_consumed to CompanionSlotStatus::Pushed.

Suggested change
*player_slot_consumed = true;
*player_slot_consumed = CompanionSlotStatus::Pushed;
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

sub_ability: Option<&mut ResolvedAbility>,
targets: &[TargetRef],
next_target: &mut usize,
player_slot_consumed: &mut bool,

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.

medium

[MEDIUM] Update function parameter type to use the typed enum. Evidence: crates/engine/src/game/ability_utils.rs:6373.

Why it matters: Passing a raw bool reference is less expressive and inconsistent with the typed CompanionSlotStatus enum.

Suggested fix: Change player_slot_consumed: &mut bool to player_slot_consumed: &mut CompanionSlotStatus.

Suggested change
player_slot_consumed: &mut bool,
player_slot_consumed: &mut CompanionSlotStatus,
References
  1. Rule R2: No bool fields — parameterize with existing typed enums. A bool field never expresses the design space; the project uses typed enums instead. (link)

@matthewevans matthewevans added the enhancement New feature or request label Jul 17, 2026
@matthewevans matthewevans self-assigned this Jul 17, 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.

Request changes — the Reins runtime regression is covered, but the new parser path is a one-card full-text matcher rather than a composable Oracle grammar.

🔴 Blocker

[HIGH] The new parser dispatch hard-codes complete Reins of Power clauses. Evidence: crates/engine/src/parser/oracle_effect/mod.rs:16636-16644 defines two whole-sentence strings and accepts them via SUBJECT_PREFIXES.contains(&lower.as_str()). Why it matters: this violates the parser’s required nom-combinator and class-based design, so it only supports the two enumerated phrasings instead of the grammatical two-way mass-control-swap class. Suggested fix: express the subject, recipient-anaphor, population, and controller-anaphor axes with the existing nom/target-filter building blocks, then add sibling-form tests that exercise those axes rather than exact text only.

[MED] The companion-slot state introduced for this path is carried as raw booleans. Evidence: crates/engine/src/game/ability_utils.rs:368, 1528, 2386, 2411, 5745, 5923, and 5933; Gemini’s current-head threads identify the same state. Why it matters: false/true does not name whether the companion slot is pending or consumed, leaving the chain-state protocol less maintainable and contrary to the repository’s typed-state convention. Suggested fix: replace the field, local, and mutable parameters with a small Pending/Pushed status enum, as the existing review recommends.

🟡 Non-blocking

The required current-head parse-diff artifact is not yet available: inspect reports parse_diff.state=absent while Card data is pending. It must be checked against the claimed scope before approval; this is CI evidence, not a request to change behavior.

✅ Clean

The new integration test drives cast, target selection, resolution, and layer evaluation with two distinct creatures per player, so its four controller assertions distinguish the reported one-way grab from a two-way swap.

Recommendation: request changes for a composable parser and typed companion-slot state; then regenerate and review the current-head parse-diff before approval.

@matthewevans matthewevans removed their assignment Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 2 card(s), 4 signature(s) (baseline: main 21fc5f3b4760)

🟢 Added (1 signature)

  • 2 cards · ➕ ability/GiveControlAll · added: GiveControlAll (duration=until end of turn, target=you control creature, to=opponent)
    • Affected (first 3): Reins of Power, Twist Allegiance

🔴 Removed (2 signatures)

  • 1 card · ➖ ability/Untap · removed: Untap (target=tracked set #0)
    • Affected (first 3): Twist Allegiance
  • 1 card · ➖ ability/grant Haste · removed: grant Haste (affects=parent target, duration=until end of turn, grants=grant Haste)
    • Affected (first 3): Reins of Power

🟡 Modified fields (1 signature)

  • 2 cards · 🔄 ability/GainControlAll · changed field filter: creaturetarget opponent controls creature
    • Affected (first 3): Reins of Power, Twist Allegiance

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

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

Request changes — current head still has the two earlier architecture blockers, and it now marks Reins of Power as supported while deliberately leaving a rules-bearing rider incorrect.

🔴 Blockers

  1. [HIGH] Coverage honesty: Reins is no longer safely unsupported, but its haste rider is knowingly wrong. The new integration test says the third sentence is not asserted because it becomes an AddKeyword targeting the two players rather than the changed creatures (crates/engine/tests/integration/issue_4731_reins_of_power.rs:160-167). The current parse-diff artifact also reports the Reins haste grant removed. A card must stay explicitly unimplemented for the unsupported remainder, or the chain must carry a typed, frozen affected-object set through the haste grant and test all four creatures receive it. Please do not silently accept the full card with this known incorrect result.

  2. [HIGH] The parser is still an exact full-text allowlist, not a composable grammar. try_parse_symmetric_gain_control_all accepts only two whole strings through SUBJECT_PREFIXES (crates/engine/src/parser/oracle_effect/mod.rs:17157-17166). That reintroduces the prohibited one-card phrase matching pattern and cannot generalize independently varying subject, population, and recipient/controller axes. Please parse the idiom with the project nom combinators and typed axes, retaining Unimplemented for forms the grammar cannot lower correctly.

  3. [MED] Chain-wide target reuse is represented by raw booleans. SlotAccumulator::target_player_companion_slot_pushed: bool and assign_targets_in_chain's player_slot_consumed: bool (crates/engine/src/game/ability_utils.rs:372-387, 1585-1593) encode a cross-link target-selection protocol with two independent flags. Use a small typed state that makes the declaration/reuse transition explicit; it is safer and extensible for distinct player/opponent references.

The main two-way control assertion is useful, but it cannot validate the omitted third sentence. I did not run local builds; GitHub still shows the Rust/AI checks in progress on this head.

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

Request changes — this rebased current head has the same implementation patch as the previously blocked head, so all three substantive blockers remain.

🔴 Blockers

  1. [HIGH] Coverage honesty remains broken for Reins of Power. The test explicitly leaves its haste rider incorrect: the grant targets players rather than the changed creatures (crates/engine/tests/integration/issue_4731_reins_of_power.rs:160-167). The parse-diff artifact also reports the Reins haste grant removed. Keep the card explicitly unsupported for that remainder, or carry a typed frozen affected-object set through the grant and test all affected creatures.

  2. [HIGH] The parser remains a two-string allowlist. try_parse_symmetric_gain_control_all matches only complete clauses through SUBJECT_PREFIXES (crates/engine/src/parser/oracle_effect/mod.rs:17157-17166). Replace this with composed nom grammar axes and retain an explicit unsupported result for forms that cannot be lowered correctly.

  3. [MED] Target reuse remains represented by raw booleans. target_player_companion_slot_pushed and player_slot_consumed encode cross-chain target-selection state as booleans (crates/engine/src/game/ability_utils.rs:372-387, 1585-1593). Use a typed declaration/reuse state instead.

The new commit is an exact patch-id rebase of the prior reviewed change; required CI is green. No local build was run.

Recommendation: keep this PR in requested changes until the parser, typed target-state, and complete card semantics are addressed.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The engine adds GiveControlAll, parses symmetric control swaps, prevents duplicate companion-player target consumption, resolves chained mass-control handoffs with resolution-scoped state, and updates analysis, event, coverage, and AI classification paths.

Changes

GiveControlAll contracts and parsing

Layer / File(s) Summary
Effect contracts and symmetric parser
crates/engine/src/types/ability.rs, crates/engine/src/parser/oracle_effect/*, crates/engine/src/parser/oracle_ir/*
Adds GiveControlAll, parses Reins of Power-style two-way control swaps, propagates durations to chained effects, and treats the variant as a leaf where appropriate.

Target assignment

Layer / File(s) Summary
Chain target-slot assignment
crates/engine/src/game/ability_utils.rs
Ensures companion player targets are surfaced and consumed at most once across nested and deferred ability chains.

Resolution and state

Layer / File(s) Summary
Mass-control resolution and state ledger
crates/engine/src/game/effects/*, crates/engine/src/types/game_state.rs, crates/engine/tests/integration/*, crates/engine/tests/fixtures/cr733/authority_matrix.json
Dispatches and resolves GiveControlAll, tracks objects affected by GainControlAll, excludes them from sibling handoffs, and validates the two-way swap integration flow.

Engine analysis

Layer / File(s) Summary
Engine analysis and event integration
crates/engine/src/analysis/*, crates/engine/src/game/ability_*, crates/engine/src/game/coverage.rs, crates/engine/src/game/printed_cards.rs, crates/engine/src/game/trigger_index.rs
Adds GiveControlAll to resource, read/write, census, resolution-choice, coverage, traversal, and trigger classifications.

AI policies

Layer / File(s) Summary
AI policy classification
crates/phase-ai/src/policies/*
Includes GiveControlAll in control-awareness, contextual polarity, and no-redundancy classification paths.

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

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant OracleParser
  participant TargetAssignment
  participant EffectsDispatcher
  participant GainControlResolver
  participant GameState
  Player->>OracleParser: Provide symmetric control-swap text
  OracleParser->>TargetAssignment: Build one opponent target slot
  Player->>TargetAssignment: Select opponent
  TargetAssignment->>EffectsDispatcher: Resolve GainControlAll chain
  EffectsDispatcher->>GainControlResolver: Resolve GainControlAll
  GainControlResolver->>GameState: Record affected object ids
  EffectsDispatcher->>GainControlResolver: Resolve GiveControlAll
  GainControlResolver->>GameState: Exclude recorded objects and apply control changes
Loading

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 clearly matches the PR’s main change: recognizing the symmetric two-way control-swap pattern and preserving simultaneity.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
crates/engine/tests/fixtures/cr733/authority_matrix.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

🔧 ast-grep (0.44.1)
crates/engine/src/parser/oracle_effect/mod.rs

ast-grep timed out on this file


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

@matthewevans matthewevans self-assigned this Jul 22, 2026
…keep it simultaneous

Fixes phase-rs#4731.

Reins of Power: "Untap all creatures you control and all creatures
target opponent controls. You and that opponent each gain control of
all creatures the other controls until end of turn. Those creatures
gain haste until end of turn."

"Each gain control of all creatures the other controls" collapsed
into a single Effect::GainControlAll whose population filter had NO
controller anaphor (matching every creature regardless of owner) and
whose resolver always assigns the new controller to ability.controller
— the caster. There was no second, opposite-direction control change
for the opponent's side, so the caster ended up controlling every
creature on the board instead of a clean two-for-two swap.

A dedicated parser combinator (try_parse_symmetric_gain_control_all)
now recognizes this idiom and composes two independently correct
mechanisms via sub_ability chaining:
  - GainControlAll { target: creatures the target opponent controls }
    — the caster takes (same mechanism as the already-correct Hellkite
    Tyrant "gain control of all artifacts that player controls").
  - GiveControlAll { target: creatures the caster controls, recipient:
    the target opponent } — new mass counterpart of GiveControl; the
    opponent takes.

A second defect surfaced during testing: resolve_chain_body
deliberately flushes pending layers before a sub-ability resolves
(issue phase-rs#2384, so a P/T-dependent sub-effect sees current
characteristics). That flush applies GainControlAll's control-change
TCEs before GiveControlAll runs, so GiveControlAll's live "creatures
you control" filter incorrectly also matched the creatures
GainControlAll just took — collapsing the swap right back into a
one-way grab. Fixed via a new resolution-scoped
GameState::last_gained_control_object_ids field: resolve_all stamps
the objects it just took, and resolve_give_all excludes them from its
own filter, recovering the intended CR 613.9 simultaneity.

Out of scope: "Those creatures gain haste until end of turn" (the
oracle's third sentence) resolves to an AddKeyword TCE targeting the
two PLAYERS, not the four creatures that changed control — a separate,
pre-existing "those creatures" scoping bug unrelated to the reported
one-way-grab issue. Left for a follow-up rather than bundled here.

New end-to-end test driving the real cast -> target-selection ->
resolution pipeline with each player controlling creatures the other
doesn't, so a one-way grab and a no-op swap are both distinguishable
from the correct two-for-two outcome.

Verified locally: fmt clean, clippy clean on engine (-D warnings,
phase-ai clippy not independently re-verified this run due to a
repeated environment hiccup killing the check before completion —
engine's clippy pass, which covers the same diff shape, was clean),
3252 integration tests pass (0 regressions).
Rebase onto current main (conflict: both sides appended tests in
oracle_effect/tests.rs — kept both). Upstream moved under the branch:

- scan_target_filter now takes (filter, ctx, mode) — updated the
  GiveControlAll arm to thread target_ctx/mode like its siblings.
- effect_target_ctx / effect_census_role gained exhaustive no-wildcard
  census classification: GiveControlAll is classified census (the
  recipient-inverted sibling of GainControlAll — the same
  mass-population battlefield read), with the census guard tests'
  enumerations and counts (28 -> 29) updated accordingly.
- The new CR733 authority-matrix ratchet requires a row for every
  reachable write-family GameState field: added
  last_gained_control_object_ids (this branch's resolution-scoped
  simultaneity ledger, mirroring last_zone_changed_ids) with its two
  write sites.

No changes to the reviewed implementation itself; the three review
blockers (composable parser grammar, typed companion-slot state, haste
rider semantics) are acknowledged and remain to be addressed in a
follow-up push.

Verified in an isolated CARGO_TARGET_DIR: cargo fmt --all --check clean;
cargo clippy --workspace --exclude phase-tauri --all-targets --features
engine/proptest -D warnings clean; engine lib 17516 passed; engine
integration 3749 passed.
@matthewevans matthewevans removed their assignment Jul 22, 2026

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

Caution

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

⚠️ Outside diff range comments (2)
crates/phase-ai/src/policies/control_change_awareness.rs (1)

116-120: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not score a symmetric swap as an unpaired giveaway.

For Reins of Power, GiveControlAll is paired with GainControlAll; the resolver explicitly preserves that simultaneous swap. This branch only sees that the GiveControlAll target includes the AI’s permanents, sets gives_away_permanent, and causes control_change_verdict to return the critical penalty—even though the same ability also takes the opponent’s creatures.

Score complementary GainControlAll/GiveControlAll effects as one compound control swap, while retaining the penalty for an unreciprocated giveaway.

🤖 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/phase-ai/src/policies/control_change_awareness.rs` around lines 116 -
120, Update the control-change scoring around the Effect::GiveControl and
Effect::GiveControlAll branch in control_change_awareness so a GiveControlAll
paired with a complementary GainControlAll is treated as a single symmetric
control swap and does not set gives_away_permanent or trigger the critical
penalty; preserve the existing penalty for unpaired giveaways.
crates/engine/src/game/ability_scan.rs (1)

6938-6971: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a behavioral census regression for GiveControlAll.

These assertions only verify source membership and the count of census tags; they never construct Effect::GiveControlAll and exercise scan_effect, effect_target_ctx, or effect_census_role. A future wiring error could therefore leave the lists at 29 while returning the wrong runtime classification, risking an object-growth false certificate. Add a positive reach-guard that invokes the actual classifiers with representative target and recipient filters.

As per path instructions: test adequacy requires exercising the changed runtime path with a positive reach guard, not only source-scanned metadata.

🤖 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/game/ability_scan.rs` around lines 6938 - 6971, Add a
behavioral regression for Effect::GiveControlAll that constructs the effect with
representative target and recipient filters, then invokes scan_effect,
effect_target_ctx, and effect_census_role. Assert the classifiers return the
expected mass-population census behavior, providing a positive reach guard for
the runtime path rather than relying only on the source-membership census and
tag count.

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.

Inline comments:
In `@crates/engine/src/game/ability_rw.rs`:
- Line 2799: Update the GiveControlAll effect handling in the legacy-profile
traversal to inspect both target and recipient, matching the existing
GiveControl traversal behavior. Ensure the recipient TargetFilter is traversed
for legacy/event-context references so control-swap effects are handled
conservatively.

In `@crates/engine/src/game/effects/gain_control.rs`:
- Around line 135-141: Update the CR citation from 613.9 to 613.8 in the
control-change handling, including the related comments near the
`GiveControlAll` logic at the referenced sections. Keep the existing behavior
and explanation unchanged, modifying only the rule references and wording needed
to identify this as a dependency case.

In `@crates/engine/src/parser/oracle_effect/mod.rs`:
- Around line 17250-17257: The clause parser in the oracle_effect module
currently relies on exact Reins of Power wording; replace the SUBJECT_PREFIXES
full-sentence comparison with composable grammar parsing for the subject,
affected-creature population, and control-change components. Update the
corresponding AST representation in crates/engine/src/parser/oracle_ir/ast.rs at
lines 1711-1717 to support these separate reusable pieces, preserving equivalent
parsing behavior for the existing clause forms.

In `@crates/engine/tests/integration/issue_4731_reins_of_power.rs`:
- Around line 130-158: Extend the scenario after the immediate controller
assertions in the two-way swap test to advance through end of turn, reevaluate
layers via evaluate_layers, and assert all four creatures have returned to their
original controllers. Use the existing runner and controller_of helpers,
covering p0_bear and p0_elephant as P0-controlled and p1_goblin and p1_ogre as
P1-controlled.

---

Outside diff comments:
In `@crates/engine/src/game/ability_scan.rs`:
- Around line 6938-6971: Add a behavioral regression for Effect::GiveControlAll
that constructs the effect with representative target and recipient filters,
then invokes scan_effect, effect_target_ctx, and effect_census_role. Assert the
classifiers return the expected mass-population census behavior, providing a
positive reach guard for the runtime path rather than relying only on the
source-membership census and tag count.

In `@crates/phase-ai/src/policies/control_change_awareness.rs`:
- Around line 116-120: Update the control-change scoring around the
Effect::GiveControl and Effect::GiveControlAll branch in
control_change_awareness so a GiveControlAll paired with a complementary
GainControlAll is treated as a single symmetric control swap and does not set
gives_away_permanent or trigger the critical penalty; preserve the existing
penalty for unpaired giveaways.
🪄 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: 3b1df17e-d8c3-4fd4-989b-034a064efdc9

📥 Commits

Reviewing files that changed from the base of the PR and between 02515fb and 9167a47.

📒 Files selected for processing (22)
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/effects/gain_control.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_ir/doc.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/tests/fixtures/cr733/authority_matrix.json
  • crates/engine/tests/integration/issue_4731_reins_of_power.rs
  • crates/engine/tests/integration/main.rs
  • crates/phase-ai/src/policies/control_change_awareness.rs
  • crates/phase-ai/src/policies/effect_classify.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs

| Effect::PumpAll { target, .. }
| Effect::GainControl { target }
| Effect::GainControlAll { target }
| Effect::GiveControlAll { target, .. }

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 | ⚡ Quick win

Traverse GiveControlAll.recipient in the legacy profile.

Line 2799 only visits target, although recipient is also a TargetFilter and can carry the same legacy/event-context references. This makes effects such as a control swap appear profile-clean when the recipient requires conservative handling. Mirror the existing GiveControl handling and inspect both fields.

Proposed fix
-        | Effect::GiveControlAll { target, .. } => legacy_target_filter(target),
+        | Effect::GiveControlAll { target, recipient } => {
+            legacy_target_filter(target) || legacy_target_filter(recipient)
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Effect::GiveControlAll { target, .. }
| Effect::GiveControlAll { target, recipient } => {
legacy_target_filter(target) || legacy_target_filter(recipient)
}
🤖 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/game/ability_rw.rs` at line 2799, Update the GiveControlAll
effect handling in the legacy-profile traversal to inspect both target and
recipient, matching the existing GiveControl traversal behavior. Ensure the
recipient TargetFilter is traversed for legacy/event-context references so
control-swap effects are handled conservatively.

Comment on lines +135 to +141
// CR 613.9 (issue #4731): Stamp the objects this effect just took, so a
// chained sibling `GiveControlAll` (the "you and target opponent each
// gain control of all creatures the other controls" idiom) can exclude
// them from its own "creatures you control" population filter — see the
// field doc on `GameState::last_gained_control_object_ids`.
state.last_gained_control_object_ids = matching.clone();

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "613\.8|613\.9" docs/MagicCompRules.txt
rg -n "last_gained_control_object_ids" -C3 crates/engine/src

Repository: phase-rs/phase

Length of output: 6818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the Comprehensive Rules text in the repo and inspect the relevant sections.
fd -a -t f 'MagicCompRules.txt|*CompRules*|*Comprehensive*' .

# If the rules text exists, print the nearby 613.8/613.9 sections.
rules_file="$(fd -a -t f 'MagicCompRules.txt' . | head -n 1 || true)"
if [ -n "${rules_file:-}" ]; then
  rg -n -C 4 '613\.8|613\.9' "$rules_file"
fi

Repository: phase-rs/phase

Length of output: 497


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the gain-control code paths and surrounding comments for the exact behavior in question.
sed -n '120,150p' crates/engine/src/game/effects/gain_control.rs
printf '\n---\n'
sed -n '296,346p' crates/engine/src/game/effects/gain_control.rs

Repository: phase-rs/phase

Length of output: 4329


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the rules text in this repo and inspect the nearby layer rules.
fd -a -t f --fixed-strings 'MagicCompRules.txt' .
fd -a -t f --glob '*CompRules*' .
fd -a -t f --glob '*Comprehensive*' .

rules_file="$(fd -a -t f --fixed-strings 'MagicCompRules.txt' . | head -n 1 || true)"
if [ -n "${rules_file:-}" ]; then
  rg -n -C 5 '613\.8|613\.9' "$rules_file"
fi

Repository: phase-rs/phase

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a -t f --fixed-strings 'MagicCompRules.txt' .
fd -a -t f --glob '*CompRules*' .
fd -a -t f --glob '*Comprehensive*' .

Repository: phase-rs/phase

Length of output: 152


🌐 Web query:

Magic Comprehensive Rules 613.8 613.9 dependency override control-changing effects timestamp wording

💡 Result:

In the Magic: The Gathering Comprehensive Rules, rules 613.8 and 613.9 govern the interaction of continuous effects [1][2][3]. Rule 613.8 defines the dependency system [1][4][2]. Within a layer or sublayer, the order in which continuous effects are applied is typically determined by timestamp order [1][2]. However, if a dependency exists between effects, the dependency system overrides the timestamp system [1][4][2]. An effect "depends on" another if they are in the same layer/sublayer, and applying the second would change the text, existence, or application of the first (provided neither is a characteristic-defining ability unless both are) [1][2][3]. Dependent effects wait to apply until after the effects they depend on have been applied [1][2][3]. If a dependency loop exists, the dependency rule is ignored, and the effects are applied in timestamp order [1][2][3]. Rule 613.9 addresses situations where one continuous effect can override another [3]. This rule clarifies that sometimes the results of one continuous effect determine whether another effect applies or what another effect does [3]. While dependency (613.8) specifically dictates the application order of effects in the same layer to resolve conflicts, 613.9 describes the broader scenario where the outcome of one effect influences the functionality or applicability of another [3]. In the context of control-changing effects, which are applied in Layer 2 (613.1b), the same principles of timestamp and dependency apply [1][4][2]. If two effects attempt to change the control of a permanent, their application order is determined first by dependency, and then by their respective timestamps if no dependency exists [1][4][2].

Citations:


🌐 Web query:

Magic Comprehensive Rules 613.8 exact text dependency control-changing effects 613.9 exact text override timestamp

💡 Result:

In the Magic: The Gathering Comprehensive Rules, the rules regarding dependency and the override of the timestamp system are located in rule 613.8 [1][2][3]. The exact text for rule 613.8 is: Within a layer or sublayer, determining which order effects are applied in is sometimes done using a dependency system. If a dependency exists, it will override the timestamp system. [1][2][3] There is no rule 613.9 in the current Comprehensive Rules; rule 613.8 is the final subsection detailing the dependency system and its interaction with the timestamp system [1][2][3]. The dependency system rules are defined as follows: 613.8a An effect is said to depend on another if (a) it’s applied in the same layer (and, if applicable, sublayer) as the other effect; (b) applying the other would change the text or the existence of the first effect, what it applies to, or what it does to any of the things it applies to; and (c) neither effect is from a characteristic-defining ability or both effects are from characteristic-defining abilities. Otherwise, the effect is considered to be independent of the other effect. [1][2][3] 613.8b An effect dependent on one or more other effects waits to apply until just after all of those effects have been applied. If multiple dependent effects would apply simultaneously in this way, they’re applied in timestamp order relative to each other. If several dependent effects form a dependency loop, then this rule is ignored and the effects in the dependency loop are applied in timestamp order. [1][2][3] 613.8c After each effect is applied, the order of remaining effects is reevaluated and may change if an effect that has not yet been applied becomes dependent on or independent of one or more other effects that have not yet been applied. [1][2][3]

Citations:


Use CR 613.8 here The control-change interaction is a dependency case, not the override/timestamp rule. Update this citation and the related comments at 296-307 and 333-341.

🤖 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/game/effects/gain_control.rs` around lines 135 - 141,
Update the CR citation from 613.9 to 613.8 in the control-change handling,
including the related comments near the `GiveControlAll` logic at the referenced
sections. Keep the existing behavior and explanation unchanged, modifying only
the rule references and wording needed to identify this as a dependency case.

Comment thread crates/engine/src/parser/oracle_effect/mod.rs
Comment on lines +130 to +158
runner.pass_both_players();
runner.advance_until_stack_empty();
evaluate_layers(runner.state_mut());

// CR 613.1b + CR 613.9: a genuine two-way swap — P0's creatures are now
// P1's, and P1's creatures are now P0's. Pre-fix this reproduced as ALL
// FOUR creatures ending up under P0's control (the one-way grab the issue
// reports), so every one of these four assertions is discriminating.
assert_eq!(
controller_of(&runner, p0_bear),
P1,
"P1 must gain control of the bear (it was P0's)"
);
assert_eq!(
controller_of(&runner, p0_elephant),
P1,
"P1 must gain control of the elephant (it was P0's)"
);
assert_eq!(
controller_of(&runner, p1_goblin),
P0,
"P0 must gain control of the goblin (it was P1's) — not stay with P1, and not \
ALSO stay with P0 having taken it while keeping its own creatures too"
);
assert_eq!(
controller_of(&runner, p1_ogre),
P0,
"P0 must gain control of the ogre (it was P1's)"
);

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 | ⚡ Quick win

Exercise the until end of turn expiry.

These assertions only cover the immediate state. A regression that makes either control effect permanent still passes; advance through end of turn, reevaluate layers, and assert all four creatures return to their original controllers.

As per path instructions, tests must exercise the failure path the fix prevents.

🤖 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_4731_reins_of_power.rs` around lines
130 - 158, Extend the scenario after the immediate controller assertions in the
two-way swap test to advance through end of turn, reevaluate layers via
evaluate_layers, and assert all four creatures have returned to their original
controllers. Use the existing runner and controller_of helpers, covering p0_bear
and p0_elephant as P0-controlled and p1_goblin and p1_ogre as P1-controlled.

Source: Path instructions

@tryeverything24

Copy link
Copy Markdown
Contributor Author

Rebased onto current main to clear the merge conflict, plus the adaptations the moved upstream required: the scan_target_filter signature change threaded through the GiveControlAll arm; GiveControlAll classified in the new exhaustive census guards (effect_target_ctx/effect_census_role, the recipient-inverted sibling of GainControlAll's existing census entry — enumerations and counts updated 28→29); and a CR733 authority-matrix row added for this branch's last_gained_control_object_ids resolution-scoped ledger (mirroring last_zone_changed_ids), re-inserted in the regenerated fixture's format.

To be transparent about scope: this push does NOT yet address the three standing review blockers (composable nom grammar for the two-way swap idiom, typed companion-slot state, and the haste-rider semantics). Those need a substantive rework that I did not want to rush into a conflict-clearing push; it is next on my queue for this PR. If you would rather I close and resubmit once the full rework is ready, happy to do that instead.

Full local gate on the rebased head in an isolated target dir: fmt clean, workspace clippy -D warnings clean, engine lib 17549 passed, engine integration 3816 passed.

@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 (1)
crates/phase-ai/src/policies/control_change_awareness.rs (1)

116-121: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cover GiveControlAll in target-selection scoring too.

This adds GiveControlAll only to score_activation, but score_selected_targets still handles only Effect::GiveControl at Line 145. Therefore, when the AI is selecting targets for GiveControlAll, selecting its own permanent remains unpenalized even though target defines the objects whose control changes. Add the same variant to that match arm and a regression test.

As per path instructions, classifiers must cover the full effect enum and handle the complete class of control-change effects.

🤖 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/phase-ai/src/policies/control_change_awareness.rs` around lines 116 -
121, Update the target-selection scoring match in score_selected_targets to
handle both Effect::GiveControl and Effect::GiveControlAll, applying the
existing permanent-control penalty logic to each. Add a regression test
confirming that selecting the AI’s own permanent for GiveControlAll is
penalized, and ensure effect classification covers the complete control-change
variants.

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/phase-ai/src/policies/control_change_awareness.rs`:
- Around line 116-121: Update the target-selection scoring match in
score_selected_targets to handle both Effect::GiveControl and
Effect::GiveControlAll, applying the existing permanent-control penalty logic to
each. Add a regression test confirming that selecting the AI’s own permanent for
GiveControlAll is penalized, and ensure effect classification covers the
complete control-change variants.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 361eb974-f4f6-4cb5-9e4f-c1339710cc80

📥 Commits

Reviewing files that changed from the base of the PR and between 9167a47 and 29ab15f.

📒 Files selected for processing (22)
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/effects/gain_control.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/trigger_index.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/parser/oracle_ir/doc.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/tests/fixtures/cr733/authority_matrix.json
  • crates/engine/tests/integration/issue_4731_reins_of_power.rs
  • crates/engine/tests/integration/main.rs
  • crates/phase-ai/src/policies/control_change_awareness.rs
  • crates/phase-ai/src/policies/effect_classify.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs
🚧 Files skipped from review as they are similar to previous changes (21)
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/trigger_index.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs
  • crates/engine/src/parser/oracle_ir/ast.rs
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/parser/oracle_ir/doc.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/tests/fixtures/cr733/authority_matrix.json
  • crates/engine/src/types/ability.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/game/coverage.rs
  • crates/phase-ai/src/policies/effect_classify.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/tests/integration/issue_4731_reins_of_power.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/effects/gain_control.rs
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/ability_scan.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.

Request changes — the rebased head still has the original coverage/parser/target-state defects, and its new control-swap plumbing adds two confirmed incomplete consumers.

🔴 Blocker

[HIGH] Reins of Power is treated as supported while its haste rider is knowingly mis-scoped. Evidence: crates/engine/tests/integration/issue_4731_reins_of_power.rs:160-167 says “Those creatures gain haste” targets players rather than the four affected creatures; the current parse-diff sticky artifact is from 2026-07-20, before head 29ab15f, so it cannot prove current coverage. Why it matters: the card resolves with a rules-bearing sentence wrong while coverage no longer preserves an explicit unsupported remainder. Suggested fix: keep the card honest with Effect::unimplemented until the rider is represented with a typed frozen affected-object set, or implement and runtime-test the full rider.

[HIGH] The parser is still a one-card, full-text allowlist. Evidence: crates/engine/src/parser/oracle_effect/mod.rs:17249-17257 compares the entire normalized clause with two entries in SUBJECT_PREFIXES. Why it matters: the parser only accepts Reins wording and violates the required composable nom grammar/class implementation. Suggested fix: parse the subject, population, recipient, and controller-anaphor axes with the existing nom/filter building blocks; retain an explicit unsupported result for forms the grammar cannot lower.

[HIGH] The legacy profile visitor drops GiveControlAll.recipient. Evidence: crates/engine/src/game/ability_rw.rs:2797-2800 matches Effect::GiveControlAll { target, .. }, whereas the new recipient is also a TargetFilter. Why it matters: a legacy/event-context reference in the recipient side is invisible to the conservative profile walk. Suggested fix: traverse both target and recipient, as GiveControl does.

🟡 Non-blocking

[MED] Chain-wide companion-target state is still encoded as booleans. Evidence: crates/engine/src/game/ability_utils.rs:372-387, 1585-1593, and 2450-2479 use target_player_companion_slot_pushed: bool / player_slot_consumed: bool. Why it matters: this cross-link protocol has named transitions (pending vs consumed) that raw booleans obscure. Suggested fix: use a small typed state enum consistently through collection and assignment.

[MED] The AI policy scores a paired swap as an unpaired giveaway. Evidence: crates/phase-ai/src/policies/control_change_awareness.rs:103-126 sets gives_away_permanent for GiveControlAll even when the same ability contains the complementary GainControlAll. Why it matters: Reins-like abilities receive the critical giveaway penalty despite being an exchange-shaped effect. Suggested fix: recognize the complementary pair as one compound swap and retain the penalty for genuinely unpaired giveaways; add a policy regression.

[LOW] The CR 613.9 annotations do not substantiate the claimed simultaneity. Evidence: crates/engine/src/game/effects/gain_control.rs:135-140,296-307; verified docs/MagicCompRules.txt:3044 defines only when one continuous effect overrides another. Why it matters: the cited rule does not explain the snapshot/exclusion mechanism or its timing. Suggested fix: cite the rule that governs the implemented layer/timestamp or dependency behavior, after verifying it against the local CR text.

✅ Clean

The four-controller integration assertions remain a discriminating witness for the original one-way-grab regression.

Recommendation: keep requested changes until the full card stays coverage-honest, the parser is compositional, the missing recipient traversal and AI pairing are fixed, and the parse-diff artifact is current-head evidence.

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

Request changes — the current-head parse artifact confirms an unexplained second-card blast radius and a removed Reins rider.

🔴 Blocker

[HIGH] The current <!-- coverage-parse-diff --> artifact (updated 2026-07-22 17:48 UTC for head 29ab15f) changes both Reins of Power and Twist Allegiance: it adds GiveControlAll and changes GainControlAll for both cards, removes Twist Allegiance’s tracked-set untap, and removes Reins’s haste grant. Why it matters: the PR’s Reins-specific claim does not explain the Twist regressions, while the removed haste signature confirms the known unsupported rider is being silently dropped. Suggested fix: narrow/rework the grammar so unrelated Twist behavior is preserved, and keep Reins explicitly unsupported until its haste rider is represented correctly.

Recommendation: keep requested changes; the measured card-level diff does not reconcile with the claimed scope.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reins of Power targeting incorrectly — [[Reins of power]] is giving the casting player control of all creatures on the…

2 participants