Skip to content

fix(parser): existential opponent control activation restrictions - #3002

Merged
matthewevans merged 4 commits into
phase-rs:mainfrom
claytonlin1110:fix/opponent-controls-more-activation-restrictions
Jun 11, 2026
Merged

fix(parser): existential opponent control activation restrictions#3002
matthewevans merged 4 commits into
phase-rs:mainfrom
claytonlin1110:fix/opponent-controls-more-activation-restrictions

Conversation

@claytonlin1110

Copy link
Copy Markdown
Contributor

Summary

  • Fixes #2908: Weathered Wayfarer's "Activate only if an opponent controls more lands than you" restriction is enforced via activation_restrictions, not the top-level condition field (which is intentionally None for resolution-time conditions).
  • Adds parser support for the related pattern "an opponent controls at least N more [type] than you" (Isolated Watchtower), placed before the generic controls + parse_ge_threshold arm so "at least two more lands" is not mis-parsed as "at least two" + type "more lands than you".
  • Routes cross-player quantity comparisons before event-based "an opponent [verb]" arms in parse_restriction_condition.

Context

The Discord report looked at abilities[0].condition: null and concluded the restriction was dropped. Activation gates belong in activation_restrictions as RequiresCondition { condition: Some(...) } per CR 602.5b. Weathered Wayfarer was already correct in card-data after #2835/#859; this PR hardens the class and adds card-database integration coverage for the production hydration path.

Test plan

  • cargo test -p engine --test integration weathered_wayfarer
  • cargo test -p engine --lib opponent_controls_at_least
  • cargo test -p engine --lib parses_activate_only_if_opponent
  • Tilt card-data regen (Isolated Watchtower export will pick up the new parse on next pipeline run; parser unit tests cover AST shape today)

@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 parsing and resolution for activation restrictions where an opponent controls more (or at least N more) of a specific card type than you, addressing issues related to cards like Weathered Wayfarer and Isolated Watchtower. Feedback suggests refactoring the parser logic in crates/engine/src/parser/oracle_nom/condition.rs to avoid nested if let Ok chains and fragile verbatim parsing by using modular nom combinators, adhering to Rule R1 of the style guide.

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 +5024 to +5060
if let Ok((rest2, _)) = tag::<_, _, OracleError<'_>>("controls at least ").parse(rest) {
if let Ok((rest3, n)) = parse_number(rest2) {
if let Ok((rest4, _)) = tag::<_, _, OracleError<'_>>(" more ").parse(rest3) {
if let Ok((rest5, type_text)) =
take_until::<_, _, OracleError<'_>>(" than you").parse(rest4)
{
let (rest5, _) = tag(" than you").parse(rest5)?;
let (type_filter, _) = parse_type_phrase(type_text.trim());
let you_filter = inject_controller_you(type_filter.clone());
return Ok((
rest5,
StaticCondition::QuantityComparison {
lhs: QuantityExpr::Ref {
qty: QuantityRef::PlayerCount {
filter: PlayerFilter::ControlsCount {
relation: PlayerRelation::Opponent,
filter: type_filter,
comparator: Comparator::GE,
count: Box::new(QuantityExpr::Offset {
inner: Box::new(QuantityExpr::Ref {
qty: QuantityRef::ObjectCount {
filter: you_filter,
},
}),
offset: n as i32,
}),
},
},
},
comparator: Comparator::GE,
rhs: QuantityExpr::Fixed { value: 1 },
},
));
}
}
}
}

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.

medium

[MEDIUM] Avoid nested if let Ok chains and fragile verbatim parsing for Oracle phrases.

Why it matters: Nested if let Ok blocks create a "pyramid of doom" that is hard to read. Furthermore, parsing compound Oracle phrases using verbatim string matches or fragile sequential tags bypasses the robust nom-based parser. Instead, we should decompose compound phrases into modular, reusable parsers for constituent parts and compose them using idiomatic combinator aggregates.

Suggested fix: Refactor the parsing logic to decompose the phrase into modular constituent parsers and compose them using idiomatic nom combinators, avoiding nested if let Ok chains.

    let parse_at_least_more = |i| {
        let (i, _) = parse_controls_at_least(i)?;
        let (i, n) = parse_number(i)?;
        let (i, _) = parse_more(i)?;
        let (i, type_filter) = parse_type_phrase(i)?;
        let (i, _) = parse_than_you(i)?;
        Ok((i, (n, type_filter)))
    };

    if let Ok((rest2, (n, type_filter))) = parse_at_least_more(rest) {
        let you_filter = inject_controller_you(type_filter.clone());
        return Ok((
            rest2,
            StaticCondition::QuantityComparison {
                lhs: QuantityExpr::Ref {
                    qty: QuantityRef::PlayerCount {
                        filter: PlayerFilter::ControlsCount {
                            relation: PlayerRelation::Opponent,
                            filter: type_filter,
                            comparator: Comparator::GE,
                            count: Box::new(QuantityExpr::Offset {
                                inner: Box::new(QuantityExpr::Ref {
                                    qty: QuantityRef::ObjectCount {
                                        filter: you_filter,
                                    },
                                }),
                                offset: n as i32,
                            }),
                        },
                    },
                },
                comparator: Comparator::GE,
                rhs: QuantityExpr::Fixed { value: 1 },
            },
        ));
    }
References
  1. Rule R1: Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. Avoid nested if let Ok matching chains (pyramid of doom) for parsing dispatch. (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 to prevent combinatorial explosion and improve maintainability.

@claytonlin1110
claytonlin1110 force-pushed the fix/opponent-controls-more-activation-restrictions branch from 487fd83 to ca217a4 Compare June 11, 2026 12:44
claytonlin1110 and others added 3 commits June 11, 2026 07:55
# Conflicts:
#	crates/engine/tests/fixtures/integration_cards.json
#	crates/engine/tests/integration/main.rs
@matthewevans matthewevans added the bug Bug fix label Jun 11, 2026
@matthewevans matthewevans self-assigned this Jun 11, 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.

Approved after maintainer cleanup and re-review on the current-base head.

Evidence checked:

  • The fix is at the activation restriction / parsed condition seam: Weathered Wayfarer-style gates are stored in activation_restrictions as RequiresCondition, not on the resolution-time condition field.
  • The cross-player count semantics are existential per opponent, not aggregate across all opponents; three-player tests cover the combined-opponents false-positive case.
  • I tightened the parser after review: the new at least N more [type] than you path is now a composed helper, and malformed type phrases fail closed instead of falling through to the generic controls at least N ... arm.
  • The generated fixture conflict from the branch being behind was resolved by taking current origin/main and grafting only the PR's weathered wayfarer fixture entry.
  • CR 109.4, 109.5, 602.5b, and 603.4 citations were checked against docs/MagicCompRules.txt.

Verification run locally on the pushed head:

  • cargo test -p engine --lib opponent_controls_more -- --nocapture
  • cargo test -p engine --lib opponent_controls_at_least -- --nocapture
  • cargo test -p engine --lib rejects_unknown_type_phrase -- --nocapture
  • cargo test -p engine --test integration weathered_wayfarer -- --nocapture
  • ./scripts/check-parser-combinators.sh
  • git diff --check

@matthewevans
matthewevans enabled auto-merge June 11, 2026 18:24
@matthewevans matthewevans removed their assignment Jun 11, 2026
@matthewevans
matthewevans added this pull request to the merge queue Jun 11, 2026
Merged via the queue into phase-rs:main with commit e93dccd Jun 11, 2026
10 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.

Weathered Wayfarer: 'Activate only if an opponent controls more lands than you' restriction is dropped (parser emits condition:null)

2 participants