Skip to main content

manabrew_engine/mana/
mana_pool.rs

1//! ManaPool — floating mana pool for a player.
2//!
3//! Mirrors Java's `ManaPool.java`.
4//! Manages floating mana objects, payment, clearing at phase transitions,
5//! and mana restriction checking.
6
7use 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/// Tracks available mana for a player during a turn.
46/// Uses individual Mana objects to support source tracking, snow, and future restrictions.
47#[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    /// `(svar_name, source_card)` pairs from mana spent during the most
56    /// recent payment whose source had `TriggersWhenSpent$`. Populated by
57    /// `try_pay*` helpers; consumed by cast/activation flows that fire the
58    /// linked trigger after the cost resolves (Path of Ancestry's scry,
59    /// etc.). Drained on read.
60    #[serde(skip)]
61    last_payment_triggers_consumed: Vec<(String, CardId)>,
62    /// When set, caps total producible mana for playability checks.
63    /// Used by `calculate_available_mana` to prevent multi-color sources
64    /// (dual lands, Command Tower) from being counted as multiple mana.
65    #[serde(skip)]
66    pub total_sources: Option<i32>,
67    /// Per-source color bitmasks for source-level matching in `can_pay`.
68    /// Each entry is a bitmask of ManaAtom colors that one mana source can produce.
69    /// Used by `calculate_available_mana` to prevent dual lands from satisfying
70    /// multiple colored requirements simultaneously.
71    #[serde(skip)]
72    pub source_colors: Option<Vec<u16>>,
73    /// Mana conversion/restriction matrix controlling what colors can pay for what.
74    /// Mirrors Java's `ManaPool` inheriting from `ManaConversionMatrix`.
75    #[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    // ── ManaConversionMatrix delegation (Java inheritance parity) ────
93
94    /// Reset the color conversion matrix to identity.
95    /// Mirrors Java's `ManaPool.restoreColorReplacements()`.
96    pub fn restore_color_replacements(&mut self) {
97        self.color_matrix.restore_color_replacements();
98    }
99
100    /// Merge another matrix into this pool's matrix.
101    /// Mirrors Java's `ManaPool.applyCardMatrix(ManaConversionMatrix)`.
102    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    /// Add mana with snow flag set (from a snow permanent source).
113    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    /// Add mana with a restriction (from RestrictValid$).
122    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    /// Count mana in pool that has the "can't be countered" flag.
129    pub fn count_uncounterable(&self) -> i32 {
130        self.mana.iter().filter(|m| m.adds_no_counter).count() as i32
131    }
132
133    /// Collect keywords that should be added to a spell based on consumed mana.
134    /// Call this before and after payment to diff.
135    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    /// Collect counter specs from mana that should be applied to permanents cast with it.
147    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    /// Collect trigger SVars from mana that should fire when spent.
159    /// Returns (svar_name, source_card_id) pairs.
160    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    /// Get the color of each mana in the pool (for tracking consumed colors).
172    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    /// Get a bitmask of all colors present in the pool.
181    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    /// Count snow mana in the pool (any color).
190    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    /// Total floating mana count.
199    /// Mirrors Java's `ManaPool.totalMana()`.
200    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    /// Remove `amount` of a given mana atom from the pool, saturating at 0.
228    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    /// Returns true if the pool contains at least `amount` of the given atom.
245    pub fn has_atom(&self, atom: u16, amount: i32) -> bool {
246        self.count_color(atom) >= amount
247    }
248
249    /// Spend generic mana from the pool, consuming colorless first then any color.
250    /// Returns the amount actually spent.
251    pub fn spend_generic(&mut self, mut amount: i32) -> i32 {
252        let spent = amount.min(self.total_mana());
253        // Consume colorless first
254        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        // Then consume from colors in WUBRG order
259        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    /// Reset the pool completely (empties all floating mana).
278    /// Mirrors Java's `ManaPool.resetPool()`.
279    pub fn reset_pool(&mut self) {
280        self.mana.clear();
281    }
282
283    /// Clear mana pool at phase transitions, retaining persistent and combat mana.
284    /// Mirrors Java's PhaseHandler.onPhaseEnd() → clearPool(true) (MTG rule 500.4).
285    pub fn clear_pool(&mut self, phase: PhaseType) -> usize {
286        self.clear_pool_with_keep(phase, 0)
287    }
288
289    /// Clear the mana pool, retaining persistent mana, combat mana (if in combat),
290    /// and mana of colors specified by `keep_colors` bitmask (from UnspentMana statics).
291    /// Returns the number of mana cleared (for mana burn calculation).
292    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    /// Try to pay a mana cost. Returns true if successful and deducts the mana.
312    /// This is a simplified payment algorithm that handles colored and generic mana.
313    pub fn can_pay(&self, cost: &forge_foundation::ManaCost) -> bool {
314        // When source_colors is available (from calculate_available_mana), use
315        // source-level matching to prevent dual lands from satisfying multiple
316        // colored requirements simultaneously.
317        if let Some(ref sources) = self.source_colors {
318            return Self::can_pay_source_matching(sources, cost, 0);
319        }
320
321        // Fallback for non-availability-estimate pools (actual mana during payment)
322        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    /// Check if the pool can pay a cost with any-color conversion active.
333    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    /// Create a clone with restricted mana filtered out based on context.
344    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    /// Check if pool can pay a cost, respecting mana restrictions for the given spell context.
351    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    /// Pay a cost, skipping restricted mana that doesn't match the context.
361    /// Returns true if successful and deducts the mana from the ORIGINAL pool.
362    pub fn try_pay_for_spell(
363        &mut self,
364        cost: &forge_foundation::ManaCost,
365        ctx: &ManaPaymentContext,
366    ) -> bool {
367        // Temporarily remove ineligible mana, try to pay, then restore unused ones
368        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        // Restore ineligible mana
380        self.mana.extend(ineligible);
381        result
382    }
383
384    /// Pay a cost with restriction filtering and optional any-color conversion.
385    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    /// Pay a spell cost with restriction filtering and phyrexian-life fallback.
411    /// Returns the life that must be paid after mana is deducted, or `None` if
412    /// the cost cannot be covered by the current pool plus phyrexian life.
413    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    /// Spend currently-floating mana against an existing unpaid cost tracker.
452    ///
453    /// This mirrors Java harness `AutoPay`: after each mana ability resolves,
454    /// `ManaPool.payManaFromAbility` / `payManaCostFromPool` immediately remove
455    /// usable mana from the pool before the next source is chosen. That matters
456    /// when a source overproduces colored mana before a later colorless source.
457    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    /// Pay a mana cost with phyrexian-life fallback and return the life paid.
516    /// Used for generic cost payments that don't need spell restriction filtering.
517    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    /// Returns true if the pool can pay `cost` plus `extra_generic` additional generic mana.
528    /// Used for commander tax checks.
529    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        // Check total source cap for availability estimates
538        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    /// Source-level matching for mana availability checks.
551    /// Prevents dual lands from satisfying multiple colored requirements simultaneously.
552    /// Each shard becomes one requirement: a source matches if it can produce any of
553    /// the shard's colors. Hybrid shards like {B/R} are a single requirement satisfied
554    /// by either B or R, matching Java's ComputerUtilMana.canPayManaCost().
555    fn can_pay_source_matching(
556        sources: &[u16],
557        cost: &forge_foundation::ManaCost,
558        extra_generic: i32,
559    ) -> bool {
560        // Build requirements: one per shard, using the shard's full color bitmask.
561        // A hybrid {B/R} becomes one requirement with (BLACK | RED) — any source
562        // producing B or R can satisfy it. Generic shards are handled separately.
563        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            // {C} requires a colorless source — COLORLESS belongs in the mask.
570            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        // Quick total check
584        if (sources.len() as i32) < (requirements.len() as i32) + generic_count {
585            return false;
586        }
587
588        // Sort requirements by number of matching sources (ascending = most constrained first),
589        // then by bitmask value (ascending) for determinism.
590        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        // Greedy matching: for each requirement, commit the most constrained source.
597        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    /// Check if a cost with phyrexian shards can be paid, allowing phyrexian
626    /// shards to fall back to life payment (2 life each) when no mana source
627    /// is available.
628    ///
629    /// Matches Java's ComputerUtilMana.payManaCost() greedy simulation:
630    /// 1. Try to match phyrexian shards with mana sources (highest priority)
631    /// 2. Unmatched phyrexian shards are paid with life
632    /// 3. Non-phyrexian colored shards must be matched with remaining sources
633    /// 4. Generic cost must be covered by remaining sources
634    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    /// Pay `extra_generic` additional generic mana from the pool.
741    /// Returns true if successful.
742    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    /// Try to pay a mana cost, deducting from the pool. Returns true if successful.
751    pub fn try_pay(&mut self, cost: &forge_foundation::ManaCost) -> bool {
752        self.last_payment_atoms.clear();
753        self.record_payment_atoms = true;
754        // First, pay colored shards
755        for shard in cost.shards() {
756            if shard.is_x() {
757                continue; // X shards are pre-resolved into generic mana before payment
758            }
759
760            let atoms = shard.shard();
761
762            // Snow shard ({S}) — pay with any snow mana
763            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            // Pure color shards
777            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                // Can pay with the color or 2 generic
784                let color_atoms = atoms & ManaAtom::COLORS_SUPERPOSITION;
785                if !self.pay_color(color_atoms) {
786                    // Try paying 2 generic instead
787                    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                // Hybrid mana — try each color
795                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                // Pure colorless (C)
816                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                // Phyrexian: pay with color or 2 life (life handled at play_card level).
824                // For can_pay checks: assume color can be paid if available, otherwise
825                // treat as payable (life payment will be resolved at cast time).
826                let color_atoms = atoms & ManaAtom::COLORS_SUPERPOSITION;
827                if !self.pay_color(color_atoms) {
828                    // Color not available — life payment assumed possible at cast time.
829                    // Don't fail here; play_card will verify life total.
830                }
831            }
832        }
833
834        // Then pay generic cost
835        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    /// Try to pay a mana cost with any-color conversion active.
849    /// All colored mana can pay for any colored shard.
850    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                // Pure colorless (C) — must be paid with colorless
872                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                // Phyrexian: any color can pay (with conversion active, even easier)
880                let color_atoms = atoms & ManaAtom::COLORS_SUPERPOSITION;
881                if color_atoms != 0 {
882                    // Try to pay with any colored mana
883                    if !self.pay_any_colored() {
884                        // Life payment assumed possible
885                    }
886                }
887            } else if shard.is_mono_color() || shard.is_multi_color() || shard.is_or_2_generic() {
888                // With any-color conversion, any colored mana can pay any colored shard
889                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    /// Pay one mana of any color from the pool.
908    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        // Pay with colorless first, then colors (WUBRG order)
943        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    // ── Java parity methods (ManaPool.java) ────────────────────────
962
963    /// Whether floating mana will be lost at end of phase.
964    /// Mirrors Java's `ManaPool.willManaBeLostAtEndOfPhase()`.
965    pub fn will_mana_be_lost_at_end_of_phase(&self) -> bool {
966        !self.mana.is_empty()
967    }
968
969    /// Whether the game has mana burn rules active.
970    /// Mirrors Java's `ManaPool.hasBurn()`.
971    pub fn has_burn(&self) -> bool {
972        false // Mana burn removed in modern rules
973    }
974
975    /// Remove a specific Mana object from the pool.
976    /// Mirrors Java's `ManaPool.removeMana(Mana)`.
977    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    /// Pay mana cost using mana produced by a mana ability.
991    /// Mirrors Java's `ManaPool.payManaFromAbility()`.
992    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    /// Try to pay a cost shard using floating mana of a specific color.
999    /// Mirrors Java's `ManaPool.tryPayCostWithColor()`.
1000    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    /// Try to pay with a specific Mana object.
1010    /// Mirrors Java's `ManaPool.tryPayCostWithMana()`.
1011    pub fn try_pay_cost_with_mana(&mut self, mana: &Mana) -> bool {
1012        self.remove_mana(mana)
1013    }
1014
1015    /// Account for mana produced by a mana ability (verify it's in the pool).
1016    /// Mirrors Java's `ManaPool.accountFor()`.
1017    pub fn account_for(&self, color: u16) -> bool {
1018        self.count_color(color) > 0
1019    }
1020
1021    /// Refund mana back to the pool.
1022    /// Mirrors Java's `ManaPool.refundMana()`.
1023    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    /// Check if a mana cost shard can be paid by a given color.
1030    /// Mirrors Java's `ManaPool.canPayForShardWithColor()`.
1031    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    /// Pay an entire mana cost from floating mana.
1039    /// Mirrors Java's `ManaPool.payManaCostFromPool()`.
1040    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    /// Drain and return the trigger metadata recorded by the most recent
1105    /// `try_pay*` call that consumed mana whose source set
1106    /// `TriggersWhenSpent$`.
1107    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    /// Pay a non-spell cost with phyrexian-life fallback.
1230    /// Unlike `try_pay_for_spell_converted_with_phyrexian_life`, this does not
1231    /// filter mana by spell restriction context first.
1232    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    /// Iterator over all floating mana.
1242    /// Mirrors Java's `ManaPool.iterator()`.
1243    pub fn iterator(&self) -> impl Iterator<Item = &Mana> {
1244        self.mana.iter()
1245    }
1246
1247    // ── Tap tracking for mana rollback ─────────────────────────────
1248
1249    /// Snapshot the pool state before a land tap. Call this BEFORE producing mana.
1250    /// Returns a snapshot (list of mana colors) that `end_tap_tracking` will diff against.
1251    pub fn begin_tap_tracking(&self) -> Vec<u16> {
1252        self.mana_colors()
1253    }
1254
1255    /// Compute what mana was produced since `begin_tap_tracking` was called.
1256    /// Returns the list of mana atoms that were added to the pool.
1257    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    /// Remove the exact mana that was produced by a previous tap.
1269    /// Used for mana rollback (untap) — removes ALL mana from that tap,
1270    /// including base production, aura triggers, static doublers, etc.
1271    pub fn rollback_tap(&mut self, produced: &[u16]) {
1272        for &atom in produced {
1273            self.remove(atom, 1);
1274        }
1275    }
1276
1277    // ── Mana production (extracted from game_loop/game_action.rs) ────
1278
1279    /// Produce mana from a mana string (e.g. "W", "U U", "R G") and add to pool.
1280    /// Handles source tracking, snow, restrictions, keywords, counters, triggers.
1281    /// This is the core mana production logic — the single source of truth.
1282    ///
1283    /// Call this from game_action.rs::resolve_mana_ability after determining
1284    /// what mana string to produce.
1285    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    /// Convert a ManaAtom to its short letter string.
1316    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}