Skip to content

Parse counted "you had N or more [type] enter this turn" conditions (Park Heights Pegasus)#5087

Open
ultrahighsuper wants to merge 2 commits into
phase-rs:mainfrom
ultrahighsuper:feat/had-count-enter-this-turn-condition
Open

Parse counted "you had N or more [type] enter this turn" conditions (Park Heights Pegasus)#5087
ultrahighsuper wants to merge 2 commits into
phase-rs:mainfrom
ultrahighsuper:feat/had-count-enter-this-turn-condition

Conversation

@ultrahighsuper

Copy link
Copy Markdown
Contributor

Problem

Park Heights Pegasus (in docs/parser-misparse-backlog.md):

Whenever this creature deals combat damage to a player, draw a card if you had two or more creatures enter the battlefield under your control this turn.

The trailing effect gate is an effect-level conditional (CR 608.2c) on a CR 400.7 this-turn ETB threshold. It was being dropped entirely: the card drew a card on every combat-damage trigger, unconditionally, instead of only when two or more creatures had entered under your control that turn.

Root cause

parse_entered_this_turn (oracle_nom/condition.rs) — the combinator behind parse_inner_condition, the single authority for game-state conditions — handled the "you had ..." surface only through a subject path that hardcoded a count of 1 and required a leading article (a/an/another). The counted "you had two or more creatures enter ..." surface matched neither the article path nor the bare past-tense "N or more [type] entered ..." branch, so it returned Err, the trailing if never re-homed, and the draw resolved unconditionally.

Fix

Extract a shared parse_or_more_entered_count combinator and use it for both surfaces:

  • bare past-tense — "N or more creatures entered the battlefield under your control this turn"
  • "you had"-auxiliary present-tense — "you had N or more creatures enter the battlefield under your control this turn"

Both denote the same CR 400.7 this-turn ETB tally, so count and suffix are the only axes that vary — one combinator, no duplication (the past-tense branch previously inlined this logic).

The combinator emits StaticCondition::QuantityComparison { EnteredThisTurn{creature/You} >= N }. The rest of the pipeline was already in place:

  • static_condition_to_ability_condition bridges QuantityComparison → AbilityCondition::QuantityCheck,
  • strip_suffix_conditional re-homes the trailing if onto the draw effect,
  • the runtime already evaluates both QuantityCheck and QuantityRef::EnteredThisTurn.

So no new engine plumbing — this is a pure parser-completeness fix that unlocks the whole "you had N or more [type] enter … this turn" class, not just this card.

Tests

  • Building block (condition.rs): test_you_had_two_or_more_enter_this_turn asserts the counted surface yields EnteredThisTurn{creature/You} >= 2 (not the singular >= 1 the article-only path produced).
  • Runtime revert-probe (crates/engine/tests/park_heights_pegasus_draw_condition_runtime.rs): parses the real draw clause through the production parse_effect_chain path and resolves it under P0.
    • Discriminator: with only one creature entered this turn, the >= 2 gate suppresses the draw. Verified this test fails without the fix (the dropped condition draws unconditionally — left: 1, right: 0).
    • Positive: with two creatures entered, exactly one card is drawn.

CR

  • CR 400.7 — an object entered the battlefield this turn (matches the established FilterProp::EnteredThisTurn / QuantityRef::EnteredThisTurn annotations).
  • CR 608.2c — conditional (if) clause on an effect.
  • CR 109.5 / CR 205 — "under your control" scopes the tally to the controller.

The condition combinator's "you had ..." branch hardcoded a count of 1 and
required a leading article, so the counted threshold surface — "you had two
or more creatures enter the battlefield under your control this turn" (Park
Heights Pegasus) — failed to parse. Downstream, the trailing effect gate was
dropped entirely and the card drew a card on every combat-damage trigger,
unconditionally instead of only when two or more creatures had entered.

