1use serde::{Deserialize, Serialize};
2
3use crate::color::{Color, ColorSet};
4
5pub struct ManaAtom;
8
9impl ManaAtom {
10 pub const WHITE: u16 = 1;
11 pub const BLUE: u16 = 2;
12 pub const BLACK: u16 = 4;
13 pub const RED: u16 = 8;
14 pub const GREEN: u16 = 16;
15 pub const COLORLESS: u16 = 32;
16 pub const GENERIC: u16 = 64;
17 pub const IS_X: u16 = 256;
18 pub const OR_2_GENERIC: u16 = 512;
19 pub const OR_2_LIFE: u16 = 1024;
20 pub const IS_SNOW: u16 = 2048;
21
22 pub const ALL_MANA_COLORS: u16 =
23 Self::WHITE | Self::BLUE | Self::BLACK | Self::RED | Self::GREEN;
24 pub const ALL_MANA_TYPES: u16 = Self::ALL_MANA_COLORS | Self::COLORLESS;
25 pub const COLORS_SUPERPOSITION: u16 = Self::ALL_MANA_COLORS;
26
27 pub fn from_name(name: &str) -> u16 {
29 match name {
30 "white" | "w" => Self::WHITE,
31 "blue" | "u" => Self::BLUE,
32 "black" | "b" => Self::BLACK,
33 "red" | "r" => Self::RED,
34 "green" | "g" => Self::GREEN,
35 "colorless" | "c" => Self::COLORLESS,
36 _ => 0,
37 }
38 }
39
40 pub fn from_char(c: char) -> u16 {
41 match c.to_ascii_uppercase() {
42 'W' => Self::WHITE,
43 'U' => Self::BLUE,
44 'B' => Self::BLACK,
45 'R' => Self::RED,
46 'G' => Self::GREEN,
47 'C' => Self::COLORLESS,
48 'P' => Self::OR_2_LIFE,
49 'S' => Self::IS_SNOW,
50 'X' => Self::IS_X,
51 '2' => Self::OR_2_GENERIC,
52 c if c.is_ascii_digit() => Self::GENERIC,
53 _ => 0,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
61pub enum ManaCostShard {
62 White,
64 Blue,
65 Black,
66 Red,
67 Green,
68 Colorless,
69 WhiteBlue,
71 WhiteBlack,
72 BlueBlack,
73 BlueRed,
74 BlackRed,
75 BlackGreen,
76 RedWhite,
77 RedGreen,
78 GreenWhite,
79 GreenBlue,
80 White2,
82 Blue2,
83 Black2,
84 Red2,
85 Green2,
86 ColorlessWhite,
88 ColorlessBlue,
89 ColorlessBlack,
90 ColorlessRed,
91 ColorlessGreen,
92 Snow,
94 Generic,
96 WhitePhyrexian,
98 BluePhyrexian,
99 BlackPhyrexian,
100 RedPhyrexian,
101 GreenPhyrexian,
102 BlackGreenPhyrexian,
104 BlackRedPhyrexian,
105 GreenBluePhyrexian,
106 GreenWhitePhyrexian,
107 RedGreenPhyrexian,
108 RedWhitePhyrexian,
109 BlueBlackPhyrexian,
110 BlueRedPhyrexian,
111 WhiteBlackPhyrexian,
112 WhiteBluePhyrexian,
113 X,
115 ColoredX,
117}
118
119impl ManaCostShard {
120 pub fn shard(self) -> u16 {
121 match self {
122 Self::White => ManaAtom::WHITE,
123 Self::Blue => ManaAtom::BLUE,
124 Self::Black => ManaAtom::BLACK,
125 Self::Red => ManaAtom::RED,
126 Self::Green => ManaAtom::GREEN,
127 Self::Colorless => ManaAtom::COLORLESS,
128 Self::WhiteBlue => ManaAtom::WHITE | ManaAtom::BLUE,
129 Self::WhiteBlack => ManaAtom::WHITE | ManaAtom::BLACK,
130 Self::BlueBlack => ManaAtom::BLUE | ManaAtom::BLACK,
131 Self::BlueRed => ManaAtom::BLUE | ManaAtom::RED,
132 Self::BlackRed => ManaAtom::BLACK | ManaAtom::RED,
133 Self::BlackGreen => ManaAtom::BLACK | ManaAtom::GREEN,
134 Self::RedWhite => ManaAtom::RED | ManaAtom::WHITE,
135 Self::RedGreen => ManaAtom::RED | ManaAtom::GREEN,
136 Self::GreenWhite => ManaAtom::GREEN | ManaAtom::WHITE,
137 Self::GreenBlue => ManaAtom::GREEN | ManaAtom::BLUE,
138 Self::White2 => ManaAtom::WHITE | ManaAtom::OR_2_GENERIC,
139 Self::Blue2 => ManaAtom::BLUE | ManaAtom::OR_2_GENERIC,
140 Self::Black2 => ManaAtom::BLACK | ManaAtom::OR_2_GENERIC,
141 Self::Red2 => ManaAtom::RED | ManaAtom::OR_2_GENERIC,
142 Self::Green2 => ManaAtom::GREEN | ManaAtom::OR_2_GENERIC,
143 Self::ColorlessWhite => ManaAtom::WHITE | ManaAtom::COLORLESS,
144 Self::ColorlessBlue => ManaAtom::BLUE | ManaAtom::COLORLESS,
145 Self::ColorlessBlack => ManaAtom::BLACK | ManaAtom::COLORLESS,
146 Self::ColorlessRed => ManaAtom::RED | ManaAtom::COLORLESS,
147 Self::ColorlessGreen => ManaAtom::GREEN | ManaAtom::COLORLESS,
148 Self::Snow => ManaAtom::IS_SNOW,
149 Self::Generic => ManaAtom::GENERIC,
150 Self::WhitePhyrexian => ManaAtom::WHITE | ManaAtom::OR_2_LIFE,
151 Self::BluePhyrexian => ManaAtom::BLUE | ManaAtom::OR_2_LIFE,
152 Self::BlackPhyrexian => ManaAtom::BLACK | ManaAtom::OR_2_LIFE,
153 Self::RedPhyrexian => ManaAtom::RED | ManaAtom::OR_2_LIFE,
154 Self::GreenPhyrexian => ManaAtom::GREEN | ManaAtom::OR_2_LIFE,
155 Self::BlackGreenPhyrexian => ManaAtom::BLACK | ManaAtom::GREEN | ManaAtom::OR_2_LIFE,
156 Self::BlackRedPhyrexian => ManaAtom::BLACK | ManaAtom::RED | ManaAtom::OR_2_LIFE,
157 Self::GreenBluePhyrexian => ManaAtom::GREEN | ManaAtom::BLUE | ManaAtom::OR_2_LIFE,
158 Self::GreenWhitePhyrexian => ManaAtom::GREEN | ManaAtom::WHITE | ManaAtom::OR_2_LIFE,
159 Self::RedGreenPhyrexian => ManaAtom::RED | ManaAtom::GREEN | ManaAtom::OR_2_LIFE,
160 Self::RedWhitePhyrexian => ManaAtom::RED | ManaAtom::WHITE | ManaAtom::OR_2_LIFE,
161 Self::BlueBlackPhyrexian => ManaAtom::BLUE | ManaAtom::BLACK | ManaAtom::OR_2_LIFE,
162 Self::BlueRedPhyrexian => ManaAtom::BLUE | ManaAtom::RED | ManaAtom::OR_2_LIFE,
163 Self::WhiteBlackPhyrexian => ManaAtom::WHITE | ManaAtom::BLACK | ManaAtom::OR_2_LIFE,
164 Self::WhiteBluePhyrexian => ManaAtom::WHITE | ManaAtom::BLUE | ManaAtom::OR_2_LIFE,
165 Self::X => ManaAtom::IS_X,
166 Self::ColoredX => {
167 ManaAtom::WHITE
168 | ManaAtom::BLUE
169 | ManaAtom::BLACK
170 | ManaAtom::RED
171 | ManaAtom::GREEN
172 | ManaAtom::IS_X
173 }
174 }
175 }
176
177 pub fn short_string(self) -> &'static str {
178 match self {
179 Self::White => "W",
180 Self::Blue => "U",
181 Self::Black => "B",
182 Self::Red => "R",
183 Self::Green => "G",
184 Self::Colorless => "C",
185 Self::WhiteBlue => "W/U",
186 Self::WhiteBlack => "W/B",
187 Self::BlueBlack => "U/B",
188 Self::BlueRed => "U/R",
189 Self::BlackRed => "B/R",
190 Self::BlackGreen => "B/G",
191 Self::RedWhite => "R/W",
192 Self::RedGreen => "R/G",
193 Self::GreenWhite => "G/W",
194 Self::GreenBlue => "G/U",
195 Self::White2 => "2/W",
196 Self::Blue2 => "2/U",
197 Self::Black2 => "2/B",
198 Self::Red2 => "2/R",
199 Self::Green2 => "2/G",
200 Self::ColorlessWhite => "C/W",
201 Self::ColorlessBlue => "C/U",
202 Self::ColorlessBlack => "C/B",
203 Self::ColorlessRed => "C/R",
204 Self::ColorlessGreen => "C/G",
205 Self::Snow => "S",
206 Self::Generic => "1",
207 Self::WhitePhyrexian => "W/P",
208 Self::BluePhyrexian => "U/P",
209 Self::BlackPhyrexian => "B/P",
210 Self::RedPhyrexian => "R/P",
211 Self::GreenPhyrexian => "G/P",
212 Self::BlackGreenPhyrexian => "B/G/P",
213 Self::BlackRedPhyrexian => "B/R/P",
214 Self::GreenBluePhyrexian => "G/U/P",
215 Self::GreenWhitePhyrexian => "G/W/P",
216 Self::RedGreenPhyrexian => "R/G/P",
217 Self::RedWhitePhyrexian => "R/W/P",
218 Self::BlueBlackPhyrexian => "U/B/P",
219 Self::BlueRedPhyrexian => "U/R/P",
220 Self::WhiteBlackPhyrexian => "W/B/P",
221 Self::WhiteBluePhyrexian => "W/U/P",
222 Self::X => "X",
223 Self::ColoredX => "1",
224 }
225 }
226
227 pub fn cmc(self) -> i32 {
228 let s = self.shard();
229 if (s & ManaAtom::IS_X) != 0 {
230 return 0;
231 }
232 if (s & ManaAtom::OR_2_GENERIC) != 0 {
233 return 2;
234 }
235 1
236 }
237
238 pub fn color_mask(self) -> u8 {
239 (self.shard() & ManaAtom::COLORS_SUPERPOSITION) as u8
240 }
241
242 pub fn color(self) -> ColorSet {
243 ColorSet::from_mask(self.color_mask())
244 }
245
246 pub fn is_phyrexian(self) -> bool {
247 (self.shard() & ManaAtom::OR_2_LIFE) != 0
248 }
249
250 pub fn to_non_phyrexian(self) -> ManaCostShard {
253 match self {
254 Self::WhitePhyrexian => Self::White,
255 Self::BluePhyrexian => Self::Blue,
256 Self::BlackPhyrexian => Self::Black,
257 Self::RedPhyrexian => Self::Red,
258 Self::GreenPhyrexian => Self::Green,
259 Self::BlackGreenPhyrexian => Self::BlackGreen,
260 Self::BlackRedPhyrexian => Self::BlackRed,
261 Self::GreenBluePhyrexian => Self::GreenBlue,
262 Self::GreenWhitePhyrexian => Self::GreenWhite,
263 Self::RedGreenPhyrexian => Self::RedGreen,
264 Self::RedWhitePhyrexian => Self::RedWhite,
265 Self::BlueBlackPhyrexian => Self::BlueBlack,
266 Self::BlueRedPhyrexian => Self::BlueRed,
267 Self::WhiteBlackPhyrexian => Self::WhiteBlack,
268 Self::WhiteBluePhyrexian => Self::WhiteBlue,
269 other => other,
270 }
271 }
272
273 pub fn is_snow(self) -> bool {
274 (self.shard() & ManaAtom::IS_SNOW) != 0
275 }
276
277 pub fn is_x(self) -> bool {
278 (self.shard() & ManaAtom::IS_X) != 0
279 }
280
281 pub fn is_generic(self) -> bool {
282 let s = self.shard();
283 (s & ManaAtom::GENERIC) != 0
284 || (s & ManaAtom::IS_X) != 0
285 || self.is_snow()
286 || self.is_or_2_generic()
287 }
288
289 pub fn is_or_2_generic(self) -> bool {
290 (self.shard() & ManaAtom::OR_2_GENERIC) != 0
291 }
292
293 pub fn is_colorless(self) -> bool {
294 (self.shard() & ManaAtom::COLORLESS) != 0
295 }
296
297 pub fn is_mono_color(self) -> bool {
298 (self.shard() & ManaAtom::COLORS_SUPERPOSITION).count_ones() == 1
299 }
300
301 pub fn is_multi_color(self) -> bool {
302 (self.shard() & ManaAtom::COLORS_SUPERPOSITION).count_ones() == 2
303 }
304
305 pub fn parse_non_generic(s: &str) -> Option<ManaCostShard> {
308 let mut atoms: u16 = 0;
309 for c in s.chars() {
310 atoms |= ManaAtom::from_char(c);
311 }
312 if atoms == ManaAtom::OR_2_GENERIC || atoms == (ManaAtom::OR_2_GENERIC | ManaAtom::GENERIC)
314 {
315 atoms = ManaAtom::GENERIC;
316 }
317 Self::from_atoms(atoms)
318 }
319
320 pub fn from_atoms(atoms: u16) -> Option<ManaCostShard> {
322 if atoms == 0 {
323 return Some(ManaCostShard::Generic);
324 }
325 const ALL: &[ManaCostShard] = &[
327 ManaCostShard::White,
328 ManaCostShard::Blue,
329 ManaCostShard::Black,
330 ManaCostShard::Red,
331 ManaCostShard::Green,
332 ManaCostShard::Colorless,
333 ManaCostShard::WhiteBlue,
334 ManaCostShard::WhiteBlack,
335 ManaCostShard::BlueBlack,
336 ManaCostShard::BlueRed,
337 ManaCostShard::BlackRed,
338 ManaCostShard::BlackGreen,
339 ManaCostShard::RedWhite,
340 ManaCostShard::RedGreen,
341 ManaCostShard::GreenWhite,
342 ManaCostShard::GreenBlue,
343 ManaCostShard::White2,
344 ManaCostShard::Blue2,
345 ManaCostShard::Black2,
346 ManaCostShard::Red2,
347 ManaCostShard::Green2,
348 ManaCostShard::ColorlessWhite,
349 ManaCostShard::ColorlessBlue,
350 ManaCostShard::ColorlessBlack,
351 ManaCostShard::ColorlessRed,
352 ManaCostShard::ColorlessGreen,
353 ManaCostShard::Snow,
354 ManaCostShard::Generic,
355 ManaCostShard::WhitePhyrexian,
356 ManaCostShard::BluePhyrexian,
357 ManaCostShard::BlackPhyrexian,
358 ManaCostShard::RedPhyrexian,
359 ManaCostShard::GreenPhyrexian,
360 ManaCostShard::BlackGreenPhyrexian,
361 ManaCostShard::BlackRedPhyrexian,
362 ManaCostShard::GreenBluePhyrexian,
363 ManaCostShard::GreenWhitePhyrexian,
364 ManaCostShard::RedGreenPhyrexian,
365 ManaCostShard::RedWhitePhyrexian,
366 ManaCostShard::BlueBlackPhyrexian,
367 ManaCostShard::BlueRedPhyrexian,
368 ManaCostShard::WhiteBlackPhyrexian,
369 ManaCostShard::WhiteBluePhyrexian,
370 ManaCostShard::X,
371 ManaCostShard::ColoredX,
372 ];
373 ALL.iter().find(|&&shard| shard.shard() == atoms).copied()
374 }
375}
376
377impl std::fmt::Display for ManaCostShard {
378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379 write!(f, "{{{}}}", self.short_string())
380 }
381}
382
383#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
386pub struct ManaCost {
387 shards: Vec<ManaCostShard>,
388 generic_cost: i32,
389 has_no_cost: bool,
390}
391
392impl ManaCost {
393 pub fn no_cost() -> Self {
395 ManaCost {
396 shards: Vec::new(),
397 generic_cost: 0,
398 has_no_cost: true,
399 }
400 }
401
402 pub fn zero() -> Self {
404 ManaCost {
405 shards: Vec::new(),
406 generic_cost: 0,
407 has_no_cost: false,
408 }
409 }
410
411 pub fn generic(n: i32) -> Self {
413 if n < 0 {
414 return Self::no_cost();
415 }
416 ManaCost {
417 shards: Vec::new(),
418 generic_cost: n,
419 has_no_cost: false,
420 }
421 }
422
423 pub fn parse(s: &str) -> Self {
426 if s.is_empty() || s == "no cost" {
427 return Self::no_cost();
428 }
429
430 let mut shards = Vec::new();
431 let mut generic_cost: i32 = 0;
432 let mut has_x = false;
433
434 for token in s.split_whitespace() {
435 if let Ok(n) = token.parse::<i32>() {
437 generic_cost += n;
438 } else {
439 if token.contains('/') {
443 if let Some(shard) = ManaCostShard::parse_non_generic(token) {
444 if shard != ManaCostShard::Generic {
445 if shard == ManaCostShard::X {
446 has_x = true;
447 }
448 shards.push(shard);
449 }
450 }
452 } else {
453 let whole = ManaCostShard::parse_non_generic(token);
457 if let Some(shard) = whole {
458 if shard.is_multi_color() || shard.is_phyrexian() {
459 shards.push(shard);
461 } else {
462 for c in token.chars() {
465 let sym = c.to_ascii_uppercase().to_string();
466 if let Some(shard) = ManaCostShard::parse_non_generic(&sym) {
467 if shard != ManaCostShard::Generic {
468 if shard == ManaCostShard::X {
469 has_x = true;
470 }
471 shards.push(shard);
472 }
473 }
474 }
475 }
476 } else {
477 for c in token.chars() {
478 let sym = c.to_ascii_uppercase().to_string();
479 if let Some(shard) = ManaCostShard::parse_non_generic(&sym) {
480 if shard != ManaCostShard::Generic {
481 if shard == ManaCostShard::X {
482 has_x = true;
483 }
484 shards.push(shard);
485 }
486 }
487 }
488 }
489 }
490 }
491 }
492
493 let has_no_cost = !has_x && generic_cost < 0;
494 if has_no_cost {
495 generic_cost = 0;
496 }
497
498 ManaCost {
499 shards,
500 generic_cost,
501 has_no_cost,
502 }
503 }
504
505 pub fn cmc(&self) -> i32 {
506 let shard_total: i32 = self.shards.iter().map(|s| s.cmc()).sum();
507 shard_total + self.generic_cost
508 }
509
510 pub fn color_profile(&self) -> u8 {
511 let mut result: u8 = 0;
512 for s in &self.shards {
513 result |= s.color_mask();
514 }
515 result
516 }
517
518 pub fn color_set(&self) -> ColorSet {
519 ColorSet::from_mask(self.color_profile())
520 }
521
522 pub fn generic_cost(&self) -> i32 {
523 self.generic_cost
524 }
525
526 pub fn shards(&self) -> &[ManaCostShard] {
527 &self.shards
528 }
529
530 pub fn is_no_cost(&self) -> bool {
531 self.has_no_cost
532 }
533
534 pub fn is_zero(&self) -> bool {
535 self.generic_cost == 0 && self.shards.is_empty() && !self.has_no_cost
536 }
537
538 pub fn is_pure_generic(&self) -> bool {
539 self.shards.is_empty() && !self.has_no_cost
540 }
541
542 pub fn count_x(&self) -> usize {
543 self.shards
544 .iter()
545 .filter(|s| **s == ManaCostShard::X)
546 .count()
547 }
548
549 pub fn has_phyrexian(&self) -> bool {
550 self.shards.iter().any(|s| s.is_phyrexian())
551 }
552
553 pub fn from_parts(shards: Vec<ManaCostShard>, generic_cost: i32) -> ManaCost {
555 ManaCost {
556 shards,
557 generic_cost,
558 has_no_cost: false,
559 }
560 }
561
562 pub fn without_x(&self) -> ManaCost {
565 ManaCost {
566 shards: self.shards.iter().filter(|s| !s.is_x()).copied().collect(),
567 generic_cost: self.generic_cost,
568 has_no_cost: self.has_no_cost,
569 }
570 }
571
572 pub fn without_phyrexian(&self) -> ManaCost {
575 ManaCost {
576 shards: self
577 .shards
578 .iter()
579 .filter(|s| !s.is_phyrexian())
580 .copied()
581 .collect(),
582 generic_cost: self.generic_cost,
583 has_no_cost: self.has_no_cost,
584 }
585 }
586
587 pub fn phyrexian_to_colored(&self) -> ManaCost {
590 ManaCost {
591 shards: self
592 .shards
593 .iter()
594 .map(|s| {
595 if s.is_phyrexian() {
596 s.to_non_phyrexian()
597 } else {
598 *s
599 }
600 })
601 .collect(),
602 generic_cost: self.generic_cost,
603 has_no_cost: self.has_no_cost,
604 }
605 }
606
607 pub fn colored_to_phyrexian(&self, color_mask: u8) -> ManaCost {
610 let shards = self
611 .shards
612 .iter()
613 .map(|s| match (*s, color_mask) {
614 (ManaCostShard::White, m) if m == ManaAtom::WHITE as u8 => {
615 ManaCostShard::WhitePhyrexian
616 }
617 (ManaCostShard::Blue, m) if m == ManaAtom::BLUE as u8 => {
618 ManaCostShard::BluePhyrexian
619 }
620 (ManaCostShard::Black, m) if m == ManaAtom::BLACK as u8 => {
621 ManaCostShard::BlackPhyrexian
622 }
623 (ManaCostShard::Red, m) if m == ManaAtom::RED as u8 => ManaCostShard::RedPhyrexian,
624 (ManaCostShard::Green, m) if m == ManaAtom::GREEN as u8 => {
625 ManaCostShard::GreenPhyrexian
626 }
627 (other, _) => other,
628 })
629 .collect();
630 ManaCost {
631 shards,
632 generic_cost: self.generic_cost,
633 has_no_cost: self.has_no_cost,
634 }
635 }
636
637 pub fn shard_count(&self, which: ManaCostShard) -> usize {
638 if which == ManaCostShard::Generic {
639 return self.generic_cost as usize;
640 }
641 self.shards.iter().filter(|s| **s == which).count()
642 }
643
644 pub fn add(&self, other: &ManaCost) -> ManaCost {
646 Self::combine(self, other)
647 }
648
649 pub fn reduce_generic(&self, amount: i32) -> ManaCost {
652 ManaCost {
653 shards: self.shards.clone(),
654 generic_cost: (self.generic_cost - amount).max(0),
655 has_no_cost: self.has_no_cost,
656 }
657 }
658
659 pub fn with_generic(&self, generic: i32) -> ManaCost {
661 ManaCost {
662 shards: self.shards.clone(),
663 generic_cost: generic.max(0),
664 has_no_cost: self.has_no_cost,
665 }
666 }
667
668 pub fn has_color_shard(&self, atom: u16) -> bool {
670 self.shards.iter().any(|s| {
671 s.is_mono_color()
672 && !s.is_phyrexian()
673 && (s.shard() & ManaAtom::COLORS_SUPERPOSITION) == atom
674 })
675 }
676
677 pub fn remove_color_shard(&self, atom: u16) -> ManaCost {
679 let mut shards = self.shards.clone();
680 if let Some(pos) = shards.iter().position(|s| {
681 s.is_mono_color()
682 && !s.is_phyrexian()
683 && (s.shard() & ManaAtom::COLORS_SUPERPOSITION) == atom
684 }) {
685 shards.remove(pos);
686 }
687 ManaCost {
688 shards,
689 generic_cost: self.generic_cost,
690 has_no_cost: self.has_no_cost,
691 }
692 }
693
694 pub fn reduce_color(&self, color: Color, count: i32, ignore_generic: bool) -> ManaCost {
699 let mut shards = self.shards.clone();
700 let mut remaining = count;
701 let color_mask = color.mask() as u16;
702
703 let mut i = 0;
705 while i < shards.len() && remaining > 0 {
706 let shard_colors = shards[i].shard() & ManaAtom::COLORS_SUPERPOSITION;
707 if shard_colors == color_mask && shards[i].is_mono_color() && !shards[i].is_phyrexian()
709 {
710 shards.remove(i);
711 remaining -= 1;
712 } else {
713 i += 1;
714 }
715 }
716
717 let generic_cost = if !ignore_generic && remaining > 0 {
718 (self.generic_cost - remaining).max(0)
719 } else {
720 self.generic_cost
721 };
722
723 ManaCost {
724 shards,
725 generic_cost,
726 has_no_cost: self.has_no_cost,
727 }
728 }
729
730 pub fn combine(a: &ManaCost, b: &ManaCost) -> ManaCost {
731 let mut shards = a.shards.clone();
732 shards.extend_from_slice(&b.shards);
733 ManaCost {
734 shards,
735 generic_cost: a.generic_cost + b.generic_cost,
736 has_no_cost: false,
737 }
738 }
739}
740
741impl std::fmt::Debug for ManaCost {
742 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
743 write!(f, "ManaCost({self})")
744 }
745}
746
747impl std::fmt::Display for ManaCost {
748 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
749 if self.has_no_cost {
750 return write!(f, "no cost");
751 }
752 for s in &self.shards {
754 if *s == ManaCostShard::X {
755 write!(f, "{s}")?;
756 }
757 }
758 if self.generic_cost > 0 || (self.generic_cost == 0 && self.shards.is_empty()) {
759 write!(f, "{{{}}}", self.generic_cost)?;
760 }
761 for s in &self.shards {
762 if *s != ManaCostShard::X {
763 write!(f, "{s}")?;
764 }
765 }
766 if self.generic_cost < 0 {
767 write!(f, " {}", self.generic_cost)?;
768 }
769 Ok(())
770 }
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn parse_simple_costs() {
779 let cost = ManaCost::parse("2 W U");
780 assert_eq!(cost.cmc(), 4);
781 assert_eq!(cost.generic_cost(), 2);
782 assert_eq!(cost.shards().len(), 2);
783 assert!(cost.color_set().has_white());
784 assert!(cost.color_set().has_blue());
785 }
786
787 #[test]
788 fn parse_zero() {
789 let cost = ManaCost::parse("0");
790 assert!(cost.is_zero());
791 assert_eq!(cost.cmc(), 0);
792 assert_eq!(format!("{}", cost), "{0}");
793 }
794
795 #[test]
796 fn parse_x_cost() {
797 let cost = ManaCost::parse("X R");
798 assert_eq!(cost.count_x(), 1);
799 assert_eq!(cost.cmc(), 1); }
801
802 #[test]
803 fn parse_hybrid() {
804 let cost = ManaCost::parse("1 W/U");
805 assert_eq!(cost.cmc(), 2);
806 assert_eq!(cost.shards().len(), 1);
807 assert!(cost.color_set().has_white());
808 assert!(cost.color_set().has_blue());
809 }
810
811 #[test]
812 fn parse_adjacent_multicolor_as_hybrid() {
813 let cost = ManaCost::parse("2 BR");
816 assert_eq!(cost.generic_cost(), 2);
817 assert_eq!(cost.shards(), &[ManaCostShard::BlackRed]);
818 assert_eq!(cost.cmc(), 3); }
820
821 #[test]
822 fn parse_separate_pips_not_hybrid() {
823 let cost = ManaCost::parse("2 B R");
825 assert_eq!(cost.generic_cost(), 2);
826 assert_eq!(cost.shards(), &[ManaCostShard::Black, ManaCostShard::Red]);
827 assert_eq!(cost.cmc(), 4); }
829
830 #[test]
831 fn parse_repeated_same_color_not_hybrid() {
832 let cost = ManaCost::parse("WW");
834 assert_eq!(cost.shards(), &[ManaCostShard::White, ManaCostShard::White]);
835 assert_eq!(cost.cmc(), 2);
836 }
837
838 #[test]
839 fn parse_phyrexian() {
840 let cost = ManaCost::parse("W/P");
842 assert!(cost.has_phyrexian());
843 assert_eq!(cost.cmc(), 1);
844 let cost2 = ManaCost::parse("GP");
846 assert!(cost2.has_phyrexian());
847 assert_eq!(cost2.cmc(), 1);
848 assert_eq!(cost2.shards().len(), 1);
849 let cost3 = ManaCost::parse("1 BP BP");
851 assert!(cost3.has_phyrexian());
852 assert_eq!(cost3.shards().len(), 2);
853 assert_eq!(cost3.generic_cost(), 1);
854 }
855
856 #[test]
857 fn no_cost() {
858 let cost = ManaCost::no_cost();
859 assert!(cost.is_no_cost());
860 assert_eq!(format!("{}", cost), "no cost");
861 }
862
863 #[test]
864 fn shard_parse_non_generic() {
865 assert_eq!(
866 ManaCostShard::parse_non_generic("W"),
867 Some(ManaCostShard::White)
868 );
869 assert_eq!(
870 ManaCostShard::parse_non_generic("W/U"),
871 Some(ManaCostShard::WhiteBlue)
872 );
873 assert_eq!(
874 ManaCostShard::parse_non_generic("2/W"),
875 Some(ManaCostShard::White2)
876 );
877 assert_eq!(
878 ManaCostShard::parse_non_generic("W/P"),
879 Some(ManaCostShard::WhitePhyrexian)
880 );
881 }
882
883 #[test]
884 fn shard_cmc() {
885 assert_eq!(ManaCostShard::White.cmc(), 1);
886 assert_eq!(ManaCostShard::White2.cmc(), 2);
887 assert_eq!(ManaCostShard::X.cmc(), 0);
888 }
889
890 #[test]
891 fn display_cost() {
892 let cost = ManaCost::parse("3 R R");
893 assert_eq!(format!("{}", cost), "{3}{R}{R}");
894 }
895}