Skip to content

fix(parser): scope spell-cast triggers to a required spell keyword (#4754)#5886

Merged
matthewevans merged 1 commit into
phase-rs:mainfrom
jaytbarimbao-collab:fix/slitherwisp-flash-spell-trigger-scope
Jul 15, 2026
Merged

fix(parser): scope spell-cast triggers to a required spell keyword (#4754)#5886
matthewevans merged 1 commit into
phase-rs:mainfrom
jaytbarimbao-collab:fix/slitherwisp-flash-spell-trigger-scope

Conversation

@jaytbarimbao-collab

Copy link
Copy Markdown
Contributor

Fixes #4754

Problem

Slitherwisp reads "Flash\nWhenever you cast another spell that has flash, you draw a card and each opponent loses 1 life." The spell-cast trigger dropped the "that has flash" clause: for the payload spell that has flash, parse_spell_qualifier_payloadparse_post_spell_modifier("that has flash") returned None, so the parse fell back to parse_type_phrase, which consumed "spell" and discarded "that has flash" as leftover. The trigger's valid_card ended up with only FilterProp::Another, so it fired on every other spell — the reported bug: casting a counterspell (no flash) wrongly triggered Slitherwisp.

Fix

Add a parse_post_spell_modifier arm that recognizes a "that has <keyword>" / "with <keyword>" suffix and emits FilterProp::WithKeyword { value }. It is gated on a fully consumed recognized keyword (via parse_keyword_line_core, checking the remainder is empty), so:

  • the numeric/colored qualifiers handled by earlier arms — "with mana value N", "with {X} in its mana cost", "with one or more <color> mana symbol" — keep their arms (they run first and return early), and
  • a non-keyword tail ("with power 3 or greater", "with an {X}") still declines here and falls through unchanged.

The Another prop is still appended by the caller's add_another_prop, so Slitherwisp's filter becomes [WithKeyword(Flash), Another]. The object matcher already honors FilterProp::WithKeyword via obj.has_keyword (game/filter.rs), so no runtime change is required — the existing valid_card_matchestarget_filter_matches_object path now filters cast spells by keyword.

Tests

  • Parser (slitherwisp_cast_another_flash_spell_scopes_to_flash): the trigger's valid_card carries both WithKeyword(Flash) and Another.
  • Runtime (slitherwisp_flash_spell_cast_trigger.rs, 2 cases): drives the real cast pipeline and asserts Slitherwisp's observable effect (each opponent loses 1 life + controller draws) fires only for a flash spell — a plain spell leaves opponent life and hand untouched. This proves the fix is not merely parse-correct-but-inert.

CR references: 702.8a (flash), 603.2 (trigger events).

…hase-rs#4754)

Slitherwisp ("Whenever you cast another spell that has flash, ...") dropped
the "that has flash" clause: parse_type_phrase consumed "spell" and discarded
the keyword tail, so the trigger's valid_card carried only FilterProp::Another
and fired on every other spell — a non-flash counterspell wrongly triggered it.

Add a parse_post_spell_modifier arm that recognizes "that has <keyword>" /
"with <keyword>" and emits FilterProp::WithKeyword, gated on a fully consumed
recognized keyword (parse_keyword_line_core with an empty remainder) so the
numeric/colored "with mana value ..." / "with ... mana symbol" qualifiers keep
their earlier arms and non-keyword tails still decline. The object matcher
already honors WithKeyword via obj.has_keyword, so no runtime change is needed.

Tests: a parser test (Slitherwisp -> WithKeyword(Flash) + Another) and a runtime
pair proving the trigger's observable effect (each opponent loses 1 life +
controller draws) fires only for a flash spell, not a plain spell.

@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 addresses issue #4754 by implementing parsing support for spell modifiers containing 'that has ' or 'with ' clauses (such as 'that has flash' on Slitherwisp), preventing spell-cast triggers from over-firing. The changes include parser updates, unit tests, and a runtime integration test. The review feedback suggests simplifying the parser logic in oracle_trigger.rs by removing redundant value combinators in favor of a simpler alt setup.

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.

Comment on lines +14725 to +14729
if let Ok((keyword_text, ())) = alt((
value((), tag::<_, _, OracleError<'_>>("that has ")),
value((), tag("with ")),
))
.parse(modifier)

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.

medium

We can simplify the parser by removing the redundant value combinators. Since both tag parsers return &str (the matched prefix), alt can directly compose them and return the matched prefix, which we can discard with a wildcard pattern _. This makes the code more concise and idiomatic.

Suggested change
if let Ok((keyword_text, ())) = alt((
value((), tag::<_, _, OracleError<'_>>("that has ")),
value((), tag("with ")),
))
.parse(modifier)
if let Ok((keyword_text, _)) = alt((
tag::<_, _, OracleError<'_>>("that has "),
tag("with "),
))
.parse(modifier)
References
  1. Decompose compound phrases into modular, reusable parsers for constituent parts and compose them using idiomatic combinator aggregates (like nested alt and tag sequences) to prevent combinatorial explosion and improve maintainability.

@github-actions

Copy link
Copy Markdown

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

2 card(s) · trigger/SpellCast · field watches: with Cascade cardwith Cascade

Examples: Aurora Phoenix, The First Doctor

2 card(s) · trigger/SpellCast · field watches: with Convoke

Examples: Joyful Stormsculptor, Saint Traft and Rem Karolus

1 card(s) · trigger/SpellCast · field watches: another cardwith Convoke another

Examples: Kasla, the Broken Halo

1 card(s) · trigger/SpellCast · field watches: another cardwith Flash another

Examples: Slitherwisp

1 card(s) · trigger/SpellCast · field watches: non-legendary with Flying creaturenon-legendary creature + with Flying

Examples: Donal, Herald of Wings

1 card(s) · trigger/SpellCast · field watches: with Infect creaturecreature + with Infect

Examples: Hand of the Praetors

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

@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

  • Gemini's value-combinator suggestion is not actionable: this parser module already uses value((), tag(...)) to normalize alternative prefixes, which is the same explicit discard pattern used here.

✅ Clean

  • The keyword predicate is added at the post-spell-modifier parser seam and reuses the strict all-consuming keyword parser.
  • The eight-card parse diff is the intended shared grammar class, with no unexplained cards.
  • The Slitherwisp integration tests use the real cast/trigger pipeline and discriminate both the allowed Flash and rejected non-Flash branches.

Recommendation: Merge via the queue.

@matthewevans matthewevans added the bug Bug fix label 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 d0371d0 Jul 15, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slitherwisp triggers when casting counterspell — Slitherwisp should only trigger when I cast another spell with flash.

2 participants