Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions crates/engine/src/parser/oracle_nom/quantity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -828,12 +828,26 @@ fn parse_chosen_number_ref(input: &str) -> OracleResult<'_, QuantityRef> {
value(QuantityRef::ChosenNumber, tag("the chosen number")).parse(input)
}

/// 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> {
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.
Expand Down Expand Up @@ -8507,6 +8521,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) =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//! 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;
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 two_mana_creature = scenario
.add_creature(P1, "Two-Mana Creature", 2, 2)
.with_mana_cost(ManaCost::Cost {
shards: vec![],
generic: 2,
})
.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).accept_optional().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[&two_mana_creature].zone,
Zone::Battlefield,
"the two-mana creature must survive after paying zero energy, not the announced X"
);
}
1 change: 1 addition & 0 deletions crates/engine/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading