feat(keywords): implement Reinforce keyword (CR 702.77a) - #12
Conversation
CR 702.77a: "Reinforce N—[cost]" means "[Cost], Discard this card:
Put N +1/+1 counters on target creature."
Implementation:
- types/keywords.rs: Add Keyword::Reinforce { count, cost } variant with
FromStr parsing and MTGJSON deserialization support
- parser/oracle_keyword.rs: Add Oracle text parser for "reinforce N—{cost}"
using nom tag combinator (allow-noncombinator for em-dash separators)
- database/synthesis.rs: Add synthesize_reinforce() producing an activated
ability with composite cost (mana + self-discard), +1/+1 counter effect,
and activation_zone = Some(Zone::Hand)
Cards unlocked (5): Earthbrawn, Hunting Triad, Break Ties, Fowl Strike,
Swell of Courage
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0505e3ebe1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // CR 702.77a: Reinforce N—{cost} — "[Cost], Discard this card: Put N +1/+1 counters | ||
| // on target creature." Same N—{cost} format as Suspend/Awaken. | ||
| if let Ok((rest, _)) = tag::<_, _, OracleError<'_>>("reinforce ").parse(text) { | ||
| if let Ok((after_count, count)) = nom_primitives::parse_number.parse(rest.trim()) { |
There was a problem hiding this comment.
Support X-valued reinforce counts
This only accepts fixed numeric counts, so printed cards with Reinforce X—... (for example Swell of Courage and Wren's Run Hydra in the repo data) still fail keyword extraction even though they are part of the reinforce mechanic being implemented. The count also needs to stay linked to the chosen X for the mana cost at activation/resolution; model it with a quantity/X-capable value rather than u32 and parse with the existing X-aware quantity primitive.
Useful? React with 👍 / 👎.
…aralysis)
Implements the missing static/replacement effect for Fear of Sleep
Paralysis: "Stun counters can't be removed from permanents your
opponents control."
Changes:
- types/statics.rs: Add StaticMode::CountersCantBeRemoved { counter_type }
variant with all required trait impls (Hash, Display, FromStr, to_kind,
as_keyword).
- game/coverage.rs: Register in is_data_carrying_static().
- game/turns.rs: Add counter_removal_blocked() helper gating the untap-step
stun-counter removal (both main path and Seedborn-Muse secondary path).
- parser/oracle_static/dispatch.rs: Add parse_counters_cant_be_removed_static()
to parse the Oracle text pattern.
- game/turns.rs (tests): Two integration tests verifying the prohibition
blocks removal and the baseline still works.
Comprehensive Rules: CR 122.1d, CR 101.2
Card: Fear of Sleep Paralysis (DSC #12)
…aralysis)
Implements the missing static/replacement effect for Fear of Sleep
Paralysis: "Stun counters can't be removed from permanents your
opponents control."
Changes:
- types/statics.rs: Add StaticMode::CountersCantBeRemoved { counter_type }
variant with all required trait impls (Hash, Display, FromStr, to_kind,
as_keyword).
- game/coverage.rs: Register in is_data_carrying_static().
- game/turns.rs: Add counter_removal_blocked() helper gating the untap-step
stun-counter removal (both main path and Seedborn-Muse secondary path).
- parser/oracle_static/dispatch.rs: Add parse_counters_cant_be_removed_static()
to parse the Oracle text pattern.
- game/turns.rs (tests): Two integration tests verifying the prohibition
blocks removal and the baseline still works.
Comprehensive Rules: CR 122.1d, CR 101.2
Card: Fear of Sleep Paralysis (DSC #12)
…aralysis)
Implements the missing static/replacement effect for Fear of Sleep
Paralysis: "Stun counters can't be removed from permanents your
opponents control."
Changes:
- types/statics.rs: Add StaticMode::CountersCantBeRemoved { counter_type }
variant with all required trait impls (Hash, Display, FromStr, to_kind,
as_keyword).
- game/coverage.rs: Register in is_data_carrying_static().
- game/turns.rs: Add counter_removal_blocked() helper gating the untap-step
stun-counter removal (both main path and Seedborn-Muse secondary path).
- parser/oracle_static/dispatch.rs: Add parse_counters_cant_be_removed_static()
to parse the Oracle text pattern.
- game/turns.rs (tests): Two integration tests verifying the prohibition
blocks removal and the baseline still works.
Comprehensive Rules: CR 122.1d, CR 101.2
Card: Fear of Sleep Paralysis (DSC #12)
…aralysis)
Implements the missing static/replacement effect for Fear of Sleep
Paralysis: "Stun counters can't be removed from permanents your
opponents control."
Changes:
- types/statics.rs: Add StaticMode::CountersCantBeRemoved { counter_type }
variant with all required trait impls (Hash, Display, FromStr, to_kind,
as_keyword).
- game/coverage.rs: Register in is_data_carrying_static().
- game/turns.rs: Add counter_removal_blocked() helper gating the untap-step
stun-counter removal (both main path and Seedborn-Muse secondary path).
- parser/oracle_static/dispatch.rs: Add parse_counters_cant_be_removed_static()
to parse the Oracle text pattern.
- game/turns.rs (tests): Two integration tests verifying the prohibition
blocks removal and the baseline still works.
Comprehensive Rules: CR 122.1d, CR 101.2
Card: Fear of Sleep Paralysis (DSC #12)
…aralysis)
Implements the missing static/replacement effect for Fear of Sleep
Paralysis: "Stun counters can't be removed from permanents your
opponents control."
Changes:
- types/statics.rs: Add StaticMode::CountersCantBeRemoved { counter_type }
variant with all required trait impls (Hash, Display, FromStr, to_kind,
as_keyword).
- game/coverage.rs: Register in is_data_carrying_static().
- game/turns.rs: Add counter_removal_blocked() helper gating the untap-step
stun-counter removal (both main path and Seedborn-Muse secondary path).
- parser/oracle_static/dispatch.rs: Add parse_counters_cant_be_removed_static()
to parse the Oracle text pattern.
- game/turns.rs (tests): Two integration tests verifying the prohibition
blocks removal and the baseline still works.
Comprehensive Rules: CR 122.1d, CR 101.2
Card: Fear of Sleep Paralysis (DSC #12)
…aralysis) (phase-rs#5663) Implements the missing static/replacement effect for Fear of Sleep Paralysis: "Stun counters can't be removed from permanents your opponents control." Changes: - types/statics.rs: Add StaticMode::CountersCantBeRemoved { counter_type } variant with all required trait impls (Hash, Display, FromStr, to_kind, as_keyword). - game/coverage.rs: Register in is_data_carrying_static(). - game/turns.rs: Add counter_removal_blocked() helper gating the untap-step stun-counter removal (both main path and Seedborn-Muse secondary path). - parser/oracle_static/dispatch.rs: Add parse_counters_cant_be_removed_static() to parse the Oracle text pattern. - game/turns.rs (tests): Two integration tests verifying the prohibition blocks removal and the baseline still works. Comprehensive Rules: CR 122.1d, CR 101.2 Card: Fear of Sleep Paralysis (DSC #12) Co-authored-by: Whovencroft <6.60056e+06+Whovencroft@users.noreply.github.com>
What
Implement the Reinforce keyword (CR 702.77a) end-to-end: parsing, deserialization, Oracle text recognition, and activated-ability synthesis.
Why
Reinforce is a gap-1 keyword — implementing it unlocks 5 cards that have no other unimplemented mechanics:
How
crates/engine/src/types/keywords.rsKeyword::Reinforce { count: u32, cost: ManaCost }variantkind()catch-all listFromStrparsing for the "reinforce N {cost}" format (MTGJSON string representation)keyword_from_taggeddeserialization for the{ "count": N, "cost": "..." }JSON shapecrates/engine/src/parser/oracle_keyword.rsnom::bytes::complete::tagcombinatorstrip_prefixwithallow-noncombinatorannotations (punctuation, not parsing dispatch)keyword_display_namearm forKeyword::Reinforcecrates/engine/src/database/synthesis.rssynthesize_reinforce()function that produces an activated ability:Composite { Mana { cost }, Discard { count: 1, self_ref: true } }PutCounter { Plus1Plus1, Fixed(N), target: Creature }activation_zone = Some(Zone::Hand)(activated from hand, discard as cost)synthesize_all()reinforce_synthesis_testsmodule (4 tests)Tests
cargo test -p engine --lib -- reinforce→ 4 new tests passcargo test -p engine --lib -- synthesis_tests→ 135 pass (no regressions)cargo test -p engine --lib -- keyword→ 364 pass (no regressions)cargo test -p engine --lib -- oracle_keyword→ 73 pass (no regressions)cargo test -p engine --lib -- coverage→ 67 pass (no regressions)cargo fmt -- --check→ cleancargo clippy -p engine→ cleanbash scripts/check-parser-combinators.sh→ EXIT 0CR Reference
Cards Unlocked