Skip to content

fix(parser): count opponents by hand-size threshold in "for each opponent who has N or fewer/more cards in hand" (Bandit's Talent)#5887

Merged
matthewevans merged 1 commit into
phase-rs:mainfrom
galuis116:fix/for-each-opponent-hand-size-threshold
Jul 15, 2026
Merged

fix(parser): count opponents by hand-size threshold in "for each opponent who has N or fewer/more cards in hand" (Bandit's Talent)#5887
matthewevans merged 1 commit into
phase-rs:mainfrom
galuis116:fix/for-each-opponent-hand-size-threshold

Conversation

@galuis116

@galuis116 galuis116 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Bandit's Talent (#5637) has a level-3 trigger:

At the beginning of your draw step, draw an additional card for each opponent who has one or fewer cards in hand.

The dynamic count was dropped. The "for each opponent who has one or fewer cards in hand" clause was swallowed (a DynamicQty SwallowedClause warning fired in card-data.json) and the draw collapsed to a flat Fixed(1) -- so it drew exactly one card regardless of how many opponents were hellbent.

Root cause

parse_for_each_opponent_player_attribute_clause (the "for each opponent who ..." count path) only recognized the count-style predicates -- "who drew N or more cards this turn" (Smuggler's Share) and "who had N or more ... enter the battlefield ..." -- and hardcoded Comparator::GE. Hand-size was never attempted on this path, and the strict "or fewer" direction had no representation, so the clause failed to parse and the count silently fell back to Fixed(1).

Fix

Thread the comparator per attribute clause:

  • New parse_hand_size_who_attr_clause combinator recognizes "who has/have N or fewer|more cards in hand", lowering "or fewer" to LE and "or more" to GE.
  • The existing GE-only count predicates are tagged with Comparator::GE via a map adapter, so their behavior is unchanged.

The clause now lowers to PlayerCount { PlayerAttribute { relation: Opponent, attr: HandSize, comparator: LE|GE, value: Fixed(N) } }. This is a class-level fix -- every "for each opponent who has N or fewer/more cards in hand" card benefits, in both comparator directions -- reusing the existing PlayerCount / PlayerAttribute / HandSize building blocks (no new AST).

Tests

  • oracle_quantity.rs:
    • for_each_opponent_hand_size_or_fewer_lowers_to_le_player_count -- LE 1.
    • for_each_opponent_hand_size_or_more_lowers_to_ge_player_count -- GE 3 (proves the comparator is carried per direction).
    • for_each_opponent_cards_drawn_still_lowers_to_ge -- regression: the map adapter preserves the GE count predicates.
  • oracle_effect/tests.rs:
    • bandits_talent_level3_draw_counts_hellbent_opponents parses the full card and asserts (1) the L3 draw count is the hand-size PlayerCount (not Fixed(1)) and (2) the DynamicQty swallow warning is cleared.

Full engine lib suite green (16,683 passed, 0 failed); cargo fmt, clippy -p engine --tests, and the parser-combinator gate clean.

Closes #5637

…nent who has N or fewer/more cards in hand" (Bandit's Talent phase-rs#5637)

Bandit's Talent's level-3 trigger — "At the beginning of your draw step, draw an
additional card for each opponent who has one or fewer cards in hand" — dropped
its dynamic count: the "for each opponent who has one or fewer cards in hand"
clause was swallowed (a `DynamicQty` swallow warning fired) and the draw
collapsed to a flat `Fixed(1)`, so it drew one card no matter how many opponents
were hellbent.

`parse_for_each_opponent_player_attribute_clause` only recognized the count-style
predicates ("who drew N or more cards this turn", "who had N or more … enter the
battlefield …") and hardcoded `Comparator::GE`. Hand-size was never tried on this
path, and the strict "or fewer" direction had no representation.

Fix: thread the comparator per attribute clause. A new
`parse_hand_size_who_attr_clause` combinator recognizes "who has/have N or
fewer|more cards in hand", lowering "or fewer" to `LE` and "or more" to `GE`; the
existing GE-only count predicates are tagged with `Comparator::GE` via a `map`
adapter. The clause now lowers to `PlayerCount { PlayerAttribute { Opponent,
HandSize, LE|GE, Fixed(N) } }`. Class-level: every "for each opponent who has N
or fewer/more cards in hand" card benefits, both comparator directions.

Tests:
- oracle_quantity.rs: `for_each_opponent_hand_size_or_fewer_lowers_to_le_player_count`
  (LE 1), `for_each_opponent_hand_size_or_more_lowers_to_ge_player_count` (GE 3),
  and `for_each_opponent_cards_drawn_still_lowers_to_ge` (regression: the map
  adapter preserves the GE count predicates).
- oracle_effect/tests.rs: `bandits_talent_level3_draw_counts_hellbent_opponents`
  parses the full card and asserts the L3 draw count is the hand-size PlayerCount
  (not Fixed(1)) and the DynamicQty swallow warning is cleared.

Closes phase-rs#5637
@galuis116 galuis116 requested a review from matthewevans as a code owner July 15, 2026 21:48

@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 updates the parser in crates/engine/src/parser/oracle_quantity.rs to support both 'or fewer' and 'or more' comparators for opponent hand-size attribute clauses (e.g., 'opponent who has one or fewer cards in hand'), mapping them to Comparator::LE and Comparator::GE respectively. It generalizes parse_for_each_opponent_player_attribute_clause to carry the comparator dynamically, using nom::combinator::map to default existing count-style clauses to Comparator::GE. Appropriate unit tests and CR annotations (CR 402.1, CR 109.5) have been added. No review comments were provided, and the implementation is fully compliant with the repository's architectural guidelines, so there is no additional feedback.

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.

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main e3667d0bc421)

1 card(s) · ability/Draw · field count: # of each opponent whose HandSize { player: ScopedPlayer } LE Fixed { value: 1 }

Examples: Bandit's Talent

@matthewevans matthewevans self-assigned this Jul 15, 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.

Verdict: Approved

🔴 Blocker

  • None.

🟡 Non-blocking

  • None.

✅ Clean

  • The parser extends the existing per-candidate player-attribute authority instead of creating a Bandit's Talent special case, and the comparator is carried where the grammar distinguishes or fewer from or more.
  • The full Oracle parser test verifies the exact one-card coverage change and absence of the prior swallowed clause; the existing multiplayer PlayerAttribute hand-size resolver test covers the runtime authority.

Recommendation: Merge via the queue.

@matthewevans matthewevans added bug Bug fix quality For high-quality minimal to no-churn PRs labels Jul 15, 2026
@matthewevans matthewevans added this pull request to the merge queue Jul 15, 2026
@matthewevans matthewevans removed their assignment Jul 15, 2026
Merged via the queue into phase-rs:main with commit b81c77f Jul 15, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix quality For high-quality minimal to no-churn PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bandit's Talent: live Replacement/DynamicQty swallow warning, untriaged — verify levels parse correctly

2 participants