Teach parse_entered_this_turn the counted "N or more [type] enter ... this
turn" form by extracting a shared parse_or_more_entered_count combinator used
by both the bare past-tense surface ("N or more creatures entered ...") and
the "you had"-auxiliary present-tense surface ("you had N or more creatures
enter ..."). Both denote the same CR 400.7 this-turn ETB tally, so the count
and suffix are the only axes that vary. The combinator emits
QuantityComparison { EnteredThisTurn{creature/You} >= N }, which the effect
pipeline already bridges onto the draw as AbilityCondition::QuantityCheck and
the runtime already evaluates — no new engine plumbing required.

Tests: a building-block condition test asserting the GE 2 threshold, plus a
runtime revert-probe (crates/engine/tests) confirming the parsed draw is
suppressed when only one creature entered this turn (the dropped condition
would draw unconditionally) and fires when two entered.

@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 resolves an issue where the parser dropped the 'two or more' condition under the 'you had' auxiliary (e.g., on Park Heights Pegasus), causing the effect to trigger unconditionally. It introduces a shared helper function parse_or_more_entered_count in condition.rs to correctly parse 'N or more [type] ' into a greater-than-or-equal-to (GE) threshold comparison. Additionally, a unit test and a comprehensive integration regression test suite have been added to verify both the parsing and runtime behavior of this condition. There are no review comments, and the changes are architecturally sound and compliant with the repository style guide.

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.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MED] The new parser path uses the live-board EnteredThisTurn quantity instead of the existing battlefield-entry history authority. Evidence: crates/engine/src/parser/oracle_nom/condition.rs:6258 builds QuantityRef::EnteredThisTurn, whose resolver only counts objects still in Zone::Battlefield with entered_battlefield_turn == Some(state.turn_number) (crates/engine/src/game/quantity.rs:2530). The repo already has QuantityRef::BattlefieldEntriesThisTurn for “who had N or more [type] enter ... this turn,” backed by battlefield_entries_this_turn snapshots that count entries even after the object leaves (crates/engine/src/types/ability.rs:4809). Why it matters: Park Heights Pegasus should still see a creature that entered under your control this turn even if it died, bounced, or was sacrificed before the combat-damage draw condition resolves; the current implementation incorrectly suppresses the draw in that legal game state. Suggested fix: lower the counted “you had N or more [type] enter ... this turn” condition to the battlefield-entry snapshot quantity/condition authority and add a regression where one of the two entering creatures has left the battlefield before the draw condition resolves.

Also, the parser-diff sticky is still absent on this head; please let the current CI produce <!-- coverage-parse-diff --> before this is reconsidered.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 14 card(s), 7 signature(s) (baseline: main db50424a3d0c)

5 card(s) · static/Continuous · field conditional: in battlefield you control permanent non-land entered this turn ≥ 2battlefield entries this turn (permanent non-land, you) ≥ 2

Examples: Armory Mice, Gallant Pie-Wielder, Goddric, Cloaked Reveler (+2 more)

5 card(s) · trigger/Phase · field condition: in battlefield you control permanent non-land entered this turn ≥ 2battlefield entries this turn (permanent non-land, you) ≥ 2

Examples: Belligerent of the Ball, Bespoke Battlegarb, Lady of Laughter (+2 more)

1 card(s) · trigger/Attacks · field condition: in battlefield you control permanent non-land entered this turn ≥ 2battlefield entries this turn (permanent non-land, you) ≥ 2

Examples: Ash, Party Crasher

1 card(s) · ability/Draw · field conditional: battlefield entries this turn (creature, you) ≥ 2

Examples: Park Heights Pegasus

1 card(s) · ability/Draw · field duration: until end of turn

Examples: Park Heights Pegasus

1 card(s) · trigger/Phase · field condition: in battlefield you control artifact entered this turn ≥ 3battlefield entries this turn (artifact, you) ≥ 3

Examples: Malcator, Purity Overseer

1 card(s) · trigger/Phase · field condition: in battlefield you control creature entered this turn ≥ 2battlefield entries this turn (creature, you) ≥ 2

Examples: Spider-UK

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

…rn threshold

