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
67pub(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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub enum AmountSpec {
118 Literal(i32),
119 X,
120 Svar(String),
124}
125
126impl AmountSpec {
127 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 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
191fn 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(¤t) 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#[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,
237 Mana {
239 cost: ManaCost,
240 #[serde(default)]
242 x_min: i32,
243 #[serde(default)]
245 is_exiled_creature_cost: bool,
246 #[serde(default)]
248 is_enchanted_creature_cost: bool,
249 #[serde(default)]
251 is_cost_pay_any_number_of_times: bool,
252 #[serde(default)]
255 max_waterbend: Option<String>,
256 },
257 PayLife(AmountSpec),
259 Sacrifice {
261 amount: AmountSpec,
262 type_filter: String,
263 },
264 Discard {
266 amount: AmountSpec,
267 type_filter: String,
268 },
269 SubCounter {
271 amount: AmountSpec,
272 counter_type: CounterType,
273 type_filter: String,
274 },
275 AddCounter {
277 amount: AmountSpec,
278 counter_type: CounterType,
279 },
280 Exile {
282 amount: AmountSpec,
283 type_filter: String,
284 from: ZoneType,
285 },
286 ExileFromAnyGrave {
288 amount: AmountSpec,
289 type_filter: String,
290 },
291 ExileFromSameGrave {
293 amount: AmountSpec,
294 type_filter: String,
295 },
296 Return {
298 amount: AmountSpec,
299 type_filter: String,
300 },
301 TapType {
305 amount: AmountSpec,
306 type_filter: String,
307 min_total_power: Option<i32>,
308 },
309 Untap,
311 UntapType {
313 amount: AmountSpec,
314 type_filter: String,
315 can_untap_source: bool,
316 },
317 PayEnergy(AmountSpec),
319 PayShards(AmountSpec),
321 DamageYou(AmountSpec),
323 Draw(AmountSpec),
325 Mill(AmountSpec),
327 Reveal {
329 amount: AmountSpec,
330 type_filter: String,
331 from: RevealFrom,
332 },
333 Exert {
335 amount: AmountSpec,
336 type_filter: String,
337 },
338 GainLife(AmountSpec),
340 GainControl {
342 amount: AmountSpec,
343 type_filter: String,
344 },
345 RemoveAnyCounter {
348 amount: AmountSpec,
349 type_filter: String,
350 counter_type: Option<CounterType>,
351 },
352 Unattach {
356 type_filter: String,
357 description: Option<String>,
358 },
359 ExiledMoveToGrave {
361 amount: AmountSpec,
362 type_filter: String,
363 },
364 AddMana {
367 amount: AmountSpec,
368 mana_type: String,
369 },
370 Waterbend { amount: AmountSpec },
373 ChooseColor(AmountSpec),
375 ChooseCreatureType(AmountSpec),
377 FlipCoin(AmountSpec),
379 RollDice {
381 amount: AmountSpec,
382 sides: i32,
383 result_svar: String,
384 },
385 ExileFromStack {
387 amount: AmountSpec,
388 type_filter: String,
389 },
390 CollectEvidence(AmountSpec),
392 Forage,
394 PutCardToLib {
396 amount: AmountSpec,
397 lib_pos: i32,
398 type_filter: String,
399 from: ZoneType,
400 same_zone: bool,
401 },
402 Enlist {
404 amount: AmountSpec,
405 type_filter: String,
406 },
407 PromiseGift,
409 RevealChosen { reveal_type: String },
411 Behold {
413 amount: AmountSpec,
414 type_filter: String,
415 exile: bool,
416 },
417 Blight(AmountSpec),
419 ExileCtrlOrGrave {
421 amount: AmountSpec,
422 type_filter: String,
423 },
424}
425
426impl CostPart {
427 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#[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!("{amount} {noun}s")
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 {v} life")),
657 _ => out.push(format!("{part:?}")),
658 }
659 }
660 out.join(", ")
661}
662
663pub 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!("{v} {{LIFE}}")),
673 _ => out.push(format!("{part:?}")),
674 }
675 }
676 out.join(", ")
677}
678
679pub 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 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 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 !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 parts.sort_by_key(|p| p.payment_order());
750
751 Cost {
752 parts,
753 has_tap,
754 mandatory,
755 }
756}
757
758pub(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
790fn 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 }
817 }
818 i += 1;
819 }
820 let token = raw[start..].trim();
822 if !token.is_empty() {
823 tokens.push(token);
824 }
825 tokens
826}
827
828pub fn matches_type_filter(game: &GameState, cid: CardId, type_filter: &str) -> bool {
831 matches_change_type(game.card(cid), type_filter, &[])
832}
833
834pub 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
844pub 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
873pub 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
888pub 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
908pub 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
922pub 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
949pub 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 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 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
1167pub 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
1187pub 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
1252pub 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
1263pub 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
1281pub 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 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 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 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 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)); 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}