Skip to main content

manabrew_engine/mana/
computer_util_mana.rs

1use forge_foundation::mana::ManaAtom;
2use forge_foundation::{ManaCost, ManaCostShard, ZoneType};
3use indexmap::IndexMap;
4use std::collections::HashMap;
5
6use crate::agent::ManaAbilityOption;
7use crate::cost::cost_part::pay_cost_from_source;
8use crate::cost::{can_pay_ignoring_mana, CostPart};
9use crate::event::RunParams;
10use crate::game::GameState;
11use crate::ids::{CardId, PlayerId};
12
13use super::mana_cost_being_paid::{can_pay_for_shard_with_color, ManaCostBeingPaid};
14use super::mana_pool::ManaPaymentOutcome;
15use super::{
16    add_produced_mana_to_pool, all_basic_subtype_atoms, atom_short, basic_land_mana_atom,
17    chosen_colors_to_atoms, tap_land_for_mana, ManaPool, ManaProductionParams,
18};
19
20#[derive(Debug, Clone)]
21struct ManaAbilityRef {
22    card_id: CardId,
23    ability_index: Option<usize>,
24    atoms: Vec<u16>,
25    amount: i32,
26    mana_text: String,
27    produced_ir: Option<crate::ability::ProducedMana>,
28    source_order: usize,
29}
30
31impl ManaAbilityRef {
32    fn can_pay_shard(&self, shard: ManaCostShard) -> bool {
33        // Java's deterministic AutoPay treats empty `Combo ColorIdentity`
34        // abilities as generic-pay candidates, then resolution produces no
35        // mana in non-Commander games. Keep that tap/continue behavior.
36        if self
37            .produced_ir
38            .as_ref()
39            .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
40            && self.atoms.is_empty()
41            && (shard == ManaCostShard::Generic || shard.is_generic())
42        {
43            return true;
44        }
45        self.atoms
46            .iter()
47            .any(|&a| can_pay_for_shard_with_color(shard, a))
48    }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct AutoTapChoice {
53    pub card_id: CardId,
54    pub mana_ability_index: Option<usize>,
55    pub chosen_atom: u16,
56    /// True when the mana ability has multiple color options and the caller
57    /// must record an explicit express choice in the trace. Mirrors Java
58    /// `AbilityManaPart.getExpressChoice()` being non-null.
59    pub needs_express_choice: bool,
60}
61
62#[derive(Debug, Clone, Default)]
63pub struct AutoTapPaymentTrace {
64    pub choices: Vec<AutoTapChoice>,
65    pub payment: ManaPaymentOutcome,
66    pub paid: bool,
67}
68
69#[derive(Debug, Clone)]
70pub struct ManaPaymentSources {
71    pub source_cards: Vec<CardId>,
72    pub mana_ability_options: Vec<ManaAbilityOption>,
73}
74
75fn mana_cost_from_cost(cost: &crate::cost::Cost) -> ManaCost {
76    let mut out = ManaCost::generic(0);
77    for part in &cost.parts {
78        if let CostPart::Mana { cost, .. } = part {
79            out = out.add(cost);
80        }
81    }
82    out
83}
84
85/// Optional callback for choosing which permanent to sacrifice during mana
86/// ability cost payment.  When `None`, the engine picks the first target after
87/// sorting by (card_name, card_id) — a deterministic fallback.  When `Some`,
88/// the callback is invoked with the sorted list of valid targets and should
89/// return the chosen card (mirrors Java's `choosePermanentsToSacrifice`
90/// which uses the harness RNG).
91pub type SacrificeChooser<'a> = &'a mut dyn FnMut(&[CardId]) -> Option<CardId>;
92
93/// Callback parameter for mana ability payment decisions.
94/// Used to dispatch both sacrifice chooser and confirm payment callbacks
95/// through a single unified interface to avoid multiple mutable borrows.
96#[derive(Debug)]
97pub enum ManaPayCallback<'a> {
98    /// Choose which permanent to sacrifice from the given list.
99    /// Return the chosen card, or None to cancel.
100    ChooseSacrifice(&'a [CardId]),
101    /// Notify the caller that auto-pay is making a color-choice prompt.
102    /// The callback may use this to preserve parity-visible prompt ordering.
103    /// The return value is ignored for this variant.
104    ChooseColor(&'a [String]),
105    /// Choose permanents for a `tapXType` mana-ability cost. The callback
106    /// writes the selected cards into `chosen`; the return value is only used
107    /// as a success/cancel signal to fit the unified callback shape.
108    ChooseTapType {
109        valid: &'a [CardId],
110        min: usize,
111        max: usize,
112        chosen: &'a mut Vec<CardId>,
113    },
114    /// Confirm whether to sacrifice the given card for a mana ability.
115    /// Return true to proceed, false to cancel.
116    /// Mirrors Java's DeterministicCostDecision.confirmPayment() path.
117    ConfirmSelfSacrifice(CardId),
118    /// Confirm whether to remove counters from the source for a mana ability.
119    /// Mirrors Java CostPayment confirm for CostRemoveCounter (SubCounter).
120    ConfirmSubCounter(CardId),
121    /// Confirm whether to exile the source for a mana ability.
122    /// Mirrors Java CostPayment confirm for source-paid CostExile.
123    ConfirmSourceExile(CardId),
124    /// Confirm whether to pay life for a mana ability.
125    /// Mirrors Java CostPayment confirm for CostPayLife.
126    ConfirmPayLife(CardId),
127    /// Execute the sacrifice of the given permanent for a mana ability.
128    /// The callback is responsible for firing Sacrificed/ChangesZone using
129    /// battlefield LKI, moving the card, and returning the same card id on
130    /// success. Returning `None` cancels the payment.
131    NotifySacrificeForMana(CardId),
132    /// Apply real ProduceMana replacements to the actual mana string this
133    /// source is about to add to the pool. The callback mutates `mana` after
134    /// running replacement choice through the caller's agents.
135    ApplyProduceManaReplacement {
136        activator: PlayerId,
137        source_card: CardId,
138        mana: &'a mut String,
139    },
140}
141
142/// Unified callback for mana payment decisions during auto-tap.
143/// Returns Some(card_id) on success, None to cancel.
144pub type ManaPayCallbackFn<'a> = &'a mut dyn FnMut(ManaPayCallback<'_>) -> Option<CardId>;
145
146/// Auto-tap lands to produce the required mana.
147/// Mirrors harness AutoPay flow used by parity tests: collect currently playable
148/// mana abilities in battlefield order, choose the first legal source for the
149/// next unpaid shard, then repeat after each activation.
150pub fn auto_tap_lands(
151    game: &mut GameState,
152    pool: &mut ManaPool,
153    player: PlayerId,
154    cost: &ManaCost,
155    current_spell: Option<CardId>,
156) -> Vec<CardId> {
157    auto_tap_lands_trace(game, pool, player, cost, current_spell)
158        .into_iter()
159        .map(|choice| choice.card_id)
160        .collect()
161}
162
163pub fn auto_tap_lands_allow_reserved_source_reuse(
164    game: &mut GameState,
165    pool: &mut ManaPool,
166    player: PlayerId,
167    cost: &ManaCost,
168    current_spell: Option<CardId>,
169) -> Vec<CardId> {
170    auto_tap_lands_allow_reserved_source_reuse_trace(game, pool, player, cost, current_spell)
171        .into_iter()
172        .map(|choice| choice.card_id)
173        .collect()
174}
175
176pub fn auto_tap_lands_trace(
177    game: &mut GameState,
178    pool: &mut ManaPool,
179    player: PlayerId,
180    cost: &ManaCost,
181    current_spell: Option<CardId>,
182) -> Vec<AutoTapChoice> {
183    auto_tap_lands_internal(
184        game,
185        pool,
186        player,
187        cost,
188        current_spell,
189        false,
190        &[],
191        &mut None,
192    )
193}
194
195pub fn auto_tap_lands_allow_reserved_source_reuse_trace(
196    game: &mut GameState,
197    pool: &mut ManaPool,
198    player: PlayerId,
199    cost: &ManaCost,
200    current_spell: Option<CardId>,
201) -> Vec<AutoTapChoice> {
202    auto_tap_lands_internal(
203        game,
204        pool,
205        player,
206        cost,
207        current_spell,
208        true,
209        &[],
210        &mut None,
211    )
212}
213
214/// Auto-tap with an explicit sacrifice chooser callback for parity with Java's
215/// `choosePermanentsToSacrifice` RNG path.
216pub fn auto_tap_lands_with_chooser(
217    game: &mut GameState,
218    pool: &mut ManaPool,
219    player: PlayerId,
220    cost: &ManaCost,
221    current_spell: Option<CardId>,
222    sacrifice_chooser: SacrificeChooser<'_>,
223) -> Vec<CardId> {
224    let mut callback = |kind: ManaPayCallback<'_>| -> Option<CardId> {
225        match kind {
226            ManaPayCallback::ChooseSacrifice(valid) => sacrifice_chooser(valid),
227            ManaPayCallback::ChooseColor(_) => None,
228            ManaPayCallback::ChooseTapType { .. } => None,
229            ManaPayCallback::ConfirmSelfSacrifice(cid) => Some(cid),
230            ManaPayCallback::ConfirmSubCounter(cid) => Some(cid),
231            ManaPayCallback::ConfirmSourceExile(cid) => Some(cid),
232            ManaPayCallback::ConfirmPayLife(cid) => Some(cid),
233            ManaPayCallback::NotifySacrificeForMana(cid) => Some(cid),
234            ManaPayCallback::ApplyProduceManaReplacement { .. } => None,
235        }
236    };
237    auto_tap_lands_internal(
238        game,
239        pool,
240        player,
241        cost,
242        current_spell,
243        false,
244        &[],
245        &mut Some(&mut callback),
246    )
247    .into_iter()
248    .map(|choice| choice.card_id)
249    .collect()
250}
251
252pub fn auto_tap_lands_allow_reserved_source_reuse_with_chooser(
253    game: &mut GameState,
254    pool: &mut ManaPool,
255    player: PlayerId,
256    cost: &ManaCost,
257    current_spell: Option<CardId>,
258    sacrifice_chooser: SacrificeChooser<'_>,
259) -> Vec<CardId> {
260    let mut callback = |kind: ManaPayCallback<'_>| -> Option<CardId> {
261        match kind {
262            ManaPayCallback::ChooseSacrifice(valid) => sacrifice_chooser(valid),
263            ManaPayCallback::ChooseColor(_) => None,
264            ManaPayCallback::ChooseTapType { .. } => None,
265            ManaPayCallback::ConfirmSelfSacrifice(cid) => Some(cid),
266            ManaPayCallback::ConfirmSubCounter(cid) => Some(cid),
267            ManaPayCallback::ConfirmSourceExile(cid) => Some(cid),
268            ManaPayCallback::ConfirmPayLife(cid) => Some(cid),
269            ManaPayCallback::NotifySacrificeForMana(cid) => Some(cid),
270            ManaPayCallback::ApplyProduceManaReplacement { .. } => None,
271        }
272    };
273    auto_tap_lands_internal(
274        game,
275        pool,
276        player,
277        cost,
278        current_spell,
279        true,
280        &[],
281        &mut Some(&mut callback),
282    )
283    .into_iter()
284    .map(|choice| choice.card_id)
285    .collect()
286}
287
288/// Auto-tap with unified callback for both sacrifice chooser and confirm payment.
289/// Used by parity tests to mirror Java's RNG-driven decision paths.
290pub fn auto_tap_lands_with_callbacks(
291    game: &mut GameState,
292    pool: &mut ManaPool,
293    player: PlayerId,
294    cost: &ManaCost,
295    current_spell: Option<CardId>,
296    callback: ManaPayCallbackFn<'_>,
297) -> Vec<CardId> {
298    auto_tap_lands_internal(
299        game,
300        pool,
301        player,
302        cost,
303        current_spell,
304        false,
305        &[],
306        &mut Some(callback),
307    )
308    .into_iter()
309    .map(|choice| choice.card_id)
310    .collect()
311}
312
313pub fn auto_tap_lands_trace_with_callbacks(
314    game: &mut GameState,
315    pool: &mut ManaPool,
316    player: PlayerId,
317    cost: &ManaCost,
318    current_spell: Option<CardId>,
319    callback: ManaPayCallbackFn<'_>,
320) -> Vec<AutoTapChoice> {
321    auto_tap_lands_internal(
322        game,
323        pool,
324        player,
325        cost,
326        current_spell,
327        false,
328        &[],
329        &mut Some(callback),
330    )
331}
332
333/// Same as [`auto_tap_lands_trace_with_callbacks`] but excludes the given
334/// permanents from the auto-payer's mana-source pool. Used during spell
335/// casting so a permanent reserved for the spell's additional sacrifice
336/// cost (`Sac<1/X>`) can't also be picked for a `Sac<1/CARDNAME>` mana
337/// ability — see the seed-62 Eviscerator's Insight divergence.
338pub fn auto_tap_lands_trace_with_callbacks_and_reserved_sacrifices(
339    game: &mut GameState,
340    pool: &mut ManaPool,
341    player: PlayerId,
342    cost: &ManaCost,
343    current_spell: Option<CardId>,
344    reserved_sacrifices: &[CardId],
345    callback: ManaPayCallbackFn<'_>,
346) -> Vec<AutoTapChoice> {
347    auto_tap_lands_internal(
348        game,
349        pool,
350        player,
351        cost,
352        current_spell,
353        false,
354        reserved_sacrifices,
355        &mut Some(callback),
356    )
357}
358
359/// Same as [`auto_tap_lands_trace_with_callbacks_and_reserved_sacrifices`]
360/// but propagates a `ManaPaymentContext` to the source-grouping pass so
361/// `RestrictValid$` mana sources are filtered out when they don't apply to
362/// the current payment (e.g. Flamebraider's "Spend only on Elemental spells/
363/// abilities" must not show up when paying an `UnlessCost`).
364pub fn auto_tap_lands_trace_with_callbacks_reserved_and_ctx(
365    game: &mut GameState,
366    pool: &mut ManaPool,
367    player: PlayerId,
368    cost: &ManaCost,
369    current_spell: Option<CardId>,
370    reserved_sacrifices: &[CardId],
371    callback: ManaPayCallbackFn<'_>,
372    payment_ctx: &crate::mana::ManaPaymentContext,
373) -> Vec<AutoTapChoice> {
374    auto_tap_lands_internal_with_ctx(
375        game,
376        pool,
377        player,
378        cost,
379        current_spell,
380        false,
381        reserved_sacrifices,
382        &mut Some(callback),
383        Some(payment_ctx),
384        false,
385        false,
386    )
387    .choices
388}
389
390#[allow(clippy::too_many_arguments)]
391pub fn auto_tap_lands_pay_incremental_with_callbacks_reserved_and_ctx(
392    game: &mut GameState,
393    pool: &mut ManaPool,
394    player: PlayerId,
395    cost: &ManaCost,
396    current_spell: Option<CardId>,
397    reserved_sacrifices: &[CardId],
398    callback: ManaPayCallbackFn<'_>,
399    payment_ctx: &crate::mana::ManaPaymentContext,
400    any_color_conversion: bool,
401) -> AutoTapPaymentTrace {
402    auto_tap_lands_internal_with_ctx(
403        game,
404        pool,
405        player,
406        cost,
407        current_spell,
408        false,
409        reserved_sacrifices,
410        &mut Some(callback),
411        Some(payment_ctx),
412        true,
413        any_color_conversion,
414    )
415}
416
417/// Determine the next mana source/ability auto-pay would use without mutating
418/// the game or pool. This lets callback-driven payment replay the exact same
419/// source choice as engine auto-pay, including multi-ability lands.
420pub fn next_auto_tap_choice(
421    game: &GameState,
422    pool: &ManaPool,
423    player: PlayerId,
424    cost: &ManaCost,
425    current_spell: Option<CardId>,
426    allow_reserved_source_reuse: bool,
427) -> Option<AutoTapChoice> {
428    next_auto_tap_choice_with_reserved_sacrifices(
429        game,
430        pool,
431        player,
432        cost,
433        current_spell,
434        allow_reserved_source_reuse,
435        &[],
436    )
437}
438
439pub fn next_auto_tap_choice_with_reserved_sacrifices(
440    game: &GameState,
441    pool: &ManaPool,
442    player: PlayerId,
443    cost: &ManaCost,
444    current_spell: Option<CardId>,
445    allow_reserved_source_reuse: bool,
446    reserved_sacrifices: &[CardId],
447) -> Option<AutoTapChoice> {
448    let mut unpaid = ManaCostBeingPaid::from_mana_cost(cost);
449    pay_cost_from_pool(&mut unpaid, pool);
450    if unpaid.is_paid() {
451        return None;
452    }
453
454    let mana_ability_map =
455        group_sources_by_mana_color(game, player, reserved_sacrifices, None, false);
456    if mana_ability_map.is_empty() {
457        return None;
458    }
459
460    let mut sources_for_shards = group_and_order_to_pay_shards(&mana_ability_map, &unpaid);
461    if sources_for_shards.is_empty() {
462        return None;
463    }
464    sort_sources_for_autopay(game, player, &mut sources_for_shards);
465
466    let to_pay = get_next_shard_to_pay(&unpaid, &sources_for_shards)?;
467    let ma_list = sources_for_shards.get(&to_pay)?;
468    let sa_payment = choose_mana_ability(
469        game,
470        player,
471        current_spell,
472        to_pay,
473        ma_list,
474        allow_reserved_source_reuse,
475        reserved_sacrifices,
476        &sources_for_shards,
477        &unpaid,
478    )?;
479    let chosen_atom = choose_atom_for_shard(&sa_payment, to_pay)?;
480    Some(AutoTapChoice {
481        card_id: sa_payment.card_id,
482        mana_ability_index: sa_payment.ability_index,
483        chosen_atom,
484        needs_express_choice: sa_payment.atoms.len() > 1,
485    })
486}
487
488pub fn auto_tap_lands_allow_reserved_source_reuse_with_callbacks(
489    game: &mut GameState,
490    pool: &mut ManaPool,
491    player: PlayerId,
492    cost: &ManaCost,
493    current_spell: Option<CardId>,
494    callback: ManaPayCallbackFn<'_>,
495) -> Vec<CardId> {
496    auto_tap_lands_internal(
497        game,
498        pool,
499        player,
500        cost,
501        current_spell,
502        true,
503        &[],
504        &mut Some(callback),
505    )
506    .into_iter()
507    .map(|choice| choice.card_id)
508    .collect()
509}
510
511pub fn auto_tap_lands_allow_reserved_source_reuse_trace_with_callbacks_and_reserved_sacrifices(
512    game: &mut GameState,
513    pool: &mut ManaPool,
514    player: PlayerId,
515    cost: &ManaCost,
516    current_spell: Option<CardId>,
517    reserved_sacrifices: &[CardId],
518    callback: ManaPayCallbackFn<'_>,
519) -> Vec<AutoTapChoice> {
520    auto_tap_lands_internal(
521        game,
522        pool,
523        player,
524        cost,
525        current_spell,
526        true,
527        reserved_sacrifices,
528        &mut Some(callback),
529    )
530}
531
532pub fn auto_tap_lands_allow_reserved_source_reuse_with_callbacks_and_reserved_sacrifices(
533    game: &mut GameState,
534    pool: &mut ManaPool,
535    player: PlayerId,
536    cost: &ManaCost,
537    current_spell: Option<CardId>,
538    reserved_sacrifices: &[CardId],
539    callback: ManaPayCallbackFn<'_>,
540) -> Vec<CardId> {
541    auto_tap_lands_internal(
542        game,
543        pool,
544        player,
545        cost,
546        current_spell,
547        true,
548        reserved_sacrifices,
549        &mut Some(callback),
550    )
551    .into_iter()
552    .map(|choice| choice.card_id)
553    .collect()
554}
555
556/// Mirrors Java AutoPay.payManaCost() — the main auto-tap loop.
557///
558/// Key parity points:
559/// - Re-collects candidates EVERY iteration (fresh source list after each tap/sacrifice)
560/// - Tries ALL shards in priority order per iteration via `choose_candidate`
561/// - Uses `is_sole_source_for_other_shard` to preserve flexible sources
562/// - Delegates sacrifice/counter costs through the callback
563fn auto_tap_lands_internal(
564    game: &mut GameState,
565    pool: &mut ManaPool,
566    player: PlayerId,
567    cost: &ManaCost,
568    current_spell: Option<CardId>,
569    allow_reserved_source_reuse: bool,
570    reserved_sacrifices: &[CardId],
571    callback: &mut Option<ManaPayCallbackFn<'_>>,
572) -> Vec<AutoTapChoice> {
573    auto_tap_lands_internal_with_ctx(
574        game,
575        pool,
576        player,
577        cost,
578        current_spell,
579        allow_reserved_source_reuse,
580        reserved_sacrifices,
581        callback,
582        None,
583        false,
584        false,
585    )
586    .choices
587}
588
589fn auto_tap_lands_internal_with_ctx(
590    game: &mut GameState,
591    pool: &mut ManaPool,
592    player: PlayerId,
593    cost: &ManaCost,
594    current_spell: Option<CardId>,
595    allow_reserved_source_reuse: bool,
596    reserved_sacrifices: &[CardId],
597    callback: &mut Option<ManaPayCallbackFn<'_>>,
598    payment_ctx: Option<&crate::mana::ManaPaymentContext>,
599    consume_incrementally: bool,
600    any_color_conversion: bool,
601) -> AutoTapPaymentTrace {
602    let mut tapped_choices: Vec<AutoTapChoice> = Vec::new();
603    let mut payment = ManaPaymentOutcome::default();
604
605    let trace = std::env::var("FORGE_PAYMENT_TRACE").is_ok();
606    if trace {
607        let turn = game.turn.turn_number;
608        let phase = format!("{:?}", game.turn.phase);
609        let spell_name = current_spell
610            .map(|cid| game.card(cid).card_name.clone())
611            .unwrap_or_else(|| "<none>".to_string());
612        eprintln!(
613            "[pay-trace-rust] T{} {} P{:?} AUTO-PAY-START cost={} spell={} pool_before={}",
614            turn,
615            phase,
616            player,
617            cost,
618            spell_name,
619            pool.total_mana(),
620        );
621    }
622
623    let mut unpaid = ManaCostBeingPaid::from_mana_cost(cost);
624    if consume_incrementally {
625        let spent = pool.pay_unpaid_for_spell_incremental(
626            &mut unpaid,
627            payment_ctx.unwrap_or(&crate::mana::ManaPaymentContext::default()),
628            any_color_conversion,
629        );
630        payment.colors_spent |= spent.colors_spent;
631        payment.paying_mana.extend(spent.paying_mana);
632    } else {
633        pay_cost_from_pool(&mut unpaid, pool);
634    }
635    if unpaid.is_paid() {
636        if trace {
637            eprintln!("[pay-trace-rust] AUTO-PAY-EXIT-EARLY paid-from-pool");
638        }
639        return AutoTapPaymentTrace {
640            choices: tapped_choices,
641            payment,
642            paid: true,
643        };
644    }
645
646    // Guard counter mirrors Java's AutoPay.payManaCost() `guard++ < 128`.
647    let mut guard = 0u32;
648    while !unpaid.is_paid() && guard < 128 {
649        guard += 1;
650
651        // Java re-collects candidates every iteration. This ensures tapped/sacrificed
652        // sources are excluded and state changes from the previous iteration are visible.
653        let mana_ability_map =
654            group_sources_by_mana_color(game, player, reserved_sacrifices, payment_ctx, false);
655        if mana_ability_map.is_empty() {
656            break;
657        }
658        let mut candidates = collect_sorted_candidates(game, player, &mana_ability_map);
659        if candidates.is_empty() {
660            break;
661        }
662
663        // Java's chooseCandidate: iterate shards in priority order, pick the
664        // least-versatile candidate that can pay.
665        let Some((sa_payment, to_pay)) = choose_candidate(
666            game,
667            player,
668            current_spell,
669            &candidates,
670            &unpaid,
671            allow_reserved_source_reuse,
672            reserved_sacrifices,
673        ) else {
674            break;
675        };
676
677        let Some(chosen_atom) = choose_atom_for_shard(&sa_payment, to_pay) else {
678            break;
679        };
680        // Pay non-tap ability costs (sacrifice, counter removal) through callback.
681        // If payment fails (e.g. sacrifice declined), remove the candidate and retry.
682        if !pay_non_tap_mana_ability_costs(
683            game,
684            player,
685            &sa_payment,
686            current_spell,
687            allow_reserved_source_reuse,
688            reserved_sacrifices,
689            callback,
690        ) {
691            // Java: candidate became unpayable; remove and continue.
692            candidates.retain(|c| c.card_id != sa_payment.card_id);
693            continue;
694        }
695
696        if let Some(fixed_atoms) = fixed_output_atoms_for_payment(game, player, &sa_payment) {
697            let is_special_output = sa_payment.mana_text.starts_with("Special ");
698            let trace_atom = if is_special_output {
699                fixed_atoms.iter().fold(0, |acc, atom| acc | *atom)
700            } else {
701                chosen_atom
702            };
703            let produced =
704                produce_mana_for_auto_pay(game, pool, player, &sa_payment, chosen_atom, callback);
705            let trigger_atoms =
706                add_taps_for_mana_trigger_mana(game, pool, player, &sa_payment, &produced);
707            if consume_incrementally {
708                let spent = pool.pay_unpaid_for_spell_incremental(
709                    &mut unpaid,
710                    payment_ctx.unwrap_or(&crate::mana::ManaPaymentContext::default()),
711                    any_color_conversion,
712                );
713                payment.colors_spent |= spent.colors_spent;
714                payment.paying_mana.extend(spent.paying_mana);
715            } else {
716                for &atom in &trigger_atoms {
717                    let _ = unpaid.try_pay_mana(atom, atom as u8);
718                }
719            }
720            tapped_choices.push(AutoTapChoice {
721                card_id: sa_payment.card_id,
722                mana_ability_index: sa_payment.ability_index,
723                chosen_atom: trace_atom,
724                needs_express_choice: is_special_output,
725            });
726        } else {
727            // Sources with more than one possible color require a color
728            // choice at resolution (Java fires `chooseColor` once per pick).
729            // `sa_payment.atoms` already accounts for Combo ColorIdentity
730            // because `group_sources_by_mana_color` resolves it against the
731            // commander identity when the Produced$ IR is ComboColorIdentity.
732            let is_empty_combo_color_identity = sa_payment
733                .produced_ir
734                .as_ref()
735                .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
736                && sa_payment.atoms.is_empty();
737            let needs_express = sa_payment.atoms.len() > 1;
738            let mut trigger_atoms_for_non_incremental: Vec<u16> = Vec::new();
739            if is_empty_combo_color_identity {
740                // Java's deterministic AutoPay taps an empty `Combo
741                // ColorIdentity` source (Arcane Signet in a non-Commander
742                // game, etc.) but produces no mana. Skip
743                // `produce_mana_for_auto_pay` entirely so the helper doesn't
744                // add a stand-in atom to the pool.
745                if source_requires_tap(game, &sa_payment) && !game.card(sa_payment.card_id).tapped {
746                    game.tap(sa_payment.card_id);
747                }
748            } else {
749                let produced = produce_mana_for_auto_pay(
750                    game,
751                    pool,
752                    player,
753                    &sa_payment,
754                    chosen_atom,
755                    callback,
756                );
757                trigger_atoms_for_non_incremental =
758                    add_taps_for_mana_trigger_mana(game, pool, player, &sa_payment, &produced);
759            }
760
761            tapped_choices.push(AutoTapChoice {
762                card_id: sa_payment.card_id,
763                mana_ability_index: sa_payment.ability_index,
764                chosen_atom,
765                needs_express_choice: needs_express,
766            });
767
768            if consume_incrementally {
769                if !is_empty_combo_color_identity {
770                    let spent = pool.pay_unpaid_for_spell_incremental(
771                        &mut unpaid,
772                        payment_ctx.unwrap_or(&crate::mana::ManaPaymentContext::default()),
773                        any_color_conversion,
774                    );
775                    payment.colors_spent |= spent.colors_spent;
776                    payment.paying_mana.extend(spent.paying_mana);
777                }
778            } else if !is_empty_combo_color_identity {
779                let _ = unpaid.try_pay_mana(chosen_atom, chosen_atom as u8);
780                for _ in 1..sa_payment.amount.max(1) {
781                    let _ = unpaid.try_pay_mana(chosen_atom, chosen_atom as u8);
782                }
783                for &atom in &trigger_atoms_for_non_incremental {
784                    let _ = unpaid.try_pay_mana(atom, atom as u8);
785                }
786            }
787            // NOTE: do not re-iterate `1..amount` here to push extra mana into
788            // the pool. `produce_mana_for_auto_pay` already adds the full
789            // `Amount$` worth of mana via `auto_pay_base_mana_string`, so an
790            // additional loop would double-count. Origin/main carried such a
791            // loop because its inline path called `tap_land_for_mana` (which
792            // adds only one mana) and had to manually back-fill extras —
793            // that's no longer needed with the helper.
794        }
795    }
796
797    // Phyrexian-life fallback: after the tap-and-pay loop finishes, any
798    // remaining unpaid shards that are phyrexian can be paid with 2 life
799    // each (CR 107.4f). Mirrors Java `ManaPool.payManaCost`'s phyrexian
800    // handling. Without this, cards like Mutagenic Growth / Dismember /
801    // Gut Shot can never be cast when the player lacks the matching
802    // colored mana even with enough life to pay.
803    if !unpaid.is_paid() && unpaid.contains_only_phyrexian_mana() {
804        // Mark the cost as paid in the unpaid tracker and accumulate the
805        // life that needs to be spent. The actual life deduction is the
806        // caller's job (cast_spell.rs invokes pay_life_cost based on
807        // result.life_paid, which routes through life-payment replacements
808        // and triggers). Deducting here would double-charge.
809        let life_required = required_phyrexian_life(&unpaid);
810        if game.player(player).life > life_required {
811            while !unpaid.is_paid() {
812                if !unpaid.pay_phyrexian() {
813                    break;
814                }
815                payment.life_paid += 2;
816            }
817        }
818    }
819
820    AutoTapPaymentTrace {
821        choices: tapped_choices,
822        payment,
823        paid: unpaid.is_paid(),
824    }
825}
826
827fn produce_mana_for_auto_pay(
828    game: &mut GameState,
829    pool: &mut ManaPool,
830    player: PlayerId,
831    ma: &ManaAbilityRef,
832    chosen_atom: u16,
833    callback: &mut Option<ManaPayCallbackFn<'_>>,
834) -> String {
835    if source_requires_tap(game, ma) && !game.card(ma.card_id).tapped {
836        game.tap(ma.card_id);
837    }
838
839    let source = game.card(ma.card_id);
840    let ab = ma
841        .ability_index
842        .and_then(|idx| source.activated_abilities.get(idx));
843    let params = ManaProductionParams {
844        source_card: ma.card_id,
845        is_snow: source.type_line.is_snow(),
846        restriction: ab.and_then(|a| a.restrict_valid.as_deref().map(str::to_string)),
847        adds_no_counter: ab.map(|a| a.adds_no_counter).unwrap_or(false),
848        adds_keywords: ab.and_then(|a| a.adds_keywords.clone()),
849        adds_keywords_valid: ab.and_then(|a| a.adds_keywords_valid.clone()),
850        adds_counters: ab.and_then(|a| a.adds_counters.clone()),
851        adds_counters_valid: ab.and_then(|a| a.adds_counters_valid.clone()),
852        triggers_when_spent: ab.and_then(|a| a.triggers_when_spent.clone()),
853    };
854
855    let mut mana_string = auto_pay_base_mana_string(game, player, ma, chosen_atom, callback);
856    if let Some(ref mut cb) = callback {
857        cb(ManaPayCallback::ApplyProduceManaReplacement {
858            activator: player,
859            source_card: ma.card_id,
860            mana: &mut mana_string,
861        });
862    }
863    add_produced_mana_to_pool(pool, &mana_string, &params);
864    mana_string
865}
866
867fn add_taps_for_mana_trigger_mana(
868    game: &GameState,
869    pool: &mut ManaPool,
870    player: PlayerId,
871    sa_payment: &ManaAbilityRef,
872    produced: &str,
873) -> Vec<u16> {
874    add_taps_for_mana_trigger_mana_impl(game, pool, player, sa_payment, produced, true)
875}
876
877fn add_taps_for_mana_trigger_mana_impl(
878    game: &GameState,
879    pool: &mut ManaPool,
880    player: PlayerId,
881    sa_payment: &ManaAbilityRef,
882    produced: &str,
883    require_tap: bool,
884) -> Vec<u16> {
885    // TapsForMana fires only when the mana ability has a Tap cost
886    // (`AbilityManaPart.tapsForMana`). Implicit basic-land taps have no parsed
887    let mut produced_atoms: Vec<u16> = Vec::new();
888    let tapped_source = sa_payment.card_id;
889    let pays_with_tap = match sa_payment.ability_index {
890        Some(idx) => game
891            .card(tapped_source)
892            .activated_abilities
893            .get(idx)
894            .is_some_and(|ab| {
895                ab.cost
896                    .parts
897                    .iter()
898                    .any(|part| matches!(part, CostPart::Tap))
899            }),
900        None => true,
901    };
902    if require_tap && !pays_with_tap {
903        return produced_atoms;
904    }
905    let params = RunParams {
906        card: Some(tapped_source),
907        player: Some(player),
908        activator: Some(player),
909        produced: Some(produced.to_string()),
910        ..Default::default()
911    };
912    for &host_id in game.cards_in_zone(ZoneType::Battlefield, player) {
913        let host = game.card(host_id);
914        for trigger in &host.triggers {
915            if trigger.kind != crate::trigger::TriggerType::TapsForMana {
916                continue;
917            }
918            if !trigger.get_mode().perform_test(trigger, &params, game) {
919                continue;
920            }
921            let Some(sa) = trigger.ensure_ability(game, host_id, player) else {
922                continue;
923            };
924            let Some(produced_ir) = sa.produced_ir() else {
925                continue;
926            };
927            let amount = sa.amount_of_mana_generated().max(1);
928            let atoms = produced_ir.to_atoms(&host.chosen_colors);
929            let Some(atom) = atoms.first().copied() else {
930                continue;
931            };
932            let Some(letter) = ManaPool::atom_to_letter(atom).chars().next() else {
933                continue;
934            };
935            let mana_string = std::iter::repeat_n(letter.to_string(), amount as usize)
936                .collect::<Vec<_>>()
937                .join(" ");
938            let mana_params = ManaProductionParams {
939                source_card: host_id,
940                is_snow: host.type_line.is_snow(),
941                restriction: None,
942                adds_no_counter: false,
943                adds_keywords: None,
944                adds_keywords_valid: None,
945                adds_counters: None,
946                adds_counters_valid: None,
947                triggers_when_spent: None,
948            };
949            // Panharmonicon: each qualifying static fires the trigger again.
950            let extra = crate::staticability::static_ability_panharmonicon::extra_triggers(
951                game, host_id, trigger, &params,
952            );
953            let total_fires = 1 + extra as usize;
954            for _ in 0..total_fires {
955                add_produced_mana_to_pool(pool, &mana_string, &mana_params);
956                for _ in 0..amount {
957                    produced_atoms.push(atom);
958                }
959            }
960        }
961    }
962    produced_atoms
963}
964
965fn auto_pay_base_mana_string(
966    game: &GameState,
967    player: PlayerId,
968    ma: &ManaAbilityRef,
969    chosen_atom: u16,
970    callback: &mut Option<ManaPayCallbackFn<'_>>,
971) -> String {
972    let base_amount = auto_pay_base_amount(game, player, ma).max(1) as usize;
973
974    // Empty Combo ColorIdentity produces nothing — `ManaEffect.resolve`.
975    if ma
976        .produced_ir
977        .as_ref()
978        .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
979        && ma.atoms.is_empty()
980    {
981        return String::new();
982    }
983
984    if let Some(fixed_atoms) = ma
985        .produced_ir
986        .as_ref()
987        .and_then(crate::ability::ProducedMana::fixed_atoms)
988    {
989        return repeat_atoms_as_mana_string(&fixed_atoms, base_amount);
990    }
991
992    if let Some(special) = ma
993        .produced_ir
994        .as_ref()
995        .and_then(crate::ability::ProducedMana::special_kind)
996    {
997        let atoms = crate::ability::effects::mana_effect::available_special_mana_atoms(
998            game, ma.card_id, player, special,
999        );
1000        return repeat_atoms_as_mana_string(&atoms, base_amount);
1001    }
1002
1003    if ma.atoms.len() > 1 {
1004        if let Some(ref mut cb) = callback {
1005            if let Some(color_name) = super::mana_atom_to_color_name(chosen_atom) {
1006                let forced = [color_name.to_string()];
1007                for _ in 0..base_amount {
1008                    cb(ManaPayCallback::ChooseColor(&forced));
1009                }
1010            }
1011        }
1012    }
1013
1014    repeat_atoms_as_mana_string(&[chosen_atom], base_amount)
1015}
1016
1017fn auto_pay_base_amount(game: &GameState, player: PlayerId, ma: &ManaAbilityRef) -> i32 {
1018    ma.ability_index
1019        .and_then(|idx| game.card(ma.card_id).activated_abilities.get(idx))
1020        .map(|ab| {
1021            parse_mana_ability_amount_with_game(ab, Some(game), Some(ma.card_id), Some(player))
1022        })
1023        .unwrap_or(1)
1024}
1025
1026fn repeat_atoms_as_mana_string(atoms: &[u16], repeats: usize) -> String {
1027    let mut out = Vec::new();
1028    for _ in 0..repeats.max(1) {
1029        for &atom in atoms {
1030            out.push(ManaPool::atom_to_letter(atom).to_string());
1031        }
1032    }
1033    out.join(" ")
1034}
1035
1036fn pay_cost_from_pool(unpaid: &mut ManaCostBeingPaid, pool: &ManaPool) {
1037    let colors = [
1038        (ManaAtom::WHITE, pool.white()),
1039        (ManaAtom::BLUE, pool.blue()),
1040        (ManaAtom::BLACK, pool.black()),
1041        (ManaAtom::RED, pool.red()),
1042        (ManaAtom::GREEN, pool.green()),
1043        (ManaAtom::COLORLESS, pool.colorless()),
1044    ];
1045
1046    for (atom, count) in colors {
1047        for _ in 0..count.max(0) {
1048            if unpaid.is_paid() {
1049                return;
1050            }
1051            let _ = unpaid.try_pay_mana(atom, atom as u8);
1052        }
1053    }
1054}
1055
1056fn get_next_shard_to_pay(
1057    unpaid: &ManaCostBeingPaid,
1058    sources_for_shards: &IndexMap<ManaCostShard, Vec<ManaAbilityRef>>,
1059) -> Option<ManaCostShard> {
1060    let mut shards_to_pay = unpaid.get_distinct_shards();
1061    shards_to_pay.sort_by_key(|shard| sources_for_shards.get(shard).map_or(0, |v| v.len()));
1062    unpaid.get_shard_to_pay_by_priority(&shards_to_pay, ManaAtom::COLORS_SUPERPOSITION as u8)
1063}
1064
1065/// Build a flat, sorted candidate list from the mana ability map.
1066/// Mirrors Java AutoPay.collectPlayableManaAbilities() — called fresh each iteration.
1067fn collect_sorted_candidates(
1068    game: &GameState,
1069    player: PlayerId,
1070    mana_ability_map: &IndexMap<i32, Vec<ManaAbilityRef>>,
1071) -> Vec<ManaAbilityRef> {
1072    collect_sorted_candidates_with_pref(game, player, mana_ability_map, false)
1073}
1074
1075fn collect_sorted_candidates_with_pref(
1076    game: &GameState,
1077    player: PlayerId,
1078    mana_ability_map: &IndexMap<i32, Vec<ManaAbilityRef>>,
1079    prefer_higher_amount: bool,
1080) -> Vec<ManaAbilityRef> {
1081    let mut out: Vec<ManaAbilityRef> = mana_ability_map
1082        .values()
1083        .flat_map(|v| v.iter().cloned())
1084        .collect();
1085    // Deduplicate by (card_id, ability_index) — same ability may appear under multiple color keys.
1086    let mut seen = std::collections::HashSet::new();
1087    out.retain(|ma| seen.insert((ma.card_id, ma.ability_index, ma.source_order)));
1088    // Sort by score, then by zone_timestamp (battlefield entry order) to match
1089    // Java's card iteration which uses timestamp order, not CardId order.
1090    out.sort_by(|a, b| {
1091        let score_a = autopay_source_score(game, player, a);
1092        let score_b = autopay_source_score(game, player, b);
1093        (score_a * 1000).cmp(&(score_b * 1000)).then_with(|| {
1094            let ts_a = game.card(a.card_id).zone_timestamp;
1095            let ts_b = game.card(b.card_id).zone_timestamp;
1096            ts_a.cmp(&ts_b)
1097                .then_with(|| {
1098                    // Probe-only tiebreak: prefer the higher-amount ability of
1099                    // the same source so the greedy picker doesn't shadow it.
1100                    if prefer_higher_amount && a.card_id == b.card_id {
1101                        b.amount.cmp(&a.amount)
1102                    } else {
1103                        std::cmp::Ordering::Equal
1104                    }
1105                })
1106                .then_with(|| a.source_order.cmp(&b.source_order))
1107        })
1108    });
1109    out
1110}
1111
1112/// Returns the chosen source and the shard it will pay.
1113fn choose_candidate(
1114    game: &GameState,
1115    player: PlayerId,
1116    current_spell: Option<CardId>,
1117    candidates: &[ManaAbilityRef],
1118    unpaid: &ManaCostBeingPaid,
1119    allow_reserved_source_reuse: bool,
1120    reserved_sacrifices: &[CardId],
1121) -> Option<(ManaAbilityRef, ManaCostShard)> {
1122    for shard in shard_priority(unpaid, candidates) {
1123        if let Some(ma) = choose_least_versatile_candidate(
1124            game,
1125            player,
1126            current_spell,
1127            candidates,
1128            shard,
1129            unpaid,
1130            allow_reserved_source_reuse,
1131            reserved_sacrifices,
1132        ) {
1133            return Some((ma, shard));
1134        }
1135    }
1136    None
1137}
1138
1139fn shard_priority(unpaid: &ManaCostBeingPaid, candidates: &[ManaAbilityRef]) -> Vec<ManaCostShard> {
1140    let mut colored = Vec::new();
1141    let mut generic = None;
1142    let mut seen = std::collections::HashSet::new();
1143    for shard in unpaid.get_distinct_shards() {
1144        if matches!(shard, ManaCostShard::X | ManaCostShard::ColoredX) {
1145            continue;
1146        }
1147        if !seen.insert(shard) {
1148            continue;
1149        }
1150        if matches!(shard, ManaCostShard::Generic) {
1151            generic = Some(shard);
1152        } else {
1153            colored.push(shard);
1154        }
1155    }
1156    // Sort colored shards by fewest available candidates (most constrained first).
1157    // Equal-count shards need a deterministic tiebreak; otherwise payment can
1158    // consume flexible sources in different orders across runs/engines.
1159    colored.sort_by(|&a, &b| {
1160        let count_a = count_candidates_for_shard(candidates, a);
1161        let count_b = count_candidates_for_shard(candidates, b);
1162        count_a
1163            .cmp(&count_b)
1164            .then_with(|| shard_color_rank(a).cmp(&shard_color_rank(b)))
1165    });
1166    if let Some(g) = generic {
1167        colored.push(g);
1168    }
1169    colored
1170}
1171
1172fn shard_color_rank(shard: ManaCostShard) -> u8 {
1173    let ordered = color_set_order_atoms(shard.color_mask() as u16);
1174    let Some(primary) = ordered.first() else {
1175        return 5;
1176    };
1177    color_set_order_atoms(ManaAtom::COLORS_SUPERPOSITION)
1178        .iter()
1179        .position(|atom| atom == primary)
1180        .map(|idx| idx as u8)
1181        .unwrap_or(5)
1182}
1183
1184fn color_set_order_atoms(mask: u16) -> &'static [u16] {
1185    match mask & ManaAtom::COLORS_SUPERPOSITION {
1186        0 => &[],
1187        1 => &[ManaAtom::WHITE],
1188        2 => &[ManaAtom::BLUE],
1189        3 => &[ManaAtom::WHITE, ManaAtom::BLUE],
1190        4 => &[ManaAtom::BLACK],
1191        5 => &[ManaAtom::WHITE, ManaAtom::BLACK],
1192        6 => &[ManaAtom::BLUE, ManaAtom::BLACK],
1193        7 => &[ManaAtom::WHITE, ManaAtom::BLUE, ManaAtom::BLACK],
1194        8 => &[ManaAtom::RED],
1195        9 => &[ManaAtom::RED, ManaAtom::WHITE],
1196        10 => &[ManaAtom::BLUE, ManaAtom::RED],
1197        11 => &[ManaAtom::BLUE, ManaAtom::RED, ManaAtom::WHITE],
1198        12 => &[ManaAtom::BLACK, ManaAtom::RED],
1199        13 => &[ManaAtom::RED, ManaAtom::WHITE, ManaAtom::BLACK],
1200        14 => &[ManaAtom::BLUE, ManaAtom::BLACK, ManaAtom::RED],
1201        15 => &[
1202            ManaAtom::WHITE,
1203            ManaAtom::BLUE,
1204            ManaAtom::BLACK,
1205            ManaAtom::RED,
1206        ],
1207        16 => &[ManaAtom::GREEN],
1208        17 => &[ManaAtom::GREEN, ManaAtom::WHITE],
1209        18 => &[ManaAtom::GREEN, ManaAtom::BLUE],
1210        19 => &[ManaAtom::GREEN, ManaAtom::WHITE, ManaAtom::BLUE],
1211        20 => &[ManaAtom::BLACK, ManaAtom::GREEN],
1212        21 => &[ManaAtom::WHITE, ManaAtom::BLACK, ManaAtom::GREEN],
1213        22 => &[ManaAtom::BLACK, ManaAtom::GREEN, ManaAtom::BLUE],
1214        23 => &[
1215            ManaAtom::GREEN,
1216            ManaAtom::WHITE,
1217            ManaAtom::BLUE,
1218            ManaAtom::BLACK,
1219        ],
1220        24 => &[ManaAtom::RED, ManaAtom::GREEN],
1221        25 => &[ManaAtom::RED, ManaAtom::GREEN, ManaAtom::WHITE],
1222        26 => &[ManaAtom::GREEN, ManaAtom::BLUE, ManaAtom::RED],
1223        27 => &[
1224            ManaAtom::RED,
1225            ManaAtom::GREEN,
1226            ManaAtom::WHITE,
1227            ManaAtom::BLUE,
1228        ],
1229        28 => &[ManaAtom::BLACK, ManaAtom::RED, ManaAtom::GREEN],
1230        29 => &[
1231            ManaAtom::BLACK,
1232            ManaAtom::RED,
1233            ManaAtom::GREEN,
1234            ManaAtom::WHITE,
1235        ],
1236        30 => &[
1237            ManaAtom::BLUE,
1238            ManaAtom::BLACK,
1239            ManaAtom::RED,
1240            ManaAtom::GREEN,
1241        ],
1242        31 => &[
1243            ManaAtom::WHITE,
1244            ManaAtom::BLUE,
1245            ManaAtom::BLACK,
1246            ManaAtom::RED,
1247            ManaAtom::GREEN,
1248        ],
1249        _ => &[],
1250    }
1251}
1252
1253/// Count how many candidates can pay a given shard.
1254fn count_candidates_for_shard(candidates: &[ManaAbilityRef], shard: ManaCostShard) -> usize {
1255    candidates.iter().filter(|c| c.can_pay_shard(shard)).count()
1256}
1257
1258fn choose_least_versatile_candidate(
1259    game: &GameState,
1260    player: PlayerId,
1261    current_spell: Option<CardId>,
1262    candidates: &[ManaAbilityRef],
1263    shard: ManaCostShard,
1264    unpaid: &ManaCostBeingPaid,
1265    allow_reserved_source_reuse: bool,
1266    reserved_sacrifices: &[CardId],
1267) -> Option<ManaAbilityRef> {
1268    let mut fallback: Option<ManaAbilityRef> = None;
1269    for ma in candidates {
1270        if Some(ma.card_id) == current_spell {
1271            continue;
1272        }
1273        if !ma.can_pay_shard(shard) {
1274            continue;
1275        }
1276        if !can_pay_non_tap_mana_ability_costs(
1277            game,
1278            player,
1279            ma,
1280            current_spell,
1281            allow_reserved_source_reuse,
1282            reserved_sacrifices,
1283        ) {
1284            continue;
1285        }
1286        if fallback.is_none() {
1287            fallback = Some(ma.clone());
1288        }
1289        if !is_sole_source_for_other_shard_candidates(ma, shard, candidates, unpaid) {
1290            return Some(ma.clone());
1291        }
1292    }
1293    fallback
1294}
1295
1296fn is_sole_source_for_other_shard_candidates(
1297    candidate: &ManaAbilityRef,
1298    current_shard: ManaCostShard,
1299    candidates: &[ManaAbilityRef],
1300    unpaid: &ManaCostBeingPaid,
1301) -> bool {
1302    let mut seen = std::collections::HashSet::new();
1303    for other_shard in unpaid.get_distinct_shards() {
1304        if other_shard == current_shard {
1305            continue;
1306        }
1307        if matches!(
1308            other_shard,
1309            ManaCostShard::Generic | ManaCostShard::X | ManaCostShard::ColoredX
1310        ) {
1311            continue;
1312        }
1313        if !seen.insert(other_shard) {
1314            continue;
1315        }
1316        if !candidate.can_pay_shard(other_shard) {
1317            continue;
1318        }
1319        let sources_for_other = candidates
1320            .iter()
1321            .filter(|alt| alt.can_pay_shard(other_shard))
1322            .count();
1323        if sources_for_other <= 1 {
1324            return true;
1325        }
1326    }
1327    false
1328}
1329
1330fn choose_mana_ability(
1331    game: &GameState,
1332    player: PlayerId,
1333    current_spell: Option<CardId>,
1334    to_pay: ManaCostShard,
1335    ma_list: &[ManaAbilityRef],
1336    allow_reserved_source_reuse: bool,
1337    reserved_sacrifices: &[CardId],
1338    sources_for_shards: &IndexMap<ManaCostShard, Vec<ManaAbilityRef>>,
1339    unpaid: &ManaCostBeingPaid,
1340) -> Option<ManaAbilityRef> {
1341    let mut fallback: Option<ManaAbilityRef> = None;
1342
1343    for ma in ma_list {
1344        if Some(ma.card_id) == current_spell {
1345            continue;
1346        }
1347        if !ma.can_pay_shard(to_pay)
1348            || !can_pay_non_tap_mana_ability_costs(
1349                game,
1350                player,
1351                ma,
1352                current_spell,
1353                allow_reserved_source_reuse,
1354                reserved_sacrifices,
1355            )
1356        {
1357            continue;
1358        }
1359
1360        if fallback.is_none() {
1361            fallback = Some(ma.clone());
1362        }
1363
1364        // Check if this candidate is the sole source for another unpaid shard.
1365        // If so, defer it — another shard needs it more.
1366        if !is_sole_source_for_other_shard(ma, to_pay, sources_for_shards, unpaid) {
1367            return Some(ma.clone());
1368        }
1369    }
1370
1371    // All valid candidates are sole sources for other shards.
1372    // Fall back to the first valid one (forced pick).
1373    fallback
1374}
1375
1376/// Returns true if `candidate` is the ONLY source that can pay for some
1377/// other unpaid colored shard (not the current one, not generic/X).
1378fn is_sole_source_for_other_shard(
1379    candidate: &ManaAbilityRef,
1380    current_shard: ManaCostShard,
1381    sources_for_shards: &IndexMap<ManaCostShard, Vec<ManaAbilityRef>>,
1382    unpaid: &ManaCostBeingPaid,
1383) -> bool {
1384    for other_shard in unpaid.get_distinct_shards() {
1385        if other_shard == current_shard {
1386            continue;
1387        }
1388        // Skip generic/X shards — they can be paid by anything.
1389        if matches!(
1390            other_shard,
1391            ManaCostShard::Generic | ManaCostShard::X | ManaCostShard::ColoredX
1392        ) {
1393            continue;
1394        }
1395        if !candidate.can_pay_shard(other_shard) {
1396            continue;
1397        }
1398        // Count how many sources in the pool can pay for this other shard.
1399        let sources_for_other = sources_for_shards
1400            .get(&other_shard)
1401            .map(|list| {
1402                list.iter()
1403                    .filter(|alt| alt.can_pay_shard(other_shard))
1404                    .count()
1405            })
1406            .unwrap_or(0);
1407        if sources_for_other <= 1 {
1408            return true; // This candidate is the only source — defer it.
1409        }
1410    }
1411    false
1412}
1413
1414fn can_pay_non_tap_mana_ability_costs(
1415    game: &GameState,
1416    player: PlayerId,
1417    ma: &ManaAbilityRef,
1418    reserved_source: Option<CardId>,
1419    allow_reserved_source_reuse: bool,
1420    reserved_sacrifices: &[CardId],
1421) -> bool {
1422    let Some(ab_idx) = ma.ability_index else {
1423        return true;
1424    };
1425    let cost_parts: Vec<_> = game.card(ma.card_id).activated_abilities[ab_idx]
1426        .cost
1427        .parts
1428        .clone();
1429    for part in &cost_parts {
1430        if !can_pay_source_paid_mana_cost_part(
1431            game,
1432            player,
1433            ma.card_id,
1434            part,
1435            reserved_source,
1436            allow_reserved_source_reuse,
1437            reserved_sacrifices,
1438        ) {
1439            return false;
1440        }
1441    }
1442    true
1443}
1444
1445pub(crate) fn reapply_non_undoable_payment_ability(
1446    game: &mut GameState,
1447    pool: &mut ManaPool,
1448    player: PlayerId,
1449    card_id: CardId,
1450    ability_index: usize,
1451) {
1452    let Some(ab) = game.card(card_id).activated_abilities.get(ability_index) else {
1453        return;
1454    };
1455    let atoms = ab
1456        .produced_ir
1457        .as_ref()
1458        .map(|ir| {
1459            ir.fixed_atoms()
1460                .unwrap_or_else(|| ir.to_atoms(&game.card(card_id).chosen_colors))
1461        })
1462        .unwrap_or_default();
1463    let amount = super::resolve_mana_ability_amount(game, card_id, player, ab);
1464    let has_tap_cost = ab.cost.parts.iter().any(|p| matches!(p, CostPart::Tap));
1465    let ma = ManaAbilityRef {
1466        card_id,
1467        ability_index: Some(ability_index),
1468        atoms: atoms.clone(),
1469        amount,
1470        mana_text: String::new(),
1471        produced_ir: ab.produced_ir.clone(),
1472        source_order: 0,
1473    };
1474    if pay_non_tap_mana_ability_costs(game, player, &ma, None, false, &[], &mut None) {
1475        if has_tap_cost {
1476            game.tap(card_id);
1477        }
1478        for &atom in &atoms {
1479            pool.add(atom, amount.max(1));
1480        }
1481    }
1482}
1483
1484fn pay_non_tap_mana_ability_costs(
1485    game: &mut GameState,
1486    player: PlayerId,
1487    ma: &ManaAbilityRef,
1488    reserved_source: Option<CardId>,
1489    allow_reserved_source_reuse: bool,
1490    reserved_sacrifices: &[CardId],
1491    callback: &mut Option<ManaPayCallbackFn<'_>>,
1492) -> bool {
1493    let Some(ab_idx) = ma.ability_index else {
1494        return true;
1495    };
1496    let cost_parts: Vec<_> = game.card(ma.card_id).activated_abilities[ab_idx]
1497        .cost
1498        .parts
1499        .clone();
1500    for part in &cost_parts {
1501        match part {
1502            CostPart::Tap | CostPart::Mana { .. } => {}
1503            CostPart::PayLife(amount) => {
1504                if game.player(player).life < amount.resolve(game, ma.card_id, player) {
1505                    return false;
1506                }
1507                if let Some(ref mut cb) = callback {
1508                    if let Some(confirmed_id) = cb(ManaPayCallback::ConfirmPayLife(ma.card_id)) {
1509                        if confirmed_id != ma.card_id {
1510                            return false;
1511                        }
1512                    } else {
1513                        return false;
1514                    }
1515                }
1516                game.player_lose_life(player, amount.resolve(game, ma.card_id, player));
1517            }
1518            CostPart::SubCounter {
1519                amount,
1520                counter_type,
1521                ..
1522            } => {
1523                if game.card(ma.card_id).counter_count(counter_type)
1524                    < amount.resolve(game, ma.card_id, player)
1525                {
1526                    return false;
1527                }
1528                if let Some(ref mut cb) = callback {
1529                    if let Some(confirmed_id) = cb(ManaPayCallback::ConfirmSubCounter(ma.card_id)) {
1530                        if confirmed_id != ma.card_id {
1531                            return false;
1532                        }
1533                    } else {
1534                        return false;
1535                    }
1536                }
1537                let amount_n = amount.resolve(game, ma.card_id, player);
1538                game.card_mut(ma.card_id)
1539                    .remove_counter(counter_type, amount_n);
1540            }
1541            CostPart::Sacrifice {
1542                type_filter,
1543                amount,
1544            } => {
1545                if type_filter == "CARDNAME" {
1546                    if amount.resolve(game, ma.card_id, player) > 1
1547                        || game.card(ma.card_id).zone != ZoneType::Battlefield
1548                    {
1549                        return false;
1550                    }
1551                    if let Some(ref mut cb) = callback {
1552                        if let Some(confirmed_id) =
1553                            cb(ManaPayCallback::ConfirmSelfSacrifice(ma.card_id))
1554                        {
1555                            if confirmed_id != ma.card_id {
1556                                return false;
1557                            }
1558                        } else {
1559                            return false; // confirmation declined
1560                        }
1561                    }
1562                    if let Some(ref mut cb) = callback {
1563                        if let Some(sacrificed_id) =
1564                            cb(ManaPayCallback::NotifySacrificeForMana(ma.card_id))
1565                        {
1566                            if sacrificed_id != ma.card_id {
1567                                return false;
1568                            }
1569                        } else {
1570                            return false;
1571                        }
1572                    } else {
1573                        let owner = game.card(ma.card_id).owner;
1574                        game.move_card(ma.card_id, ZoneType::Graveyard, owner);
1575                    }
1576                } else {
1577                    let mut targets = crate::cost::get_sacrifice_targets_for_cost(
1578                        game,
1579                        player,
1580                        type_filter,
1581                        None,
1582                    );
1583                    targets.retain(|cid| !reserved_sacrifices.contains(cid));
1584                    if !allow_reserved_source_reuse {
1585                        if let Some(reserved) = reserved_source {
1586                            targets.retain(|&cid| cid != reserved);
1587                        }
1588                    }
1589                    targets.sort_by(|&a, &b| {
1590                        game.card(a)
1591                            .card_name
1592                            .cmp(&game.card(b).card_name)
1593                            .then_with(|| a.index().cmp(&b.index()))
1594                    });
1595                    let required = (amount.resolve(game, ma.card_id, player)).max(0) as usize;
1596                    if targets.len() < required {
1597                        return false;
1598                    }
1599                    for _ in 0..required {
1600                        let chosen = if let Some(ref mut cb) = callback {
1601                            cb(ManaPayCallback::ChooseSacrifice(&targets))
1602                        } else {
1603                            targets.first().copied()
1604                        };
1605                        if let Some(cid) = chosen {
1606                            targets.retain(|&c| c != cid);
1607                            if let Some(ref mut cb) = callback {
1608                                if let Some(sacrificed_id) =
1609                                    cb(ManaPayCallback::NotifySacrificeForMana(cid))
1610                                {
1611                                    if sacrificed_id != cid {
1612                                        return false;
1613                                    }
1614                                } else {
1615                                    return false;
1616                                }
1617                            } else {
1618                                let owner = game.card(cid).owner;
1619                                game.move_card(cid, ZoneType::Graveyard, owner);
1620                            }
1621                        }
1622                    }
1623                }
1624            }
1625            CostPart::Exile { amount, from, .. } => {
1626                if !pay_cost_from_source(part)
1627                    || amount.resolve(game, ma.card_id, player) > 1
1628                    || game.card(ma.card_id).zone != *from
1629                {
1630                    return false;
1631                }
1632                if let Some(ref mut cb) = callback {
1633                    if let Some(confirmed_id) = cb(ManaPayCallback::ConfirmSourceExile(ma.card_id))
1634                    {
1635                        if confirmed_id != ma.card_id {
1636                            return false;
1637                        }
1638                    } else {
1639                        return false;
1640                    }
1641                }
1642                let owner = game.card(ma.card_id).owner;
1643                game.move_card(ma.card_id, ZoneType::Exile, owner);
1644            }
1645            CostPart::TapType { .. } => {
1646                let targets = choose_tap_type_targets_for_mana_ability_with_callback(
1647                    game,
1648                    player,
1649                    ma.card_id,
1650                    part,
1651                    reserved_source,
1652                    allow_reserved_source_reuse,
1653                    reserved_sacrifices,
1654                    callback,
1655                );
1656                if targets.is_empty() {
1657                    return false;
1658                }
1659                for cid in targets {
1660                    game.tap(cid);
1661                }
1662            }
1663            _ => return false,
1664        }
1665    }
1666    true
1667}
1668
1669fn can_pay_source_paid_mana_cost_part(
1670    game: &GameState,
1671    player: PlayerId,
1672    source_id: CardId,
1673    part: &CostPart,
1674    reserved_source: Option<CardId>,
1675    allow_reserved_source_reuse: bool,
1676    reserved_sacrifices: &[CardId],
1677) -> bool {
1678    match part {
1679        CostPart::Tap | CostPart::Mana { .. } => true,
1680        CostPart::PayLife(amount) => {
1681            game.player(player).life >= amount.resolve(game, source_id, player)
1682        }
1683        CostPart::SubCounter {
1684            amount,
1685            counter_type,
1686            ..
1687        } => {
1688            game.card(source_id).counter_count(counter_type)
1689                >= amount.resolve(game, source_id, player)
1690        }
1691        CostPart::Sacrifice {
1692            type_filter,
1693            amount,
1694        } => {
1695            if type_filter == "CARDNAME" {
1696                amount.resolve(game, source_id, player) <= 1
1697                    && game.card(source_id).zone == ZoneType::Battlefield
1698                    && !reserved_sacrifices.contains(&source_id)
1699            } else {
1700                let targets = get_payable_mana_sacrifice_targets(
1701                    game,
1702                    player,
1703                    type_filter,
1704                    reserved_source,
1705                    allow_reserved_source_reuse,
1706                    reserved_sacrifices,
1707                );
1708                (targets.len() as i32) >= amount.resolve(game, source_id, player)
1709            }
1710        }
1711        CostPart::Exile { amount, from, .. } => {
1712            pay_cost_from_source(part)
1713                && amount.resolve(game, source_id, player) <= 1
1714                && game.card(source_id).zone == *from
1715        }
1716        CostPart::TapType { .. } => !choose_tap_type_targets_for_mana_ability(
1717            game,
1718            player,
1719            source_id,
1720            part,
1721            reserved_source,
1722            allow_reserved_source_reuse,
1723            reserved_sacrifices,
1724        )
1725        .is_empty(),
1726        _ => false,
1727    }
1728}
1729
1730fn choose_tap_type_targets_for_mana_ability(
1731    game: &GameState,
1732    player: PlayerId,
1733    source_id: CardId,
1734    part: &CostPart,
1735    reserved_source: Option<CardId>,
1736    allow_reserved_source_reuse: bool,
1737    reserved_sacrifices: &[CardId],
1738) -> Vec<CardId> {
1739    let mut callback = None;
1740    choose_tap_type_targets_for_mana_ability_with_callback(
1741        game,
1742        player,
1743        source_id,
1744        part,
1745        reserved_source,
1746        allow_reserved_source_reuse,
1747        reserved_sacrifices,
1748        &mut callback,
1749    )
1750}
1751
1752fn choose_tap_type_targets_for_mana_ability_with_callback(
1753    game: &GameState,
1754    player: PlayerId,
1755    source_id: CardId,
1756    part: &CostPart,
1757    reserved_source: Option<CardId>,
1758    allow_reserved_source_reuse: bool,
1759    reserved_sacrifices: &[CardId],
1760    callback: &mut Option<ManaPayCallbackFn<'_>>,
1761) -> Vec<CardId> {
1762    let CostPart::TapType {
1763        amount,
1764        type_filter,
1765        min_total_power,
1766    } = part
1767    else {
1768        return Vec::new();
1769    };
1770    let mut targets = crate::cost::get_tap_type_targets(game, player, type_filter, source_id);
1771    targets.retain(|cid| !reserved_sacrifices.contains(cid));
1772    if !allow_reserved_source_reuse {
1773        if let Some(reserved) = reserved_source {
1774            targets.retain(|&cid| cid != reserved);
1775        }
1776    }
1777
1778    if let Some(power_threshold) = min_total_power {
1779        targets.sort_by(|&a, &b| {
1780            crate::cost::cost_tap_type::tap_power_value(game, b, None)
1781                .cmp(&crate::cost::cost_tap_type::tap_power_value(game, a, None))
1782                .then_with(|| {
1783                    game.card(a)
1784                        .card_name
1785                        .cmp(&game.card(b).card_name)
1786                        .then_with(|| a.index().cmp(&b.index()))
1787                })
1788        });
1789        if let Some(cb) = callback {
1790            let mut chosen = Vec::new();
1791            if cb(ManaPayCallback::ChooseTapType {
1792                valid: &targets,
1793                min: 1,
1794                max: targets.len(),
1795                chosen: &mut chosen,
1796            })
1797            .is_none()
1798            {
1799                return Vec::new();
1800            }
1801            chosen.retain(|cid| targets.contains(cid));
1802            chosen.dedup();
1803            let chosen_power: i32 = chosen
1804                .iter()
1805                .map(|&cid| crate::cost::cost_tap_type::tap_power_value(game, cid, None))
1806                .sum();
1807            if chosen_power >= *power_threshold {
1808                return chosen;
1809            }
1810            return Vec::new();
1811        }
1812        let mut chosen = Vec::new();
1813        let mut total = 0;
1814        for cid in targets {
1815            total += crate::cost::cost_tap_type::tap_power_value(game, cid, None);
1816            chosen.push(cid);
1817            if total >= *power_threshold {
1818                return chosen;
1819            }
1820        }
1821        return Vec::new();
1822    }
1823
1824    let required = (amount.resolve(game, source_id, player)).max(0) as usize;
1825    if targets.len() < required {
1826        return Vec::new();
1827    }
1828    targets.sort_by(|&a, &b| {
1829        game.card(a)
1830            .card_name
1831            .cmp(&game.card(b).card_name)
1832            .then_with(|| a.index().cmp(&b.index()))
1833    });
1834    if let Some(cb) = callback {
1835        let mut chosen = Vec::new();
1836        if cb(ManaPayCallback::ChooseTapType {
1837            valid: &targets,
1838            min: required,
1839            max: required,
1840            chosen: &mut chosen,
1841        })
1842        .is_none()
1843        {
1844            return Vec::new();
1845        }
1846        chosen.retain(|cid| targets.contains(cid));
1847        chosen.dedup();
1848        if chosen.len() < required {
1849            return Vec::new();
1850        }
1851        chosen.truncate(required);
1852        return chosen;
1853    }
1854    targets.truncate(required);
1855    targets
1856}
1857
1858fn get_payable_mana_sacrifice_targets(
1859    game: &GameState,
1860    player: PlayerId,
1861    type_filter: &str,
1862    reserved_source: Option<CardId>,
1863    allow_reserved_source_reuse: bool,
1864    reserved_sacrifices: &[CardId],
1865) -> Vec<CardId> {
1866    let mut targets = crate::cost::get_sacrifice_targets_for_cost(game, player, type_filter, None);
1867    targets.retain(|cid| !reserved_sacrifices.contains(cid));
1868    if !allow_reserved_source_reuse {
1869        if let Some(reserved) = reserved_source {
1870            targets.retain(|&cid| cid != reserved);
1871        }
1872    }
1873    targets
1874}
1875
1876fn choose_atom_for_shard(mana_ab: &ManaAbilityRef, shard: ManaCostShard) -> Option<u16> {
1877    if shard.is_colorless() && mana_ab.atoms.contains(&ManaAtom::COLORLESS) {
1878        return Some(ManaAtom::COLORLESS);
1879    }
1880
1881    if shard == ManaCostShard::Generic || shard.is_generic() {
1882        if mana_ab
1883            .produced_ir
1884            .as_ref()
1885            .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
1886            && mana_ab.atoms.is_empty()
1887        {
1888            return Some(ManaAtom::WHITE);
1889        }
1890        return mana_ab.atoms.first().copied();
1891    }
1892
1893    mana_ab
1894        .atoms
1895        .iter()
1896        .copied()
1897        .find(|&a| can_pay_for_shard_with_color(shard, a))
1898}
1899
1900fn group_and_order_to_pay_shards(
1901    mana_ability_map: &IndexMap<i32, Vec<ManaAbilityRef>>,
1902    cost: &ManaCostBeingPaid,
1903) -> IndexMap<ManaCostShard, Vec<ManaAbilityRef>> {
1904    let mut res: IndexMap<ManaCostShard, Vec<ManaAbilityRef>> = IndexMap::new();
1905
1906    if (cost.get_generic_mana_amount() > 0 || cost.has_any_kind(ManaAtom::OR_2_GENERIC))
1907        && mana_ability_map.contains_key(&(ManaAtom::GENERIC as i32))
1908    {
1909        res.insert(
1910            ManaCostShard::Generic,
1911            mana_ability_map
1912                .get(&(ManaAtom::GENERIC as i32))
1913                .cloned()
1914                .unwrap_or_default(),
1915        );
1916    }
1917
1918    for shard in cost.get_distinct_shards() {
1919        if shard.is_or_2_generic() {
1920            let color_key = shard.color_mask() as i32;
1921            if let Some(list) = mana_ability_map.get(&color_key) {
1922                res.entry(shard).or_default().extend(list.clone());
1923            }
1924            if let Some(list) = mana_ability_map.get(&(ManaAtom::GENERIC as i32)) {
1925                res.entry(shard).or_default().extend(list.clone());
1926            }
1927            continue;
1928        }
1929
1930        if shard == ManaCostShard::Generic {
1931            continue;
1932        }
1933
1934        for (color_key, list) in mana_ability_map {
1935            let key_color =
1936                (*color_key as u16) & (ManaAtom::COLORS_SUPERPOSITION | ManaAtom::COLORLESS);
1937            if can_pay_for_shard_with_color(shard, key_color) {
1938                let bucket = res.entry(shard).or_default();
1939                for ma in list {
1940                    if !bucket
1941                        .iter()
1942                        .any(|x| x.card_id == ma.card_id && x.ability_index == ma.ability_index)
1943                    {
1944                        bucket.push(ma.clone());
1945                    }
1946                }
1947            }
1948        }
1949    }
1950
1951    res
1952}
1953
1954#[allow(dead_code)]
1955fn sort_mana_abilities(
1956    game: &GameState,
1957    player: PlayerId,
1958    current_spell: Option<CardId>,
1959    mana_ability_map: &mut IndexMap<ManaCostShard, Vec<ManaAbilityRef>>,
1960    colors_most_common: &[u16],
1961) {
1962    let mut mana_card_score: HashMap<CardId, i32> = HashMap::new();
1963    let mut ordered_cards: Vec<CardId> = Vec::new();
1964
1965    for abilities in mana_ability_map.values() {
1966        for ability in abilities {
1967            if mana_card_score.contains_key(&ability.card_id) {
1968                continue;
1969            }
1970            let score = score_mana_producing_card(game, ability.card_id, player);
1971            mana_card_score.insert(ability.card_id, score);
1972            ordered_cards.push(ability.card_id);
1973        }
1974    }
1975
1976    ordered_cards.sort_by_key(|cid| mana_card_score.get(cid).copied().unwrap_or(0));
1977
1978    let shards: Vec<ManaCostShard> = mana_ability_map.keys().copied().collect();
1979    for shard in shards {
1980        let Some(existing) = mana_ability_map.get(&shard).cloned() else {
1981            continue;
1982        };
1983        let mut new_abilities = existing.clone();
1984        let existing_index: HashMap<(CardId, Option<usize>), usize> = existing
1985            .iter()
1986            .enumerate()
1987            .map(|(i, a)| ((a.card_id, a.ability_index), i))
1988            .collect();
1989
1990        let cmp = |a: &ManaAbilityRef, b: &ManaAbilityRef| -> std::cmp::Ordering {
1991            let idx_a = ordered_cards
1992                .iter()
1993                .position(|&c| c == a.card_id)
1994                .unwrap_or(usize::MAX);
1995            let idx_b = ordered_cards
1996                .iter()
1997                .position(|&c| c == b.card_id)
1998                .unwrap_or(usize::MAX);
1999            let mut pre_order = (idx_a as isize) - (idx_b as isize);
2000
2001            if pre_order != 0 {
2002                if shard.is_generic()
2003                    && mana_card_score.get(&a.card_id) == mana_card_score.get(&b.card_id)
2004                {
2005                    for &col in colors_most_common {
2006                        let a_can = a.atoms.contains(&col);
2007                        let b_can = b.atoms.contains(&col);
2008                        if a_can && !b_can {
2009                            return std::cmp::Ordering::Greater;
2010                        }
2011                        if !a_can && b_can {
2012                            return std::cmp::Ordering::Less;
2013                        }
2014                    }
2015                }
2016
2017                let a_pos = existing_index
2018                    .get(&(a.card_id, a.ability_index))
2019                    .copied()
2020                    .unwrap_or(usize::MAX);
2021                let b_pos = existing_index
2022                    .get(&(b.card_id, b.ability_index))
2023                    .copied()
2024                    .unwrap_or(usize::MAX);
2025                pre_order += (a_pos as isize) - (b_pos as isize);
2026
2027                return pre_order.cmp(&0);
2028            }
2029
2030            let shard_mana = shard.short_string();
2031            let pay_with_a = a.mana_text.contains(shard_mana);
2032            let pay_with_b = b.mana_text.contains(shard_mana);
2033            if pay_with_a && !pay_with_b {
2034                return std::cmp::Ordering::Less;
2035            }
2036            if pay_with_b && !pay_with_a {
2037                return std::cmp::Ordering::Greater;
2038            }
2039
2040            a.ability_index
2041                .cmp(&b.ability_index)
2042                .then(a.source_order.cmp(&b.source_order))
2043        };
2044        for i in 1..new_abilities.len() {
2045            let pivot = new_abilities[i].clone();
2046            // Binary search: find leftmost position where pivot should go.
2047            let mut lo = 0usize;
2048            let mut hi = i;
2049            while lo < hi {
2050                let mid = (lo + hi) / 2;
2051                if cmp(&pivot, &new_abilities[mid]).is_lt() {
2052                    hi = mid;
2053                } else {
2054                    lo = mid + 1;
2055                }
2056            }
2057            // Shift [lo..i) right by one, then place pivot at lo.
2058            if lo < i {
2059                for j in (lo..i).rev() {
2060                    new_abilities.swap(j, j + 1);
2061                }
2062                new_abilities[lo] = pivot;
2063            }
2064        }
2065
2066        let _ = current_spell;
2067        mana_ability_map.insert(shard, new_abilities);
2068    }
2069}
2070
2071fn group_sources_by_mana_color(
2072    game: &GameState,
2073    player: PlayerId,
2074    reserved_sacrifices: &[CardId],
2075    payment_ctx: Option<&crate::mana::ManaPaymentContext>,
2076    filter_reflected_replacements: bool,
2077) -> IndexMap<i32, Vec<ManaAbilityRef>> {
2078    group_sources_by_mana_color_inner(
2079        game,
2080        player,
2081        reserved_sacrifices,
2082        payment_ctx,
2083        filter_reflected_replacements,
2084        false,
2085    )
2086}
2087
2088fn group_sources_by_mana_color_inner(
2089    game: &GameState,
2090    player: PlayerId,
2091    reserved_sacrifices: &[CardId],
2092    payment_ctx: Option<&crate::mana::ManaPaymentContext>,
2093    filter_reflected_replacements: bool,
2094    skip_self_restriction: bool,
2095) -> IndexMap<i32, Vec<ManaAbilityRef>> {
2096    let mut mana_map: IndexMap<i32, Vec<ManaAbilityRef>> = IndexMap::new();
2097    let mut source_order = 0usize;
2098
2099    for card_id in get_available_mana_sources(game, player, reserved_sacrifices) {
2100        let card = game.card(card_id);
2101        let mut explicit_mana_added = false;
2102
2103        for ab in &card.activated_abilities {
2104            if !is_payable_mana_ability_with_self_check(
2105                game,
2106                player,
2107                card_id,
2108                ab,
2109                reserved_sacrifices,
2110                payment_ctx,
2111                !skip_self_restriction,
2112            ) {
2113                continue;
2114            }
2115            // Handle ManaReflected abilities (e.g. The Grey Havens).
2116            // Java has two paths here:
2117            // - `ComputerUtilMana.groupSourcesByManaColor` predicts
2118            //   ProduceMana replacements against the placeholder original
2119            //   mana ("1"), which can hide reflected colors from castability
2120            //   probes when an amount-only replacement such as Nyxbloom is
2121            //   active.
2122            // - harness `AutoPay.producedAtoms` uses actual reflected colors
2123            //   for real payment.
2124            if ab.is_mana_reflected {
2125                let reflected_atoms = if filter_reflected_replacements {
2126                    super::reflected_atoms_for_availability(game, player, card_id, ab)
2127                } else {
2128                    super::compute_reflected_atoms(game, player, card_id, ab)
2129                };
2130                if !reflected_atoms.is_empty() {
2131                    explicit_mana_added = true;
2132                    let ma = ManaAbilityRef {
2133                        card_id,
2134                        ability_index: Some(ab.ability_index),
2135                        atoms: reflected_atoms,
2136                        amount: parse_mana_ability_amount_with_game(
2137                            ab,
2138                            Some(game),
2139                            Some(card_id),
2140                            Some(player),
2141                        ),
2142                        mana_text: ab
2143                            .produced_ir
2144                            .as_ref()
2145                            .map(crate::ability::ProducedMana::as_script_text)
2146                            .unwrap_or("1".into())
2147                            .into_owned(),
2148                        produced_ir: ab.produced_ir.clone(),
2149                        source_order,
2150                    };
2151                    source_order += 1;
2152                    add_mana_ability_to_color_map(&mut mana_map, &ma);
2153                }
2154                continue;
2155            }
2156
2157            let Some(produced_ir) = ab.produced_ir.as_ref() else {
2158                continue;
2159            };
2160            let produced = produced_ir.as_script_text();
2161            // Combo ColorIdentity (e.g. Arcane Signet): atoms come from the
2162            // commander's color identity, not the produced string literal.
2163            // Must be handled here so auto-pay can see these sources — the
2164            // availability check in `mana::mod.rs` already honours the same
2165            // rule for playability.
2166            // Special <kind> (e.g. Bloom Tender's "Special EachColorAmong_Valid Permanent.YouCtrl"):
2167            // atoms are computed by inspecting permanents at availability time and the
2168            // ability produces one mana per distinct color (so the fixed multiplier
2169            // matches the atom count — keeps the auto-pay budget aligned with reality).
2170            let mut special_atom_multiplier: Option<i32> = None;
2171            let atoms = if ab
2172                .produced_ir
2173                .as_ref()
2174                .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
2175            {
2176                let colors = game.player_commander_color_identity(player);
2177                if colors.is_empty() {
2178                    Vec::new()
2179                } else {
2180                    chosen_colors_to_atoms(&colors)
2181                }
2182            } else if let Some(special) = ab
2183                .produced_ir
2184                .as_ref()
2185                .and_then(crate::ability::ProducedMana::special_kind)
2186            {
2187                let special_atoms =
2188                    crate::ability::effects::mana_effect::available_special_mana_atoms(
2189                        game, card_id, player, special,
2190                    );
2191                special_atom_multiplier = Some(special_atoms.len().max(1) as i32);
2192                special_atoms
2193            } else {
2194                let intrinsic = produced_ir.to_atoms(&card.chosen_colors);
2195                super::java_replacement_filtered_atoms_for_availability(
2196                    game, player, card_id, ab, &intrinsic,
2197                )
2198            };
2199            if atoms.is_empty()
2200                && !ab
2201                    .produced_ir
2202                    .as_ref()
2203                    .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
2204            {
2205                continue;
2206            }
2207
2208            explicit_mana_added = true;
2209            let fixed_output_multiplier = special_atom_multiplier
2210                .or_else(|| produced_ir.fixed_atoms().map(|a| a.len() as i32))
2211                .unwrap_or(1);
2212            let replacement_multiplier = atoms
2213                .iter()
2214                .map(|&atom| {
2215                    super::replacement_adjusted_atoms_for_availability(game, player, card_id, atom)
2216                        .len() as i32
2217                })
2218                .max()
2219                .unwrap_or(1)
2220                .max(1);
2221            let ma = ManaAbilityRef {
2222                card_id,
2223                ability_index: Some(ab.ability_index),
2224                atoms: atoms.clone(),
2225                amount: parse_mana_ability_amount_with_game(
2226                    ab,
2227                    Some(game),
2228                    Some(card_id),
2229                    Some(player),
2230                ) * fixed_output_multiplier
2231                    * replacement_multiplier,
2232                mana_text: produced.to_string(),
2233                produced_ir: ab.produced_ir.clone(),
2234                source_order,
2235            };
2236            source_order += 1;
2237            add_mana_ability_to_color_map(&mut mana_map, &ma);
2238        }
2239
2240        if !explicit_mana_added
2241            && card.zone == ZoneType::Battlefield
2242            && card.is_land()
2243            && !card.tapped
2244        {
2245            let mut atoms = all_basic_subtype_atoms(card);
2246            if atoms.is_empty() {
2247                if let Some(a) = basic_land_mana_atom(card) {
2248                    atoms.push(a);
2249                }
2250            }
2251            for atom in atoms {
2252                let replacement_multiplier =
2253                    super::replacement_adjusted_atoms_for_availability(game, player, card_id, atom)
2254                        .len() as i32;
2255                let ma = ManaAbilityRef {
2256                    card_id,
2257                    ability_index: None,
2258                    atoms: vec![atom],
2259                    amount: replacement_multiplier.max(1),
2260                    mana_text: atom_short(atom).to_string(),
2261                    produced_ir: None,
2262                    source_order,
2263                };
2264                source_order += 1;
2265                add_mana_ability_to_color_map(&mut mana_map, &ma);
2266            }
2267        }
2268    }
2269
2270    mana_map
2271}
2272
2273fn add_mana_ability_to_color_map(
2274    map: &mut IndexMap<i32, Vec<ManaAbilityRef>>,
2275    ma: &ManaAbilityRef,
2276) {
2277    map.entry(ManaAtom::GENERIC as i32)
2278        .or_default()
2279        .push(ma.clone());
2280
2281    for &atom in &ma.atoms {
2282        map.entry(atom as i32).or_default().push(ma.clone());
2283    }
2284}
2285
2286pub fn collect_mana_payment_sources(
2287    game: &GameState,
2288    player: PlayerId,
2289    reserved_sacrifices: &[CardId],
2290) -> ManaPaymentSources {
2291    let source_cards = get_available_mana_sources(game, player, reserved_sacrifices);
2292    let mut mana_ability_options = Vec::new();
2293
2294    for &card_id in &source_cards {
2295        let card = game.card(card_id);
2296        for ab in &card.activated_abilities {
2297            if !is_payable_mana_ability(game, player, card_id, ab, reserved_sacrifices, None) {
2298                continue;
2299            }
2300            let (produced_mana, produced_mana_amount) =
2301                crate::mana::mana_ability_prompt_metadata(game, card_id, player, ab);
2302            mana_ability_options.push(ManaAbilityOption {
2303                card_id,
2304                ability_index: ab.ability_index,
2305                description: ab.ability_text.clone(),
2306                cost: ab.cost_string(),
2307                produced_mana,
2308                produced_mana_amount,
2309            });
2310        }
2311    }
2312
2313    ManaPaymentSources {
2314        source_cards,
2315        mana_ability_options,
2316    }
2317}
2318
2319pub fn can_pay_mana_cost_with_reserved_sacrifices(
2320    game: &GameState,
2321    pool: &ManaPool,
2322    player: PlayerId,
2323    excluded_source: CardId,
2324    cost: &crate::cost::Cost,
2325    reserved_sacrifices: &[CardId],
2326    payment_ctx: Option<&crate::mana::ManaPaymentContext>,
2327) -> bool {
2328    let mana_cost = mana_cost_from_cost(cost);
2329    let mut source_masks: Vec<u16> = Vec::new();
2330
2331    for _ in 0..pool.white() {
2332        source_masks.push(ManaAtom::WHITE);
2333    }
2334    for _ in 0..pool.blue() {
2335        source_masks.push(ManaAtom::BLUE);
2336    }
2337    for _ in 0..pool.black() {
2338        source_masks.push(ManaAtom::BLACK);
2339    }
2340    for _ in 0..pool.red() {
2341        source_masks.push(ManaAtom::RED);
2342    }
2343    for _ in 0..pool.green() {
2344        source_masks.push(ManaAtom::GREEN);
2345    }
2346    source_masks.extend(std::iter::repeat_n(0, pool.colorless() as usize));
2347
2348    for &card_id in game.cards_in_zone(ZoneType::Battlefield, player) {
2349        if card_id == excluded_source {
2350            continue;
2351        }
2352        let card = game.card(card_id);
2353        let mut source_mask = 0u16;
2354        for ab in &card.activated_abilities {
2355            if !ab.is_mana_ability
2356                || ab
2357                    .cost
2358                    .parts
2359                    .iter()
2360                    .any(|p| matches!(p, CostPart::Mana { .. }))
2361            {
2362                continue;
2363            }
2364            if !is_payable_mana_ability(game, player, card_id, ab, reserved_sacrifices, payment_ctx)
2365            {
2366                continue;
2367            }
2368            if ab.is_mana_reflected {
2369                for atom in super::compute_reflected_atoms(game, player, card_id, ab) {
2370                    source_mask |= atom;
2371                }
2372            } else if let Some(produced_ir) = ab.produced_ir.as_ref() {
2373                if produced_ir.is_combo_color_identity() {
2374                    let colors = game.player_commander_color_identity(player);
2375                    if !colors.is_empty() {
2376                        let mut combo = 0u16;
2377                        for atom in chosen_colors_to_atoms(&colors) {
2378                            combo |= atom;
2379                        }
2380                        source_mask |= combo;
2381                    }
2382                } else if let Some(fixed_atoms) = produced_ir.fixed_atoms() {
2383                    for atom in fixed_atoms {
2384                        source_masks.push(atom);
2385                    }
2386                    source_mask = 0;
2387                    break;
2388                } else {
2389                    for atom in produced_ir.to_atoms(&card.chosen_colors) {
2390                        source_mask |= atom;
2391                    }
2392                }
2393            }
2394        }
2395
2396        if source_mask != 0 {
2397            source_masks.push(source_mask);
2398            continue;
2399        }
2400
2401        if card.is_land() && !card.tapped {
2402            let implicit_atoms = all_basic_subtype_atoms(card);
2403            if !implicit_atoms.is_empty() {
2404                let mut implicit_mask = 0u16;
2405                for atom in implicit_atoms {
2406                    implicit_mask |= atom;
2407                }
2408                source_masks.push(implicit_mask);
2409            } else if let Some(atom) = basic_land_mana_atom(card) {
2410                source_masks.push(atom);
2411            }
2412        }
2413    }
2414
2415    let mut requirements = Vec::new();
2416    for shard in mana_cost.shards() {
2417        let color_mask = u16::from(shard.color_mask());
2418        if color_mask != 0 {
2419            requirements.push(color_mask);
2420        }
2421    }
2422    let generic_count = mana_cost.generic_cost();
2423    if source_masks.len() < requirements.len() + generic_count as usize {
2424        return false;
2425    }
2426
2427    requirements.sort_by(|&a, &b| {
2428        let count_a = source_masks.iter().filter(|src| (**src & a) != 0).count();
2429        let count_b = source_masks.iter().filter(|src| (**src & b) != 0).count();
2430        count_a.cmp(&count_b).then(a.cmp(&b))
2431    });
2432
2433    let mut committed = std::collections::HashSet::new();
2434    for requirement in requirements {
2435        let mut best_index: Option<usize> = None;
2436        let mut best_pop = usize::MAX;
2437        let mut best_mask = u16::MAX;
2438        for (i, source_mask) in source_masks.iter().copied().enumerate() {
2439            if committed.contains(&i) || (source_mask & requirement) == 0 {
2440                continue;
2441            }
2442            let pop = source_mask.count_ones() as usize;
2443            if pop < best_pop || (pop == best_pop && source_mask < best_mask) {
2444                best_index = Some(i);
2445                best_pop = pop;
2446                best_mask = source_mask;
2447            }
2448        }
2449        let Some(best_index) = best_index else {
2450            return false;
2451        };
2452        committed.insert(best_index);
2453    }
2454
2455    source_masks.len() - committed.len() >= generic_count as usize
2456}
2457
2458pub fn can_pay_spell_mana_cost_for_action_space(
2459    game: &GameState,
2460    pool: &ManaPool,
2461    player: PlayerId,
2462    current_spell: CardId,
2463    cost: &forge_foundation::ManaCost,
2464    payment_ctx: &crate::mana::ManaPaymentContext,
2465) -> bool {
2466    let mut unpaid = ManaCostBeingPaid::from_mana_cost(cost);
2467    let mut simulated_pool = pool.clone();
2468    simulated_pool.pay_unpaid_for_spell_incremental(&mut unpaid, payment_ctx, false);
2469    if unpaid.is_paid() {
2470        return true;
2471    }
2472
2473    let mut used_sources = std::collections::HashSet::new();
2474    let mut guard = 0u32;
2475    while !unpaid.is_paid() && guard < 128 {
2476        guard += 1;
2477
2478        let mana_ability_map =
2479            group_sources_by_mana_color_inner(game, player, &[], Some(payment_ctx), true, true);
2480        if mana_ability_map.is_empty() {
2481            break;
2482        }
2483
2484        let mut candidates =
2485            collect_sorted_candidates_with_pref(game, player, &mana_ability_map, true);
2486        candidates.retain(|candidate| {
2487            !used_sources.contains(&candidate.card_id) && candidate.card_id != current_spell
2488        });
2489        if candidates.is_empty() {
2490            break;
2491        }
2492
2493        let Some((sa_payment, to_pay)) = choose_candidate(
2494            game,
2495            player,
2496            Some(current_spell),
2497            &candidates,
2498            &unpaid,
2499            false,
2500            &[],
2501        ) else {
2502            break;
2503        };
2504
2505        let Some(chosen_atom) = choose_atom_for_shard(&sa_payment, to_pay) else {
2506            break;
2507        };
2508        let produced = if let Some(fixed_atoms) =
2509            fixed_output_atoms_for_payment(game, player, &sa_payment)
2510        {
2511            let repeats = (sa_payment.amount.max(1) as usize)
2512                .checked_div(fixed_atoms.len().max(1))
2513                .unwrap_or(1)
2514                .max(1);
2515            let adjusted_atoms = replacement_adjusted_atoms_for_payment(
2516                game,
2517                player,
2518                sa_payment.card_id,
2519                &fixed_atoms,
2520                repeats,
2521            );
2522            let mana_string = atoms_as_mana_string(&adjusted_atoms);
2523            let params = ManaProductionParams {
2524                source_card: sa_payment.card_id,
2525                is_snow: game.card(sa_payment.card_id).type_line.is_snow(),
2526                restriction: None,
2527                adds_no_counter: false,
2528                adds_keywords: None,
2529                adds_keywords_valid: None,
2530                adds_counters: None,
2531                adds_counters_valid: None,
2532                triggers_when_spent: None,
2533            };
2534            add_produced_mana_to_pool(&mut simulated_pool, &mana_string, &params);
2535            mana_string
2536        } else {
2537            let mut callback = None;
2538            let mana_string =
2539                auto_pay_base_mana_string(game, player, &sa_payment, chosen_atom, &mut callback);
2540            let produced_ir = crate::ability::ProducedMana::from_raw_boundary(&mana_string);
2541            let adjusted_atoms = produced_ir
2542                .fixed_atoms()
2543                .unwrap_or_else(|| produced_ir.to_atoms(&[]))
2544                .into_iter()
2545                .flat_map(|atom| {
2546                    super::replacement_adjusted_atoms_for_availability(
2547                        game,
2548                        player,
2549                        sa_payment.card_id,
2550                        atom,
2551                    )
2552                })
2553                .collect::<Vec<_>>();
2554            let mana_string = if adjusted_atoms.is_empty() {
2555                mana_string
2556            } else {
2557                atoms_as_mana_string(&adjusted_atoms)
2558            };
2559            let params = ManaProductionParams {
2560                source_card: sa_payment.card_id,
2561                is_snow: game.card(sa_payment.card_id).type_line.is_snow(),
2562                restriction: None,
2563                adds_no_counter: false,
2564                adds_keywords: None,
2565                adds_keywords_valid: None,
2566                adds_counters: None,
2567                adds_counters_valid: None,
2568                triggers_when_spent: None,
2569            };
2570            add_produced_mana_to_pool(&mut simulated_pool, &mana_string, &params);
2571            mana_string
2572        };
2573        add_taps_for_mana_trigger_mana_impl(
2574            game,
2575            &mut simulated_pool,
2576            player,
2577            &sa_payment,
2578            &produced,
2579            false,
2580        );
2581        simulated_pool.pay_unpaid_for_spell_incremental(&mut unpaid, payment_ctx, false);
2582
2583        used_sources.insert(sa_payment.card_id);
2584    }
2585
2586    unpaid.is_paid()
2587        || (unpaid.contains_only_phyrexian_mana()
2588            && game.player(player).life > required_phyrexian_life(&unpaid))
2589}
2590
2591fn replacement_adjusted_atoms_for_payment(
2592    game: &GameState,
2593    player: PlayerId,
2594    source: CardId,
2595    atoms: &[u16],
2596    repeats: usize,
2597) -> Vec<u16> {
2598    let mut adjusted = Vec::new();
2599    for &atom in atoms {
2600        for _ in 0..repeats {
2601            adjusted.extend(super::replacement_adjusted_atoms_for_availability(
2602                game, player, source, atom,
2603            ));
2604        }
2605    }
2606    adjusted
2607}
2608
2609fn atoms_as_mana_string(atoms: &[u16]) -> String {
2610    atoms
2611        .iter()
2612        .map(|&atom| atom_short(atom))
2613        .collect::<Vec<_>>()
2614        .join(" ")
2615}
2616
2617fn fixed_output_atoms_for_payment(
2618    game: &GameState,
2619    player: PlayerId,
2620    mana_ability: &ManaAbilityRef,
2621) -> Option<Vec<u16>> {
2622    if let Some(fixed_atoms) = mana_ability
2623        .produced_ir
2624        .as_ref()
2625        .and_then(crate::ability::ProducedMana::fixed_atoms)
2626    {
2627        return Some(fixed_atoms);
2628    }
2629    let special = mana_ability
2630        .produced_ir
2631        .as_ref()
2632        .and_then(crate::ability::ProducedMana::special_kind)?;
2633    let atoms = crate::ability::effects::mana_effect::available_special_mana_atoms(
2634        game,
2635        mana_ability.card_id,
2636        player,
2637        special,
2638    );
2639    if atoms.is_empty() {
2640        None
2641    } else {
2642        Some(atoms)
2643    }
2644}
2645
2646fn get_available_mana_sources(
2647    game: &GameState,
2648    player: PlayerId,
2649    reserved_sacrifices: &[CardId],
2650) -> Vec<CardId> {
2651    let mut sources: Vec<CardId> = game.cards_in_zone(ZoneType::Battlefield, player).to_vec();
2652
2653    for &cid in game.cards_in_zone(ZoneType::Hand, player) {
2654        let card = game.card(cid);
2655        if card
2656            .activated_abilities
2657            .iter()
2658            .any(|ab| is_payable_mana_ability(game, player, cid, ab, reserved_sacrifices, None))
2659        {
2660            sources.push(cid);
2661        }
2662    }
2663
2664    sources.retain(|&cid| {
2665        let card = game.card(cid);
2666        for ab in &card.activated_abilities {
2667            if is_payable_mana_ability(game, player, cid, ab, reserved_sacrifices, None) {
2668                return true;
2669            }
2670        }
2671        if card.zone != ZoneType::Battlefield || card.tapped || !card.is_land() {
2672            return false;
2673        }
2674        let has_subtype = !all_basic_subtype_atoms(card).is_empty();
2675        let has_basic = basic_land_mana_atom(card).is_some();
2676        has_subtype || has_basic
2677    });
2678    sources
2679}
2680
2681fn is_payable_mana_ability(
2682    game: &GameState,
2683    player: PlayerId,
2684    card_id: CardId,
2685    ab: &crate::ability::activated::ActivatedAbility,
2686    reserved_sacrifices: &[CardId],
2687    payment_ctx: Option<&crate::mana::ManaPaymentContext>,
2688) -> bool {
2689    is_payable_mana_ability_with_self_check(
2690        game,
2691        player,
2692        card_id,
2693        ab,
2694        reserved_sacrifices,
2695        payment_ctx,
2696        true,
2697    )
2698}
2699
2700fn is_payable_mana_ability_with_self_check(
2701    game: &GameState,
2702    player: PlayerId,
2703    card_id: CardId,
2704    ab: &crate::ability::activated::ActivatedAbility,
2705    reserved_sacrifices: &[CardId],
2706    payment_ctx: Option<&crate::mana::ManaPaymentContext>,
2707    apply_self_check: bool,
2708) -> bool {
2709    if !ab.is_mana_ability {
2710        return false;
2711    }
2712    let card = game.card(card_id);
2713    match card.zone {
2714        ZoneType::Battlefield => {
2715            if ab.activation_zone == Some(ZoneType::Hand) {
2716                return false;
2717            }
2718        }
2719        ZoneType::Hand => {
2720            if ab.activation_zone != Some(ZoneType::Hand) {
2721                return false;
2722            }
2723        }
2724        _ => return false,
2725    }
2726    if ab
2727        .cost
2728        .parts
2729        .iter()
2730        .any(|p| matches!(p, CostPart::Mana { .. }))
2731    {
2732        return false;
2733    }
2734    if !can_pay_ignoring_mana(&ab.cost, game, card_id, player) {
2735        return false;
2736    }
2737    if !crate::mana::mana_ability_meets_script_requirements(game, card_id, ab) {
2738        return false;
2739    }
2740    if let Some(ctx) = payment_ctx {
2741        if let Some(raw) = ab.restrict_valid.as_deref() {
2742            let card = game.card(card_id);
2743            let resolved = if raw.contains("ChosenType") {
2744                let chosen = card.chosen_type.clone().unwrap_or_default();
2745                raw.replace("ChosenType", &chosen)
2746            } else {
2747                raw.to_string()
2748            };
2749            if !crate::mana::mana_meets_restriction(&resolved, ctx) {
2750                return false;
2751            }
2752            // ActionSpace.java:277 (permissive) vs AutoPay.canPayShard:273
2753            // (strict self-check). Action-space probes use the permissive form.
2754            if apply_self_check {
2755                let self_ctx = crate::mana::ManaPaymentContext {
2756                    is_spell: false,
2757                    is_activated_ability: true,
2758                    sa_on_stack: false,
2759                    type_line: Some(card.type_line.clone()),
2760                    card_name: Some(card.card_name.clone()),
2761                    card_color: Some(card.color),
2762                    chosen_types_by_source: ctx.chosen_types_by_source.clone(),
2763                };
2764                if !crate::mana::mana_meets_restriction(&resolved, &self_ctx) {
2765                    return false;
2766                }
2767            }
2768        }
2769    }
2770    can_pay_mana_ability_costs_with_reserved(
2771        game,
2772        player,
2773        card_id,
2774        &ab.cost.parts,
2775        reserved_sacrifices,
2776    )
2777}
2778
2779fn can_pay_mana_ability_costs_with_reserved(
2780    game: &GameState,
2781    player: PlayerId,
2782    source_id: CardId,
2783    cost_parts: &[CostPart],
2784    reserved_sacrifices: &[CardId],
2785) -> bool {
2786    for part in cost_parts {
2787        if !can_pay_source_paid_mana_cost_part(
2788            game,
2789            player,
2790            source_id,
2791            part,
2792            None,
2793            true,
2794            reserved_sacrifices,
2795        ) {
2796            return false;
2797        }
2798    }
2799    true
2800}
2801
2802fn required_phyrexian_life(unpaid: &ManaCostBeingPaid) -> i32 {
2803    unpaid
2804        .get_distinct_shards()
2805        .into_iter()
2806        .filter(|shard| shard.is_phyrexian())
2807        .map(|shard| unpaid.get_unpaid_shards(shard) * 2)
2808        .sum()
2809}
2810
2811fn score_mana_producing_card(game: &GameState, card_id: CardId, player: PlayerId) -> i32 {
2812    let card = game.card(card_id);
2813    let mut score = 0;
2814    let mut has_mana_ability = false;
2815
2816    for ab in &card.activated_abilities {
2817        if ab.is_mana_ability {
2818            score += score_mana_ability(game, card_id, ab, None);
2819            has_mana_ability = true;
2820        } else if can_pay_ignoring_mana(&ab.cost, game, card_id, player) {
2821            score += 13;
2822        }
2823    }
2824
2825    if !has_mana_ability && card.is_land() {
2826        let mut subtype_atoms = all_basic_subtype_atoms(card);
2827        if subtype_atoms.is_empty() {
2828            if let Some(a) = basic_land_mana_atom(card) {
2829                subtype_atoms.push(a);
2830            }
2831        }
2832        for atom in subtype_atoms {
2833            score += score_implicit_land_mana_ability(atom);
2834        }
2835    }
2836
2837    if card.can_attack() {
2838        score += 13;
2839    }
2840    if card.can_block() {
2841        score += 13;
2842    }
2843
2844    score
2845}
2846
2847fn score_mana_ability(
2848    game: &GameState,
2849    card_id: CardId,
2850    ab: &crate::ability::activated::ActivatedAbility,
2851    produced_override: Option<&crate::ability::ProducedMana>,
2852) -> i32 {
2853    let mut score = 0;
2854    let card = game.card(card_id);
2855
2856    let orig_produced = ab.produced_ir.as_ref();
2857    if ab
2858        .produced_ir
2859        .as_ref()
2860        .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
2861    {
2862        score += 7;
2863        for part in &ab.cost.parts {
2864            match part {
2865                CostPart::PayLife(_) => score += 3,
2866                CostPart::Sacrifice { type_filter, .. } => {
2867                    score += 6;
2868                    if type_filter != "CARDNAME" {
2869                        score += 40;
2870                    }
2871                }
2872                CostPart::Discard { .. } => score += 6,
2873                _ => {}
2874            }
2875            score += 1;
2876        }
2877        return score;
2878    }
2879    let is_any_mana = ab
2880        .produced_ir
2881        .as_ref()
2882        .is_some_and(crate::ability::ProducedMana::is_any_like);
2883    if is_any_mana {
2884        score += 7;
2885    } else if orig_produced.is_none() {
2886        score += 2;
2887    } else if let Some(produced) = produced_override.or(orig_produced) {
2888        let mana_text = ability_mana_text_for_score_ir(produced, &card.chosen_colors);
2889        if mana_text == "Any" {
2890            score += 7;
2891        } else {
2892            let tokens = mana_text
2893                .split_whitespace()
2894                .filter(|t| !t.is_empty())
2895                .count();
2896            score += tokens.max(1) as i32;
2897            if !mana_text.contains('C') {
2898                score += 1;
2899            }
2900        }
2901    } else {
2902        score += 1;
2903    }
2904
2905    for part in &ab.cost.parts {
2906        match part {
2907            CostPart::PayLife(_) => score += 3,
2908            CostPart::Sacrifice { type_filter, .. } => {
2909                score += 6;
2910                if type_filter != "CARDNAME" {
2911                    score += 40;
2912                }
2913            }
2914            CostPart::Discard { .. } => score += 6,
2915            _ => {}
2916        }
2917        score += 1;
2918    }
2919
2920    score
2921}
2922
2923/// Lower scores are picked first. Lands score low; creatures score high (+26).
2924/// This ensures lands are tapped before valuable mana dorks.
2925fn sort_sources_for_autopay(
2926    game: &GameState,
2927    player: PlayerId,
2928    sources_for_shards: &mut IndexMap<ManaCostShard, Vec<ManaAbilityRef>>,
2929) {
2930    for abilities in sources_for_shards.values_mut() {
2931        abilities.sort_by(|a, b| {
2932            // Score per-ability (not per-card) so that different abilities on the same
2933            // card (e.g. Yavimaya Coast's {C} vs {G}/{U}) get accurate individual scores.
2934            let sa = autopay_source_score(game, player, a) * 1000 + a.source_order as i32;
2935            let sb = autopay_source_score(game, player, b) * 1000 + b.source_order as i32;
2936            sa.cmp(&sb)
2937        });
2938    }
2939}
2940
2941/// - Mana ability score based on produced colors
2942/// - +cost_parts.size() for activation cost complexity
2943/// - +13 per combat role (attack/block) for creatures
2944fn autopay_source_score(game: &GameState, _player: PlayerId, ma: &ManaAbilityRef) -> i32 {
2945    let card = game.card(ma.card_id);
2946    let mut score = if ma
2947        .produced_ir
2948        .as_ref()
2949        .is_some_and(crate::ability::ProducedMana::is_combo_color_identity)
2950    {
2951        score_atoms_for_autopay(&ma.atoms).unwrap_or(2)
2952    } else if ma
2953        .produced_ir
2954        .as_ref()
2955        .is_some_and(crate::ability::ProducedMana::is_any_like)
2956    {
2957        7
2958    } else if ma.mana_text == "Any" {
2959        7
2960    } else if ma.mana_text == "1" && ma.atoms.is_empty() {
2961        1
2962    } else {
2963        let produced = ma.mana_text.clone();
2964        let tokens = produced
2965            .split_whitespace()
2966            .filter(|token| !token.is_empty())
2967            .count();
2968        let mut score = tokens.max(1) as i32;
2969        if !produced.contains('C') {
2970            score += 1;
2971        }
2972        score
2973    };
2974
2975    if let Some(ab_idx) = ma.ability_index {
2976        if let Some(ab) = card.activated_abilities.get(ab_idx) {
2977            score += ab.cost.parts.len() as i32;
2978        }
2979    } else {
2980        score = score_implicit_land_mana_ability(
2981            ma.atoms.first().copied().unwrap_or(ManaAtom::COLORLESS),
2982        );
2983    }
2984
2985    if card.is_creature() {
2986        score += 13;
2987        score += 13;
2988    }
2989
2990    score
2991}
2992
2993fn score_atoms_for_autopay(atoms: &[u16]) -> Option<i32> {
2994    if atoms.is_empty() {
2995        return None;
2996    }
2997    let mut produced = atoms
2998        .iter()
2999        .copied()
3000        .filter(|&atom| atom != ManaAtom::GENERIC)
3001        .map(atom_short)
3002        .collect::<Vec<_>>();
3003    produced.sort_unstable();
3004    produced.dedup();
3005    if produced.is_empty() {
3006        return None;
3007    }
3008    let mut score = produced.len().max(1) as i32;
3009    if !produced.contains(&"C") {
3010        score += 1;
3011    }
3012    Some(score)
3013}
3014
3015fn score_implicit_land_mana_ability(atom: u16) -> i32 {
3016    let mut score = 0;
3017    let text = atom_short(atom);
3018    score += text.len() as i32;
3019    if atom != ManaAtom::COLORLESS {
3020        score += 1;
3021    }
3022    score += 1;
3023    score
3024}
3025
3026fn ability_mana_text_for_score_ir(
3027    produced_ir: &crate::ability::ProducedMana,
3028    chosen_colors: &[String],
3029) -> String {
3030    if produced_ir.is_any_like() {
3031        return "Any".to_string();
3032    }
3033    let atoms = produced_ir.to_atoms(chosen_colors);
3034    if atoms.is_empty() {
3035        return String::new();
3036    }
3037
3038    atoms
3039        .into_iter()
3040        .map(atom_short)
3041        .collect::<Vec<_>>()
3042        .join(" ")
3043}
3044
3045/// Auto-tap untapped lands to produce `needed` additional generic mana.
3046/// Used for paying commander tax on top of the regular cost.
3047pub fn auto_tap_lands_generic(
3048    game: &mut GameState,
3049    pool: &mut ManaPool,
3050    player: PlayerId,
3051    needed: i32,
3052) -> Vec<CardId> {
3053    let deficit = (needed - pool.total_mana()).max(0);
3054    if deficit <= 0 {
3055        return Vec::new();
3056    }
3057
3058    let mut remaining = deficit;
3059    let mut tapped_lands: Vec<CardId> = Vec::new();
3060
3061    for card_id in get_available_mana_sources(game, player, &[]) {
3062        if remaining <= 0 {
3063            break;
3064        }
3065        let card = game.card(card_id);
3066        if !card.is_land() || card.tapped {
3067            continue;
3068        }
3069        let mut atoms = all_basic_subtype_atoms(card);
3070        if atoms.is_empty() {
3071            if let Some(a) = basic_land_mana_atom(card) {
3072                atoms.push(a);
3073            }
3074        }
3075
3076        let atom = if atoms.contains(&ManaAtom::COLORLESS) {
3077            ManaAtom::COLORLESS
3078        } else {
3079            atoms.first().copied().unwrap_or(ManaAtom::COLORLESS)
3080        };
3081
3082        tap_land_for_mana(
3083            game,
3084            pool,
3085            player,
3086            card_id,
3087            atom,
3088            true,
3089            &mut tapped_lands,
3090            None,
3091        );
3092        remaining -= 1;
3093    }
3094
3095    tapped_lands
3096}
3097
3098fn source_requires_tap(game: &GameState, ma: &ManaAbilityRef) -> bool {
3099    match ma.ability_index {
3100        // Implicit mana abilities (basic/subtype lands) always require tapping.
3101        None => true,
3102        Some(ab_idx) => game.card(ma.card_id).activated_abilities[ab_idx]
3103            .cost
3104            .parts
3105            .iter()
3106            .any(|p| matches!(p, CostPart::Tap)),
3107    }
3108}
3109
3110/// Resolve the Amount param for a mana ability, supporting SVar expressions
3111/// like `IncubationAmount` → `Count$Compare Y GE1.3.1`.
3112fn parse_mana_ability_amount_with_game(
3113    ab: &crate::ability::activated::ActivatedAbility,
3114    game: Option<&GameState>,
3115    card_id: Option<CardId>,
3116    player: Option<PlayerId>,
3117) -> i32 {
3118    let Some(amount_str) = ab.amount.as_deref() else {
3119        return 1;
3120    };
3121    // Try direct integer parse first
3122    if let Ok(n) = amount_str.parse::<i32>() {
3123        return if n > 0 { n } else { 1 };
3124    }
3125    // It's an SVar reference — resolve it using the source card's SVars
3126    if let (Some(game), Some(cid), Some(pid)) = (game, card_id, player) {
3127        if let Some(svar_expr) = game.card(cid).svars.get(amount_str) {
3128            if svar_expr.starts_with("Count$") {
3129                return crate::ability::effects::resolve_count_svar(svar_expr, game, cid, pid);
3130            }
3131            return svar_expr.parse::<i32>().unwrap_or(1);
3132        }
3133    }
3134    1
3135}
3136
3137#[cfg(test)]
3138mod tests {
3139    use super::*;
3140    use crate::card::Card;
3141    use forge_foundation::{CardTypeLine, ColorSet};
3142
3143    fn make_card(
3144        id: u32,
3145        owner: PlayerId,
3146        name: &str,
3147        type_line: &str,
3148        abilities: Vec<&str>,
3149    ) -> Card {
3150        Card::new(
3151            CardId(id),
3152            name.to_string(),
3153            owner,
3154            CardTypeLine::parse(type_line),
3155            ManaCost::no_cost(),
3156            ColorSet::COLORLESS,
3157            None,
3158            None,
3159            vec![],
3160            abilities.into_iter().map(|s| s.to_string()).collect(),
3161        )
3162    }
3163
3164    #[test]
3165    fn auto_tap_does_not_spend_reserved_source_on_mana_sacrifice_costs_by_default() {
3166        let mut game = GameState::new(&["P1", "P2"], 20);
3167        let player = PlayerId(0);
3168        let mut pool = ManaPool::new();
3169
3170        let reserved_food = game.create_card(make_card(
3171            1,
3172            player,
3173            "Food Token",
3174            "Artifact Food",
3175            vec!["AB$ GainLife | Cost$ 2 T Sac<1/CARDNAME> | LifeAmount$ 3"],
3176        ));
3177        let goose = game.create_card(make_card(
3178            2,
3179            player,
3180            "Gilded Goose",
3181            "Creature Bird",
3182            vec!["AB$ Mana | Cost$ T Sac<1/Food> | Produced$ Any"],
3183        ));
3184        let forest = game.create_card(make_card(
3185            3,
3186            player,
3187            "Forest",
3188            "Land Forest",
3189            vec!["AB$ Mana | Cost$ T | Produced$ G"],
3190        ));
3191
3192        game.add_card_to_zone(ZoneType::Battlefield, player, reserved_food);
3193        game.add_card_to_zone(ZoneType::Battlefield, player, goose);
3194        game.add_card_to_zone(ZoneType::Battlefield, player, forest);
3195        game.card_mut(reserved_food).zone = ZoneType::Battlefield;
3196        game.card_mut(goose).zone = ZoneType::Battlefield;
3197        game.card_mut(forest).zone = ZoneType::Battlefield;
3198        game.card_mut(reserved_food).summoning_sick = false;
3199        game.card_mut(goose).summoning_sick = false;
3200        game.card_mut(forest).summoning_sick = false;
3201
3202        let tapped = auto_tap_lands(
3203            &mut game,
3204            &mut pool,
3205            player,
3206            &ManaCost::parse("2"),
3207            Some(reserved_food),
3208        );
3209
3210        assert_eq!(pool.total_mana(), 1);
3211        assert_eq!(tapped, vec![forest]);
3212        assert!(!game.card(goose).tapped);
3213        assert_eq!(game.card(goose).zone, ZoneType::Battlefield);
3214        assert_eq!(game.card(reserved_food).zone, ZoneType::Battlefield);
3215    }
3216
3217    #[test]
3218    fn auto_tap_can_spend_reserved_source_when_explicitly_allowed() {
3219        let mut game = GameState::new(&["P1", "P2"], 20);
3220        let player = PlayerId(0);
3221        let mut pool = ManaPool::new();
3222
3223        let reserved_food = game.create_card(make_card(
3224            1,
3225            player,
3226            "Food Token",
3227            "Artifact Food",
3228            vec!["AB$ GainLife | Cost$ 2 T Sac<1/CARDNAME> | LifeAmount$ 3"],
3229        ));
3230        let goose = game.create_card(make_card(
3231            2,
3232            player,
3233            "Gilded Goose",
3234            "Creature Bird",
3235            vec!["AB$ Mana | Cost$ T Sac<1/Food> | Produced$ Any"],
3236        ));
3237        let forest = game.create_card(make_card(
3238            3,
3239            player,
3240            "Forest",
3241            "Land Forest",
3242            vec!["AB$ Mana | Cost$ T | Produced$ G"],
3243        ));
3244
3245        for cid in [reserved_food, goose, forest] {
3246            game.add_card_to_zone(ZoneType::Battlefield, player, cid);
3247            game.card_mut(cid).zone = ZoneType::Battlefield;
3248            game.card_mut(cid).summoning_sick = false;
3249        }
3250
3251        let tapped = auto_tap_lands_allow_reserved_source_reuse(
3252            &mut game,
3253            &mut pool,
3254            player,
3255            &ManaCost::parse("2"),
3256            Some(reserved_food),
3257        );
3258
3259        assert_eq!(pool.total_mana(), 2);
3260        // Auto-tapper prefers simpler sources: Forest (score 3) before Goose (score 35).
3261        assert_eq!(tapped, vec![forest, goose]);
3262        assert!(game.card(goose).tapped);
3263        assert!(game.card(forest).tapped);
3264        assert_eq!(game.card(goose).zone, ZoneType::Battlefield);
3265        assert_eq!(game.card(reserved_food).zone, ZoneType::Graveyard);
3266    }
3267
3268    #[test]
3269    fn auto_tap_uses_battlefield_order_for_generic_payment() {
3270        let mut game = GameState::new(&["P1", "P2"], 20);
3271        let player = PlayerId(0);
3272        let mut pool = ManaPool::new();
3273
3274        let plains = game.create_card(make_card(
3275            1,
3276            player,
3277            "Plains",
3278            "Land",
3279            vec!["AB$ Mana | Cost$ T | Produced$ W"],
3280        ));
3281        let mountain = game.create_card(make_card(
3282            2,
3283            player,
3284            "Mountain",
3285            "Land",
3286            vec!["AB$ Mana | Cost$ T | Produced$ R"],
3287        ));
3288        let forest = game.create_card(make_card(
3289            3,
3290            player,
3291            "Forest",
3292            "Land Forest",
3293            vec!["AB$ Mana | Cost$ T | Produced$ G"],
3294        ));
3295
3296        for cid in [plains, mountain, forest] {
3297            game.add_card_to_zone(ZoneType::Battlefield, player, cid);
3298            game.card_mut(cid).zone = ZoneType::Battlefield;
3299            game.card_mut(cid).summoning_sick = false;
3300        }
3301
3302        let tapped = auto_tap_lands(&mut game, &mut pool, player, &ManaCost::parse("2"), None);
3303
3304        assert_eq!(pool.total_mana(), 2);
3305        assert_eq!(tapped, vec![plains, mountain]);
3306        assert!(game.card(plains).tapped);
3307        assert!(game.card(mountain).tapped);
3308        assert!(!game.card(forest).tapped);
3309    }
3310
3311    #[test]
3312    fn auto_tap_calls_confirm_payment_for_self_sacrifice() {
3313        let mut game = GameState::new(&["P1", "P2"], 20);
3314        let player = PlayerId(0);
3315
3316        // Create a Treasure Token (self-sacrifice for mana)
3317        let treasure = game.create_card(make_card(
3318            1,
3319            player,
3320            "Treasure Token",
3321            "Artifact Treasure",
3322            vec!["AB$ Mana | Cost$ T Sac<1/CARDNAME> | Produced$ Any"],
3323        ));
3324
3325        game.add_card_to_zone(ZoneType::Battlefield, player, treasure);
3326        game.card_mut(treasure).zone = ZoneType::Battlefield;
3327        game.card_mut(treasure).summoning_sick = false;
3328
3329        // Test 1: confirm_payment returns true (ACCEPT)
3330        {
3331            let mut pool = ManaPool::new();
3332            let tapped = {
3333                let game_ptr: *mut GameState = &mut game;
3334                let mut callback = |kind: ManaPayCallback<'_>| -> Option<CardId> {
3335                    match kind {
3336                        ManaPayCallback::ChooseSacrifice(_) => None,
3337                        ManaPayCallback::ChooseColor(_) => None,
3338                        ManaPayCallback::ChooseTapType { .. } => None,
3339                        ManaPayCallback::ConfirmSelfSacrifice(cid) => {
3340                            assert_eq!(cid, treasure); // should be asking about Treasure
3341                            Some(cid) // confirm
3342                        }
3343                        ManaPayCallback::ConfirmSubCounter(cid) => Some(cid),
3344                        ManaPayCallback::ConfirmSourceExile(cid) => Some(cid),
3345                        ManaPayCallback::ConfirmPayLife(cid) => Some(cid),
3346                        ManaPayCallback::NotifySacrificeForMana(cid) => unsafe {
3347                            let game = &mut *game_ptr;
3348                            let owner = game.card(cid).owner;
3349                            game.move_card(cid, ZoneType::Graveyard, owner);
3350                            Some(cid)
3351                        },
3352                        ManaPayCallback::ApplyProduceManaReplacement { .. } => None,
3353                    }
3354                };
3355
3356                auto_tap_lands_with_callbacks(
3357                    &mut game,
3358                    &mut pool,
3359                    player,
3360                    &ManaCost::parse("1"),
3361                    None,
3362                    &mut callback,
3363                )
3364            };
3365
3366            // The confirm callback was called if the treasure was sacrificed
3367            assert_eq!(tapped, vec![treasure]);
3368            assert_eq!(game.card(treasure).zone, ZoneType::Graveyard);
3369            assert_eq!(pool.total_mana(), 1);
3370        }
3371
3372        // Reset for test 2: create new treasure and add a Forest as fallback
3373        let treasure2 = game.create_card(make_card(
3374            2,
3375            player,
3376            "Treasure Token",
3377            "Artifact Treasure",
3378            vec!["AB$ Mana | Cost$ T Sac<1/CARDNAME> | Produced$ Any"],
3379        ));
3380        let forest = game.create_card(make_card(
3381            3,
3382            player,
3383            "Forest",
3384            "Land Forest",
3385            vec!["AB$ Mana | Cost$ T | Produced$ G"],
3386        ));
3387        game.add_card_to_zone(ZoneType::Battlefield, player, treasure2);
3388        game.add_card_to_zone(ZoneType::Battlefield, player, forest);
3389        game.card_mut(treasure2).zone = ZoneType::Battlefield;
3390        game.card_mut(treasure2).summoning_sick = false;
3391        game.card_mut(forest).zone = ZoneType::Battlefield;
3392        game.card_mut(forest).summoning_sick = false;
3393
3394        // Test 2: confirm_payment returns false (DECLINE)
3395        {
3396            let mut pool = ManaPool::new();
3397            let tapped = {
3398                let game_ptr: *mut GameState = &mut game;
3399                let mut callback = |kind: ManaPayCallback<'_>| -> Option<CardId> {
3400                    match kind {
3401                        ManaPayCallback::ChooseSacrifice(_) => None,
3402                        ManaPayCallback::ChooseColor(_) => None,
3403                        ManaPayCallback::ChooseTapType { .. } => None,
3404                        ManaPayCallback::ConfirmSelfSacrifice(cid) => {
3405                            assert_eq!(cid, treasure2);
3406                            None // decline
3407                        }
3408                        ManaPayCallback::ConfirmSubCounter(cid) => Some(cid),
3409                        ManaPayCallback::ConfirmSourceExile(cid) => Some(cid),
3410                        ManaPayCallback::ConfirmPayLife(cid) => Some(cid),
3411                        ManaPayCallback::NotifySacrificeForMana(cid) => unsafe {
3412                            let game = &mut *game_ptr;
3413                            let owner = game.card(cid).owner;
3414                            game.move_card(cid, ZoneType::Graveyard, owner);
3415                            Some(cid)
3416                        },
3417                        ManaPayCallback::ApplyProduceManaReplacement { .. } => None,
3418                    }
3419                };
3420
3421                auto_tap_lands_with_callbacks(
3422                    &mut game,
3423                    &mut pool,
3424                    player,
3425                    &ManaCost::parse("1"),
3426                    None,
3427                    &mut callback,
3428                )
3429            };
3430
3431            // When declined, should fall back to Forest
3432            assert_eq!(tapped, vec![forest]);
3433            assert_eq!(game.card(treasure2).zone, ZoneType::Battlefield); // not sacrificed
3434            assert_eq!(game.card(forest).zone, ZoneType::Battlefield);
3435            assert_eq!(pool.total_mana(), 1);
3436        }
3437    }
3438}