From dbedffc443ef88f45d65f2b6166c834750cf5ac5 Mon Sep 17 00:00:00 2001 From: ultrahighsuper Date: Sat, 4 Jul 2026 08:02:03 +0900 Subject: [PATCH 1/2] fix(engine): Coalition Victory only wins if you control a land of each basic type and a creature of each color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coalition Victory's Oracle text is "You win the game if you control a land of each basic land type and a creature of each color." The trailing "if …" condition was not parsed, so the spell reduced to a bare Effect::WinTheGame and casting it won the game immediately regardless of the board. Teach the condition parser the two "of each …" clauses and compose them: parse_control_conditions now recognizes "you control a land of each basic land type and a creature of each color" and yields And[ BasicLandTypeCount{You} >= 5, DistinctColorsAmongPermanents{creatures you control} >= 5 ] reusing the existing domain and distinct-color quantities. The two sub-clauses are factored into standalone combinators (parse_land_of_each_basic_land_type, parse_creature_of_each_color) so they cover every "of each basic land type" / "of each color" clause, not just this card. WinTheGame is already trailing-condition-eligible, so the parsed condition attaches to the spell and is checked once on resolution (CR 104.2a + CR 603.4). Tests (runtime, real cast pipeline): - coalition_victory_does_not_win_without_the_board: casting with an empty board does NOT win (condition unmet). Fails on the old bare-WinTheGame parse, which won unconditionally (revert-probed). - coalition_victory_wins_with_full_domain_and_all_colors: controlling a land of each basic type and a creature of each color, casting wins the game. --- .../engine/src/parser/oracle_nom/condition.rs | 58 ++++++++++++ ...coalition_victory_win_condition_runtime.rs | 92 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 crates/engine/tests/coalition_victory_win_condition_runtime.rs diff --git a/crates/engine/src/parser/oracle_nom/condition.rs b/crates/engine/src/parser/oracle_nom/condition.rs index a0ef7fe8be..9c55b72a80 100644 --- a/crates/engine/src/parser/oracle_nom/condition.rs +++ b/crates/engine/src/parser/oracle_nom/condition.rs @@ -2852,10 +2852,68 @@ fn creatures_you_controlled_left_battlefield_this_turn_ref() -> QuantityRef { } } +/// CR 205.3i + CR 603.4: "a land of each basic land type" — you control a land +/// with each of the five basic land types (i.e. full domain). Expressed as +/// `BasicLandTypeCount{You} >= 5`, reusing the domain quantity so the semantics +/// match every "of each basic land type" clause, not just Coalition Victory. +fn parse_land_of_each_basic_land_type(input: &str) -> OracleResult<'_, StaticCondition> { + let (rest, _) = tag("a land of each basic land type").parse(input)?; + Ok(( + rest, + make_quantity_ge( + QuantityRef::BasicLandTypeCount { + controller: ControllerRef::You, + }, + 5, + ), + )) +} + +/// CR 105.2 + CR 603.4: "a creature of each color" — you control creatures +/// spanning all five colors. Expressed as +/// `DistinctColorsAmongPermanents{creatures you control} >= 5`, reusing the +/// distinct-color quantity so the semantics match every "of each color" clause. +fn parse_creature_of_each_color(input: &str) -> OracleResult<'_, StaticCondition> { + let (rest, _) = tag("a creature of each color").parse(input)?; + Ok(( + rest, + make_quantity_ge( + QuantityRef::DistinctColorsAmongPermanents { + filter: TargetFilter::Typed(TypedFilter::creature().controller(ControllerRef::You)), + }, + 5, + ), + )) +} + +/// CR 104.2a + CR 603.4: "you control a land of each basic land type and a +/// creature of each color" — Coalition Victory's win condition. Composes the two +/// reusable "of each …" sub-combinators with `And`; each is a standalone +/// building block usable wherever its clause appears. +fn parse_control_land_each_basic_and_creature_each_color( + input: &str, +) -> OracleResult<'_, StaticCondition> { + let (rest, _) = tag("you control ").parse(input)?; + let (rest, land) = parse_land_of_each_basic_land_type(rest)?; + let (rest, _) = tag(" and ").parse(rest)?; + let (rest, creature) = parse_creature_of_each_color(rest)?; + Ok(( + rest, + StaticCondition::And { + conditions: vec![land, creature], + }, + )) +} + /// Parse "you control" condition patterns. Exposed for rule-static parsers that /// attach a trailing "unless you control " clause as a negated condition. pub(crate) fn parse_control_conditions(input: &str) -> OracleResult<'_, StaticCondition> { alt(( + // CR 104.2a: "you control a land of each basic land type and a creature + // of each color" (Coalition Victory) — tried before the generic + // `you control a/an [type]` arm so its "a land …" prefix isn't + // mis-classified as a bare presence check. + parse_control_land_each_basic_and_creature_each_color, // CR 201.2 + CR 603.4: "you control N or more [type] with different names" // → QuantityComparison(ObjectCountDistinct[Name] >= N). Tried before the // plain ObjectCount arm so the `with different names` suffix is not diff --git a/crates/engine/tests/coalition_victory_win_condition_runtime.rs b/crates/engine/tests/coalition_victory_win_condition_runtime.rs new file mode 100644 index 0000000000..bd07747af6 --- /dev/null +++ b/crates/engine/tests/coalition_victory_win_condition_runtime.rs @@ -0,0 +1,92 @@ +//! Runtime (engine-path) regression for Coalition Victory. +//! +//! Oracle: "You win the game if you control a land of each basic land type and a +//! creature of each color." The trailing "if …" condition was dropped, so the +//! spell parsed to a bare `Effect::WinTheGame` and casting it won the game +//! immediately regardless of the board (CR 104.2a + CR 603.4 — the intervening +//! condition must be checked on resolution). This drives the real cast pipeline: +//! casting with an incomplete board must NOT win; casting while controlling a +//! land of each basic type and a creature of each color must win. + +use engine::game::scenario::{GameScenario, P0}; +use engine::types::game_state::WaitingFor; +use engine::types::mana::{ManaColor, ManaCost}; +use engine::types::phase::Phase; + +const ORACLE: &str = + "You win the game if you control a land of each basic land type and a creature of each color."; + +fn caster_won(runner: &engine::game::scenario::GameRunner) -> bool { + matches!(runner.state().waiting_for, WaitingFor::GameOver { winner: Some(w) } if w == P0) +} + +/// Condition FALSE (empty board): casting Coalition Victory must NOT win. +/// Revert-probe: on the old bare-`WinTheGame` parse this wins unconditionally. +#[test] +fn coalition_victory_does_not_win_without_the_board() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let cv = scenario + .add_spell_to_hand_from_oracle(P0, "Coalition Victory", false, ORACLE) + .with_mana_cost(ManaCost::generic(0)) + .id(); + let mut runner = scenario.build(); + + runner.cast(cv).resolve(); + + assert!( + !caster_won(&runner), + "Coalition Victory must NOT win with no lands/creatures (its condition is unmet); \ + the old bare WinTheGame parse won unconditionally" + ); +} + +/// Condition TRUE: controlling a land of each basic land type and a creature of +/// each color, casting Coalition Victory wins the game. +#[test] +fn coalition_victory_wins_with_full_domain_and_all_colors() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let cv = scenario + .add_spell_to_hand_from_oracle(P0, "Coalition Victory", false, ORACLE) + .with_mana_cost(ManaCost::generic(0)) + .id(); + // A land of each basic land type (full domain). + for color in [ + ManaColor::White, + ManaColor::Blue, + ManaColor::Black, + ManaColor::Red, + ManaColor::Green, + ] { + scenario.add_basic_land(P0, color); + } + // A creature of each color. + let mut creature_ids = Vec::new(); + for color in [ + ManaColor::White, + ManaColor::Blue, + ManaColor::Black, + ManaColor::Red, + ManaColor::Green, + ] { + let id = scenario.add_creature(P0, "Coalition Soldier", 1, 1).id(); + creature_ids.push((id, color)); + } + let mut runner = scenario.build(); + // Paint each creature its color (DistinctColorsAmongPermanents reads obj.color). + for (id, color) in creature_ids { + let obj = runner.state_mut().objects.get_mut(&id).unwrap(); + obj.color = vec![color]; + obj.base_color = vec![color]; + } + + runner.cast(cv).resolve(); + + assert!( + caster_won(&runner), + "Coalition Victory must win the game when its controller has a land of each basic \ + land type and a creature of each color; waiting_for = {:?}", + runner.state().waiting_for + ); +} From 82e1e7a3a142eafb45897cc33b377720739db738 Mon Sep 17 00:00:00 2001 From: ultrahighsuper Date: Sat, 4 Jul 2026 08:35:25 +0900 Subject: [PATCH 2/2] docs(engine): correct CR annotations for Coalition Victory win condition Coalition Victory's effect-based win condition is a spell, not a triggered ability, so CR 603.4 (the intervening-"if" rule, which applies only to triggered abilities) does not apply, and CR 104.2a is the last-player-standing rule rather than the effect-states-you-win rule. - Win-condition sites now cite CR 104.2b (an effect may state that a player wins the game) + CR 608.2h (game-state information is determined once, as the spell resolves). - The "of each basic land type" / "of each color" sub-combinators keep only their subject-matter rules (CR 205.3i basic land types, CR 105.2 colors) and drop the erroneous CR 603.4. Comment-only change; no logic or test behavior changes. --- crates/engine/src/parser/oracle_nom/condition.rs | 16 +++++++++------- .../coalition_victory_win_condition_runtime.rs | 5 +++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/engine/src/parser/oracle_nom/condition.rs b/crates/engine/src/parser/oracle_nom/condition.rs index 9c55b72a80..29da6dd8bc 100644 --- a/crates/engine/src/parser/oracle_nom/condition.rs +++ b/crates/engine/src/parser/oracle_nom/condition.rs @@ -2852,7 +2852,7 @@ fn creatures_you_controlled_left_battlefield_this_turn_ref() -> QuantityRef { } } -/// CR 205.3i + CR 603.4: "a land of each basic land type" — you control a land +/// CR 205.3i: "a land of each basic land type" — you control a land /// with each of the five basic land types (i.e. full domain). Expressed as /// `BasicLandTypeCount{You} >= 5`, reusing the domain quantity so the semantics /// match every "of each basic land type" clause, not just Coalition Victory. @@ -2869,7 +2869,7 @@ fn parse_land_of_each_basic_land_type(input: &str) -> OracleResult<'_, StaticCon )) } -/// CR 105.2 + CR 603.4: "a creature of each color" — you control creatures +/// CR 105.2: "a creature of each color" — you control creatures /// spanning all five colors. Expressed as /// `DistinctColorsAmongPermanents{creatures you control} >= 5`, reusing the /// distinct-color quantity so the semantics match every "of each color" clause. @@ -2886,10 +2886,12 @@ fn parse_creature_of_each_color(input: &str) -> OracleResult<'_, StaticCondition )) } -/// CR 104.2a + CR 603.4: "you control a land of each basic land type and a -/// creature of each color" — Coalition Victory's win condition. Composes the two -/// reusable "of each …" sub-combinators with `And`; each is a standalone -/// building block usable wherever its clause appears. +/// CR 104.2b + CR 608.2h: "you control a land of each basic land type and a +/// creature of each color" — Coalition Victory's win condition. CR 104.2b: an +/// effect may state that a player wins the game; CR 608.2h: the game-state +/// information the condition needs is determined once, as the spell resolves. +/// Composes the two reusable "of each …" sub-combinators with `And`; each is a +/// standalone building block usable wherever its clause appears. fn parse_control_land_each_basic_and_creature_each_color( input: &str, ) -> OracleResult<'_, StaticCondition> { @@ -2909,7 +2911,7 @@ fn parse_control_land_each_basic_and_creature_each_color( /// attach a trailing "unless you control " clause as a negated condition. pub(crate) fn parse_control_conditions(input: &str) -> OracleResult<'_, StaticCondition> { alt(( - // CR 104.2a: "you control a land of each basic land type and a creature + // CR 104.2b: "you control a land of each basic land type and a creature // of each color" (Coalition Victory) — tried before the generic // `you control a/an [type]` arm so its "a land …" prefix isn't // mis-classified as a bare presence check. diff --git a/crates/engine/tests/coalition_victory_win_condition_runtime.rs b/crates/engine/tests/coalition_victory_win_condition_runtime.rs index bd07747af6..1ce0910c8e 100644 --- a/crates/engine/tests/coalition_victory_win_condition_runtime.rs +++ b/crates/engine/tests/coalition_victory_win_condition_runtime.rs @@ -3,8 +3,9 @@ //! Oracle: "You win the game if you control a land of each basic land type and a //! creature of each color." The trailing "if …" condition was dropped, so the //! spell parsed to a bare `Effect::WinTheGame` and casting it won the game -//! immediately regardless of the board (CR 104.2a + CR 603.4 — the intervening -//! condition must be checked on resolution). This drives the real cast pipeline: +//! immediately regardless of the board (CR 104.2b — an effect may state a player +//! wins the game; CR 608.2h — the game-state check is made once, as the spell +//! resolves). This drives the real cast pipeline: //! casting with an incomplete board must NOT win; casting while controlling a //! land of each basic type and a creature of each color must win.