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
Conversation
…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
There was a problem hiding this comment.
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.
Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
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 fewerfromor more. - The full Oracle parser test verifies the exact one-card coverage change and absence of the prior swallowed clause; the existing multiplayer
PlayerAttributehand-size resolver test covers the runtime authority.
Recommendation: Merge via the queue.
Problem
Bandit's Talent (#5637) has a level-3 trigger:
The dynamic count was dropped. The "for each opponent who has one or fewer cards in hand" clause was swallowed (a
DynamicQtySwallowedClausewarning fired incard-data.json) and the draw collapsed to a flatFixed(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 hardcodedComparator::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 toFixed(1).Fix
Thread the comparator per attribute clause:
parse_hand_size_who_attr_clausecombinator recognizes"who has/have N or fewer|more cards in hand", lowering "or fewer" toLEand "or more" toGE.Comparator::GEvia amapadapter, 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 existingPlayerCount/PlayerAttribute/HandSizebuilding 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: themapadapter preserves the GE count predicates.oracle_effect/tests.rs:bandits_talent_level3_draw_counts_hellbent_opponentsparses the full card and asserts (1) the L3 draw count is the hand-sizePlayerCount(notFixed(1)) and (2) theDynamicQtyswallow warning is cleared.Full
enginelib suite green (16,683 passed, 0 failed);cargo fmt,clippy -p engine --tests, and the parser-combinator gate clean.Closes #5637