Skip to main content

manabrew_engine/cost/
mod.rs

1pub mod cost_add_mana;
2pub mod cost_adjustment;
3pub mod cost_behold;
4pub mod cost_behold_exile;
5pub mod cost_blight;
6pub mod cost_choose_color;
7pub mod cost_choose_creature_type;
8pub mod cost_collect_evidence;
9pub mod cost_damage;
10pub mod cost_discard;
11pub mod cost_draw;
12pub mod cost_enlist;
13pub mod cost_exert;
14pub mod cost_exile;
15pub mod cost_exile_ctrl_or_grave;
16pub mod cost_exile_from_stack;
17pub mod cost_exiled_move_to_grave;
18pub mod cost_flip_coin;
19pub mod cost_forage;
20pub mod cost_gain_control;
21pub mod cost_gain_life;
22pub mod cost_mill;
23mod cost_parser;
24pub mod cost_part;
25pub mod cost_part_mana;
26pub mod cost_part_with_list;
27pub mod cost_part_with_trigger;
28pub mod cost_pay_energy;
29pub mod cost_pay_life;
30pub mod cost_pay_shards;
31pub mod cost_payment;
32pub mod cost_promise_gift;
33pub mod cost_put_card_to_lib;
34pub mod cost_put_counter;
35pub mod cost_remove_any_counter;
36pub mod cost_remove_counter;
37pub mod cost_return;
38pub mod cost_reveal;
39pub mod cost_reveal_chosen;
40pub mod cost_roll_dice;
41pub mod cost_sacrifice;
42pub mod cost_sub_counter;
43pub mod cost_tap;
44pub mod cost_tap_type;
45pub mod cost_unattach;
46pub mod cost_untap;
47pub mod cost_untap_type;
48pub mod cost_waterbend;
49pub mod individual_cost_payment_instance;
50pub mod payment_decision;
51pub mod selector_domain;
52pub mod trait_cost_decision_maker;
53pub mod trait_cost_visitor;
54
55use forge_foundation::{ManaCost, ZoneType};
56use serde::{Deserialize, Serialize};
57
58use crate::ability::effects::matches_change_type;
59use crate::card::{Card, CounterType};
60use crate::game::GameState;
61use crate::ids::{CardId, PlayerId};
62use crate::mana::ManaPool;
63use crate::parsing::CostTokenKind;
64use crate::spellability::SpellAbility;
65use crate::staticability::static_ability_cant_sacrifice::cant_sacrifice;
66
67/// Sentinel passed through [`resolve_dynamic_amount`] for the legacy `X`
68/// pathway. Kept for the few outer-API consumers (`GameLoop::pay_life_cost`,
69/// `AmountSpec::X.resolve`) that still receive an `i32`. New code should use
70/// `AmountSpec::X` directly.
71pub(crate) const DYNAMIC_X_SENTINEL: i32 = i32::MIN;
72
73pub fn resolve_dynamic_amount(
74    game: &GameState,
75    source: CardId,
76    player: PlayerId,
77    amount: i32,
78) -> i32 {
79    if amount != DYNAMIC_X_SENTINEL {
80        return amount;
81    }
82    let source_card = game.card(source);
83
84    if let Some(paid_x) = source_card
85        .svars
86        .get("XPaid")
87        .and_then(|s| s.parse::<i32>().ok())
88    {
89        return paid_x;
90    }
91
92    if let Some(x_expr) = source_card.get_s_var("X") {
93        if x_expr == "Count$xPaid" || x_expr == "Count$XPaid" {
94            return source_card
95                .svars
96                .get("XPaid")
97                .and_then(|s| s.parse::<i32>().ok())
98                .unwrap_or(0);
99        }
100        if let Ok(n) = x_expr.parse::<i32>() {
101            return n;
102        }
103        if x_expr.starts_with("Count$") {
104            return crate::ability::effects::resolve_count_svar(x_expr, game, source, player);
105        }
106    }
107
108    0
109}
110
111/// The amount slot of a [`CostPart`]. Replaces the legacy `i32` (with
112/// `DYNAMIC_X_SENTINEL`) once consumers are migrated. Mirrors Java's
113/// `CostPart.convertAmount` + `AbilityUtils.calculateAmount` split:
114/// literals resolve trivially, `X` reads `XPaid` / `SVar:X`, named SVar
115/// identifiers walk the host card's SVar table at call time.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub enum AmountSpec {
118    Literal(i32),
119    X,
120    /// SVar name on the host card (e.g. `Y` in `ExileFromHand<Y/Card.Blue>`
121    /// — the only legitimate source today is the `RaiseCost` SVar walker
122    /// in `cost_adjustment`). Reserved for explicit, audited parser sites.
123    Svar(String),
124}
125
126impl AmountSpec {
127    /// Parse a script amount token (`"3"`, `"X"`). Strips any trailing
128    /// `/extra` suffix so DSL forms like `Draw<1/You>` parse as `1`. Non-int,
129    /// non-X tokens fall back to `Literal(default)` — `Self::Svar` is reserved
130    /// for explicit parsers that want SVar-chain resolution (only
131    /// `cost_adjustment` today).
132    pub fn parse_or(raw: &str, default: i32) -> Self {
133        let head = raw.split('/').next().unwrap_or(raw).trim();
134        if head.is_empty() {
135            return Self::Literal(default);
136        }
137        if head.eq_ignore_ascii_case("X") {
138            Self::X
139        } else if let Ok(n) = head.parse::<i32>() {
140            Self::Literal(n)
141        } else {
142            Self::Literal(default)
143        }
144    }
145
146    /// Resolve to an `i32` at call time. `X` reads `XPaid`/`SVar:X` via the
147    /// legacy `resolve_dynamic_amount` flow; `Svar(name)` walks the host's
148    /// SVar chain to its first numeric value.
149    pub fn resolve(&self, game: &GameState, source: CardId, player: PlayerId) -> i32 {
150        match self {
151            Self::Literal(n) => *n,
152            Self::X => resolve_dynamic_amount(game, source, player, DYNAMIC_X_SENTINEL),
153            Self::Svar(name) => resolve_named_svar(game, source, player, name),
154        }
155    }
156
157    pub fn as_literal(&self) -> Option<i32> {
158        match self {
159            Self::Literal(n) => Some(*n),
160            _ => None,
161        }
162    }
163
164    pub fn is_x(&self) -> bool {
165        matches!(self, Self::X)
166    }
167}
168
169impl Default for AmountSpec {
170    fn default() -> Self {
171        Self::Literal(0)
172    }
173}
174
175impl From<i32> for AmountSpec {
176    fn from(n: i32) -> Self {
177        Self::Literal(n)
178    }
179}
180
181impl std::fmt::Display for AmountSpec {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            Self::Literal(n) => write!(f, "{n}"),
185            Self::X => f.write_str("X"),
186            Self::Svar(name) => f.write_str(name),
187        }
188    }
189}
190
191/// Walk a named SVar on the host card to its first numeric value.
192/// Used by `AmountSpec::Svar(name).resolve()` (and, transitively, by the
193/// `RaiseCost` parser once it routes through `AmountSpec`).
194fn resolve_named_svar(game: &GameState, source: CardId, player: PlayerId, name: &str) -> i32 {
195    let host = game.card(source);
196    let mut current = name.to_string();
197    for _ in 0..8 {
198        let Some(raw) = host.svars.get(&current) else {
199            return 0;
200        };
201        if let Some(rest) = raw.strip_prefix("SVar$") {
202            let next = rest.split('/').next().unwrap_or("").trim();
203            if next.is_empty() {
204                return 0;
205            }
206            current = next.to_string();
207            continue;
208        }
209        if let Some(num_str) = raw.strip_prefix("Number$") {
210            return num_str.trim().parse().unwrap_or(0);
211        }
212        if let Ok(n) = raw.trim().parse::<i32>() {
213            return n;
214        }
215        if raw.starts_with("Count$") {
216            return crate::ability::effects::resolve_count_svar(raw, game, source, player);
217        }
218        return 0;
219    }
220    0
221}
222
223/// A single component of an ability cost.
224/// Mirrors Java's CostPart hierarchy.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub enum RevealFrom {
227    Hand,
228    Exile,
229    HandOrBattlefield,
230    All,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub enum CostPart {
235    /// Tap the source permanent. {T}
236    Tap,
237    /// Pay mana. Mirrors Java `CostPartMana`.
238    Mana {
239        cost: ManaCost,
240        /// Minimum value for X in the mana cost (parsed from restriction "XMin<N>").
241        #[serde(default)]
242        x_min: i32,
243        /// When true, the base mana cost is augmented by the exiled creature's mana cost.
244        #[serde(default)]
245        is_exiled_creature_cost: bool,
246        /// When true, the base mana cost is augmented by the enchanted creature's mana cost.
247        #[serde(default)]
248        is_enchanted_creature_cost: bool,
249        /// When true, the base mana cost is multiplied by an SVar-determined count.
250        #[serde(default)]
251        is_cost_pay_any_number_of_times: bool,
252        /// Maximum number of permanents that can be tapped to reduce the mana cost
253        /// via the Waterbend mechanic. Mirrors Java's `CostPartMana.maxWaterbend`.
254        #[serde(default)]
255        max_waterbend: Option<String>,
256    },
257    /// Pay life. Mirrors CostPayLife.
258    PayLife(AmountSpec),
259    /// Sacrifice permanents. type_filter "CARDNAME" means sacrifice self.
260    Sacrifice {
261        amount: AmountSpec,
262        type_filter: String,
263    },
264    /// Discard cards. type_filter "CARDNAME" means discard self.
265    Discard {
266        amount: AmountSpec,
267        type_filter: String,
268    },
269    /// Remove counters from a permanent (e.g. SubCounter<1/DREAM/NICKNAME>).
270    SubCounter {
271        amount: AmountSpec,
272        counter_type: CounterType,
273        type_filter: String,
274    },
275    /// Add counters to the source permanent (e.g. AddCounter<1/LOYALTY>). Mirrors CostPutCounter.
276    AddCounter {
277        amount: AmountSpec,
278        counter_type: CounterType,
279    },
280    /// Exile cards from a specific zone (own zone) as cost. Mirrors CostExile.
281    Exile {
282        amount: AmountSpec,
283        type_filter: String,
284        from: ZoneType,
285    },
286    /// Exile cards from any player's graveyard as cost (ExileAnyGrave). Mirrors CostExile zoneMode=-1.
287    ExileFromAnyGrave {
288        amount: AmountSpec,
289        type_filter: String,
290    },
291    /// Exile cards from the same graveyard as cost (ExileSameGrave). Mirrors CostExile zoneMode=0.
292    ExileFromSameGrave {
293        amount: AmountSpec,
294        type_filter: String,
295    },
296    /// Return permanents to owner's hand as cost. Mirrors CostReturn.
297    Return {
298        amount: AmountSpec,
299        type_filter: String,
300    },
301    /// Tap other permanents of a type as cost (tapXType<n/filter>). Mirrors CostTapType.
302    /// When `min_total_power` is Some(N), tap any number of creatures whose total power >= N
303    /// (used by Crew). When None, tap exactly `amount` matching permanents.
304    TapType {
305        amount: AmountSpec,
306        type_filter: String,
307        min_total_power: Option<i32>,
308    },
309    /// Untap permanents as cost. Mirrors CostUntap.
310    Untap,
311    /// Untap other permanents of a type as cost (untapYType<n/filter>). Mirrors CostUntapType.
312    UntapType {
313        amount: AmountSpec,
314        type_filter: String,
315        can_untap_source: bool,
316    },
317    /// Pay energy counters. Mirrors CostPayEnergy.
318    PayEnergy(AmountSpec),
319    /// Pay shard counters. Mirrors CostPayShards.
320    PayShards(AmountSpec),
321    /// Deal damage to the source's controller as cost. Mirrors CostDamage.
322    DamageYou(AmountSpec),
323    /// Draw cards as cost. Mirrors CostDraw.
324    Draw(AmountSpec),
325    /// Mill cards as cost. Mirrors CostMill.
326    Mill(AmountSpec),
327    /// Reveal cards as cost. Mirrors CostReveal.
328    Reveal {
329        amount: AmountSpec,
330        type_filter: String,
331        from: RevealFrom,
332    },
333    /// Exert permanent(s) as cost. Mirrors CostExert.
334    Exert {
335        amount: AmountSpec,
336        type_filter: String,
337    },
338    /// Opponent gains life as cost. Mirrors CostGainLife.
339    GainLife(AmountSpec),
340    /// Gain control of permanents matching type_filter as cost. Mirrors CostGainControl.
341    GainControl {
342        amount: AmountSpec,
343        type_filter: String,
344    },
345    /// Remove any counter type from permanents matching type_filter. Mirrors CostRemoveAnyCounter.
346    /// `counter_type` is None means any counter type.
347    RemoveAnyCounter {
348        amount: AmountSpec,
349        type_filter: String,
350        counter_type: Option<CounterType>,
351    },
352    /// Unattach equipment as a cost. Mirrors CostUnattach.
353    /// `type_filter` can be "CARDNAME" (source is the equipment), "OriginalHost",
354    /// or a card filter expression (source is the equipped creature, filter matches equipment).
355    Unattach {
356        type_filter: String,
357        description: Option<String>,
358    },
359    /// Move cards from exile to graveyard as cost. Mirrors CostExiledMoveToGrave.
360    ExiledMoveToGrave {
361        amount: AmountSpec,
362        type_filter: String,
363    },
364    /// Add mana to the pool as a cost (AddMana<amount/type>). Mirrors CostAddMana.
365    /// Always payable. Payment adds the specified mana to the activating player's pool.
366    AddMana {
367        amount: AmountSpec,
368        mana_type: String,
369    },
370    /// Waterbend cost (Waterbend<N>). Mirrors CostWaterbend.
371    /// Pay N generic mana, but you can tap your artifacts and creatures to help (each tapped = {1}).
372    Waterbend { amount: AmountSpec },
373    /// Choose one or more colors as a cost. Mirrors CostChooseColor.
374    ChooseColor(AmountSpec),
375    /// Choose a creature type as a cost. Mirrors CostChooseCreatureType.
376    ChooseCreatureType(AmountSpec),
377    /// Flip one or more coins as a cost. Mirrors CostFlipCoin.
378    FlipCoin(AmountSpec),
379    /// Roll dice as a cost. Mirrors CostRollDice.
380    RollDice {
381        amount: AmountSpec,
382        sides: i32,
383        result_svar: String,
384    },
385    /// Exile spells from stack as a cost. Mirrors CostExileFromStack.
386    ExileFromStack {
387        amount: AmountSpec,
388        type_filter: String,
389    },
390    /// Collect evidence N (exile cards from your graveyard with total MV >= N).
391    CollectEvidence(AmountSpec),
392    /// Forage: exile 3 from your graveyard or sacrifice a Food.
393    Forage,
394    /// Put card(s) to library from a zone as a cost. Mirrors CostPutCardToLib.
395    PutCardToLib {
396        amount: AmountSpec,
397        lib_pos: i32,
398        type_filter: String,
399        from: ZoneType,
400        same_zone: bool,
401    },
402    /// Enlist another creature as a cost. Mirrors CostEnlist.
403    Enlist {
404        amount: AmountSpec,
405        type_filter: String,
406    },
407    /// Promise gift to an opponent as a cost. Mirrors CostPromiseGift.
408    PromiseGift,
409    /// Reveal previously chosen player/type as a cost. Mirrors CostRevealChosen.
410    RevealChosen { reveal_type: String },
411    /// Behold (reveal from hand/battlefield), optionally exile revealed cards.
412    Behold {
413        amount: AmountSpec,
414        type_filter: String,
415        exile: bool,
416    },
417    /// Blight N = put N -1/-1 counters on creature(s) you control.
418    Blight(AmountSpec),
419    /// Exile from battlefield or graveyard as a combined cost (craft).
420    ExileCtrlOrGrave {
421        amount: AmountSpec,
422        type_filter: String,
423    },
424}
425
426impl CostPart {
427    /// Payment ordering — mirrors Java's CostPart.paymentOrder.
428    /// Lower numbers are paid first.
429    fn payment_order(&self) -> i32 {
430        match self {
431            CostPart::Tap => -1,
432            CostPart::Untap => 20,
433            CostPart::Mana {
434                is_exiled_creature_cost,
435                ..
436            } => {
437                if *is_exiled_creature_cost {
438                    200
439                } else {
440                    0
441                }
442            }
443            CostPart::PayEnergy(_) => 7,
444            CostPart::PayShards(_) => 7,
445            CostPart::SubCounter { .. } => 8,
446            CostPart::AddCounter { .. } => 6,
447            CostPart::PayLife(_) => 7,
448            CostPart::DamageYou(_) => 8,
449            CostPart::GainLife(_) => 5,
450            CostPart::Reveal { from, .. } => match from {
451                RevealFrom::Hand => 5,
452                RevealFrom::HandOrBattlefield => 5,
453                _ => -1,
454            },
455            CostPart::Draw(_) => 20,
456            CostPart::Mill(_) => 20,
457            CostPart::Discard { .. } => 10,
458            CostPart::Sacrifice { .. } => 15,
459            CostPart::Exile { from, .. } => {
460                if *from == ZoneType::Library {
461                    20
462                } else {
463                    15
464                }
465            }
466            CostPart::ExileFromAnyGrave { .. } => 15,
467            CostPart::ExileFromSameGrave { .. } => 15,
468            CostPart::Return { .. } => 10,
469            CostPart::TapType { .. } => 18,
470            CostPart::UntapType { .. } => 18,
471            CostPart::GainControl { .. } => 8,
472            CostPart::RemoveAnyCounter { .. } => 8,
473            CostPart::Unattach { .. } => 5,
474            CostPart::ExiledMoveToGrave { .. } => 15,
475            CostPart::AddMana { .. } => 5,
476            CostPart::Waterbend { .. } => 0,
477            CostPart::Exert { .. } => 5,
478            CostPart::ChooseColor(_) => 8,
479            CostPart::ChooseCreatureType(_) => 5,
480            CostPart::FlipCoin(_) => 22,
481            CostPart::RollDice { .. } => 5,
482            CostPart::ExileFromStack { .. } => 15,
483            CostPart::CollectEvidence(_) => 15,
484            CostPart::Forage => 5,
485            CostPart::PutCardToLib { .. } => 10,
486            CostPart::Enlist { .. } => 5,
487            CostPart::PromiseGift => -1,
488            CostPart::RevealChosen { .. } => 20,
489            CostPart::Behold { .. } => 5,
490            CostPart::Blight(_) => 6,
491            CostPart::ExileCtrlOrGrave { .. } => 15,
492        }
493    }
494}
495
496/// The complete cost to activate an ability.
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct Cost {
499    pub parts: Vec<CostPart>,
500    pub has_tap: bool,
501    pub mandatory: bool,
502}
503
504impl Cost {
505    pub fn has_tap_cost(&self) -> bool {
506        self.has_tap
507    }
508
509    pub fn has_no_mana_cost(&self) -> bool {
510        !self
511            .parts
512            .iter()
513            .any(|p| matches!(p, CostPart::Mana { .. }))
514    }
515
516    pub fn has_mana_cost(&self) -> bool {
517        !self.has_no_mana_cost()
518    }
519
520    pub fn has_specific_cost_type(&self, probe: &CostPart) -> bool {
521        let tag = std::mem::discriminant(probe);
522        self.parts.iter().any(|p| std::mem::discriminant(p) == tag)
523    }
524
525    pub fn has_only_specific_cost_type(&self, probe: &CostPart) -> bool {
526        let tag = std::mem::discriminant(probe);
527        self.parts.iter().all(|p| std::mem::discriminant(p) == tag)
528    }
529
530    pub fn sort(&mut self) {
531        self.parts.sort_by_key(|p| p.payment_order());
532    }
533
534    pub fn copy(&self) -> Self {
535        self.clone()
536    }
537
538    pub fn copy_with_no_mana(&self) -> Self {
539        let mut out = self.clone();
540        out.parts.retain(|p| !matches!(p, CostPart::Mana { .. }));
541        out
542    }
543
544    pub fn copy_with_defined_mana(&self, mana_cost: ManaCost) -> Self {
545        let mut out = self.clone();
546        out.parts.retain(|p| !matches!(p, CostPart::Mana { .. }));
547        out.parts.push(CostPart::Mana {
548            cost: mana_cost,
549            x_min: 0,
550            is_exiled_creature_cost: false,
551            is_enchanted_creature_cost: false,
552            is_cost_pay_any_number_of_times: false,
553            max_waterbend: None,
554        });
555        out.sort();
556        out
557    }
558
559    pub fn refund_paid_cost(&self, game: &mut GameState, source: CardId, player: PlayerId) {
560        for part in self.parts.iter().rev() {
561            crate::cost::cost_part::refund(game, source, player, part);
562        }
563    }
564
565    pub fn to_string_alt(&self) -> String {
566        to_simple_string(self)
567    }
568
569    pub fn to_simple_string(&self) -> String {
570        to_simple_string(self)
571    }
572
573    pub fn is_zero_cost(&self) -> bool {
574        self.parts.is_empty()
575            || (self.parts.len() == 1
576                && matches!(&self.parts[0], CostPart::Mana { cost: mana, .. } if mana.is_zero()))
577    }
578}
579
580pub fn convert_amount_type_to_words(amount: i32, amount_expr: &str, noun: &str) -> String {
581    if amount_expr == "X" {
582        format!("X {}", noun)
583    } else if amount == 1 {
584        format!("a {}", noun)
585    } else {
586        format!("{} {}s", amount, noun)
587    }
588}
589
590pub fn convert_int_and_type_to_words(amount: i32, noun: &str) -> String {
591    convert_amount_type_to_words(amount, &amount.to_string(), noun)
592}
593
594pub fn merge_to(dst: &mut Cost, src: &Cost) {
595    dst.parts.extend(src.parts.clone());
596    dst.has_tap = dst.has_tap || src.has_tap;
597    dst.mandatory = dst.mandatory || src.mandatory;
598    dst.sort();
599}
600
601pub fn add(cost: &mut Cost, part: CostPart) {
602    cost.has_tap = cost.has_tap || matches!(part, CostPart::Tap);
603    cost.parts.push(part);
604    cost.sort();
605}
606
607pub fn apply_text_change_effects(cost: &mut Cost, game: &GameState, host: CardId) {
608    for part in &mut cost.parts {
609        crate::cost::cost_part::apply_text_change_effects(part, game, host);
610    }
611}
612
613pub fn has_x_in_any_cost_part(cost: &Cost) -> bool {
614    cost.parts.iter().any(|p| match p {
615        CostPart::Mana { cost, .. } => cost.count_x() > 0,
616        _ => cost_part::convert_amount(p).is_some_and(AmountSpec::is_x),
617    })
618}
619
620pub fn get_max_for_non_mana_x(
621    cost: &Cost,
622    game: &GameState,
623    ability: &SpellAbility,
624    payer: PlayerId,
625    effect: bool,
626) -> Option<i32> {
627    let mut val: Option<i32> = None;
628    for p in &cost.parts {
629        if !cost_part::convert_amount(p).is_some_and(AmountSpec::is_x) {
630            continue;
631        }
632        let part_max = cost_part::get_max_amount_x(game, ability, payer, p, effect);
633        val = match (val, part_max) {
634            (Some(a), Some(b)) => Some(a.min(b)),
635            (a, b) => a.or(b),
636        };
637    }
638    if let Some(v) = val {
639        if v <= 0
640            && cost.has_mana_cost()
641            && cost.parts.iter().any(|p| cost_part_mana::get_x_min(p) > 0)
642        {
643            return None;
644        }
645    }
646    val
647}
648
649pub fn to_simple_string(cost: &Cost) -> String {
650    let mut out = Vec::new();
651    for part in &cost.parts {
652        match part {
653            CostPart::Tap => out.push("{T}".to_string()),
654            CostPart::Untap => out.push("{Q}".to_string()),
655            CostPart::Mana { cost, .. } => out.push(format!("{}", cost)),
656            CostPart::PayLife(v) => out.push(format!("Pay {} life", v)),
657            _ => out.push(format!("{:?}", part)),
658        }
659    }
660    out.join(", ")
661}
662
663/// Cost rendered for a player-facing prompt. Life uses the `{LIFE}` token so the
664/// UI can draw a heart; mana keeps its `{...}` symbols.
665pub fn to_prompt_string(cost: &Cost) -> String {
666    let mut out = Vec::new();
667    for part in &cost.parts {
668        match part {
669            CostPart::Tap => out.push("{T}".to_string()),
670            CostPart::Untap => out.push("{Q}".to_string()),
671            CostPart::Mana { cost, .. } => out.push(format!("{}", cost)),
672            CostPart::PayLife(v) => out.push(format!("{} {{LIFE}}", v)),
673            _ => out.push(format!("{:?}", part)),
674        }
675    }
676    out.join(", ")
677}
678
679/// Parse a Cost$ value from the DSL.
680///
681/// Examples:
682/// - `"T"` → tap
683/// - `"1 G"` → mana cost {1}{G}
684/// - `"T 1 G"` → tap + mana
685/// - `"Sac<1/CARDNAME>"` → sacrifice self
686/// - `"PayLife<3>"` → pay 3 life
687/// - `"Exile<1/Creature>"` → exile a creature from battlefield
688/// - `"ExileFromHand<1/Card>"` → exile from hand
689/// - `"Return<1/CARDNAME>"` → return self to hand
690/// - `"tapXType<1/Creature>"` → tap a creature
691/// - `"AddCounter<1/LOYALTY>"` → add a loyalty counter
692/// - `"SubCounter<1/LOYALTY/CARDNAME>"` → remove loyalty counter
693pub fn parse_cost(raw: &str) -> Cost {
694    let tokens = split_cost_tokens(raw);
695    let mut parts = Vec::new();
696    let mut has_tap = false;
697    let mut mandatory = false;
698    let mut mana_tokens: Vec<&str> = Vec::new();
699
700    // Pre-scan for untap cost (Q/Untap), mirroring Java's pre-scan for hasUntapInPrice.
701    let has_untap = tokens.iter().any(|token| {
702        CostTokenKind::parse(token).is_some_and(|parsed| parsed.kind == CostTokenKind::Untap)
703    });
704
705    for token in &tokens {
706        match cost_parser::parse_cost_token(token) {
707            cost_parser::TokenResult::Part(part) => parts.push(part),
708            cost_parser::TokenResult::Tap => {
709                parts.push(CostPart::Tap);
710                has_tap = true;
711            }
712            cost_parser::TokenResult::Mandatory => {
713                mandatory = true;
714            }
715            cost_parser::TokenResult::Mana => {
716                mana_tokens.push(token);
717            }
718        }
719    }
720
721    // Post-process: set can_untap_source on UntapType parts.
722    // Mirrors Java: canUntapSource = !hasUntapInPrice
723    for part in &mut parts {
724        if let CostPart::UntapType {
725            can_untap_source, ..
726        } = part
727        {
728            *can_untap_source = !has_untap;
729        }
730    }
731
732    // If we have mana tokens, combine them into a ManaCost
733    if !mana_tokens.is_empty() {
734        let mana_str = mana_tokens.join(" ");
735        let mana_cost = ManaCost::parse(&mana_str);
736        if mana_cost.cmc() > 0 || !mana_str.is_empty() {
737            parts.push(CostPart::Mana {
738                cost: mana_cost,
739                x_min: 0,
740                is_exiled_creature_cost: false,
741                is_enchanted_creature_cost: false,
742                is_cost_pay_any_number_of_times: false,
743                max_waterbend: None,
744            });
745        }
746    }
747
748    // Sort by payment order
749    parts.sort_by_key(|p| p.payment_order());
750
751    Cost {
752        parts,
753        has_tap,
754        mandatory,
755    }
756}
757
758/// Parse `"amount/filter"` inner content, returning (amount, filter).
759/// If there's no slash, defaults to amount=1 and filter=inner.
760pub(super) fn parse_amount_filter(inner: &str) -> (AmountSpec, String) {
761    if let Some(slash_idx) = inner.find('/') {
762        let amt = AmountSpec::parse_or(&inner[..slash_idx], 1);
763        let rest = &inner[slash_idx + 1..];
764        let filter = if let Some(desc_idx) = rest.find('/') {
765            rest[..desc_idx].to_string()
766        } else {
767            rest.to_string()
768        };
769        (amt, filter)
770    } else {
771        (AmountSpec::Literal(1), inner.to_string())
772    }
773}
774
775pub(super) fn parse_amount_filter_dynamic(inner: &str) -> (AmountSpec, String) {
776    if let Some(slash_idx) = inner.find('/') {
777        let amt = AmountSpec::parse_or(&inner[..slash_idx], 1);
778        let rest = &inner[slash_idx + 1..];
779        let filter = if let Some(desc_idx) = rest.find('/') {
780            rest[..desc_idx].to_string()
781        } else {
782            rest.to_string()
783        };
784        (amt, filter)
785    } else {
786        (AmountSpec::parse_or(inner, 1), inner.to_string())
787    }
788}
789
790/// Split cost string on spaces, keeping `<...>` groups together.
791fn split_cost_tokens(raw: &str) -> Vec<&str> {
792    let mut tokens = Vec::new();
793    let mut start = 0;
794    let mut depth = 0;
795    let bytes = raw.as_bytes();
796
797    let mut i = 0;
798    while i < bytes.len() {
799        match bytes[i] {
800            b'<' => depth += 1,
801            b'>' => {
802                if depth > 0 {
803                    depth -= 1;
804                }
805            }
806            b' ' if depth == 0 => {
807                let token = raw[start..i].trim();
808                if !token.is_empty() {
809                    tokens.push(token);
810                }
811                start = i + 1;
812            }
813            _ => {
814                // This is a character-parsing loop, not a semantic dispatch
815                // No warning needed here — we're just walking through characters
816            }
817        }
818        i += 1;
819    }
820    // Last token
821    let token = raw[start..].trim();
822    if !token.is_empty() {
823        tokens.push(token);
824    }
825    tokens
826}
827
828/// Check if a card matches a type filter string.
829/// Thin wrapper around `matches_change_type` for use by individual cost modules.
830pub fn matches_type_filter(game: &GameState, cid: CardId, type_filter: &str) -> bool {
831    matches_change_type(game.card(cid), type_filter, &[])
832}
833
834/// Find valid sacrifice targets on the battlefield for a player, filtered by type.
835/// Mirrors Java's `CostSacrifice.getMaxAmountX()` + `CardLists.getValidCards()`.
836pub fn get_sacrifice_targets(game: &GameState, player: PlayerId, type_filter: &str) -> Vec<CardId> {
837    game.cards_in_zone(ZoneType::Battlefield, player)
838        .to_vec()
839        .into_iter()
840        .filter(|&cid| matches_change_type(game.card(cid), type_filter, &[]))
841        .collect()
842}
843
844/// Find cards that can have counters removed for `SubCounter<.../Type/...>`.
845pub fn get_sub_counter_targets(
846    game: &GameState,
847    player: PlayerId,
848    source: CardId,
849    type_filter: &str,
850) -> Vec<CardId> {
851    if type_filter.eq_ignore_ascii_case("OriginalHost") {
852        return Vec::new();
853    }
854    let source_card = game.card(source);
855    game.cards_in_zone(ZoneType::Battlefield, player)
856        .to_vec()
857        .into_iter()
858        .filter(|&cid| {
859            if type_filter == "Card" || type_filter.is_empty() {
860                return true;
861            }
862            let selector = crate::parsing::cached_compiled_selector(type_filter);
863            crate::card::valid_filter::matches_valid_card_selector_in_game(
864                &selector,
865                game.card(cid),
866                source_card,
867                game,
868            )
869        })
870        .collect()
871}
872
873/// Find valid sacrifice targets for a cost, filtered both by type and
874/// CantSacrifice-style legality before RNG selection.
875pub fn get_sacrifice_targets_for_cost(
876    game: &GameState,
877    player: PlayerId,
878    type_filter: &str,
879    ability: Option<&SpellAbility>,
880) -> Vec<CardId> {
881    let static_sources = static_ability_source_cards(game);
882    get_sacrifice_targets(game, player, type_filter)
883        .into_iter()
884        .filter(|&cid| !cant_sacrifice(&static_sources, game.card(cid), ability, true))
885        .collect()
886}
887
888/// Find valid exile/return targets in a given zone for a player, filtered by type.
889pub fn get_zone_targets(
890    game: &GameState,
891    player: PlayerId,
892    zone: ZoneType,
893    type_filter: &str,
894) -> Vec<CardId> {
895    game.cards_in_zone(zone, player)
896        .to_vec()
897        .into_iter()
898        .filter(|&cid| {
899            if type_filter == "Card" || type_filter.is_empty() {
900                true
901            } else {
902                matches_change_type(game.card(cid), type_filter, &[])
903            }
904        })
905        .collect()
906}
907
908/// Find cards in exile across all players matching a type filter.
909/// Used by ExiledMoveToGrave and can_pay checks.
910pub fn get_exiled_targets(game: &GameState, type_filter: &str) -> Vec<CardId> {
911    game.players
912        .iter()
913        .flat_map(|p| game.cards_in_zone(ZoneType::Exile, p.id).to_vec())
914        .filter(|&cid| {
915            type_filter == "Card"
916                || type_filter.is_empty()
917                || matches_change_type(game.card(cid), type_filter, &[])
918        })
919        .collect()
920}
921
922/// Find valid tap-type targets (untapped permanents matching filter, excluding source).
923pub fn get_tap_type_targets(
924    game: &GameState,
925    player: PlayerId,
926    type_filter: &str,
927    exclude: CardId,
928) -> Vec<CardId> {
929    game.cards_in_zone(ZoneType::Battlefield, player)
930        .to_vec()
931        .into_iter()
932        .filter(|&cid| {
933            if cid == exclude {
934                return false;
935            }
936            let card = game.card(cid);
937            if card.tapped {
938                return false;
939            }
940            if type_filter == "Card" || type_filter.is_empty() {
941                true
942            } else {
943                matches_change_type(card, type_filter, &[])
944            }
945        })
946        .collect()
947}
948
949/// Find cards available to enlist: untapped, non-attacking, non-summoning-sick creatures you control.
950pub fn get_enlist_targets(game: &GameState, player: PlayerId) -> Vec<CardId> {
951    game.cards_in_zone(ZoneType::Battlefield, player)
952        .to_vec()
953        .into_iter()
954        .filter(|&cid| {
955            let c = game.card(cid);
956            // Mirror Java CostEnlist.getCardsForEnlisting():
957            // c.canTap() && !c.isSick() && !c.isAttacking()
958            // where isSick() is false for creatures with haste.
959            c.is_creature()
960                && !c.tapped
961                && !c.phased_out
962                && (!c.summoning_sick || c.has_haste())
963                && c.attacking_player.is_none()
964        })
965        .collect()
966}
967
968pub fn matches_exile_from_stack_filter(
969    game: &GameState,
970    card_id: CardId,
971    source: CardId,
972    _player: PlayerId,
973    type_filter: &str,
974) -> bool {
975    if type_filter == "All" || type_filter.is_empty() {
976        return true;
977    }
978    let card = game.card(card_id);
979    let source_card = game.card(source);
980    for clause in type_filter.split(';') {
981        let clause = clause.trim();
982        if clause.is_empty() {
983            continue;
984        }
985        let normalized = normalize_stack_clause_for_valid_cards(clause);
986        let selector = crate::parsing::cached_compiled_selector(&normalized);
987        if crate::card::valid_filter::matches_valid_card_selector_in_game(
988            &selector,
989            card,
990            source_card,
991            game,
992        ) {
993            return true;
994        }
995    }
996    false
997}
998
999fn is_valid_cards_type_token(token: &str) -> bool {
1000    matches!(
1001        token,
1002        "Card"
1003            | "Permanent"
1004            | "Creature"
1005            | "Land"
1006            | "Artifact"
1007            | "Enchantment"
1008            | "Planeswalker"
1009            | "Instant"
1010            | "Sorcery"
1011            | "Plains"
1012            | "Island"
1013            | "Swamp"
1014            | "Mountain"
1015            | "Forest"
1016    )
1017}
1018
1019fn normalize_stack_clause_for_valid_cards(clause: &str) -> String {
1020    let mut tokens: Vec<&str> = clause
1021        .split(['.', '+'])
1022        .map(str::trim)
1023        .filter(|s| !s.is_empty() && !s.eq_ignore_ascii_case("Spell"))
1024        .collect();
1025
1026    if tokens.is_empty() {
1027        return "Card".to_string();
1028    }
1029
1030    let type_idx = tokens
1031        .iter()
1032        .position(|t| is_valid_cards_type_token(t))
1033        .unwrap_or(usize::MAX);
1034
1035    if type_idx == usize::MAX {
1036        let mut out = String::from("Card");
1037        for t in tokens.drain(..) {
1038            out.push('.');
1039            out.push_str(t);
1040        }
1041        return out;
1042    }
1043
1044    let type_part = tokens[type_idx].to_string();
1045    let mut qualifiers: Vec<&str> = Vec::with_capacity(tokens.len().saturating_sub(1));
1046    qualifiers.extend(tokens[..type_idx].iter().copied());
1047    qualifiers.extend(tokens[type_idx + 1..].iter().copied());
1048
1049    if qualifiers.is_empty() {
1050        type_part
1051    } else {
1052        format!("{}.{}", type_part, qualifiers.join("."))
1053    }
1054}
1055
1056pub fn strip_exile_type_modifiers(type_filter: &str) -> String {
1057    let mut t = type_filter.to_string();
1058    if t.contains("FromTopGrave") {
1059        t = t.replace("FromTopGrave", "");
1060    }
1061    if let Some((left, _)) = t.split_once("+withTotalCMCEQ") {
1062        t = left.to_string();
1063    }
1064    if let Some((left, _)) = t.split_once("+withTotalCMCGE") {
1065        t = left.to_string();
1066    }
1067    if t.contains("+withSharedCardType") {
1068        t = t.replace("+withSharedCardType", "");
1069    }
1070    if let Some((left, _)) = t.split_once("+withTypesGE") {
1071        t = left.to_string();
1072    }
1073    t
1074}
1075
1076pub fn normalize_exile_base_filter(type_filter: &str) -> String {
1077    let t = strip_exile_type_modifiers(type_filter);
1078    if t.is_empty() || t.eq_ignore_ascii_case("All") || t.contains('X') {
1079        "Card".to_string()
1080    } else {
1081        t
1082    }
1083}
1084
1085pub(crate) fn parse_exile_total_cmc_eq(type_filter: &str) -> Option<&str> {
1086    type_filter
1087        .split_once("+withTotalCMCEQ")
1088        .map(|(_, rhs)| rhs.trim())
1089}
1090
1091pub(crate) fn parse_exile_total_cmc_ge(type_filter: &str) -> Option<&str> {
1092    type_filter
1093        .split_once("+withTotalCMCGE")
1094        .map(|(_, rhs)| rhs.trim())
1095}
1096
1097pub(crate) fn parse_exile_types_ge(type_filter: &str) -> Option<i32> {
1098    type_filter
1099        .split_once("+withTypesGE")
1100        .and_then(|(_, rhs)| rhs.trim().parse::<i32>().ok())
1101}
1102
1103pub(crate) fn exile_requires_shared_card_type(type_filter: &str) -> bool {
1104    type_filter.contains("+withSharedCardType")
1105}
1106
1107pub(crate) fn reveal_candidates(
1108    game: &GameState,
1109    player: PlayerId,
1110    source: CardId,
1111    type_filter: &str,
1112    from: &RevealFrom,
1113) -> Vec<CardId> {
1114    let mut cards: Vec<CardId> = match from {
1115        RevealFrom::Hand => game.cards_in_zone(ZoneType::Hand, player).to_vec(),
1116        RevealFrom::Exile => game.cards_in_zone(ZoneType::Exile, player).to_vec(),
1117        RevealFrom::HandOrBattlefield => {
1118            let mut v = game.cards_in_zone(ZoneType::Hand, player).to_vec();
1119            v.extend(
1120                game.cards_in_zone(ZoneType::Battlefield, player)
1121                    .iter()
1122                    .copied(),
1123            );
1124            v
1125        }
1126        RevealFrom::All => {
1127            let mut v = game.cards_in_zone(ZoneType::Hand, player).to_vec();
1128            v.extend(
1129                game.cards_in_zone(ZoneType::Battlefield, player)
1130                    .iter()
1131                    .copied(),
1132            );
1133            v.extend(
1134                game.cards_in_zone(ZoneType::Graveyard, player)
1135                    .iter()
1136                    .copied(),
1137            );
1138            v.extend(
1139                game.cards_in_zone(ZoneType::Library, player)
1140                    .iter()
1141                    .copied(),
1142            );
1143            v.extend(game.cards_in_zone(ZoneType::Exile, player).iter().copied());
1144            v
1145        }
1146    };
1147
1148    // Spell costs can't pay reveal from the source card itself while in hand.
1149    if matches!(
1150        from,
1151        RevealFrom::Hand | RevealFrom::HandOrBattlefield | RevealFrom::All
1152    ) && game.card(source).zone == ZoneType::Hand
1153    {
1154        cards.retain(|&cid| cid != source);
1155    }
1156
1157    if type_filter == "Card" || type_filter.is_empty() || type_filter == "Hand" {
1158        return cards;
1159    }
1160
1161    cards
1162        .into_iter()
1163        .filter(|&cid| matches_change_type(game.card(cid), type_filter, &[]))
1164        .collect()
1165}
1166
1167/// Check if a cost can be paid by the given player for the given source card.
1168/// `available_mana` is the total mana available (pool + untapped sources).
1169pub fn can_pay(
1170    cost: &Cost,
1171    game: &GameState,
1172    available_mana: Option<&ManaPool>,
1173    source: CardId,
1174    player: PlayerId,
1175    ability: Option<&SpellAbility>,
1176) -> bool {
1177    let _perf_scope =
1178        crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Cost);
1179    for part in &cost.parts {
1180        if !can_pay_part_distributed(part, game, available_mana, source, player, ability) {
1181            return false;
1182        }
1183    }
1184    true
1185}
1186
1187/// Java parity helper: canPay(cost, ability-context).
1188pub fn can_pay_with_ability(
1189    cost: &Cost,
1190    game: &GameState,
1191    available_mana: &ManaPool,
1192    source: CardId,
1193    player: PlayerId,
1194    ability: Option<&SpellAbility>,
1195) -> bool {
1196    can_pay(cost, game, Some(available_mana), source, player, ability)
1197}
1198
1199pub fn can_pay_with_ability_and_reserved(
1200    cost: &Cost,
1201    game: &GameState,
1202    available_mana: &ManaPool,
1203    source: CardId,
1204    player: PlayerId,
1205    ability: Option<&SpellAbility>,
1206    reserved_sacrifices: &[CardId],
1207) -> bool {
1208    for part in &cost.parts {
1209        match part {
1210            CostPart::Sacrifice {
1211                type_filter,
1212                amount,
1213            } => {
1214                if type_filter == "CARDNAME"
1215                    || type_filter == "NICKNAME"
1216                    || type_filter == "OriginalHost"
1217                {
1218                    if !cost_sacrifice::can_pay(game, available_mana, source, player, ability, part)
1219                    {
1220                        return false;
1221                    }
1222                    continue;
1223                }
1224
1225                let mut valid = get_sacrifice_targets_for_cost(game, player, type_filter, ability);
1226                valid.retain(|cid| !reserved_sacrifices.contains(cid));
1227                if type_filter.eq_ignore_ascii_case("All") {
1228                    if valid.is_empty() {
1229                        return false;
1230                    }
1231                } else if (valid.len() as i32) < amount.resolve(game, source, player) {
1232                    return false;
1233                }
1234            }
1235            _ => {
1236                if !can_pay_part_distributed(
1237                    part,
1238                    game,
1239                    Some(available_mana),
1240                    source,
1241                    player,
1242                    ability,
1243                ) {
1244                    return false;
1245                }
1246            }
1247        }
1248    }
1249    true
1250}
1251
1252/// Check if a cost can be paid ignoring mana requirements.
1253/// Used for mana ability availability checks (to avoid circular dependency).
1254pub fn can_pay_ignoring_mana(
1255    cost: &Cost,
1256    game: &GameState,
1257    source: CardId,
1258    player: PlayerId,
1259) -> bool {
1260    can_pay(cost, game, None, source, player, None)
1261}
1262
1263/// Check if a cost can be paid ignoring mana requirements while preserving
1264/// the full spell/ability context for non-mana legality checks.
1265///
1266/// This is needed for action-space generation paths that separately validate
1267/// mana production (for example when some permanents are reserved to be
1268/// sacrificed and therefore cannot also be tapped for mana), but still need
1269/// `ValidCause$ Activated` / `ValidCause$ Spell` statics such as Yasharn to
1270/// see the real ability being paid.
1271pub fn can_pay_ignoring_mana_with_ability(
1272    cost: &Cost,
1273    game: &GameState,
1274    source: CardId,
1275    player: PlayerId,
1276    ability: &SpellAbility,
1277) -> bool {
1278    can_pay(cost, game, None, source, player, Some(ability))
1279}
1280
1281/// Check if a cost can be paid ignoring mana requirements, for a spell.
1282/// Passes a minimal SpellAbility with `is_spell = true` so that CantSacrifice
1283/// checks (e.g. Yasharn) can properly evaluate `ValidCause$ Spell` restrictions.
1284pub fn can_pay_ignoring_mana_for_spell(
1285    cost: &Cost,
1286    game: &GameState,
1287    source: CardId,
1288    player: PlayerId,
1289) -> bool {
1290    let mut stub = SpellAbility::new_empty(Some(source), player);
1291    stub.is_spell = true;
1292    can_pay(cost, game, None, source, player, Some(&stub))
1293}
1294
1295fn can_pay_part_distributed(
1296    part: &CostPart,
1297    game: &GameState,
1298    available_mana: Option<&ManaPool>,
1299    source: CardId,
1300    player: PlayerId,
1301    ability: Option<&SpellAbility>,
1302) -> bool {
1303    let empty_pool = ManaPool::new();
1304    let pool = available_mana.unwrap_or(&empty_pool);
1305
1306    match part {
1307        CostPart::Tap => cost_tap::can_pay(game, pool, source, player, ability, part),
1308        CostPart::Untap => cost_untap::can_pay(game, pool, source, player, ability, part),
1309        CostPart::Mana { .. } => {
1310            available_mana.is_none()
1311                || cost_part_mana::can_pay(game, pool, source, player, ability, part)
1312        }
1313        CostPart::PayLife(_) => cost_pay_life::can_pay(game, pool, source, player, ability, part),
1314        CostPart::Sacrifice { .. } => {
1315            cost_sacrifice::can_pay(game, pool, source, player, ability, part)
1316        }
1317        CostPart::Discard { .. } => {
1318            cost_discard::can_pay(game, pool, source, player, ability, part)
1319        }
1320        CostPart::SubCounter { .. } => {
1321            cost_remove_counter::can_pay(game, pool, source, player, ability, part)
1322        }
1323        CostPart::AddCounter { .. } => {
1324            cost_put_counter::can_pay(game, pool, source, player, ability, part)
1325        }
1326        CostPart::Exile { .. }
1327        | CostPart::ExileFromAnyGrave { .. }
1328        | CostPart::ExileFromSameGrave { .. } => {
1329            cost_exile::can_pay(game, pool, source, player, ability, part)
1330        }
1331        CostPart::Return { .. } => cost_return::can_pay(game, pool, source, player, ability, part),
1332        CostPart::TapType { .. } => {
1333            cost_tap_type::can_pay(game, pool, source, player, ability, part)
1334        }
1335        CostPart::UntapType { .. } => {
1336            cost_untap_type::can_pay(game, pool, source, player, ability, part)
1337        }
1338        CostPart::PayEnergy(_) => {
1339            cost_pay_energy::can_pay(game, pool, source, player, ability, part)
1340        }
1341        CostPart::PayShards(_) => {
1342            cost_pay_shards::can_pay(game, pool, source, player, ability, part)
1343        }
1344        CostPart::DamageYou(_) => cost_damage::can_pay(game, pool, source, player, ability, part),
1345        CostPart::Draw(_) => cost_draw::can_pay(game, pool, source, player, ability, part),
1346        CostPart::Mill(_) => cost_mill::can_pay(game, pool, source, player, ability, part),
1347        CostPart::Reveal { .. } => cost_reveal::can_pay(game, pool, source, player, ability, part),
1348        CostPart::Exert { .. } => cost_exert::can_pay(game, pool, source, player, ability, part),
1349        CostPart::GainLife(_) => cost_gain_life::can_pay(game, pool, source, player, ability, part),
1350        CostPart::GainControl { .. } => {
1351            cost_gain_control::can_pay(game, pool, source, player, ability, part)
1352        }
1353        CostPart::RemoveAnyCounter { .. } => {
1354            cost_remove_any_counter::can_pay(game, pool, source, player, ability, part)
1355        }
1356        CostPart::Unattach { .. } => {
1357            cost_unattach::can_pay(game, pool, source, player, ability, part)
1358        }
1359        CostPart::ExiledMoveToGrave { .. } => {
1360            cost_exiled_move_to_grave::can_pay(game, pool, source, player, ability, part)
1361        }
1362        CostPart::AddMana { .. } => {
1363            cost_add_mana::can_pay(game, pool, source, player, ability, part)
1364        }
1365        CostPart::Waterbend { .. } => {
1366            cost_waterbend::can_pay(game, available_mana, source, player, part)
1367        }
1368        CostPart::ChooseColor(_) => {
1369            cost_choose_color::can_pay(game, pool, source, player, ability, part)
1370        }
1371        CostPart::ChooseCreatureType(_) => {
1372            cost_choose_creature_type::can_pay(game, pool, source, player, ability, part)
1373        }
1374        CostPart::FlipCoin(_) => cost_flip_coin::can_pay(game, pool, source, player, ability, part),
1375        CostPart::RollDice { .. } => {
1376            cost_roll_dice::can_pay(game, pool, source, player, ability, part)
1377        }
1378        CostPart::ExileFromStack { .. } => {
1379            cost_exile_from_stack::can_pay(game, pool, source, player, ability, part)
1380        }
1381        CostPart::CollectEvidence(_) => {
1382            cost_collect_evidence::can_pay(game, pool, source, player, ability, part)
1383        }
1384        CostPart::Forage => cost_forage::can_pay(game, pool, source, player, ability, part),
1385        CostPart::PutCardToLib { .. } => {
1386            cost_put_card_to_lib::can_pay(game, pool, source, player, ability, part)
1387        }
1388        CostPart::Enlist { .. } => cost_enlist::can_pay(game, pool, source, player, ability, part),
1389        CostPart::PromiseGift => {
1390            cost_promise_gift::can_pay(game, pool, source, player, ability, part)
1391        }
1392        CostPart::RevealChosen { .. } => {
1393            cost_reveal_chosen::can_pay(game, pool, source, player, ability, part)
1394        }
1395        CostPart::Behold { exile, .. } => {
1396            if *exile {
1397                cost_behold_exile::can_pay(game, pool, source, player, ability, part)
1398            } else {
1399                cost_behold::can_pay(game, pool, source, player, ability, part)
1400            }
1401        }
1402        CostPart::Blight(_) => cost_blight::can_pay(game, source, player, part),
1403        CostPart::ExileCtrlOrGrave { .. } => {
1404            cost_exile_ctrl_or_grave::can_pay(game, source, player, ability, part)
1405        }
1406    }
1407}
1408pub fn static_ability_source_cards(game: &GameState) -> Vec<Card> {
1409    use std::collections::HashSet;
1410
1411    let mut ids: HashSet<CardId> = HashSet::new();
1412    for p in &game.players {
1413        for &zone in &[
1414            ZoneType::Battlefield,
1415            ZoneType::Graveyard,
1416            ZoneType::Exile,
1417            ZoneType::Command,
1418        ] {
1419            for &cid in game.cards_in_zone(zone, p.id) {
1420                ids.insert(cid);
1421            }
1422        }
1423    }
1424    for entry in game.stack.iter() {
1425        if let Some(cid) = entry.spell_ability.source {
1426            ids.insert(cid);
1427        }
1428    }
1429
1430    ids.into_iter().map(|cid| game.card(cid).clone()).collect()
1431}
1432
1433pub(crate) fn shares_creature_type(game: &GameState, a: CardId, b: CardId) -> bool {
1434    let ca = game.card(a);
1435    let cb = game.card(b);
1436    ca.shares_creature_type_with(cb)
1437}
1438
1439pub(crate) fn shares_card_type(game: &GameState, a: CardId, b: CardId) -> bool {
1440    let ca = game.card(a);
1441    let cb = game.card(b);
1442    ca.type_line
1443        .core_types
1444        .iter()
1445        .any(|t| cb.type_line.core_types.contains(t))
1446}
1447
1448pub(crate) fn cmc_can_sum_to(target: i32, values: &[i32]) -> bool {
1449    if target < 0 {
1450        return false;
1451    }
1452    let mut reachable: std::collections::BTreeSet<i32> = std::collections::BTreeSet::new();
1453    reachable.insert(0);
1454    for &v in values {
1455        if v < 0 {
1456            continue;
1457        }
1458        let mut next = reachable.clone();
1459        for &r in &reachable {
1460            let nv = r + v;
1461            if nv <= target {
1462                next.insert(nv);
1463            }
1464        }
1465        reachable = next;
1466        if reachable.contains(&target) {
1467            return true;
1468        }
1469    }
1470    reachable.contains(&target)
1471}
1472
1473#[cfg(test)]
1474mod tests {
1475    use super::*;
1476
1477    #[test]
1478    fn parse_tap_only() {
1479        let cost = parse_cost("T");
1480        assert!(cost.has_tap);
1481        assert_eq!(cost.parts.len(), 1);
1482        assert!(matches!(cost.parts[0], CostPart::Tap));
1483    }
1484
1485    #[test]
1486    fn parse_mana_only() {
1487        let cost = parse_cost("1 G");
1488        assert!(!cost.has_tap);
1489        assert_eq!(cost.parts.len(), 1);
1490        match &cost.parts[0] {
1491            CostPart::Mana { cost: mc, .. } => assert_eq!(mc.cmc(), 2),
1492            _ => panic!("expected Mana cost part"),
1493        }
1494    }
1495
1496    #[test]
1497    fn parse_tap_and_mana() {
1498        let cost = parse_cost("T 1 G");
1499        assert!(cost.has_tap);
1500        assert_eq!(cost.parts.len(), 2);
1501        // Tap should come first (payment_order = -1)
1502        assert!(matches!(cost.parts[0], CostPart::Tap));
1503        assert!(matches!(cost.parts[1], CostPart::Mana { .. }));
1504    }
1505
1506    #[test]
1507    fn parse_sacrifice() {
1508        let cost = parse_cost("Sac<1/CARDNAME>");
1509        assert_eq!(cost.parts.len(), 1);
1510        match &cost.parts[0] {
1511            CostPart::Sacrifice {
1512                amount,
1513                type_filter,
1514            } => {
1515                assert_eq!(amount.as_literal(), Some(1));
1516                assert_eq!(type_filter, "CARDNAME");
1517            }
1518            _ => panic!("expected Sacrifice cost part"),
1519        }
1520    }
1521
1522    #[test]
1523    fn parse_pay_life() {
1524        let cost = parse_cost("PayLife<3>");
1525        assert_eq!(cost.parts.len(), 1);
1526        match &cost.parts[0] {
1527            CostPart::PayLife(n) => assert_eq!(n.as_literal(), Some(3)),
1528            _ => panic!("expected PayLife cost part"),
1529        }
1530    }
1531
1532    #[test]
1533    fn parse_compound_cost() {
1534        let cost = parse_cost("T Sac<1/CARDNAME>");
1535        assert!(cost.has_tap);
1536        assert_eq!(cost.parts.len(), 2);
1537        // Tap first (order -1), then sacrifice (order 15)
1538        assert!(matches!(cost.parts[0], CostPart::Tap));
1539        assert!(matches!(cost.parts[1], CostPart::Sacrifice { .. }));
1540    }
1541
1542    #[test]
1543    fn parse_sacrifice_creature() {
1544        let cost = parse_cost("B Sac<1/Creature>");
1545        assert_eq!(cost.parts.len(), 2);
1546        // Mana (order 0) before Sacrifice (order 15)
1547        assert!(matches!(cost.parts[0], CostPart::Mana { .. }));
1548        match &cost.parts[1] {
1549            CostPart::Sacrifice {
1550                amount,
1551                type_filter,
1552            } => {
1553                assert_eq!(amount.as_literal(), Some(1));
1554                assert_eq!(type_filter, "Creature");
1555            }
1556            _ => panic!("expected Sacrifice cost part"),
1557        }
1558    }
1559
1560    #[test]
1561    fn payment_order_sorting() {
1562        // PayLife, Tap, Mana, Sacrifice — should sort to: Tap, Mana, PayLife, Sacrifice
1563        let cost = parse_cost("PayLife<2> T 1 G Sac<1/CARDNAME>");
1564        assert_eq!(cost.parts.len(), 4);
1565        assert!(matches!(cost.parts[0], CostPart::Tap));
1566        assert!(matches!(cost.parts[1], CostPart::Mana { .. }));
1567        assert!(matches!(cost.parts[2], CostPart::PayLife(_)));
1568        assert!(matches!(cost.parts[3], CostPart::Sacrifice { .. }));
1569    }
1570
1571    #[test]
1572    fn parse_exile_from_hand() {
1573        let cost = parse_cost("ExileFromHand<1/Card>");
1574        assert_eq!(cost.parts.len(), 1);
1575        match &cost.parts[0] {
1576            CostPart::Exile {
1577                amount,
1578                type_filter,
1579                from,
1580            } => {
1581                assert_eq!(amount.as_literal(), Some(1));
1582                assert_eq!(type_filter, "Card");
1583                assert_eq!(*from, ZoneType::Hand);
1584            }
1585            _ => panic!("expected Exile cost part"),
1586        }
1587    }
1588
1589    #[test]
1590    fn parse_add_counter() {
1591        let cost = parse_cost("AddCounter<1/LOYALTY>");
1592        assert_eq!(cost.parts.len(), 1);
1593        match &cost.parts[0] {
1594            CostPart::AddCounter {
1595                amount,
1596                counter_type,
1597            } => {
1598                assert_eq!(amount.as_literal(), Some(1));
1599                assert_eq!(*counter_type, CounterType::Loyalty);
1600            }
1601            _ => panic!("expected AddCounter cost part"),
1602        }
1603    }
1604
1605    #[test]
1606    fn parse_return() {
1607        let cost = parse_cost("Return<1/CARDNAME>");
1608        assert_eq!(cost.parts.len(), 1);
1609        match &cost.parts[0] {
1610            CostPart::Return {
1611                amount,
1612                type_filter,
1613            } => {
1614                assert_eq!(amount.as_literal(), Some(1));
1615                assert_eq!(type_filter, "CARDNAME");
1616            }
1617            _ => panic!("expected Return cost part"),
1618        }
1619    }
1620
1621    #[test]
1622    fn parse_tap_type() {
1623        let cost = parse_cost("tapXType<2/Creature>");
1624        assert_eq!(cost.parts.len(), 1);
1625        match &cost.parts[0] {
1626            CostPart::TapType {
1627                amount,
1628                type_filter,
1629                min_total_power,
1630            } => {
1631                assert_eq!(amount.as_literal(), Some(2));
1632                assert_eq!(type_filter, "Creature");
1633                assert_eq!(*min_total_power, None);
1634            }
1635            _ => panic!("expected TapType cost part"),
1636        }
1637    }
1638
1639    #[test]
1640    fn parse_tap_type_with_total_power() {
1641        let cost = parse_cost("tapXType<Any/Creature.Other+withTotalPowerGE{3}>");
1642        assert_eq!(cost.parts.len(), 1);
1643        match &cost.parts[0] {
1644            CostPart::TapType {
1645                amount,
1646                type_filter,
1647                min_total_power,
1648            } => {
1649                assert_eq!(amount.as_literal(), Some(1)); // "Any" defaults to 1
1650                assert_eq!(type_filter, "Creature.Other");
1651                assert_eq!(*min_total_power, Some(3));
1652            }
1653            _ => panic!("expected TapType cost part"),
1654        }
1655    }
1656
1657    #[test]
1658    fn parse_pay_energy() {
1659        let cost = parse_cost("PayEnergy<3>");
1660        assert_eq!(cost.parts.len(), 1);
1661        match &cost.parts[0] {
1662            CostPart::PayEnergy(n) => assert_eq!(n.as_literal(), Some(3)),
1663            _ => panic!("expected PayEnergy cost part"),
1664        }
1665    }
1666
1667    #[test]
1668    fn parse_explicit_mana_token() {
1669        let cost = parse_cost("Mana<2 G>");
1670        assert_eq!(cost.parts.len(), 1);
1671        assert!(matches!(cost.parts[0], CostPart::Mana { .. }));
1672    }
1673
1674    #[test]
1675    fn parse_collect_evidence() {
1676        let cost = parse_cost("CollectEvidence<6>");
1677        assert_eq!(cost.parts.len(), 1);
1678        assert!(
1679            matches!(&cost.parts[0], CostPart::CollectEvidence(n) if n.as_literal() == Some(6))
1680        );
1681    }
1682
1683    #[test]
1684    fn parse_forage() {
1685        let cost = parse_cost("Forage");
1686        assert_eq!(cost.parts.len(), 1);
1687        assert!(matches!(cost.parts[0], CostPart::Forage));
1688    }
1689
1690    #[test]
1691    fn parse_put_card_to_lib_from_grave() {
1692        let cost = parse_cost("PutCardToLibFromGrave<1/0/Card>");
1693        assert_eq!(cost.parts.len(), 1);
1694        match &cost.parts[0] {
1695            CostPart::PutCardToLib {
1696                amount,
1697                lib_pos,
1698                type_filter,
1699                from,
1700                same_zone,
1701            } => {
1702                assert_eq!(amount.as_literal(), Some(1));
1703                assert_eq!(*lib_pos, 0);
1704                assert_eq!(type_filter, "Card");
1705                assert_eq!(*from, ZoneType::Graveyard);
1706                assert!(!same_zone);
1707            }
1708            _ => panic!("expected PutCardToLib cost part"),
1709        }
1710    }
1711
1712    #[test]
1713    fn parse_exile_from_stack() {
1714        let cost = parse_cost("ExileFromStack<1/Spell>");
1715        assert_eq!(cost.parts.len(), 1);
1716        match &cost.parts[0] {
1717            CostPart::ExileFromStack {
1718                amount,
1719                type_filter,
1720            } => {
1721                assert_eq!(amount.as_literal(), Some(1));
1722                assert_eq!(type_filter, "Spell");
1723            }
1724            _ => panic!("expected ExileFromStack cost part"),
1725        }
1726    }
1727
1728    #[test]
1729    fn parse_exile_ctrl_or_grave() {
1730        let cost = parse_cost("ExileCtrlOrGrave<2/Artifact>");
1731        assert_eq!(cost.parts.len(), 1);
1732        match &cost.parts[0] {
1733            CostPart::ExileCtrlOrGrave {
1734                amount,
1735                type_filter,
1736            } => {
1737                assert_eq!(amount.as_literal(), Some(2));
1738                assert_eq!(type_filter, "Artifact");
1739            }
1740            _ => panic!("expected ExileCtrlOrGrave cost part"),
1741        }
1742    }
1743}