fix: gate attacks-alone triggers on zero co-attackers (closes #5159)#5162
fix: gate attacks-alone triggers on zero co-attackers (closes #5159)#5162RealDiligent wants to merge 1 commit into
Conversation
…rs#5159) Parse "attacks alone" into MinCoAttackers { minimum: 0 } so Sharon Carter / Exalted-class triggers fire only when a single creature attacks. Co-authored-by: Cursor <[email protected]>
There was a problem hiding this comment.
Code Review
This pull request implements support for the "attacks alone" trigger qualifier by encoding it as a zero-co-attacker gate (TriggerCondition::MinCoAttackers { minimum: 0 }) in both the native parser and the mtgish-import converter, accompanied by integration tests. The review feedback identifies violations of R1 due to using standard string methods (strip_prefix) instead of nom combinators in the parser, as well as violations of R6 regarding missing or incorrect CR citations (citing CR 508.3a instead of CR 506.5).
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.
| /// CR 508.3a: Strip a trailing " alone" qualifier from attack trigger event | ||
| /// text ("attacks alone", Exalted / Sharon Carter class). The co-attacker gate | ||
| /// is encoded separately as `TriggerCondition::MinCoAttackers { minimum: 0 }`. | ||
| fn strip_attack_alone_qualifier(after: &str) -> (bool, &str) { | ||
| if let Some(rest) = after.strip_prefix(" alone") { | ||
| (true, rest) | ||
| } else { | ||
| (false, after) | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Use nom combinators and correct CR rule citation for "attacks alone". Evidence: crates/engine/src/parser/oracle_trigger.rs:7995.
Why it matters: R1 requires all new parser dispatch to use nom combinators rather than standard string methods like strip_prefix, and R6 requires verified CR annotations (CR 506.5 defines "attacks alone", not CR 508.3a). Suggested fix: Rewrite the helper using nom's tag combinator and update the CR citation.
| /// CR 508.3a: Strip a trailing " alone" qualifier from attack trigger event | |
| /// text ("attacks alone", Exalted / Sharon Carter class). The co-attacker gate | |
| /// is encoded separately as `TriggerCondition::MinCoAttackers { minimum: 0 }`. | |
| fn strip_attack_alone_qualifier(after: &str) -> (bool, &str) { | |
| if let Some(rest) = after.strip_prefix(" alone") { | |
| (true, rest) | |
| } else { | |
| (false, after) | |
| } | |
| } | |
| /// CR 506.5: Strip a trailing " alone" qualifier from attack trigger event | |
| /// text ("attacks alone", Exalted / Sharon Carter class). The co-attacker gate | |
| /// is encoded separately as TriggerCondition::MinCoAttackers { minimum: 0 }. | |
| fn strip_attack_alone_qualifier(after: &str) -> (bool, &str) { | |
| if let Ok((rest, _)) = nom::bytes::complete::tag::<_, _, nom::error::Error<&str>>(" alone")(after) { | |
| (true, rest) | |
| } else { | |
| (false, after) | |
| } | |
| } |
References
- R1. Nom combinators on the first pass — no exceptions. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. (link)
- R6. CR annotations are mandatory and verified. Every rules-touching line of engine code must carry a comment of the form CR : . (link)
- 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.
| if attacks_alone { | ||
| def.condition = Some(TriggerCondition::MinCoAttackers { | ||
| minimum: 0, | ||
| filter: None, | ||
| }); | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Missing mandatory CR annotation on rules-touching code. Evidence: crates/engine/src/parser/oracle_trigger.rs:8365.
Why it matters: R6 requires every rules-touching line of engine code to carry a comment of the form CR <number>: <description> to ensure compliance and traceability. Suggested fix: Add a comment citing CR 506.5 above the condition block.
| if attacks_alone { | |
| def.condition = Some(TriggerCondition::MinCoAttackers { | |
| minimum: 0, | |
| filter: None, | |
| }); | |
| } | |
| if attacks_alone { | |
| // CR 506.5: A creature attacks alone if it's the only creature declared as an attacker. | |
| def.condition = Some(TriggerCondition::MinCoAttackers { | |
| minimum: 0, | |
| filter: None, | |
| }); | |
| } |
References
- R6. CR annotations are mandatory and verified. Every rules-touching line of engine code must carry a comment of the form CR : . (link)
| // CR 508.3a: "Whenever [a creature] attacks alone" — same firing axis | ||
| // as a regular attack trigger; the "alone" qualifier (single-attacker | ||
| // batch) has no engine field today. Mirrors native parser at | ||
| // oracle_trigger.rs:8273 (collapses to `TriggerMode::Attacks`). | ||
| // as a regular attack trigger; the "alone" qualifier is a | ||
| // zero-co-attacker gate via `MinCoAttackers { minimum: 0 }`. |
There was a problem hiding this comment.
[MEDIUM] Incorrect CR rule citation for "attacks alone". Evidence: crates/mtgish-import/src/convert/trigger.rs:575.
Why it matters: CR 508.3a describes general attack triggers, whereas CR 506.5 is the specific rule that defines what it means for a creature to "attack alone". Suggested fix: Update the comment to reference CR 506.5.
| // CR 508.3a: "Whenever [a creature] attacks alone" — same firing axis | |
| // as a regular attack trigger; the "alone" qualifier (single-attacker | |
| // batch) has no engine field today. Mirrors native parser at | |
| // oracle_trigger.rs:8273 (collapses to `TriggerMode::Attacks`). | |
| // as a regular attack trigger; the "alone" qualifier is a | |
| // zero-co-attacker gate via `MinCoAttackers { minimum: 0 }`. | |
| // CR 506.5: "Whenever [a creature] attacks alone" — same firing axis | |
| // as a regular attack trigger; the "alone" qualifier is a | |
| // zero-co-attacker gate via MinCoAttackers { minimum: 0 }. |
References
- R6. CR annotations are mandatory and verified. Every rules-touching line of engine code must carry a comment of the form CR : . (link)
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for taking this on, but the current encoding does not implement the attacks alone gate.
TriggerCondition::MinCoAttackers is an at-least condition: the runtime counts same-controller co-attackers and checks co_attacker_count >= minimum. Setting minimum: 0 therefore always passes, including when the source attacks with one or more other creatures. That means the new parser/converter shape still allows the Agent 13 trigger to fire during multi-creature attacks, which is the bug this PR is meant to fix.
Rules evidence: CR 506.5 defines a creature as attacking alone only when it is attacking and no other creatures are. Please model this as an exact/maximum zero co-attacker gate, or add an appropriate parameterized condition shape, and keep the multi-attacker regression covering that runtime behavior. Also note the PR is currently DIRTY against base and still needs the parse-diff evidence before it can be considered for approval.
Summary
"attacks alone"on attack triggers intoTriggerCondition::MinCoAttackers { minimum: 0 }(CR 508.3a — zero co-attackers).WhenACreatureAttacksAlone.Closes #5159.
Test plan
trigger_samurai_or_warrior_attacks_aloneparser unit testissue_5159_solo_attack_investigates_onceissue_5159_multi_attack_does_not_investigate