From b32294f9b166fb5db6cbd6f68d0215c4c982fd3b Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 12 Aug 2026 13:58:39 -0700 Subject: [PATCH 1/3] fix(parser): bind paid-energy resolution thresholds --- .../engine/src/parser/oracle_nom/quantity.rs | 24 ++++++ .../issue_6473_wrath_of_the_skies.rs | 81 +++++++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 3 files changed, 106 insertions(+) create mode 100644 crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs diff --git a/crates/engine/src/parser/oracle_nom/quantity.rs b/crates/engine/src/parser/oracle_nom/quantity.rs index 54c32533ce..fc9e4a7ef1 100644 --- a/crates/engine/src/parser/oracle_nom/quantity.rs +++ b/crates/engine/src/parser/oracle_nom/quantity.rs @@ -828,12 +828,24 @@ fn parse_chosen_number_ref(input: &str) -> OracleResult<'_, QuantityRef> { value(QuantityRef::ChosenNumber, tag("the chosen number")).parse(input) } +/// The amount of energy paid in the immediately preceding resolution-time payment. +/// `PayAmountChoice` records this value in `last_effect_count` before it resumes +/// the chained effect, which is the runtime carrier for `EventContextAmount`. +fn parse_paid_energy_this_way_ref(input: &str) -> OracleResult<'_, QuantityRef> { + value( + QuantityRef::EventContextAmount, + preceded(opt(tag("the ")), tag("amount of {e} paid this way")), + ) + .parse(input) +} + pub fn parse_quantity_ref(input: &str) -> OracleResult<'_, QuantityRef> { alt(( alt(( parse_guessed_number_ref, parse_object_count_by_shared_quality, parse_chosen_number_ref, + parse_paid_energy_this_way_ref, parse_intensity_ref, // CR 120.10: must precede the generic damage/number arms so the // "excess" channel wins over a plain damage reading. @@ -8507,6 +8519,18 @@ mod tests { assert_eq!(rest, ""); } + #[test] + fn parse_paid_energy_this_way_uses_resolution_payment_amount() { + for phrase in [ + "the amount of {e} paid this way", + "amount of {e} paid this way", + ] { + let (rest, qty) = parse_quantity_ref(phrase).unwrap(); + assert_eq!(rest, "", "{phrase:?} must fully consume"); + assert_eq!(qty, QuantityRef::EventContextAmount, "{phrase:?}"); + } + } + #[test] fn test_parse_opponents_total_life_lost_this_turn() { let (rest, q) = diff --git a/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs b/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs new file mode 100644 index 0000000000..690211fd0e --- /dev/null +++ b/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs @@ -0,0 +1,81 @@ +//! Regression for issue #6473: Wrath of the Skies must use the energy actually +//! paid during resolution, rather than the X chosen while casting. + +use engine::game::scenario::{GameScenario, P0, P1}; +use engine::types::actions::GameAction; +use engine::types::game_state::WaitingFor; +use engine::types::identifiers::ObjectId; +use engine::types::mana::{ManaCost, ManaCostShard, ManaType, ManaUnit}; +use engine::types::phase::Phase; +use engine::types::zones::Zone; + +const WRATH_OF_THE_SKIES: &str = "You get X {E} (energy counters), then you may pay any amount of {E}. Destroy each artifact, creature, and enchantment with mana value less than or equal to the amount of {E} paid this way."; + +fn mana(color: ManaType) -> ManaUnit { + ManaUnit::new(color, ObjectId(0), false, vec![]) +} + +#[test] +fn wrath_uses_energy_paid_not_announced_x_for_destroy_threshold() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let zero_mana_artifact = scenario + .add_creature(P1, "Ornithopter", 0, 2) + .as_artifact() + .with_mana_cost(ManaCost::Cost { + shards: vec![], + generic: 0, + }) + .id(); + let thought_knot_seer = scenario + .add_creature(P1, "Thought-Knot Seer", 4, 4) + .with_mana_cost(ManaCost::Cost { + shards: vec![], + generic: 4, + }) + .id(); + let wrath = scenario + .add_spell_to_hand_from_oracle(P0, "Wrath of the Skies", false, WRATH_OF_THE_SKIES) + .with_mana_cost(ManaCost::Cost { + shards: vec![ManaCostShard::X, ManaCostShard::White, ManaCostShard::White], + generic: 0, + }) + .id(); + scenario.with_mana_pool( + P0, + vec![ + mana(ManaType::White), + mana(ManaType::White), + mana(ManaType::Colorless), + mana(ManaType::Colorless), + ], + ); + + let mut runner = scenario.build(); + let outcome = runner.cast(wrath).x(2).resolve(); + + match outcome.final_waiting_for() { + WaitingFor::PayAmountChoice { player, max, .. } => { + assert_eq!(*player, P0); + assert_eq!(*max, 2, "Wrath must offer the two energy it created"); + } + other => panic!("expected energy payment choice, got {other:?}"), + } + + runner + .act(GameAction::SubmitPayAmount { amount: 0 }) + .expect("paying zero energy must resume Wrath"); + + assert_eq!(runner.state().players[P0.0 as usize].energy, 2); + assert_eq!( + runner.state().objects[&zero_mana_artifact].zone, + Zone::Graveyard, + "the zero-mana artifact is within the paid-energy threshold" + ); + assert_eq!( + runner.state().objects[&thought_knot_seer].zone, + Zone::Battlefield, + "the four-mana creature must survive after paying zero energy" + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index f2ab7531f2..5e1ac95397 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -671,6 +671,7 @@ mod issue_6435_mosswort_bridge_hideaway_play; mod issue_6437_fight_rigging_exiled_card_target; mod issue_6440_mockingbird_uncast_copy_ceiling; mod issue_6459_scheming_symmetry; +mod issue_6473_wrath_of_the_skies; mod issue_6477_wandering_archaic_optional_payment; mod issue_6498_portent_of_calamity; mod issue_6499_flickering_ward_protection_exemption; From 61c3fc0d92bae23c4894e4d68a872ddd2996338b Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 12 Aug 2026 14:04:28 -0700 Subject: [PATCH 2/3] test(parser): strengthen Wrath paid-energy regression --- crates/engine/src/parser/oracle_nom/quantity.rs | 4 +++- .../integration/issue_6473_wrath_of_the_skies.rs | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/parser/oracle_nom/quantity.rs b/crates/engine/src/parser/oracle_nom/quantity.rs index fc9e4a7ef1..8d8e5887ae 100644 --- a/crates/engine/src/parser/oracle_nom/quantity.rs +++ b/crates/engine/src/parser/oracle_nom/quantity.rs @@ -828,7 +828,9 @@ fn parse_chosen_number_ref(input: &str) -> OracleResult<'_, QuantityRef> { value(QuantityRef::ChosenNumber, tag("the chosen number")).parse(input) } -/// The amount of energy paid in the immediately preceding resolution-time payment. +/// CR 608.2c: The amount of energy paid in the immediately preceding +/// resolution-time payment, because resolving instructions follow their written +/// order. /// `PayAmountChoice` records this value in `last_effect_count` before it resumes /// the chained effect, which is the runtime carrier for `EventContextAmount`. fn parse_paid_energy_this_way_ref(input: &str) -> OracleResult<'_, QuantityRef> { diff --git a/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs b/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs index 690211fd0e..a33c47fbbb 100644 --- a/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs +++ b/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs @@ -1,5 +1,7 @@ //! Regression for issue #6473: Wrath of the Skies must use the energy actually //! paid during resolution, rather than the X chosen while casting. +//! CR 608.2c: Resolving instructions follow their written order, so "paid this +//! way" reads the immediately preceding resolution-time payment. use engine::game::scenario::{GameScenario, P0, P1}; use engine::types::actions::GameAction; @@ -28,11 +30,11 @@ fn wrath_uses_energy_paid_not_announced_x_for_destroy_threshold() { generic: 0, }) .id(); - let thought_knot_seer = scenario - .add_creature(P1, "Thought-Knot Seer", 4, 4) + let two_mana_creature = scenario + .add_creature(P1, "Two-Mana Creature", 2, 2) .with_mana_cost(ManaCost::Cost { shards: vec![], - generic: 4, + generic: 2, }) .id(); let wrath = scenario @@ -74,8 +76,8 @@ fn wrath_uses_energy_paid_not_announced_x_for_destroy_threshold() { "the zero-mana artifact is within the paid-energy threshold" ); assert_eq!( - runner.state().objects[&thought_knot_seer].zone, + runner.state().objects[&two_mana_creature].zone, Zone::Battlefield, - "the four-mana creature must survive after paying zero energy" + "the two-mana creature must survive after paying zero energy, not the announced X" ); } From ebcafa38762ea49bb84764761e3ab0c24668aa56 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Wed, 12 Aug 2026 14:39:55 -0700 Subject: [PATCH 3/3] test(engine): accept Wrath optional energy payment --- .../engine/tests/integration/issue_6473_wrath_of_the_skies.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs b/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs index a33c47fbbb..34bdfcc614 100644 --- a/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs +++ b/crates/engine/tests/integration/issue_6473_wrath_of_the_skies.rs @@ -55,7 +55,7 @@ fn wrath_uses_energy_paid_not_announced_x_for_destroy_threshold() { ); let mut runner = scenario.build(); - let outcome = runner.cast(wrath).x(2).resolve(); + let outcome = runner.cast(wrath).x(2).accept_optional().resolve(); match outcome.final_waiting_for() { WaitingFor::PayAmountChoice { player, max, .. } => {