1use forge_foundation::mana::ManaAtom;
8use forge_foundation::PhaseType;
9use serde::{Deserialize, Serialize};
10
11use super::mana_conversion_matrix::ManaConversionMatrix;
12use super::mana_cost_being_paid::ManaCostBeingPaid;
13use super::{mana_meets_restriction, Mana, ManaPaymentContext};
14use crate::ids::CardId;
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct ManaPaymentOutcome {
18 pub life_paid: i32,
19 pub colors_spent: u16,
20 pub paying_mana: Vec<u16>,
21}
22
23fn mana_matches_context(mana: &Mana, ctx: &ManaPaymentContext) -> bool {
24 let Some(restriction) = &mana.restriction else {
25 return true;
26 };
27
28 let effective = if restriction.contains("ChosenType") {
29 if let Some(source_card) = mana.source_card {
30 if let Some(chosen_type) = ctx.chosen_types_by_source.get(&source_card) {
31 restriction.replace("ChosenType", chosen_type)
32 } else {
33 restriction.clone()
34 }
35 } else {
36 restriction.clone()
37 }
38 } else {
39 restriction.clone()
40 };
41
42 mana_meets_restriction(&effective, ctx)
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
48pub struct ManaPool {
49 #[serde(skip)]
50 mana: Vec<Mana>,
51 #[serde(skip)]
52 last_payment_atoms: Vec<u16>,
53 #[serde(skip)]
54 record_payment_atoms: bool,
55 #[serde(skip)]
61 last_payment_triggers_consumed: Vec<(String, CardId)>,
62 #[serde(skip)]
66 pub total_sources: Option<i32>,
67 #[serde(skip)]
72 pub source_colors: Option<Vec<u16>>,
73 #[serde(skip)]
76 pub color_matrix: ManaConversionMatrix,
77}
78
79impl ManaPool {
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn clear_last_payment_atoms(&mut self) {
85 self.last_payment_atoms.clear();
86 }
87
88 pub fn last_payment_atoms(&self) -> &[u16] {
89 &self.last_payment_atoms
90 }
91
92 pub fn restore_color_replacements(&mut self) {
97 self.color_matrix.restore_color_replacements();
98 }
99
100 pub fn apply_card_matrix(&mut self, other: &ManaConversionMatrix) {
103 self.color_matrix.apply_card_matrix(other);
104 }
105
106 pub fn add(&mut self, atom: u16, amount: i32) {
107 for _ in 0..amount {
108 self.mana.push(Mana::simple(atom));
109 }
110 }
111
112 pub fn add_snow(&mut self, atom: u16, amount: i32) {
114 for _ in 0..amount {
115 let mut m = Mana::simple(atom);
116 m.is_snow = true;
117 self.mana.push(m);
118 }
119 }
120
121 pub fn add_restricted(&mut self, atom: u16, restriction: String) {
123 let mut m = Mana::simple(atom);
124 m.restriction = Some(restriction);
125 self.mana.push(m);
126 }
127
128 pub fn count_uncounterable(&self) -> i32 {
130 self.mana.iter().filter(|m| m.adds_no_counter).count() as i32
131 }
132
133 pub fn collect_keyword_mana(&self) -> Vec<(String, Option<String>)> {
136 self.mana
137 .iter()
138 .filter_map(|m| {
139 m.adds_keywords
140 .as_ref()
141 .map(|kw| (kw.clone(), m.adds_keywords_valid.clone()))
142 })
143 .collect()
144 }
145
146 pub fn collect_counter_mana(&self) -> Vec<(String, Option<String>)> {
148 self.mana
149 .iter()
150 .filter_map(|m| {
151 m.adds_counters
152 .as_ref()
153 .map(|cs| (cs.clone(), m.adds_counters_valid.clone()))
154 })
155 .collect()
156 }
157
158 pub fn collect_trigger_mana(&self) -> Vec<(String, CardId)> {
161 self.mana
162 .iter()
163 .filter_map(|m| {
164 m.triggers_when_spent
165 .as_ref()
166 .and_then(|svar| m.source_card.map(|src| (svar.clone(), src)))
167 })
168 .collect()
169 }
170
171 pub fn mana_colors(&self) -> Vec<u16> {
173 self.mana.iter().map(|m| m.color).collect()
174 }
175
176 pub fn mana_entries(&self) -> &[Mana] {
177 &self.mana
178 }
179
180 pub fn colors_present(&self) -> u16 {
182 let mut mask = 0u16;
183 for m in &self.mana {
184 mask |= m.color;
185 }
186 mask
187 }
188
189 pub fn count_snow(&self) -> i32 {
191 self.mana.iter().filter(|m| m.is_snow).count() as i32
192 }
193
194 pub fn add_mana(&mut self, m: Mana) {
195 self.mana.push(m);
196 }
197
198 pub fn total_mana(&self) -> i32 {
201 self.mana.len() as i32
202 }
203
204 pub fn count_color(&self, atom: u16) -> i32 {
205 self.mana.iter().filter(|m| m.color == atom).count() as i32
206 }
207
208 pub fn white(&self) -> i32 {
209 self.count_color(ManaAtom::WHITE)
210 }
211 pub fn blue(&self) -> i32 {
212 self.count_color(ManaAtom::BLUE)
213 }
214 pub fn black(&self) -> i32 {
215 self.count_color(ManaAtom::BLACK)
216 }
217 pub fn red(&self) -> i32 {
218 self.count_color(ManaAtom::RED)
219 }
220 pub fn green(&self) -> i32 {
221 self.count_color(ManaAtom::GREEN)
222 }
223 pub fn colorless(&self) -> i32 {
224 self.count_color(ManaAtom::COLORLESS)
225 }
226
227 pub fn remove(&mut self, atom: u16, amount: i32) {
229 let mut remaining = amount;
230 let mut idx = 0usize;
231 while remaining > 0 && idx < self.mana.len() {
232 if self.mana[idx].color == atom {
233 if self.record_payment_atoms {
234 self.last_payment_atoms.push(atom);
235 }
236 self.mana.remove(idx);
237 remaining -= 1;
238 } else {
239 idx += 1;
240 }
241 }
242 }
243
244 pub fn has_atom(&self, atom: u16, amount: i32) -> bool {
246 self.count_color(atom) >= amount
247 }
248
249 pub fn spend_generic(&mut self, mut amount: i32) -> i32 {
252 let spent = amount.min(self.total_mana());
253 let colorless_count = self.colorless();
255 let from_colorless = amount.min(colorless_count);
256 self.remove(ManaAtom::COLORLESS, from_colorless);
257 amount -= from_colorless;
258 for &color in &[
260 ManaAtom::WHITE,
261 ManaAtom::BLUE,
262 ManaAtom::BLACK,
263 ManaAtom::RED,
264 ManaAtom::GREEN,
265 ] {
266 if amount <= 0 {
267 break;
268 }
269 let available = self.count_color(color);
270 let take = amount.min(available);
271 self.remove(color, take);
272 amount -= take;
273 }
274 spent
275 }
276
277 pub fn reset_pool(&mut self) {
280 self.mana.clear();
281 }
282
283 pub fn clear_pool(&mut self, phase: PhaseType) -> usize {
286 self.clear_pool_with_keep(phase, 0)
287 }
288
289 pub fn clear_pool_with_keep(&mut self, phase: PhaseType, keep_colors: u16) -> usize {
293 let before = self.mana.len();
294 let in_combat = matches!(
295 phase,
296 PhaseType::CombatBegin
297 | PhaseType::CombatDeclareAttackers
298 | PhaseType::CombatDeclareBlockers
299 | PhaseType::CombatFirstStrikeDamage
300 | PhaseType::CombatDamage
301 | PhaseType::CombatEnd
302 );
303 self.mana.retain(|m| {
304 m.is_persistent
305 || (m.is_combat_mana && in_combat)
306 || (keep_colors != 0 && (m.color & keep_colors) != 0)
307 });
308 before - self.mana.len()
309 }
310
311 pub fn can_pay(&self, cost: &forge_foundation::ManaCost) -> bool {
314 if let Some(ref sources) = self.source_colors {
318 return Self::can_pay_source_matching(sources, cost, 0);
319 }
320
321 if let Some(max) = self.total_sources {
323 if cost.cmc() > max {
324 return false;
325 }
326 }
327
328 let mut pool = self.clone();
329 pool.try_pay(cost)
330 }
331
332 pub fn can_pay_any_color(&self, cost: &forge_foundation::ManaCost) -> bool {
334 if let Some(max) = self.total_sources {
335 if cost.cmc() > max {
336 return false;
337 }
338 }
339 let mut pool = self.clone();
340 pool.try_pay_any_color(cost)
341 }
342
343 fn filtered_for_context(&self, ctx: &ManaPaymentContext) -> ManaPool {
345 let mut pool = self.clone();
346 pool.mana.retain(|m| mana_matches_context(m, ctx));
347 pool
348 }
349
350 pub fn can_pay_for_spell(
352 &self,
353 cost: &forge_foundation::ManaCost,
354 ctx: &ManaPaymentContext,
355 ) -> bool {
356 let filtered = self.filtered_for_context(ctx);
357 filtered.can_pay(cost)
358 }
359
360 pub fn try_pay_for_spell(
363 &mut self,
364 cost: &forge_foundation::ManaCost,
365 ctx: &ManaPaymentContext,
366 ) -> bool {
367 let mut ineligible: Vec<Mana> = Vec::new();
369 let mut eligible: Vec<Mana> = Vec::new();
370 for m in self.mana.drain(..) {
371 if !mana_matches_context(&m, ctx) {
372 ineligible.push(m);
373 continue;
374 }
375 eligible.push(m);
376 }
377 self.mana = eligible;
378 let result = self.try_pay(cost);
379 self.mana.extend(ineligible);
381 result
382 }
383
384 pub fn try_pay_for_spell_converted(
386 &mut self,
387 cost: &forge_foundation::ManaCost,
388 ctx: &ManaPaymentContext,
389 any_color: bool,
390 ) -> bool {
391 let mut ineligible: Vec<Mana> = Vec::new();
392 let mut eligible: Vec<Mana> = Vec::new();
393 for m in self.mana.drain(..) {
394 if !mana_matches_context(&m, ctx) {
395 ineligible.push(m);
396 continue;
397 }
398 eligible.push(m);
399 }
400 self.mana = eligible;
401 let result = if any_color {
402 self.try_pay_any_color(cost)
403 } else {
404 self.try_pay(cost)
405 };
406 self.mana.extend(ineligible);
407 result
408 }
409
410 pub fn try_pay_for_spell_converted_with_phyrexian_life(
414 &mut self,
415 cost: &forge_foundation::ManaCost,
416 ctx: &ManaPaymentContext,
417 any_color: bool,
418 player_life: i32,
419 ) -> Option<i32> {
420 self.try_pay_for_spell_converted_with_phyrexian_life_result(
421 cost,
422 ctx,
423 any_color,
424 player_life,
425 )
426 .map(|outcome| outcome.life_paid)
427 }
428
429 pub fn try_pay_for_spell_converted_with_phyrexian_life_result(
430 &mut self,
431 cost: &forge_foundation::ManaCost,
432 ctx: &ManaPaymentContext,
433 any_color: bool,
434 player_life: i32,
435 ) -> Option<ManaPaymentOutcome> {
436 let mut ineligible: Vec<Mana> = Vec::new();
437 let mut eligible: Vec<Mana> = Vec::new();
438 for m in self.mana.drain(..) {
439 if !mana_matches_context(&m, ctx) {
440 ineligible.push(m);
441 continue;
442 }
443 eligible.push(m);
444 }
445 self.mana = eligible;
446 let result = self.try_pay_with_phyrexian_life_result(cost, any_color, player_life);
447 self.mana.extend(ineligible);
448 result
449 }
450
451 pub(crate) fn pay_unpaid_for_spell_incremental(
458 &mut self,
459 unpaid: &mut ManaCostBeingPaid,
460 ctx: &ManaPaymentContext,
461 any_color: bool,
462 ) -> ManaPaymentOutcome {
463 let mut outcome = ManaPaymentOutcome::default();
464
465 loop {
466 if unpaid.is_paid() {
467 break;
468 }
469
470 let mut paid_index: Option<(usize, u16)> = None;
471 for &color in &[
472 ManaAtom::WHITE,
473 ManaAtom::BLUE,
474 ManaAtom::BLACK,
475 ManaAtom::RED,
476 ManaAtom::GREEN,
477 ManaAtom::COLORLESS,
478 ] {
479 let Some(idx) = self
480 .mana
481 .iter()
482 .position(|m| m.color == color && mana_matches_context(m, ctx))
483 else {
484 continue;
485 };
486 let payment_color = if any_color && color != ManaAtom::COLORLESS {
487 ManaAtom::COLORS_SUPERPOSITION
488 } else {
489 color
490 };
491 if unpaid
492 .try_pay_mana(payment_color, payment_color as u8)
493 .is_some()
494 {
495 paid_index = Some((idx, color));
496 break;
497 }
498 }
499
500 let Some((idx, spent_color)) = paid_index else {
501 break;
502 };
503 let mana = self.mana.remove(idx);
504 outcome.colors_spent |= spent_color;
505 outcome.paying_mana.push(spent_color);
506 if let (Some(svar), Some(src)) = (mana.triggers_when_spent, mana.source_card) {
507 self.last_payment_triggers_consumed.push((svar, src));
508 }
509 }
510
511 self.last_payment_atoms = outcome.paying_mana.clone();
512 outcome
513 }
514
515 pub fn try_pay_cost_with_phyrexian_life(
518 &mut self,
519 cost: &forge_foundation::ManaCost,
520 any_color: bool,
521 player_life: i32,
522 ) -> Option<i32> {
523 self.try_pay_with_phyrexian_life_result(cost, any_color, player_life)
524 .map(|outcome| outcome.life_paid)
525 }
526
527 pub fn can_pay_with_extra_generic(
530 &self,
531 cost: &forge_foundation::ManaCost,
532 extra_generic: i32,
533 ) -> bool {
534 if let Some(ref sources) = self.source_colors {
535 return Self::can_pay_source_matching(sources, cost, extra_generic);
536 }
537 if let Some(max) = self.total_sources {
539 if cost.cmc() + extra_generic > max {
540 return false;
541 }
542 }
543 let mut pool = self.clone();
544 if !pool.try_pay(cost) {
545 return false;
546 }
547 pool.total_mana() >= extra_generic
548 }
549
550 fn can_pay_source_matching(
556 sources: &[u16],
557 cost: &forge_foundation::ManaCost,
558 extra_generic: i32,
559 ) -> bool {
560 let mut requirements: Vec<u16> = Vec::new();
564 for shard in cost.shards() {
565 if shard.is_x() {
566 continue;
567 }
568 let atoms = shard.shard();
569 let color_mask = atoms
571 & (ManaAtom::WHITE
572 | ManaAtom::BLUE
573 | ManaAtom::BLACK
574 | ManaAtom::RED
575 | ManaAtom::GREEN
576 | ManaAtom::COLORLESS);
577 if color_mask != 0 {
578 requirements.push(color_mask);
579 }
580 }
581 let generic_count = cost.generic_cost() + extra_generic;
582
583 if (sources.len() as i32) < (requirements.len() as i32) + generic_count {
585 return false;
586 }
587
588 requirements.sort_by(|a, b| {
591 let count_a = sources.iter().filter(|&&s| (s & a) != 0).count();
592 let count_b = sources.iter().filter(|&&s| (s & b) != 0).count();
593 count_a.cmp(&count_b).then_with(|| a.cmp(b))
594 });
595
596 let mut committed = vec![false; sources.len()];
598 for req in &requirements {
599 let mut best_idx: Option<usize> = None;
600 let mut best_pop: u32 = u32::MAX;
601 let mut best_mask: u16 = u16::MAX;
602 for (i, &src) in sources.iter().enumerate() {
603 if committed[i] {
604 continue;
605 }
606 if (src & req) != 0 {
607 let pop = src.count_ones();
608 if pop < best_pop || (pop == best_pop && src < best_mask) {
609 best_idx = Some(i);
610 best_pop = pop;
611 best_mask = src;
612 }
613 }
614 }
615 match best_idx {
616 Some(idx) => committed[idx] = true,
617 None => return false,
618 }
619 }
620
621 let remaining = committed.iter().filter(|&&c| !c).count() as i32;
622 remaining >= generic_count
623 }
624
625 pub fn can_pay_with_phyrexian_life(
635 &self,
636 cost: &forge_foundation::ManaCost,
637 player_life: i32,
638 ) -> bool {
639 let sources = match self.source_colors {
640 Some(ref s) => s.as_slice(),
641 None => {
642 let mut pool = self.clone();
643 return pool
644 .try_pay_with_phyrexian_life(cost, false, player_life)
645 .is_some();
646 }
647 };
648 use super::mana_cost_being_paid::{can_pay_for_shard_with_color, ManaCostBeingPaid};
649
650 fn search_sources(
651 sources: &[u16],
652 source_index: usize,
653 unpaid: ManaCostBeingPaid,
654 reserved_generic: i32,
655 player_life: i32,
656 ) -> bool {
657 if source_index >= sources.len() {
658 let mut remaining_unpaid = unpaid;
659 let mut life_needed = 0;
660 while remaining_unpaid.contains_phyrexian_mana() {
661 if player_life < life_needed + 2 {
662 return false;
663 }
664 if !remaining_unpaid.pay_phyrexian() {
665 break;
666 }
667 life_needed += 2;
668 }
669
670 let remaining_cost = remaining_unpaid.to_mana_cost();
671 let has_non_generic = remaining_cost.shards().iter().any(|shard| {
672 !shard.is_x()
673 && !shard.is_phyrexian()
674 && !matches!(shard, forge_foundation::ManaCostShard::Generic)
675 });
676 !has_non_generic && reserved_generic >= remaining_cost.generic_cost()
677 } else {
678 if search_sources(
679 sources,
680 source_index + 1,
681 unpaid.clone(),
682 reserved_generic + 1,
683 player_life,
684 ) {
685 return true;
686 }
687
688 let source_mask = sources[source_index];
689 for payment_color in [
690 ManaAtom::WHITE,
691 ManaAtom::BLUE,
692 ManaAtom::BLACK,
693 ManaAtom::RED,
694 ManaAtom::GREEN,
695 ManaAtom::COLORLESS,
696 ] {
697 if payment_color != ManaAtom::COLORLESS && (source_mask & payment_color) == 0 {
698 continue;
699 }
700 if payment_color == ManaAtom::COLORLESS && source_mask != 0 {
701 continue;
702 }
703
704 for shard in unpaid.get_distinct_shards().into_iter().filter(|&shard| {
705 shard != forge_foundation::ManaCostShard::Generic
706 && can_pay_for_shard_with_color(shard, payment_color)
707 }) {
708 let mut next_unpaid = unpaid.clone();
709 if next_unpaid
710 .pay_specific_shard(shard, payment_color)
711 .is_none()
712 {
713 continue;
714 }
715 if search_sources(
716 sources,
717 source_index + 1,
718 next_unpaid,
719 reserved_generic,
720 player_life,
721 ) {
722 return true;
723 }
724 }
725 }
726
727 false
728 }
729 }
730
731 search_sources(
732 sources,
733 0,
734 super::mana_cost_being_paid::ManaCostBeingPaid::from_mana_cost(cost),
735 0,
736 player_life,
737 )
738 }
739
740 pub fn try_pay_extra_generic(&mut self, extra_generic: i32) -> bool {
743 if self.total_mana() < extra_generic {
744 return false;
745 }
746 self.pay_generic(extra_generic);
747 true
748 }
749
750 pub fn try_pay(&mut self, cost: &forge_foundation::ManaCost) -> bool {
752 self.last_payment_atoms.clear();
753 self.record_payment_atoms = true;
754 for shard in cost.shards() {
756 if shard.is_x() {
757 continue; }
759
760 let atoms = shard.shard();
761
762 if shard.is_snow() {
764 if let Some(idx) = self.mana.iter().position(|m| m.is_snow) {
765 if self.record_payment_atoms {
766 self.last_payment_atoms.push(self.mana[idx].color);
767 }
768 self.mana.remove(idx);
769 continue;
770 } else {
771 self.record_payment_atoms = false;
772 return false;
773 }
774 }
775
776 if shard.is_mono_color() && !shard.is_phyrexian() && !shard.is_or_2_generic() {
778 let paid = self.pay_color(atoms);
779 if !paid {
780 return false;
781 }
782 } else if shard.is_or_2_generic() {
783 let color_atoms = atoms & ManaAtom::COLORS_SUPERPOSITION;
785 if !self.pay_color(color_atoms) {
786 if self.total_mana() < 2 {
788 self.record_payment_atoms = false;
789 return false;
790 }
791 self.pay_generic(2);
792 }
793 } else if shard.is_multi_color() && !shard.is_phyrexian() {
794 let color_atoms = atoms & ManaAtom::COLORS_SUPERPOSITION;
796 let mut paid = false;
797 for &bit in &[
798 ManaAtom::WHITE,
799 ManaAtom::BLUE,
800 ManaAtom::BLACK,
801 ManaAtom::RED,
802 ManaAtom::GREEN,
803 ] {
804 if (color_atoms & bit) != 0 && self.count_color(bit) > 0 {
805 self.pay_color(bit);
806 paid = true;
807 break;
808 }
809 }
810 if !paid {
811 self.record_payment_atoms = false;
812 return false;
813 }
814 } else if shard.is_colorless() && !shard.is_multi_color() {
815 if self.colorless() > 0 {
817 self.remove(ManaAtom::COLORLESS, 1);
818 } else {
819 self.record_payment_atoms = false;
820 return false;
821 }
822 } else if shard.is_phyrexian() {
823 let color_atoms = atoms & ManaAtom::COLORS_SUPERPOSITION;
827 if !self.pay_color(color_atoms) {
828 }
831 }
832 }
833
834 let generic = cost.generic_cost();
836 if generic > 0 {
837 if self.total_mana() < generic {
838 self.record_payment_atoms = false;
839 return false;
840 }
841 self.pay_generic(generic);
842 }
843
844 self.record_payment_atoms = false;
845 true
846 }
847
848 pub fn try_pay_any_color(&mut self, cost: &forge_foundation::ManaCost) -> bool {
851 self.last_payment_atoms.clear();
852 self.record_payment_atoms = true;
853 for shard in cost.shards() {
854 if shard.is_x() {
855 continue;
856 }
857 let atoms = shard.shard();
858 if shard.is_snow() {
859 if let Some(idx) = self.mana.iter().position(|m| m.is_snow) {
860 if self.record_payment_atoms {
861 self.last_payment_atoms.push(self.mana[idx].color);
862 }
863 self.mana.remove(idx);
864 continue;
865 } else {
866 self.record_payment_atoms = false;
867 return false;
868 }
869 }
870 if shard.is_colorless() && !shard.is_multi_color() {
871 if self.colorless() > 0 {
873 self.remove(ManaAtom::COLORLESS, 1);
874 } else {
875 self.record_payment_atoms = false;
876 return false;
877 }
878 } else if shard.is_phyrexian() {
879 let color_atoms = atoms & ManaAtom::COLORS_SUPERPOSITION;
881 if color_atoms != 0 {
882 if !self.pay_any_colored() {
884 }
886 }
887 } else if shard.is_mono_color() || shard.is_multi_color() || shard.is_or_2_generic() {
888 if !self.pay_any_colored() {
890 self.record_payment_atoms = false;
891 return false;
892 }
893 }
894 }
895 let generic = cost.generic_cost();
896 if generic > 0 {
897 if self.total_mana() < generic {
898 self.record_payment_atoms = false;
899 return false;
900 }
901 self.pay_generic(generic);
902 }
903 self.record_payment_atoms = false;
904 true
905 }
906
907 fn pay_any_colored(&mut self) -> bool {
909 for &color in &[
910 ManaAtom::WHITE,
911 ManaAtom::BLUE,
912 ManaAtom::BLACK,
913 ManaAtom::RED,
914 ManaAtom::GREEN,
915 ManaAtom::COLORLESS,
916 ] {
917 if self.count_color(color) > 0 {
918 self.remove(color, 1);
919 return true;
920 }
921 }
922 false
923 }
924
925 pub fn pay_color(&mut self, atoms: u16) -> bool {
926 for &color in &[
927 ManaAtom::WHITE,
928 ManaAtom::BLUE,
929 ManaAtom::BLACK,
930 ManaAtom::RED,
931 ManaAtom::GREEN,
932 ] {
933 if (atoms & color) != 0 && self.count_color(color) > 0 {
934 self.remove(color, 1);
935 return true;
936 }
937 }
938 false
939 }
940
941 pub fn pay_generic(&mut self, mut amount: i32) {
942 for &color in &[
944 ManaAtom::COLORLESS,
945 ManaAtom::WHITE,
946 ManaAtom::BLUE,
947 ManaAtom::BLACK,
948 ManaAtom::RED,
949 ManaAtom::GREEN,
950 ] {
951 if amount <= 0 {
952 break;
953 }
954 let available = self.count_color(color);
955 let take = amount.min(available);
956 self.remove(color, take);
957 amount -= take;
958 }
959 }
960
961 pub fn will_mana_be_lost_at_end_of_phase(&self) -> bool {
966 !self.mana.is_empty()
967 }
968
969 pub fn has_burn(&self) -> bool {
972 false }
974
975 pub fn remove_mana(&mut self, mana: &Mana) -> bool {
978 if let Some(pos) = self
979 .mana
980 .iter()
981 .position(|m| m.color == mana.color && m.source_card == mana.source_card)
982 {
983 self.mana.remove(pos);
984 true
985 } else {
986 false
987 }
988 }
989
990 pub fn pay_mana_from_ability(&mut self, produced_color: u16, amount: i32) {
993 for _ in 0..amount {
994 self.add(produced_color, 1);
995 }
996 }
997
998 pub fn try_pay_cost_with_color(&mut self, color: u16) -> bool {
1001 if self.count_color(color) > 0 {
1002 self.remove(color, 1);
1003 true
1004 } else {
1005 false
1006 }
1007 }
1008
1009 pub fn try_pay_cost_with_mana(&mut self, mana: &Mana) -> bool {
1012 self.remove_mana(mana)
1013 }
1014
1015 pub fn account_for(&self, color: u16) -> bool {
1018 self.count_color(color) > 0
1019 }
1020
1021 pub fn refund_mana(&mut self, mana_spent: &mut Vec<Mana>) {
1024 for m in mana_spent.drain(..) {
1025 self.add_mana(m);
1026 }
1027 }
1028
1029 pub fn can_pay_for_shard_with_color(&self, shard_color: u16, pay_color: u16) -> bool {
1032 if shard_color == 0 {
1033 return true;
1034 }
1035 (shard_color & pay_color) != 0
1036 }
1037
1038 pub fn pay_mana_cost_from_pool(&mut self, cost: &forge_foundation::ManaCost) -> bool {
1041 self.try_pay(cost)
1042 }
1043
1044 fn try_pay_with_phyrexian_life(
1045 &mut self,
1046 cost: &forge_foundation::ManaCost,
1047 any_color: bool,
1048 player_life: i32,
1049 ) -> Option<i32> {
1050 self.try_pay_with_phyrexian_life_result(cost, any_color, player_life)
1051 .map(|outcome| outcome.life_paid)
1052 }
1053
1054 fn try_pay_with_phyrexian_life_result(
1055 &mut self,
1056 cost: &forge_foundation::ManaCost,
1057 any_color: bool,
1058 player_life: i32,
1059 ) -> Option<ManaPaymentOutcome> {
1060 use super::mana_cost_being_paid::ManaCostBeingPaid;
1061 self.last_payment_atoms.clear();
1062 self.record_payment_atoms = true;
1063 let unpaid = ManaCostBeingPaid::from_mana_cost(cost);
1064 let mut best: Option<(i32, Vec<usize>)> = None;
1065 self.search_phyrexian_payment(
1066 0,
1067 unpaid,
1068 any_color,
1069 player_life,
1070 &mut Vec::new(),
1071 &mut best,
1072 );
1073
1074 let Some((life_to_pay, spent_indices)) = best else {
1075 self.record_payment_atoms = false;
1076 return None;
1077 };
1078 let mut colors_spent = 0u16;
1079 let mut paying_mana = Vec::new();
1080 let mut triggers_consumed: Vec<(String, CardId)> = Vec::new();
1081 for &idx in &spent_indices {
1082 colors_spent |= self.mana[idx].color;
1083 paying_mana.push(self.mana[idx].color);
1084 if let (Some(svar), Some(src)) = (
1085 self.mana[idx].triggers_when_spent.as_ref(),
1086 self.mana[idx].source_card,
1087 ) {
1088 triggers_consumed.push((svar.clone(), src));
1089 }
1090 }
1091 for idx in spent_indices.into_iter().rev() {
1092 self.mana.remove(idx);
1093 }
1094 self.last_payment_atoms = paying_mana.clone();
1095 self.last_payment_triggers_consumed = triggers_consumed;
1096 self.record_payment_atoms = false;
1097 Some(ManaPaymentOutcome {
1098 life_paid: life_to_pay,
1099 colors_spent,
1100 paying_mana,
1101 })
1102 }
1103
1104 pub fn take_last_payment_triggers_consumed(&mut self) -> Vec<(String, CardId)> {
1108 std::mem::take(&mut self.last_payment_triggers_consumed)
1109 }
1110
1111 fn search_phyrexian_payment(
1112 &self,
1113 mana_index: usize,
1114 unpaid: super::mana_cost_being_paid::ManaCostBeingPaid,
1115 any_color: bool,
1116 player_life: i32,
1117 chosen_indices: &mut Vec<usize>,
1118 best: &mut Option<(i32, Vec<usize>)>,
1119 ) {
1120 use super::mana_cost_being_paid::{can_pay_for_shard_with_color, ManaCostBeingPaid};
1121 use forge_foundation::ManaCostShard;
1122
1123 if matches!(best, Some((0, _))) {
1124 return;
1125 }
1126
1127 if mana_index >= self.mana.len() {
1128 let mut remaining_unpaid = unpaid;
1129 let mut life_to_pay = 0;
1130 while remaining_unpaid.contains_phyrexian_mana() {
1131 if player_life < life_to_pay + 2 {
1132 return;
1133 }
1134 if !remaining_unpaid.pay_phyrexian() {
1135 break;
1136 }
1137 life_to_pay += 2;
1138 }
1139
1140 let mut remaining_pool = ManaPool::new();
1141 let mut remaining_indices: Vec<usize> = Vec::new();
1142 for (idx, mana) in self.mana.iter().enumerate() {
1143 if !chosen_indices.contains(&idx) {
1144 remaining_indices.push(idx);
1145 remaining_pool.mana.push(mana.clone());
1146 }
1147 }
1148
1149 let remaining_cost = remaining_unpaid.to_mana_cost();
1150 let can_finish = if any_color {
1151 remaining_pool.try_pay_any_color(&remaining_cost)
1152 } else {
1153 remaining_pool.try_pay(&remaining_cost)
1154 };
1155 if !can_finish {
1156 return;
1157 }
1158
1159 let mut kept = vec![false; remaining_indices.len()];
1160 for leftover in remaining_pool.mana_entries() {
1161 if let Some(pos) = remaining_indices.iter().enumerate().find_map(|(pos, _)| {
1162 if kept[pos] {
1163 return None;
1164 }
1165 (self.mana[remaining_indices[pos]] == *leftover).then_some(pos)
1166 }) {
1167 kept[pos] = true;
1168 }
1169 }
1170
1171 let mut spent = chosen_indices.clone();
1172 for (pos, &idx) in remaining_indices.iter().enumerate() {
1173 if !kept[pos] {
1174 spent.push(idx);
1175 }
1176 }
1177 spent.sort_unstable();
1178
1179 match best {
1180 Some((best_life, _)) if *best_life <= life_to_pay => {}
1181 _ => *best = Some((life_to_pay, spent)),
1182 }
1183 return;
1184 }
1185
1186 self.search_phyrexian_payment(
1187 mana_index + 1,
1188 unpaid.clone(),
1189 any_color,
1190 player_life,
1191 chosen_indices,
1192 best,
1193 );
1194
1195 let mana = &self.mana[mana_index];
1196 let payment_color = if any_color && mana.color != ManaAtom::COLORLESS {
1197 ManaAtom::COLORS_SUPERPOSITION
1198 } else {
1199 mana.color
1200 };
1201 let payable_shards: Vec<ManaCostShard> = unpaid
1202 .get_distinct_shards()
1203 .into_iter()
1204 .filter(|&shard| shard != ManaCostShard::Generic)
1205 .filter(|&shard| can_pay_for_shard_with_color(shard, payment_color))
1206 .collect();
1207
1208 for shard in payable_shards {
1209 let mut next_unpaid: ManaCostBeingPaid = unpaid.clone();
1210 if next_unpaid
1211 .pay_specific_shard(shard, payment_color)
1212 .is_none()
1213 {
1214 continue;
1215 }
1216 chosen_indices.push(mana_index);
1217 self.search_phyrexian_payment(
1218 mana_index + 1,
1219 next_unpaid,
1220 any_color,
1221 player_life,
1222 chosen_indices,
1223 best,
1224 );
1225 chosen_indices.pop();
1226 }
1227 }
1228
1229 pub fn try_pay_with_phyrexian_life_unrestricted(
1233 &mut self,
1234 cost: &forge_foundation::ManaCost,
1235 player_life: i32,
1236 ) -> Option<i32> {
1237 self.try_pay_with_phyrexian_life_result(cost, false, player_life)
1238 .map(|outcome| outcome.life_paid)
1239 }
1240
1241 pub fn iterator(&self) -> impl Iterator<Item = &Mana> {
1244 self.mana.iter()
1245 }
1246
1247 pub fn begin_tap_tracking(&self) -> Vec<u16> {
1252 self.mana_colors()
1253 }
1254
1255 pub fn end_tap_tracking(&self, pool_before: &[u16]) -> Vec<u16> {
1258 let pool_after = self.mana_colors();
1259 let mut produced = pool_after;
1260 for &atom in pool_before {
1261 if let Some(pos) = produced.iter().position(|&a| a == atom) {
1262 produced.remove(pos);
1263 }
1264 }
1265 produced
1266 }
1267
1268 pub fn rollback_tap(&mut self, produced: &[u16]) {
1272 for &atom in produced {
1273 self.remove(atom, 1);
1274 }
1275 }
1276
1277 pub fn produce_mana_from_string(
1286 &mut self,
1287 mana_string: &str,
1288 source_card: Option<CardId>,
1289 is_snow: bool,
1290 restriction: Option<String>,
1291 adds_no_counter: bool,
1292 adds_keywords: Option<String>,
1293 adds_keywords_valid: Option<String>,
1294 adds_counters: Option<String>,
1295 adds_counters_valid: Option<String>,
1296 triggers_when_spent: Option<String>,
1297 ) {
1298 for tok in mana_string.split_whitespace() {
1299 if let Some(atom) = super::mana_atom_from_produced(tok) {
1300 let mut m = Mana::simple(atom);
1301 m.source_card = source_card;
1302 m.is_snow = is_snow;
1303 m.restriction = restriction.clone();
1304 m.adds_no_counter = adds_no_counter;
1305 m.adds_keywords = adds_keywords.clone();
1306 m.adds_keywords_valid = adds_keywords_valid.clone();
1307 m.adds_counters = adds_counters.clone();
1308 m.adds_counters_valid = adds_counters_valid.clone();
1309 m.triggers_when_spent = triggers_when_spent.clone();
1310 self.add_mana(m);
1311 }
1312 }
1313 }
1314
1315 pub fn atom_to_letter(atom: u16) -> &'static str {
1317 match atom {
1318 ManaAtom::WHITE => "W",
1319 ManaAtom::BLUE => "U",
1320 ManaAtom::BLACK => "B",
1321 ManaAtom::RED => "R",
1322 ManaAtom::GREEN => "G",
1323 ManaAtom::COLORLESS => "C",
1324 _ => "C",
1325 }
1326 }
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331 use super::*;
1332 use forge_foundation::ManaCost;
1333
1334 #[test]
1335 fn phyrexian_payment_reserves_mana_for_generic_costs() {
1336 let mut pool = ManaPool::new();
1337 pool.add(ManaAtom::BLUE, 4);
1338
1339 let life_paid =
1340 pool.try_pay_cost_with_phyrexian_life(&ManaCost::parse("4 BP BP BP"), false, 20);
1341
1342 assert_eq!(life_paid, Some(6));
1343 assert_eq!(pool.total_mana(), 0);
1344 }
1345
1346 #[test]
1347 fn phyrexian_payment_uses_matching_mana_before_life_when_possible() {
1348 let mut pool = ManaPool::new();
1349 pool.add(ManaAtom::BLUE, 4);
1350 pool.add(ManaAtom::BLACK, 1);
1351
1352 let life_paid =
1353 pool.try_pay_cost_with_phyrexian_life(&ManaCost::parse("4 BP BP BP"), false, 20);
1354
1355 assert_eq!(life_paid, Some(4));
1356 assert_eq!(pool.total_mana(), 0);
1357 }
1358
1359 #[test]
1360 fn phyrexian_castability_allows_generic_plus_life_with_off_color_sources() {
1361 let mut pool = ManaPool::new();
1362 pool.source_colors = Some(vec![ManaAtom::GREEN, ManaAtom::RED]);
1363 pool.total_sources = Some(2);
1364
1365 assert!(pool.can_pay_with_phyrexian_life(&ManaCost::parse("1 BP BP"), 20));
1366 }
1367
1368 #[test]
1369 fn phyrexian_payment_charges_life_when_generic_uses_off_color_mana() {
1370 let mut pool = ManaPool::new();
1371 pool.add(ManaAtom::RED, 1);
1372
1373 let life_paid =
1374 pool.try_pay_cost_with_phyrexian_life(&ManaCost::parse("1 BP BP"), false, 20);
1375
1376 assert_eq!(life_paid, Some(4));
1377 assert_eq!(pool.total_mana(), 0);
1378 }
1379}