Skip to content

parser: compose leading action with as-enters persisted choice (Anointed Peacekeeper ETB)#1

Open
keloide wants to merge 1 commit into
mainfrom
claude/phase-developer-track-cwj807
Open

parser: compose leading action with as-enters persisted choice (Anointed Peacekeeper ETB)#1
keloide wants to merge 1 commit into
mainfrom
claude/phase-developer-track-cwj807

Conversation

@keloide

@keloide keloide commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the previously-Unimplemented ETB line of Anointed Peacekeeper — "As ~ enters the battlefield, look at an opponent's hand, then choose any card name." — by composing existing engine primitives into four general, reusable building blocks. No new engine enum variant.

Implementation method (required)

  • Produced via the /engine-implementer pipeline (plan → review-plan → implement → review-impl → commit)
  • Not /engine-implementer — explain why below

The plan-review caught two Delta phase-rs#4 defects (offset-into-original vs. ~-substituted text; an over-broad peeled-slot guard) which were folded in before implementation; the implementation-review returned "essentially clean, no HIGH/MED," and its three LOW items were resolved.

CR references

All reused verbatim from existing in-repo annotations (docs/MagicCompRules.txt is network-blocked in this environment and could not be freshly grepped):

  • CR 614.1c / 614.1d — "as ~ enters" self-replacement scope & composition (oracle_replacement.rs).
  • CR 701.20a / 701.20e — reveal (public) vs. look-at (private) hand; RevealHand { reveal: false } (reveal_hand.rs).
  • CR 800.4a — "an opponent" controller choice; first-opponent resolution is two-player-exact, multiplayer-simplified (honestly annotated).
  • CR 201.3 / 607.2d / 613.1 — card-name choice / persisted chosen attribute (test annotations).

Verification

Developer track — Tilt is down in this environment, so cargo was run directly.

  • cargo fmt --all — clean.
  • cargo test -p engine --lib14,686 pass, 0 real failures, including regressions as_enters_choose_a_color, as_enters_choose_two_colors, as_enters_choose_a_creature_type, as_enters_choose_a_number_sentence_ending_period, as_enters_choose_does_not_match_shock_land, enters_tapped_then_choose_color_composes_tap_and_choice (Thriving land). (The 2 apparent failures were a truncated local card-data.json artifact — snuff_out_from_card_database_* / witherbloom_card_object_* self-skip on a clean tree, confirmed.)
  • cargo test -p engine --test anointed_peacekeeper_etb1 pass (drives the real cast → ETB replacement → private hand-look → name-a-card pipeline; asserts persisted ChosenAttribute::CardName, RevealHand { reveal:false } over the opponent's hand not the controller's, and both cost-tax statics bound).
  • cargo clippy -p engine --lib --test anointed_peacekeeper_etb -- -D warnings — clean, exit 0.
  • Gate A./scripts/check-parser-combinators.sh exit 0 (no output on success; no violations).

cargo test -p engine (all 145 integration binaries) was intentionally not run — it link-fills the container disk. Only --lib and the single new --test were built. CI has full disk headroom.

What changed (build for the class, not the card)

# Delta File Class unlocked
1 Accept "any card name" in try_parse_named_choice parser/oracle_effect/mod.rs every "choose any card name" card (alongside the a/nonland/creature/land forms)
2 Accept non-targeted "an opponent's hand" in parse_hand_possessive_target parser/oracle_effect/imperative.rs every non-targeted "look at / reveal an opponent's hand" card
3 RevealHand player-from-filter fallback via collect_player_targets game/effects/reveal_hand.rs every non-targeted RevealHand reached with no player target slot
4 Compose an optional leading action before the persisted choice in parse_as_enters_choose parser/oracle_replacement.rs the general "As ~ enters, ⟨action⟩, then choose ⟨X⟩" template

Lines 3 & 4 of the card (the spell / activated-ability cost taxes keyed to the chosen name) already parsed and already consume a persisted ChosenAttribute::CardName; this change makes the ETB establish that persisted name plus the private hand-look.

Anchored on (≥2 analogous implementations per delta, same combinator family)

Scope expansion

