From 860630816352523affb9ec91ac8016a0d7f0eaa5 Mon Sep 17 00:00:00 2001 From: matthewevans Date: Tue, 11 Aug 2026 11:56:23 -0700 Subject: [PATCH] Count auto-tapped mana toward Expend --- crates/engine/src/game/casting_costs.rs | 62 +++++-------------- crates/engine/src/types/game_state.rs | 1 - .../issue_7232_expend_auto_land_payment.rs | 50 +++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 4 files changed, 68 insertions(+), 46 deletions(-) create mode 100644 crates/engine/tests/integration/issue_7232_expend_auto_land_payment.rs diff --git a/crates/engine/src/game/casting_costs.rs b/crates/engine/src/game/casting_costs.rs index fcfe846c90..8c9a9f056a 100644 --- a/crates/engine/src/game/casting_costs.rs +++ b/crates/engine/src/game/casting_costs.rs @@ -49,6 +49,16 @@ use super::life_costs::PayLifeCostResult; const TERMINAL_CAST_CANCELLATION_ERROR: &str = "__terminal_cast_cancellation__"; +/// The mana payment authority stamps this on the spell object before casting +/// finalization publishes the spell-cast event. +fn recorded_mana_spent_to_cast(state: &GameState, object_id: ObjectId) -> u32 { + state + .objects + .get(&object_id) + .expect("spell object must exist while its cast is being finalized") + .mana_spent_to_cast_amount +} + fn stamp_controller_controlled_as_cast( state: &GameState, ability: &mut ResolvedAbility, @@ -1975,7 +1985,6 @@ fn finish_cost_object_moves( phyrexian_choices, cascade_cast_transformed, resolution_success_waiting_for, - pool_before, prepaid_actual_mana_spent, } => { let returned_creature = chosen @@ -1988,14 +1997,8 @@ fn finish_cost_object_moves( .retain(|attacker| attacker.object_id != returned_creature); combat.blocker_assignments.remove(&returned_creature); } - let pool_after = state - .players - .iter() - .find(|candidate| candidate.id == player) - .map(|candidate| candidate.mana_pool.produced_mana_total()) - .unwrap_or(0); let actual_mana_spent = prepaid_actual_mana_spent - .unwrap_or_else(|| pool_before.saturating_sub(pool_after) as u32); + .unwrap_or_else(|| recorded_mana_spent_to_cast(state, pending.object_id)); let deferred_life_resume_pending = pending.clone(); finalize_cast_with_phyrexian_choices_inner( state, @@ -9115,13 +9118,6 @@ fn finalize_cast_with_phyrexian_choices_inner( return Ok(waiting_for); } - // CR 700.14: Snapshot pool size before payment to compute actual mana spent. - let pool_before = state - .players - .iter() - .find(|p| p.id == player) - .map(|p| p.mana_pool.produced_mana_total()) - .unwrap_or(0); let cast_transformed = cascade_cast_transformed || super::casting::selected_exile_alt_cost_permission_casts_transformed( state, @@ -9163,14 +9159,8 @@ fn finalize_cast_with_phyrexian_choices_inner( ) })?; pending.cost = ManaCost::NoCost; - let pool_after = state - .players - .iter() - .find(|candidate| candidate.id == player) - .map(|candidate| candidate.mana_pool.produced_mana_total()) - .unwrap_or(0); pending.prepaid_actual_mana_spent = - Some(pool_before.saturating_sub(pool_after) as u32); + Some(recorded_mana_spent_to_cast(state, object_id)); state.pending_deferred_life_cost_resume = Some(crate::types::game_state::DeferredLifeCostResume::Cast { player, @@ -9212,7 +9202,6 @@ fn finalize_cast_with_phyrexian_choices_inner( phyrexian_choices: phyrexian_choices.map(|choices| choices.to_vec()), cascade_cast_transformed, resolution_success_waiting_for: resolution_success_waiting_for.map(Box::new), - pool_before, prepaid_actual_mana_spent, }, cost_event_start, @@ -9221,15 +9210,10 @@ fn finalize_cast_with_phyrexian_choices_inner( ); } - // CR 700.14: Compute actual mana deducted from pool (not declared cost). - let pool_after = state - .players - .iter() - .find(|p| p.id == player) - .map(|p| p.mana_pool.produced_mana_total()) - .unwrap_or(0); + // CR 700.14: Use payment's recorded amount; auto-tapped mana can be + // produced and spent between pool snapshots. let actual_mana_spent = - prepaid_actual_mana_spent.unwrap_or_else(|| pool_before.saturating_sub(pool_after) as u32); + prepaid_actual_mana_spent.unwrap_or_else(|| recorded_mana_spent_to_cast(state, object_id)); // CR 603.4 + CR 903.8: `origin_zone` preserves the pre-announcement zone so // that "cast from hand/graveyard/exile" conditions evaluate correctly and @@ -12620,15 +12604,9 @@ fn finalize_mana_payment_with_resume( Some(&mana_resume), events, )? { - let pool_after = state - .players - .iter() - .find(|candidate| candidate.id == player) - .map(|candidate| candidate.mana_pool.total()) - .unwrap_or(0); pending.cost = ManaCost::NoCost; pending.prepaid_actual_mana_spent = - Some(pool_before.saturating_sub(pool_after) as u32); + Some(recorded_mana_spent_to_cast(state, pending.object_id)); state.pending_deferred_life_cost_resume = Some(crate::types::game_state::DeferredLifeCostResume::Cast { player, @@ -13011,15 +12989,9 @@ pub fn finalize_mana_payment_with_phyrexian_choices( Some(&mana_resume), events, )? { - let pool_after = state - .players - .iter() - .find(|candidate| candidate.id == player) - .map(|candidate| candidate.mana_pool.total()) - .unwrap_or(0); pending.cost = ManaCost::NoCost; pending.prepaid_actual_mana_spent = - Some(pool_before.saturating_sub(pool_after) as u32); + Some(recorded_mana_spent_to_cast(state, pending.object_id)); state.pending_deferred_life_cost_resume = Some(crate::types::game_state::DeferredLifeCostResume::Cast { player, diff --git a/crates/engine/src/types/game_state.rs b/crates/engine/src/types/game_state.rs index 6b1ee0e89e..9ccc9c61fe 100644 --- a/crates/engine/src/types/game_state.rs +++ b/crates/engine/src/types/game_state.rs @@ -5920,7 +5920,6 @@ pub enum PendingCostMoveCompletion { phyrexian_choices: Option>, cascade_cast_transformed: bool, resolution_success_waiting_for: Option>, - pool_before: usize, prepaid_actual_mana_spent: Option, }, } diff --git a/crates/engine/tests/integration/issue_7232_expend_auto_land_payment.rs b/crates/engine/tests/integration/issue_7232_expend_auto_land_payment.rs new file mode 100644 index 0000000000..40949a2aef --- /dev/null +++ b/crates/engine/tests/integration/issue_7232_expend_auto_land_payment.rs @@ -0,0 +1,50 @@ +//! Issue #7232: auto-tapping lands must count toward Expend. +//! +//! Bakersbane Duo's Oracle text (Scryfall): +//! "Whenever you expend 4, this creature gets +1/+1 until end of turn." + +use engine::game::layers::evaluate_layers; +use engine::game::scenario::{GameScenario, P0}; +use engine::types::mana::{ManaColor, ManaCost}; +use engine::types::phase::Phase; + +const BAKERSBANE_DUO_ORACLE: &str = "When this creature enters, create a Food token.\n\ +Whenever you expend 4, this creature gets +1/+1 until end of turn. (You expend 4 as you \ +spend your fourth total mana to cast spells during a turn.)"; + +#[test] +fn auto_tapped_lands_count_toward_expend() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + let duo = scenario + .add_creature_from_oracle(P0, "Bakersbane Duo", 2, 2, BAKERSBANE_DUO_ORACLE) + .id(); + for _ in 0..4 { + scenario.add_basic_land(P0, ManaColor::Green); + } + let spell = scenario + .add_creature_to_hand(P0, "Four-Mana Test Creature", 2, 2) + .with_mana_cost(ManaCost::generic(4)) + .id(); + let mut runner = scenario.build(); + + runner.cast(spell).commit(); + + assert_eq!( + runner.state().mana_spent_on_spells_this_turn.get(&P0), + Some(&4), + "auto-tapped lands must contribute their four mana to Expend" + ); + + runner.advance_until_stack_empty(); + runner.state_mut().layers_dirty.mark_full(); + evaluate_layers(runner.state_mut()); + assert_eq!( + ( + runner.state().objects[&duo].power, + runner.state().objects[&duo].toughness, + ), + (Some(3), Some(3)), + "crossing Expend 4 must resolve Bakersbane Duo's +1/+1 trigger" + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 951a41254b..9ed4634ee4 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -691,6 +691,7 @@ mod issue_7063_library_reorder; mod issue_7087_recruit_discard_provenance; mod issue_709_regression; mod issue_718_dina_sacrifice_draw; +mod issue_7232_expend_auto_land_payment; mod issue_735_amalia_power_threshold; mod issue_735_cost_paid_object_non_regression; mod issue_735_lily_bowen_power_double;