Skip to content

Fix True-Name Nemesis chosen-player protection - #7213

Merged
matthewevans merged 6 commits into
phase-rs:mainfrom
traemyn:fix/issue-5941-true-name-protection
Aug 12, 2026
Merged

Fix True-Name Nemesis chosen-player protection#7213
matthewevans merged 6 commits into
phase-rs:mainfrom
traemyn:fix/issue-5941-true-name-protection

Conversation

@traemyn

@traemyn traemyn commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #5941. Fixes the True-Name Nemesis protection misparse by representing “the chosen player” as a typed protection target and resolving it from the protected permanent’s persisted choice, so objects controlled by that player cannot target it.

Files changed

  • crates/engine/src/game/game_object.rs
  • crates/engine/src/game/keywords.rs
  • crates/engine/src/game/replacement.rs
  • crates/engine/src/game/static_abilities.rs
  • crates/engine/src/types/keywords.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs

Track

Developer

LLM

Model: GitHub Copilot (via GitHub Copilot; canonical id not exposed)
Tier: Frontier
Thinking: high

Implementation method (required)

Method: /engine-implementer

CR references

  • CR 702.16k
  • CR 109.4
  • CR 108.4a

Verification

  • Required checks ran clean for the current head; local verification is listed below.

  • CI run 31627339178 — all required checks passed.

  • Gate A output below is for the current committed head.

  • Final review-impl below is clean for the current committed head.

  • Both anchors cite existing analogous code at the same seam.

  • cargo test -p phase-engine --test integration issue_5941_true_name_chosen_player_protection -- --nocapture — passed: 1 passed, 0 failed.

  • Command-zone revert discrimination — reverting the command-zone authority failed at the ordinary-command assertion; restoring it passed.

  • Production-revert discrimination — reverting the ChosenPlayer authority failed at the selected-owner assertion; restoring it passed.

  • cargo clippy-strict — passed.

  • cargo test -p phase-engine — passed: 4810 passed, 0 failed, 2 ignored.

  • cargo fmt --all — passed.

  • git diff --check — passed.

Gate A

Gate G PASS (router/grant architecture: strict router vs permissive grant boundary intact)
Gate A PASS head=981c479eb8e8308902a843a131551973f6d36a3e base=97591656218103d8e8c7315725b24cfe64645dd4

Anchored on

  • crates/engine/src/game/keywords.rs:533 — existing chosen-color and chosen-card-type protection matching reads durable choices from the protected object.
  • crates/engine/src/types/keywords.rs:2877 — existing chosen-color and chosen-card-type parser arms use typed protection variants for runtime resolution.
  • crates/engine/tests/integration/serras_emissary_chosen_card_type_protection.rs — existing production-path choice-to-targeting regression structure.

Final review-impl

Final review-impl PASS head=981c479eb8e8308902a843a131551973f6d36a3e

Claimed parse impact

None.

Scope Expansion

None.

Validation Failures

None.

CI Failures

None.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed protection effects that apply to a chosen player.
    • Sources controlled by the chosen player can no longer interact with protected permanents.
    • Sources controlled by other players remain eligible to interact as expected.
    • Improved recognition of “chosen player” protection wording.
    • Protection now correctly accounts for control across battlefield, stack, command zone, and other card locations.
  • Tests

    • Added coverage for chosen-player selection, protection behavior, and source control versus ownership.

@traemyn
traemyn requested a review from matthewevans as a code owner August 10, 2026 17:41
@coderabbitai

coderabbitai Bot commented Aug 10, 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: 801365a6-138e-4a10-9cd0-13e98c760b74

📥 Commits

Reviewing files that changed from the base of the PR and between 2ba1c0e and 981c479.

📒 Files selected for processing (2)
  • crates/engine/src/game/game_object.rs
  • crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/engine/src/game/game_object.rs
  • crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs

📝 Walkthrough

Walkthrough