Per review: the counted "you had N or more [type] enter the battlefield under
your control this turn" condition must count entries even after the object has
left. The prior implementation used QuantityRef::EnteredThisTurn, whose
resolver only counts objects still on the battlefield with
entered_battlefield_turn == current turn, so Park Heights Pegasus would wrongly
suppress the draw when one of the entering creatures died, bounced, or was
sacrificed before the combat-damage draw resolved.

Lower the shared parse_or_more_entered_count combinator (both the past-tense
"N or more ... entered ..." and the present-tense "you had N or more ... enter
..." surfaces) to QuantityRef::BattlefieldEntriesThisTurn { player:
PlayerScope::Controller, filter } — the battlefield_entries_this_turn snapshot
authority (CR 403.3 + CR 608.2h) that persists across the object leaving.
PlayerScope::Controller scopes the tally to "under your control", so the type
filter carries no controller of its own (mirroring the existing "who had N or
more ... enter" for-each authority).

Tests: update the two counted-condition unit assertions to the snapshot
authority, and add a runtime regression where both entering creatures have
already left the battlefield before the draw resolves — the draw still fires
(it would incorrectly fail under the live-board EnteredThisTurn count).
@ultrahighsuper

Copy link
Copy Markdown
Contributor Author

Thanks — good catch, fixed in fb2e8344b.

Authority switched to the battlefield-entry snapshot. The counted parse_or_more_entered_count combinator now lowers to QuantityRef::BattlefieldEntriesThisTurn { player: PlayerScope::Controller, filter } instead of the live-board EnteredThisTurn. That reads the battlefield_entries_this_turn snapshots (CR 403.3 + CR 608.2h), so a creature that entered under your control this turn still counts after it has died / bounced / been sacrificed before the combat-damage draw resolves. PlayerScope::Controller scopes "under your control" (the runtime keys on record.controller), so the type filter carries no controller of its own — matching the existing who had N or more … enter for-each authority.

This applies to both counted surfaces the shared combinator serves — the present-tense you had N or more … enter … (Park Heights Pegasus) and the bare past-tense N or more … entered … — since both describe the same historical entry tally; the two counted-condition unit assertions were updated accordingly.

Regression added (pegasus_draws_when_entered_creatures_have_since_left): two creatures are recorded as having entered under P0's control this turn, but neither is on the battlefield when the draw resolves — the draw still fires. That test fails under the old live-board count (0 survivors → suppressed) and passes on the snapshot authority.

The parser-diff sticky should populate once this head's CI finishes (it was still mid-run at the previous review).

@matthewevans matthewevans self-assigned this Jul 4, 2026
@matthewevans

Copy link
Copy Markdown
Member

Reviewed the updated fb2e8344b head.

The previous code concern looks resolved: the counted you had N or more ... enter ... this turn path now lowers to QuantityRef::BattlefieldEntriesThisTurn { player: PlayerScope::Controller, ... }, and the added runtime coverage includes the important case where the entered creatures have already left the battlefield before the draw condition resolves.

I am still holding this from approval/enqueue for process proof, not for CI: the PR body is missing the required AI-contributor proof sections (Summary, Files changed, Track, LLM, and Verification). Please update the PR body with those sections and include the exact verification you ran / are relying on. Once that is present, this can be reconsidered against the current head.

@matthewevans matthewevans removed their assignment Jul 4, 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.

Reviewed current head fb2e8344bd5377f2baed5ccd9cf85fc042068b2f.

The previous runtime concern looks addressed: the counted you had N or more ... enter ... this turn path now uses the battlefield-entry snapshot authority, and the runtime regression covers entered permanents that have already left before the condition resolves.

I still need one cleanup before approval. The new CR annotations cite the wrong rule for the historical look-back behavior. In crates/engine/src/parser/oracle_nom/condition.rs and the Park Heights Pegasus runtime test, the comments describe objects still counting after leaving the battlefield but cite CR 608.2h; in the current rules that look-back exception is CR 608.2i, while the battlefield-zone rule should be cited separately. Please update the annotations to cite the correct rule pair for historical ETB membership, and keep CR 608.2h only where the code is specifically relying on information read during resolution.

Also please refresh the PR body so the proof matches the current implementation; it still describes the old EnteredThisTurn lowering.

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