None. Pure composition of existing effects (RevealHand + Choose) plus one additive resolver fallback. No new Effect/enum variant; no frontend/AI/multiplayer-filter/targeting registration touched (lines 3/4 already consume the chosen name this ETB now establishes).

Known limitation (honest, out of scope)

This card does not flip to coverage-supported after this change, because of a separate, pre-existing gap unrelated to line 2: line 4 ("Activated abilities of sources with the chosen name cost {2} more to activate unless they're mana abilities") parses to StaticMode::ReduceAbilityCost, which has no exemption field, so the CR 605.1a "unless they're mana abilities" clause is dropped and the swallow-check detect_condition_unless correctly emits a Condition_Unless swallowed-clause warning. This is proven independent of this change: a probe card containing only lines 1/3/4 (no ETB) emits the identical warning. Modeling that exemption spans the cost-modification subsystem (adding exemption: ActivationExemption across ~17 ReduceAbilityCost sites + resolver application + a resolver test) and belongs in a dedicated follow-up PR. This diff does not mask the dropped line-4 semantic; it only implements line 2.

🤖 Generated with Claude Code

https://claude.ai/code/session_019mjar7ovGZua3ANz9r76fk


Generated by Claude Code

…ted Peacekeeper ETB)

Implement Anointed Peacekeeper's previously-Unimplemented ETB line
("As ~ enters the battlefield, look at an opponent's hand, then choose any
card name.") by composing existing engine primitives into four general
building blocks — no new engine enum variant:

1. try_parse_named_choice: accept "any card name" alongside the existing
   a/nonland/creature/land card-name forms (CardName choice).
2. parse_hand_possessive_target: accept non-targeted "an opponent's hand"
   (controller-scoped Opponent), routing "look at an opponent's hand" to a
   private RevealHand { reveal: false }.
3. reveal_hand::resolve: additive player-from-filter fallback via
   collect_player_targets when no explicit player target slot exists
   (replacement/resolution context). Explicit TargetRef::Player fast path
   and Any-target MissingParam behavior unchanged.
4. parse_as_enters_choose: compose an optional leading action before the
   persisted Choose, anchoring on the last enters-frame; honest-coverage
   gate (returns None, leaving the line Unimplemented) if the leading
   clause does not parse, so no semantics are silently dropped.

Lines 3 & 4 (cost taxes keyed to the chosen name) already consumed a
persisted ChosenAttribute::CardName; this ETB now establishes it plus the
private hand-look.

CR annotations reused from existing in-repo citations (614.1c/d, 701.20a/e,
800.4a, 201.3, 607.2d, 613.1).

Tests: parser unit tests (any-card-name accept + negatives; an-opponent's-hand
private look + siblings), resolver tests (opponent-hand fallback with
multi-authority hostile fixture, explicit-target fast path, Any->MissingParam),
as-enters composition + honest-gate tests, and an end-to-end cast pipeline test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_019mjar7ovGZua3ANz9r76fk
matthewevans added a commit that referenced this pull request Jul 4, 2026
… — play historic from TOP OF LI (phase-rs#4341)

* fix(parser): UNSUPPORTED cluster: The Fourth Doctor (Blast COMMANDER) — play historic from TOP OF LI

* fix(PR-4341): address maintainer review — honest unsupported rider, land-play frequency, text/lower desync, context restore

Maintainer @matthewevans raised four issues on PR phase-rs#4341 (The Fourth Doctor
parser cluster). All four are addressed here.

1. Reflexive trigger provenance (issue #1):
   Replace the rules-incorrect `TriggerMode::PlayCard` trigger with a
   `TriggerMode::Unknown` gap marker. A global PlayCard trigger cannot
   distinguish which permission authorized a given play (CR 603.12), so
   the "When you do" rider was firing even when a different top-of-library
   permission authorized the play. The Unknown trigger keeps the gap
   visible in coverage until the casting/land-play pipeline gains
   permission-provenance tracking.

2. Land-play frequency consumption (issue #2):
   Add `record_top_of_library_land_permission` in engine.rs and call it
   from all three completion arms of `handle_play_land`. A
   `OncePerTurn` `TopOfLibraryCastPermission { play_mode: Play }` now
   consumes the per-turn slot on land plays, mirroring how
   `finalize_cast` records this for spell casts (CR 401.5 + CR 601.2a).

3. Context mutation without restore (issue phase-rs#3 / Gemini):
   The reflexive rider is no longer parsed at all (Unknown trigger is
   emitted unconditionally), eliminating the ctx.subject/ctx.actor
   mutation that was leaking into subsequent lines.

4. text/lower desync on frequency-prefix strip (issue phase-rs#4 / Gemini):
   In `try_parse_top_of_library_cast_permission`, shadow both `text` and
   `lower` together when stripping the "once each turn, " / "once during
   each of your turns, " prefix, so downstream helpers receive aligned
   slices (Gemini phase-rs#3 suggestion, CR 601.2a).

5. Redundant scan_contains pre-check removed (Gemini #2):
   The `scan_contains` guard before `split_once_on_lower` in oracle.rs
   was redundant — `try_parse_top_of_library_cast_permission` already
   validates the anchor. Removed for clarity.

Test: `the_fourth_doctor_full_card_parse` updated to expect
`TriggerMode::Unknown` instead of `TriggerMode::PlayCard`.

Verification: cargo clippy -p engine --all-targets -- -D warnings: clean.
cargo test -p engine: all pass.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Claude-Session: https://claude.ai/code/session_019aQYsGCjiRn71Z4vQDo9QR

* fix(PR-4341): capture top-of-library land permission before zone change

The `record_top_of_library_land_permission` helper was called in the
land-play epilogue — after `zone_pipeline::deliver` had already moved the
land from Library → Battlefield. At that point `top_of_library_permission_source`
reads `player.library.front()`, which now points to the *next* card, so
`top_id != object_id` always triggered and the slot was silently left
unconsumed. A `OncePerTurn` top-of-library `Play` permission (The Fourth
Doctor shape) could therefore be reused indefinitely for lands.

Fix: capture `(src_id, frequency)` from `top_of_library_permission_source`
*before* the replacement pipeline, alongside the existing
`in_library_with_permission` eligibility check. The simplified
`record_top_of_library_land_permission` now accepts the pre-captured pair
instead of re-deriving it post-delivery. All three epilogue paths
(Execute + `NeedsChoice` replacement prompt, `NeedsChoice` directly, and
the normal path) use the captured value.

Also adds `once_per_turn_library_land_play_consumes_slot_and_blocks_second_play`,
a production-path regression test that plays two historic lands from the
top of the library under a `OncePerTurn` `play_mode: Play` permission,
asserts the first play stamps `top_of_library_cast_permissions_used`, and
asserts the second play is rejected — the discriminating assertion that
the pre-capture bug made impossible to satisfy.

Addresses matt's round-4 review blocker (2026-06-28).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Claude-Session: https://claude.ai/code/session_019aQYsGCjiRn71Z4vQDo9QR

* fix(CR-annotations): replace CR 601.2a with CR 305.1/116.2a/401.5 on land-play paths

Matt's 2026-07-01 review (CHANGES_REQUESTED) identified that all six
CR 601.2a annotations in the top-of-library land-play path cite the
spell-casting-to-stack procedure (601.2a) instead of the rules that
actually govern this path:
  - CR 305.1: playing a land is a special action, not a spell cast
  - CR 116.2a: playing a land puts the card onto the battlefield from
    the zone it was in (zero stack involvement)
  - CR 401.5: top-of-library visibility closes after the special action

Fixed locations:
  - engine.rs: doc-comment on record_top_of_library_land_permission (×1)
  - engine.rs: inline comment in handle_play_land pre-capture block (×1)
  - engine.rs: three call-sites in the success/NeedsChoice/fallthrough
    branches of handle_play_land that record the permission slot (×3)
  - engine_tests.rs: doc-comment on the once-per-turn slot regression
    test (×1)

All six now cite CR 305.1 + CR 116.2a + CR 401.5 with explanatory text.
The two remaining mentions of CR 601.2a in the updated comments are
explicit "does not apply here" notes, which is the correct documentation.

Verification: cargo clippy -p engine --all-targets -- -D warnings (clean),
cargo test -p engine (all pass), cargo fmt --all (no changes).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Claude-Session: https://claude.ai/code/session_019aQYsGCjiRn71Z4vQDo9QR

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: matthewevans <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants