feat(parser): recognize "Players may spend mana as though it were mana of any color"#5558
Conversation
…a of any color"
CR 609.4b: Mycosynth Lattice and Mycosynthwave read "Players may spend mana as
though it were mana of any color" — the all-players form of the board-wide
any-color concession. The dispatcher only matched the controller form ("You
may spend mana …") via a verbatim string `==` check, so the "Players may"
subject dropped the entire static and no player gained the permission.
Fix: replace the verbatim `==` match with a nom recognizer
`parse_board_wide_spend_any_color` (mirroring `is_speed_unlock_sentence`) that
accepts both the "You may" and "Players may" subject prefixes on the bare form
(no activation-source / spell-class qualifier tail). Both map to the same
`SpendManaAsAnyColor` static with `affected: TargetFilter::Player`, which
`check_static_ability` already scopes to every player — so no runtime change is
needed. The tailed forms ("… to activate abilities of X", "… to cast it") are
consumed by earlier arms and never reach this recognizer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a parser helper parse_board_wide_spend_any_color to match board-wide mana-spending concessions (such as Chromatic Orrery and Mycosynth Lattice) and replaces a verbatim string check. However, the feedback points out a critical rule fidelity issue: mapping both 'you may' and 'players may' to the same all-players concession violates the Magic Comprehensive Rules (CR 109.5 and CR 609.4b), as 'you may' should only apply to the controller. The reviewer suggests refactoring the parser to return the correct TargetFilter and properly propagate parser errors.
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.
| fn parse_board_wide_spend_any_color(lower: &str) -> bool { | ||
| all_consuming(terminated( | ||
| preceded( | ||
| alt(( | ||
| tag::<_, _, OracleError<'_>>("you may "), | ||
| tag("players may "), | ||
| )), | ||
| tag("spend mana as though it were mana of any color"), | ||
| ), | ||
| opt(tag(".")), | ||
| )) | ||
| .parse(lower) | ||
| .is_ok() | ||
| } |
There was a problem hiding this comment.
[HIGH] Incorrect player scoping for Chromatic Orrery vs Mycosynth Lattice. Evidence: crates/engine/src/parser/oracle_static/dispatch.rs:100.
Why it matters: Mapping both "You may" (Chromatic Orrery) and "Players may" (Mycosynth Lattice) to TargetFilter::Player allows all players to spend mana as any color under Chromatic Orrery, violating CR 109.5 and CR 609.4b.
Suggested fix: Refactor the parser to return the correct TargetFilter (TargetFilter::Controller for "you may" and TargetFilter::Player for "players may"). Decompose the compound phrase into modular, reusable parsers for constituent parts (subject and action) to avoid verbatim string matching. Additionally, ensure unexpected parser errors (like nom::Err::Failure) are propagated instead of being swallowed by .ok().
| fn parse_board_wide_spend_any_color(lower: &str) -> bool { | |
| all_consuming(terminated( | |
| preceded( | |
| alt(( | |
| tag::<_, _, OracleError<'_>>("you may "), | |
| tag("players may "), | |
| )), | |
| tag("spend mana as though it were mana of any color"), | |
| ), | |
| opt(tag(".")), | |
| )) | |
| .parse(lower) | |
| .is_ok() | |
| } | |
| fn parse_board_wide_spend_any_color(lower: &str) -> Result<Option<TargetFilter>, nom::Err<OracleError<'_>>> { | |
| let mut parser = all_consuming(terminated( | |
| pair( | |
| alt(( | |
| value(TargetFilter::Controller, tag("you")), | |
| value(TargetFilter::Player, tag("players")), | |
| )), | |
| tag(" may spend mana as though it were mana of any color"), | |
| ), | |
| opt(tag(".")), | |
| )); | |
| match parser.parse(lower) { | |
| Ok((_, (filter, _))) => Ok(Some(filter)), | |
| Err(nom::Err::Error(_)) => Ok(None), | |
| Err(e) => Err(e), | |
| } | |
| } |
References
- Strict fidelity to the MTG Comprehensive Rules (CR) — every game rule, validation, and computed value matches the CR exactly. Convenience shortcuts that get rules wrong are not simpler; they are wrong. (link)
- Avoid swallowing all errors (e.g., using
.ok()) when only specific errors are expected to be handled or ignored. Propagate unexpected errors (such as invariant violations or invalid actions) to prevent masking critical bugs. - Avoid verbatim string equality for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts (e.g., subjects, conjunctions) and compose them using idiomatic combinator aggregates (like nested
altandtagsequences) to prevent combinatorial explosion and improve maintainability.
Parse changes introduced by this PR · 2 card(s), 2 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Verdict: clean, functional fix — anti-verbatim-string refactor that correctly delivers the all-players concession. Holding only for CI.
✅ Clean
- Right refactor. Replaces a verbatim
== "you may spend mana as though it were mana of any color"string match with aparse_board_wide_spend_any_colornom recognizer (all_consuming(terminated(preceded(alt((tag("you may "), tag("players may "))), tag("spend mana as though it were mana of any color")), opt(tag("."))))), mirroring the existingis_speed_unlock_sentencehelper. This is exactly the anti-verbatim-string mandate — the subject is now onealt()axis, so bothMycosynth Lattice/Mycosynthwave("Players may") and the controller form share one recognizer. - Functional, not cosmetic. Verified the produced shape actually grants to every player:
affected: Some(TargetFilter::Player)→static_filter_matches(static_abilities.rs:1819) returnstruefor allplayer_idon theTargetFilter::Playerarm ("All players match"). So opponents genuinely gain the any-color concession, which is the #5290-class bug being fixed. - CR 609.4b grep-verified verbatim ("allows a player to spend mana 'as though it were mana of any [type or color]'").
- Discriminating tests —
static_players_may_spend_mana_as_any_colorasserts the mode +affected == Some(TargetFilter::Player)(fails on the pre-fix drop), plus astatic_you_may_...still_parsesregression guard.
🟡 Non-blocking (pre-existing, not introduced here)
- The
returnblock is unchanged, so the bare"You may spend mana as though it were mana of any color."form still maps toaffected: TargetFilter::Player(all players). For a genuinely controller-scoped bare "You may" card that would over-grant to opponents (TargetFilter::Controllerwould be the correct affected filter). In practice the tailed forms (Chromatic Orrery's "… to cast spells") are consumed by earlier arms and never reach here, so this is latent, not a regression. Worth a follow-up but out of scope for this PR.
Recommendation: approve as bug on settled green.
matthewevans
left a comment
There was a problem hiding this comment.
Approve — clean anti-verbatim-string refactor; the board-wide any-color concession now reaches all players.
✅ Clean
- Right seam / anti-verbatim. Replaces the
== "you may spend mana…"verbatim string check withparse_board_wide_spend_any_color, a nom recognizer folding theyou may/players maysubject into a singlealt()axis (mirrorsis_speed_unlock_sentence). This is exactly the decomposition the parser mandate calls for — no card-name or full-sentence==. - Functional, not cosmetic.
affected: TargetFilter::Playerroutes throughstatic_filter_matches→ the "all players match" arm (static_abilities.rs:1819), so Mycosynth Lattice / Mycosynthwave genuinely grant every player the "spend mana as though any color" permission — the "Players may" subject no longer drops the entire static. - CR 609.4b grep-verified for the mana-spending permission.
- Discriminating tests + a regression guard on the dropped-subject path.
🟡 Non-blocking (pre-existing, not a regression)
- The unchanged return still maps a bare "You may" form to all-players scope. Latent in the prior code too; out of scope here.
Recommendation: approve and enqueue as bug. CI is fully green (both Rust shards, lint, coverage-gate, card-data) on the reviewed head.
CR 609.4b
Bug
Mycosynth Lattice and Mycosynthwave read "Players may spend mana as though it were mana of any color" — the all-players form of the board-wide any-color concession. But the dispatcher only matched the controller form ("You may spend mana …") via a verbatim string
==check, so the "Players may" subject dropped the entire static and no player gained the permission.Fix
Replace the verbatim
==match with a nom recognizerparse_board_wide_spend_any_color(mirroring the existingis_speed_unlock_sentencehelper) that accepts both the "You may" and "Players may" subject prefixes on the bare form. Both map to the sameSpendManaAsAnyColorstatic withaffected: TargetFilter::Player, whichcheck_static_abilityalready scopes to every player — so no runtime change is needed.Why it's the right seam / minimal blast radius
tp.lower.trim_end_matches('.') == "…") in favor of a combinator — the same idiom the siblingis_speed_unlock_sentenceuses.all_consumingkeeps the recognizer to the bare form; the qualified tails ("… to activate abilities of X", "… to cast it") are consumed by earlier arms (try_parse_spend_any_color_to_activate_abilities, the type_change "to cast it" grant) and never reach here — verified unchanged.Tests
static_players_may_spend_mana_as_any_color— "Players may …" →SpendManaAsAnyColor { .. }withaffected: Some(TargetFilter::Player).static_you_may_spend_mana_as_any_color_still_parses— regression: the "You may" form still parses to the same shape.parser::oracle_static::tests(1047 passed, 0 failed), including the "… to activate abilities of creatures you control" activation-source-filtered case.CR verification
docs/MagicCompRules.txt.AI-contributor notes
TargetFilter::Playerto all players.cargo fmt, CI-exactclippy -p engine --all-targets --features proptest -D warnings, and the full oracle_static module are green.🤖 Generated with Claude Code