Skip to content

fix: gate discard triggers on madness intervening-if (closes #5143)#5146

Merged
matthewevans merged 10 commits into
phase-rs:mainfrom
RealDiligent:fix/issue-5143-anje-madness-discard-trigger
Jul 5, 2026
Merged

fix: gate discard triggers on madness intervening-if (closes #5143)#5146
matthewevans merged 10 commits into
phase-rs:mainfrom
RealDiligent:fix/issue-5143-anje-madness-discard-trigger

Conversation

@RealDiligent

Copy link
Copy Markdown
Contributor

Summary

  • Fix Anje Falkenrath (and similar cards) firing their discard untap rider on every discard by hoisting the "if it has madness" intervening-if instead of dropping it.
  • Add TriggerCondition::EventObjectMatchesFilter plus KeywordKind::Madness so event-object predicates can gate on off-zone keyword grants.
  • Wire mtgish discard triggers (WhenAPlayerDiscardsACard, DiscardedCardPassesFilter) for the mtgish card-data path.

Closes #5143.

Test plan

  • Parser regression: trigger_intervening_if_discarded_card_has_madness
  • Engine unit test: event_object_madness_intervening_if_gates_discard_trigger
  • CI green on Rust lint + test shards

)

Anje Falkenrath's untap rider was firing on every discard because the
madness intervening-if was dropped at parse time. Add EventObjectMatchesFilter
for event-object intervening-ifs, KeywordKind::Madness for off-zone keyword
checks, and mtgish discard-trigger conversion for the mtgish card path.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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 implements support for the "if it has madness" intervening-if condition on discard triggers (such as for Anje Falkenrath) by introducing a new EventObjectMatchesFilter trigger condition that falls back to Last Known Information (LKI) when necessary. It also updates the parser and importer to handle these discard-card filters. The review feedback correctly identifies a critical compilation error due to a duplicate definition of the discard_trigger function in crates/mtgish-import/src/convert/trigger.rs, as well as a violation of rule R1 in crates/engine/src/parser/oracle_trigger.rs where a verbatim .find() string search is used for parsing dispatch instead of nom combinators.

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 +878 to +905
/// CR 701.9 + CR 603: Build a `Discarded` trigger with the player axis on
/// `valid_target` and optional discarded-card predicates on `valid_card`.
fn discard_trigger(
players: &Players,
cards: &CardsInHand,
idiom: &'static str,
) -> ConvResult<TriggerDefinition> {
let mut def = TriggerDefinition::new(TriggerMode::Discarded);
if !matches!(players, Players::AnyPlayer) {
let controller = players_to_controller(players)?;
def.valid_target = Some(TargetFilter::Typed(
TypedFilter::default().controller(controller),
));
}
match cards {
CardsInHand::AnyCard => {}
CardsInHand::SingleCardInHand(crate::schema::types::CardInHand::ThisCardInHand) => {
def.valid_card = Some(TargetFilter::SelfRef);
}
other => {
return Err(ConversionGap::EnginePrerequisiteMissing {
engine_type: "TriggerDefinition",
needed_variant: format!("{idiom} with discarded-card filter: CardsInHand::{other:?}"),
});
}
}
Ok(def)
}

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.

critical

[CRITICAL] Duplicate definition of discard_trigger function. Evidence: crates/mtgish-import/src/convert/trigger.rs:878-905.\n\nWhy it matters: Defining the same function twice in the same module will cause a compilation error.\n\nSuggested fix: Remove the duplicate definition of discard_trigger.

// discard triggers whose subject is the discarded card (Anje Falkenrath).
// MUST precede the zone-change-object "if it " arms below, which would
// otherwise mis-route this predicate.
if let Some(pos) = tp.find("if it has madness") {

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.

high

[HIGH] Verbatim string search .find() used for parsing dispatch instead of nom combinators / parse_inner_condition. Evidence: crates/engine/src/parser/oracle_trigger.rs:3898.\n\nWhy it matters: This violates R1 (no .find for parsing dispatch) and the condition extraction rule requiring delegation to parse_inner_condition in oracle_nom/condition.rs.\n\nSuggested fix: Move the "if it has madness" parsing logic into parse_inner_condition in crates/engine/src/parser/oracle_nom/condition.rs using nom combinators, and delegate to it.

References
  1. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. Any new .find() used for parsing dispatch in non-test parser code is a violation. (link)
  2. 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 and compose them using idiomatic combinator aggregates.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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.

Thanks for working on the madness intervening-if path. I can't approve this head yet because there are two blocking issues that need to be cleaned up first.

  1. crates/mtgish-import/src/convert/trigger.rs defines discard_trigger twice in the same module. The first copy starts at line 851 and the second starts at line 880, so this head cannot compile. The current CI failures line up with that local evidence. Please collapse the new discard-card handling into the existing helper rather than adding a duplicate definition.

  2. crates/engine/src/parser/oracle_trigger.rs handles this condition with a one-off tp.find("if it has madness") check around line 3898. Parser dispatch in this repo needs to use the existing nom/combinator path or a shared condition parser so the grammar stays extensible. Please replace the verbatim phrase search with a composable parser path for the intervening-if clause.

Once those are fixed, the runtime test for the trigger condition is the right kind of boundary to keep: it should continue proving that a madness discard satisfies the condition and a non-madness discard does not.

@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.

Thanks for the quick update. The duplicate discard_trigger definition is fixed on this head.

I still can't approve yet because the parser-seam blocker remains: crates/engine/src/parser/oracle_trigger.rs still handles the madness intervening-if with tp.find("if it has madness") around line 3899. This is still a verbatim phrase search in parser dispatch rather than a composed parser/condition path, so it does not meet the parser architecture bar for a new trigger condition.

Please replace that branch with a nom/combinator path or a shared intervening-if condition parser, then keep the current positive/negative runtime boundary for EventObjectMatchesFilter.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 2 card(s), 3 signature(s) (baseline: main c8198e7f56c7)

1 card(s) · ability/Discard · added: Discard (count=1, target=controller)

Examples: Pollywog Symbiote

1 card(s) · trigger/Discarded · field condition: event object is with madness card

Examples: Anje Falkenrath

1 card(s) · trigger/SpellCast · field condition: event object is with mutate card

Examples: Pollywog Symbiote

@RealDiligent

Copy link
Copy Markdown
Contributor Author

CI green on head 9b468e370 — workflow 28745811956:

  • Rust lint (fmt, clippy, parser gate) — success
  • Rust tests (shard 1/2, shard 2/2) — success
  • Card data (generate, validate, coverage) — success
  • Frontend (lint, type-check, test) — success
  • WASM compile check — success
  • Rust aggregate gate — success

@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.

Thanks for the latest update. The previous duplicate discard_trigger definition is fixed, and the old tp.find("if it has madness") dispatch blocker is gone. I still can't approve this head because the native parser is still a one-keyword branch instead of using the existing alternate-cost keyword parser seam.

[MED] The native parser still special-cases Anje's exact rider instead of using the existing alt-cost keyword parser seam. Evidence: crates/engine/src/parser/oracle_trigger.rs:4352 hardcodes tag("if it has madness"), while crates/engine/src/parser/oracle_nom/primitives.rs:700 already owns named alternate-cost keyword parsing via parse_alt_cost_keyword_name_to_kind and was not extended with KeywordKind::Madness. Why it matters: this keeps the prior wrong-seam risk alive as a one-card parser branch; the parse-diff currently shows exactly one Anje-only signature change, but the parser infrastructure has a reusable keyword-name axis for this class. Suggested fix: add Madness to parse_alt_cost_keyword_name_to_kind and parse if it has <alt-cost keyword> into the same HasKeywordKind event-object condition through that shared combinator.

Add Madness to parse_alt_cost_keyword_name_to_kind and parse
"if it has <alt-cost keyword>" via the shared combinator instead of a
hardcoded madness-only tag branch.

Co-authored-by: Cursor <cursoragent@cursor.com>
@RealDiligent

Copy link
Copy Markdown
Contributor Author

Addressed the alt-cost keyword seam feedback on head 3cc40a177:

  • Added KeywordKind::Madness to parse_alt_cost_keyword_name_to_kind in oracle_nom/primitives.rs
  • Replaced the hardcoded tag("if it has madness") branch with parse_event_object_has_alt_cost_keyword_intervening_if, which delegates the keyword name to the shared combinator

Ready for another look when you have a moment.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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.

The prior parser-seam blocker looks addressed on this head: parse_event_object_has_alt_cost_keyword_intervening_if now delegates the keyword name through parse_alt_cost_keyword_name_to_kind, and Madness is registered there. I still can't approve/enqueue because the exact current head is failing the mechanical Rust lint gate.

[HIGH] Current head fails rustfmt. Evidence: crates/engine/src/parser/oracle_trigger.rs:3898 and crates/engine/src/parser/oracle_trigger.rs:4360; cargo fmt --all -- --check on head 3d64338f37f545ffaca835d3b3696ffcdcc736f8 rewrites both spots, and the live Rust lint (fmt, clippy, parser gate) job is failing for this head. Why it matters: this exact head cannot pass the required Rust lint gate, so it is not ready for handler approval/enqueue. Suggested fix: run cargo fmt --all, commit the resulting oracle_trigger.rs formatting, and repush.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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.

The parser-seam issue remains resolved on this head, but the exact current head still fails the mechanical Rust formatting gate.

[HIGH] Current head fails rustfmt. Evidence: crates/engine/src/parser/oracle_trigger.rs:4359; cargo fmt --all -- --check on head 3952b06557744ee3a7ee4eda4d0e5310a50f9bb0 rewrites the EventObjectMatchesFilter construction. Why it matters: this exact head cannot pass the required Rust lint gate, so it is not ready for approval/enqueue. Suggested fix: run cargo fmt --all, commit the resulting oracle_trigger.rs formatting, and repush.

RealDiligent and others added 3 commits July 6, 2026 01:04
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@RealDiligent

Copy link
Copy Markdown
Contributor Author

CI green on head 20d442a36 — all workflow jobs success.

  • Matthew feedback: madness intervening-if now routes through shared parse_alt_cost_keyword_name_to_kind + event_object_has_alt_cost_keyword_condition helper (not a one-card madness tag branch).
  • rustfmt: helper filter: block reformatted; restored KeywordKind import dropped in an earlier cleanup commit.

Ready for re-review.

@matthewevans matthewevans self-assigned this Jul 5, 2026
@matthewevans matthewevans added the bug Bug fix label Jul 5, 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.

Maintainer sweep: reviewed current head after the follow-up fixes. The madness intervening-if gate is at the trigger scan/read-write seam, has a discriminating engine test, and current CI is green.

@matthewevans matthewevans added this pull request to the merge queue Jul 5, 2026
@matthewevans matthewevans removed their assignment Jul 5, 2026
Merged via the queue into phase-rs:main with commit bcdde1f Jul 5, 2026
11 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.

[Card Bug] Anje Falkenrath discard trigger always untaps her disregarding whether the discarded card has madness or not

2 participants