diff --git a/crates/engine/src/parser/oracle_nom/condition.rs b/crates/engine/src/parser/oracle_nom/condition.rs index 0974540cba..a9f81717d5 100644 --- a/crates/engine/src/parser/oracle_nom/condition.rs +++ b/crates/engine/src/parser/oracle_nom/condition.rs @@ -3262,10 +3262,70 @@ fn creatures_you_controlled_left_battlefield_this_turn_ref() -> QuantityRef { } } +/// 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. +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: "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.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> { + 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.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. + 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..1ce0910c8e --- /dev/null +++ b/crates/engine/tests/coalition_victory_win_condition_runtime.rs @@ -0,0 +1,93 @@ +//! 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.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. + +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 + ); +}