The engine parses ProtectionTarget::ChosenPlayer, stores the selected player, and blocks sources controlled by that player from targeting the protected permanent. Tests cover parsing, player resolution, and target legality.

Changes

Chosen-player protection

Layer / File(s) Summary
Protection contract and parsing
crates/engine/src/types/keywords.rs
Adds ProtectionTarget::ChosenPlayer, parses both chosen-player text forms, and tests Keyword::from_str integration.
Controller-or-owner resolution
crates/engine/src/game/game_object.rs, crates/engine/src/game/replacement.rs
Adds shared controller-or-owner resolution and uses it for replacement-source player selection.
Protection matching and integration
crates/engine/src/game/keywords.rs, crates/engine/src/game/static_abilities.rs, crates/engine/tests/integration/*
Matches sources against the chosen player, excludes unsupported player-level protection checks, and verifies target filtering across zones and ownership assignments.

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

Sequence Diagram(s)

sequenceDiagram
  participant TrueNameNemesis
  participant ChooseOption
  participant ProtectionMatcher
  participant Source
  TrueNameNemesis->>ChooseOption: present player choice
  ChooseOption-->>TrueNameNemesis: persist chosen player
  Source->>ProtectionMatcher: check targeting legality
  ProtectionMatcher->>TrueNameNemesis: compare source controller-or-owner with chosen player
  ProtectionMatcher-->>Source: allow or reject target
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement chosen-player protection and add regression coverage for issue #5941.
Out of Scope Changes check ✅ Passed The parser, runtime, replacement, and test changes directly support the linked issue and PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing True-Name Nemesis protection from the chosen player.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/game/keywords.rs`:
- Around line 545-549: Update ProtectionTarget::ChosenPlayer in
crates/engine/src/game/keywords.rs:545-549 to match the chosen player against
source.owner when source.controller is absent, while retaining controller
matching for controlled sources and using owner for nonbattlefield player-scoped
queries. Update the documentation in crates/engine/src/types/keywords.rs:505-507
to cite CR 702.16k and describe both controller and ownership protection
behavior.

In
`@crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs`:
- Around line 35-41: Update the test setup around the direct
WaitingFor::NamedChoice assignment to enter True-Name Nemesis through the
production enter-the-battlefield path instead of prepopulating post-dispatch
state. Assert that this path produces the expected WaitingFor::NamedChoice, then
submit GameAction::ChooseOption and retain the existing target-legality
assertions.
🪄 Autofix

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: bae2b917-67e3-4060-b068-437d4d432a3e

📥 Commits

Reviewing files that changed from the base of the PR and between 1bb6c1d and b71eec3.

📒 Files selected for processing (5)
  • crates/engine/src/game/keywords.rs
  • crates/engine/src/game/static_abilities.rs
  • crates/engine/src/types/keywords.rs
  • crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs
  • crates/engine/tests/integration/main.rs

Comment thread crates/engine/src/game/keywords.rs Outdated
Comment thread crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs Outdated
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Generated for head 981c479eb8e8308902a843a131551973f6d36a3e.

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans matthewevans self-assigned this Aug 10, 2026
@matthewevans matthewevans added the bug Bug fix label Aug 10, 2026
@matthewevans

Copy link
Copy Markdown
Member

Held pending current-head CI. I pushed maintainer commit 3c6294a50bfdc307b981ba70ebce890c6a2d2191 to make the regression exercise the actual cast → as-enters → named-choice path. Please let the required checks and the head-bound parse-diff artifact complete; I will resume approval and enqueue once that evidence is green for this head.

@matthewevans

Copy link
Copy Markdown
Member

Held pending current-head validation. I pushed 67d9d4506154098ead179d8638099c5b29b5ffe0, which merges current main and advances the regression to the existing legal main-phase test helper before its real cast → as-enters → named-choice path. CI and the head-bound parse-diff artifact are queued for that exact head.

I also confirmed the remaining unresolved CodeRabbit finding is rules-valid: CR 702.16k protects from objects the chosen player controls and objects that player owns but no other player controls. The present ChosenPlayer matcher checks only source.controller; resolving that semantic gap needs a deliberately scoped engine fix and discriminating coverage, so this PR is not approved or enqueued on the current 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.

[HIGH] ChosenPlayer implements only the controller half of protection from a player. Evidence: crates/engine/src/game/keywords.rs:547-549 compares only source.controller, while CR 702.16k also covers an object the chosen player owns when no other player controls it. This is not theoretical in this engine: crates/engine/src/game/effects/change_zone.rs:1698-1719 and its regression at :4582-4620 document that a stolen permanent retains its former controller after moving to its owner's graveyard, where player-scoped semantics must use owner. Why it matters: a chosen player's off-battlefield source can incorrectly bypass protection, and a stale controller can make an object owned by someone else incorrectly match. Suggested fix: make the ChosenPlayer arm use the engine's owner-vs-controller-by-zone authority (controller for controlled sources; owner where the object has no controller), document CR 702.16k on the variant, and add discriminating tests for both a selected owner's stolen-then-died source and the inverse stale-controller case.

Current-head CI also fails before the claimed production regression reaches its as-enters path: Rust tests (shard 2/4) reports InvalidAction("apply_as_current: no authorized submitter (game over?)") at crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs:46. This is supporting evidence only; it does not replace the rules-correctness fix above.

@matthewevans matthewevans removed their assignment Aug 11, 2026
@traemyn

traemyn commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Validated and fixed both current-head blockers in 2ba1c0e. The cast-path regression now stocks the draw step, and CR 702.16k source authority uses controller on controlled-zone objects and owner on owner-zone objects via the shared CR 109.4/108.4a helper. Added opposite stale-controller fixtures; reverting the production fix makes the selected-owner assertion fail. Local gates: focused regression PASS, cargo clippy-strict PASS, cargo test -p phase-engine PASS (4810 passed, 2 ignored), final review-impl PASS, Gate A PASS. Fresh CI is running for this head.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
crates/engine/src/game/game_object.rs (1)

2949-2959: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve zero final chapter number and gate Saga actions on chapter abilities.

final_chapter_number() must return Some(0) for an empty Saga and None only for a non-Saga. CR 714.3c and CR 714.4 still require one or more chapter abilities. Update crates/engine/src/game/sba.rs:2068-2119 and crates/engine/src/game/turns.rs:2611-2623 to check chapter-ability presence separately. Add regression coverage for an empty Saga.

🤖 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/game_object.rs` around lines 2949 - 2959, Update
GameObject::final_chapter_number to return Some(0) for a Saga with no chapter
abilities and None only for non-Sagas. In the CR 714.3c and CR 714.4 logic
within the saga action and sacrifice handling, check chapter-ability presence
separately rather than using final_chapter_number as the Saga-action gate. Add
regression coverage verifying empty Sagas retain final chapter number zero and
are excluded from those actions.

Sources: Path instructions, MCP tools

🤖 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/game_object.rs`:
- Around line 1459-1466: Update GameObject::controller_or_owner so ordinary
cards in Zone::Command resolve to self.owner rather than preserving a
potentially stale self.controller; retain self.controller only for explicitly
modeled command-zone exceptions such as emblems, while keeping Battlefield and
Stack behavior unchanged.

---

Outside diff comments:
In `@crates/engine/src/game/game_object.rs`:
- Around line 2949-2959: Update GameObject::final_chapter_number to return
Some(0) for a Saga with no chapter abilities and None only for non-Sagas. In the
CR 714.3c and CR 714.4 logic within the saga action and sacrifice handling,
check chapter-ability presence separately rather than using final_chapter_number
as the Saga-action gate. Add regression coverage verifying empty Sagas retain
final chapter number zero and are excluded from those actions.
🪄 Autofix

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: ad1de6b1-ac74-4f0f-ab0c-f453de662c0c

📥 Commits

Reviewing files that changed from the base of the PR and between 3c6294a and 2ba1c0e.

📒 Files selected for processing (6)
  • crates/engine/src/game/game_object.rs
  • crates/engine/src/game/keywords.rs
  • crates/engine/src/game/replacement.rs
  • crates/engine/src/types/keywords.rs
  • crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/game/keywords.rs
  • crates/engine/src/types/keywords.rs

Comment thread crates/engine/src/game/game_object.rs
@matthewevans matthewevans self-assigned this Aug 12, 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.

Blocked — the current head still assigns an ordinary command-zone card to its stored controller.

🔴 Blocker

[HIGH] controller_or_owner treats every command-zone object as controlled. Evidence: crates/engine/src/game/game_object.rs:1459-1467 returns self.controller for Zone::Command; crates/engine/src/game/zones.rs:1169-1197 changes the zone and incarnation without normalizing that field; and crates/engine/src/game/keywords.rs:545-551 uses the helper for ProtectionTarget::ChosenPlayer. CR 109.4 says, “Only objects on the stack or on the battlefield have a controller,” except for its enumerated cases. CR 702.16k separately protects from objects the chosen player controls and objects that player owns when no other player controls them. Why it matters: a stolen ordinary card sent to the command zone retains a stale controller, so chosen-player protection can attribute it to the wrong player.

Use an exception-aware authority: preserve the controller for the explicitly modeled command-zone exception (the is_emblem path), but use owner for ordinary command-zone cards. Please add a discriminating stale-command regression for both the selected owner's card and the inverse stale-controller case, plus an exception fixture proving an emblem retains its controller. The existing integration regression covers the analogous graveyard fixtures at crates/engine/tests/integration/issue_5941_true_name_chosen_player_protection.rs:104-125, but never reaches Zone::Command.

✅ Clean

The current head’s cast → as-enters → named-choice regression is production-wired, and the head-bound parse artifact reports no card-parse changes.

Recommendation: request changes for the command-zone authority and its two-sided regression coverage; then re-review the new head.

@matthewevans matthewevans removed their assignment Aug 12, 2026
@traemyn

traemyn commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the remaining current-head blocker in 981c479. Ordinary command-zone objects now resolve source authority from owner per CR 109.4/108.4a; only explicitly modeled emblems retain controller authority under CR 109.4c. Added inverse stale-command fixtures and an emblem exception fixture. Focused test, full engine suite, strict clippy, fresh review-impl, and Gate A all pass locally. Current-head CI is queued.

@traemyn

traemyn commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Current head 981c479 is fully validated: CI run 31627339178 is green, current-head parse-diff reports no card-parse changes, and the command-zone blocker is fixed with owner authority for ordinary cards and controller authority only for modeled emblems. The fresh implementation review is clean and Gate A passes for this SHA. The remaining CHANGES_REQUESTED review is bound to 2ba1c0e; please re-review this current head.

@traemyn
traemyn requested a review from matthewevans August 12, 2026 18:49
@matthewevans matthewevans self-assigned this Aug 12, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved for the current head 981c479eb8e8308902a843a131551973f6d36a3e: chosen-player protection now uses owner for ordinary command-zone cards and preserves the emblem exception, with discriminating production-path regressions.

@matthewevans
matthewevans added this pull request to the merge queue Aug 12, 2026
@matthewevans matthewevans removed their assignment Aug 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
@matthewevans matthewevans self-assigned this Aug 12, 2026
@matthewevans
matthewevans added this pull request to the merge queue Aug 12, 2026
@matthewevans matthewevans removed their assignment Aug 12, 2026
Merged via the queue into phase-rs:main with commit 1f662a6 Aug 12, 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.

true name nemesis — True name nemesis was able to be targeted by my opponent's Song of the Dryads even though i named p…

2 participants