fix(engine): parse and resolve Nether Spirit's dropped self-reanimation intervening-if#5884
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements parsing and runtime support for Nether Spirit's intervening-if condition ("~ is the only [card] in "). It introduces the parse_source_is_only_type_in_zone parser, which correctly hoists off-battlefield zones into a SourceInZone conjunct to allow proper trigger detection and zone origin derivation. Comprehensive unit and integration tests are added to verify the behavior. The reviewer suggests simplifying the parser's return statement by returning the parsed remainder subslice directly instead of calculating a consumed offset via pointer arithmetic.
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.
| let consumed = remainder.as_ptr() as usize - input.as_ptr() as usize; | ||
| Ok((&input[consumed..], condition)) |
…on intervening-if
CR 603.4: Nether Spirit's "if this card is the only creature card in your
graveyard" clause was never hoisted into TriggerDefinition.condition -
split_trigger's first-comma split had nothing to land on since "At the
beginning of your upkeep" has no comma, so the entire intervening-if +
effect fell through to the effect side untouched, leaving condition: null.
That also left trigger_zones stuck at the structural default
["Battlefield"], making the trigger undetectable from the graveyard, and
ChangeZone.origin null.
Adds parse_source_is_only_type_in_zone (oracle_nom/condition.rs), a new
combinator for "~ is the only <type> [card] in <zone>" composed from
existing building blocks (parse_source_self_token, parse_type_phrase,
ObjectCount == 1 QuantityComparison). When the type-phrase filter carries
a non-battlefield zone, wraps the condition in
StaticCondition::And{[SourceInZone{zone}, quantity]} so the existing,
unmodified trigger_condition_source_zones walker (CR 113.6 / CR 113.6b)
auto-derives trigger_zones and ChangeZone.origin - zero runtime code
changed. Precedent: Jocasta, Automaton Avenger already drives the same
And{SourceInZone, ...} derivation end-to-end.
Also removes a stale game/coverage.rs exclusion that was silencing the
semantic auditor for this exact bug.
8fbcc4a to
7b16bc6
Compare
Parse changes introduced by this PR · 1 card(s), 3 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Verdict: Approved
🔴 Blocker
None.
🟡 Non-blocking
None.
✅ Clean
- The condition is parsed at the existing state-condition authority and uses the established
ObjectCount == 1plusSourceInZonerepresentation, allowing existing trigger-zone and return-origin derivation to do the runtime work. - The current parse diff is exactly one card, Nether Spirit: condition, active trigger zone, and
ChangeZoneorigin all move from absent/default state to the intended graveyard semantics. - The registered integration suite reaches trigger collection, the intervening-if fire-time and resolution-time checks, the optional choice, and the graveyard-to-battlefield move. The reviewer-requested direct remainder return is included in the current head.
Recommendation: approved and queued for merge.
Summary
Nether Spirit (
{1}{B}{B}, Tempest): "At the beginning of your upkeep, if this card is the only creature card in your graveyard, you may return this card to the battlefield."The intervening-if was dropped entirely by the parser (
condition: nullin card-data.json).split_triggersplits Oracle text at the first unrecognized comma; since "At the beginning of your upkeep" has no comma, the whole "if this card is the only creature card in your graveyard, you may return this card to the battlefield" fell through to the effect side untouched — there was no combinator inparse_remaining_state_presence_conditionsrecognizing "~is the only<type>[card] in<zone>". That also lefttrigger_zonesstuck at the structural default["Battlefield"](making the trigger undetectable while the card sits in the graveyard) andChangeZone.originnull.parse_source_is_only_type_in_zone(oracle_nom/condition.rs), composed entirely from existing building blocks (parse_source_self_token,parse_type_phrase, anObjectCount == 1QuantityComparison). When the type-phrase filter carries a non-battlefield zone, wraps the condition inStaticCondition::And{[SourceInZone{zone}, quantity]}so the existing, unmodifiedtrigger_condition_source_zoneswalker auto-derivestrigger_zones/ChangeZone.origin— zero runtime code changed. Precedent: Jocasta, Automaton Avenger already drives the identicalAnd{SourceInZone, ...}derivation end-to-end.game/coverage.rsexclusion that was silencing the semantic auditor for this exact bug (confirmed via card-data.json scan: the excluded substring matches exactly this one card).CR 603.4 (intervening-if, checked at trigger time and re-checked at resolution), CR 113.6 / CR 113.6b (ability functions off-battlefield only from the zone it states).
Test plan
condition.rs): graveyard form →And[SourceInZone(Graveyard), ObjectCount==1]; battlefield form → bareQuantityComparison(no spuriousSourceInZone); rejection guard (no "only" → parse error)oracle_trigger_tests.rs): assertscondition.is_some(),trigger_zones == [Graveyard],ChangeZone.origin == Some(Graveyard)for Nether Spirit's verbatim Oracle textnether_spirit_only_creature_card_intervening_if.rs) driving the realprocess_triggers/stack::resolve_toppipeline: does-not-fire with 2 creature cards, positive reach-guard, fires-and-returns when sole, decline leaves it in graveyard, CR 603.4 resolution-time recheck fizzles when a 2nd creature card enters in response, two-copies-neither-firescargo fmt --all, parser combinator gate, CR-annotation gate all passcargo clippy -p engine --lib --tests -- -D warningscleancargo test -p engine --lib(16687+ tests) and--test integrationgreen after rebase onto current main