parser: compose leading action with as-enters persisted choice (Anointed Peacekeeper ETB)#1
Open
keloide wants to merge 1 commit into
Open
parser: compose leading action with as-enters persisted choice (Anointed Peacekeeper ETB)#1keloide wants to merge 1 commit into
keloide wants to merge 1 commit into
Conversation
…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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the previously-
UnimplementedETB 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)
/engine-implementerpipeline (plan → review-plan → implement → review-impl → commit)/engine-implementer— explain why belowThe 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.txtis network-blocked in this environment and could not be freshly grepped):oracle_replacement.rs).RevealHand { reveal: false }(reveal_hand.rs).Verification
Developer track — Tilt is down in this environment, so cargo was run directly.
cargo fmt --all— clean.cargo test -p engine --lib— 14,686 pass, 0 real failures, including regressionsas_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 localcard-data.jsonartifact —snuff_out_from_card_database_*/witherbloom_card_object_*self-skip on a clean tree, confirmed.)cargo test -p engine --test anointed_peacekeeper_etb— 1 pass (drives the real cast → ETB replacement → private hand-look → name-a-card pipeline; asserts persistedChosenAttribute::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../scripts/check-parser-combinators.shexit 0 (no output on success; no violations).What changed (build for the class, not the card)
try_parse_named_choiceparser/oracle_effect/mod.rsparse_hand_possessive_targetparser/oracle_effect/imperative.rsRevealHandplayer-from-filter fallback viacollect_player_targetsgame/effects/reveal_hand.rsRevealHandreached with no player target slotparse_as_enters_chooseparser/oracle_replacement.rsLines 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)
tag("a card name")/tag("a nonland card name")(oracle_effect/mod.rs:17754-17757); determiner-variant armstag("a color other than ")/tag("a color")(mod.rs:17731,17737).value(Typed(Opponent), tag("target opponent's hand"))(imperative.rs:3137-3140);value(Player, tag("target player's hand"))(imperative.rs:3136).collect_player_targets(state, ability, &target)ineffects/phase_out.rs:51,91andeffects/effect.rs:411.scan_preceded(...)inoracle_effect/search.rs:93,105,201&oracle_modal.rs:561; the existingSetTapState{SelfRef}.sub_ability(choose)composition in the same function (oracle_replacement.rs:1756-1762).Scope expansion
None. Pure composition of existing effects (
RevealHand+Choose) plus one additive resolver fallback. No newEffect/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-
supportedafter 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 toStaticMode::ReduceAbilityCost, which has noexemptionfield, so the CR 605.1a "unless they're mana abilities" clause is dropped and the swallow-checkdetect_condition_unlesscorrectly emits aCondition_Unlessswallowed-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 (addingexemption: ActivationExemptionacross ~17ReduceAbilityCostsites + 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