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 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 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
85pub type SacrificeChooser<'a> = &'a mut dyn FnMut(&[CardId]) -> Option<CardId>;
92
93#[derive(Debug)]
97pub enum ManaPayCallback<'a> {
98 ChooseSacrifice(&'a [CardId]),
101 ChooseColor(&'a [String]),
105 ChooseTapType {
109 valid: &'a [CardId],
110 min: usize,
111 max: usize,
112 chosen: &'a mut Vec<CardId>,
113 },
114 ConfirmSelfSacrifice(CardId),
118 ConfirmSubCounter(CardId),
121 ConfirmSourceExile(CardId),
124 ConfirmPayLife(CardId),
127 NotifySacrificeForMana(CardId),
132 ApplyProduceManaReplacement {
136 activator: PlayerId,
137 source_card: CardId,
138 mana: &'a mut String,
139 },
140}
141
142pub type ManaPayCallbackFn<'a> = &'a mut dyn FnMut(ManaPayCallback<'_>) -> Option<CardId>;
145
146pub 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
214pub 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
288pub 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
333pub 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
359pub 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
417pub 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
556fn 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 let mut guard = 0u32;
648 while !unpaid.is_paid() && guard < 128 {
649 guard += 1;
650
651 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 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 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 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 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 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 }
795 }
796
797 if !unpaid.is_paid() && unpaid.contains_only_phyrexian_mana() {
804 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, ¶ms);
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 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, ¶ms, 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 let extra = crate::staticability::static_ability_panharmonicon::extra_triggers(
951 game, host_id, trigger, ¶ms,
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 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
1065fn 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 let mut seen = std::collections::HashSet::new();
1087 out.retain(|ma| seen.insert((ma.card_id, ma.ability_index, ma.source_order)));
1088 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 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
1112fn 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 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
1253fn 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 if !is_sole_source_for_other_shard(ma, to_pay, sources_for_shards, unpaid) {
1367 return Some(ma.clone());
1368 }
1369 }
1370
1371 fallback
1374}
1375
1376fn 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 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 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; }
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; }
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 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 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 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 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, ¶ms);
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, ¶ms);
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 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
2923fn 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 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
2941fn 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
3045pub 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 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
3110fn 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 if let Ok(n) = amount_str.parse::<i32>() {
3123 return if n > 0 { n } else { 1 };
3124 }
3125 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 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 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 {
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); Some(cid) }
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 assert_eq!(tapped, vec![treasure]);
3368 assert_eq!(game.card(treasure).zone, ZoneType::Graveyard);
3369 assert_eq!(pool.total_mana(), 1);
3370 }
3371
3372 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 {
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 }
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 assert_eq!(tapped, vec![forest]);
3433 assert_eq!(game.card(treasure2).zone, ZoneType::Battlefield); assert_eq!(game.card(forest).zone, ZoneType::Battlefield);
3435 assert_eq!(pool.total_mana(), 1);
3436 }
3437 }
3438}