Skip to main content

forge_foundation/
color.rs

1use serde::{Deserialize, Serialize};
2
3/// Individual MTG colors, matching Java MagicColor byte constants.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5#[repr(u8)]
6pub enum Color {
7    White = 1,
8    Blue = 2,
9    Black = 4,
10    Red = 8,
11    Green = 16,
12}
13
14impl Color {
15    pub const ALL: [Color; 5] = [
16        Color::White,
17        Color::Blue,
18        Color::Black,
19        Color::Red,
20        Color::Green,
21    ];
22
23    pub fn short_name(self) -> &'static str {
24        match self {
25            Color::White => "W",
26            Color::Blue => "U",
27            Color::Black => "B",
28            Color::Red => "R",
29            Color::Green => "G",
30        }
31    }
32
33    pub fn long_name(self) -> &'static str {
34        match self {
35            Color::White => "white",
36            Color::Blue => "blue",
37            Color::Black => "black",
38            Color::Red => "red",
39            Color::Green => "green",
40        }
41    }
42
43    pub fn symbol(self) -> &'static str {
44        match self {
45            Color::White => "{W}",
46            Color::Blue => "{U}",
47            Color::Black => "{B}",
48            Color::Red => "{R}",
49            Color::Green => "{G}",
50        }
51    }
52
53    pub fn basic_land_type(self) -> &'static str {
54        match self {
55            Color::White => "Plains",
56            Color::Blue => "Island",
57            Color::Black => "Swamp",
58            Color::Red => "Mountain",
59            Color::Green => "Forest",
60        }
61    }
62
63    pub fn mask(self) -> u8 {
64        self as u8
65    }
66
67    pub fn from_char(c: char) -> Option<Color> {
68        match c.to_ascii_uppercase() {
69            'W' => Some(Color::White),
70            'U' => Some(Color::Blue),
71            'B' => Some(Color::Black),
72            'R' => Some(Color::Red),
73            'G' => Some(Color::Green),
74            _ => None,
75        }
76    }
77
78    pub fn from_name(s: &str) -> Option<Color> {
79        if s.len() == 1 {
80            return Color::from_char(s.as_bytes()[0] as char);
81        }
82        match s.to_ascii_lowercase().as_str() {
83            "white" => Some(Color::White),
84            "blue" => Some(Color::Blue),
85            "black" => Some(Color::Black),
86            "red" => Some(Color::Red),
87            "green" => Some(Color::Green),
88            _ => None,
89        }
90    }
91}
92
93/// A set of 0-5 colors, stored as a u8 bitmask. All 32 combinations are valid.
94/// Mirrors Java `ColorSet` — the ordinal IS the bitmask.
95#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
96pub struct ColorSet(u8);
97
98impl ColorSet {
99    pub const COLORLESS: ColorSet = ColorSet(0);
100    pub const WHITE: ColorSet = ColorSet(1);
101    pub const BLUE: ColorSet = ColorSet(2);
102    pub const BLACK: ColorSet = ColorSet(4);
103    pub const RED: ColorSet = ColorSet(8);
104    pub const GREEN: ColorSet = ColorSet(16);
105    pub const ALL_COLORS: ColorSet = ColorSet(0b11111);
106
107    pub fn from_mask(mask: u8) -> Self {
108        ColorSet(mask & 0b11111)
109    }
110
111    pub fn mask(self) -> u8 {
112        self.0
113    }
114
115    pub fn is_colorless(self) -> bool {
116        self.0 == 0
117    }
118
119    pub fn is_multicolor(self) -> bool {
120        self.count_colors() > 1
121    }
122
123    pub fn is_mono_color(self) -> bool {
124        self.count_colors() == 1
125    }
126
127    pub fn count_colors(self) -> u32 {
128        self.0.count_ones()
129    }
130
131    pub fn has_color(self, color: Color) -> bool {
132        (self.0 & color.mask()) != 0
133    }
134
135    pub fn has_white(self) -> bool {
136        self.has_color(Color::White)
137    }
138    pub fn has_blue(self) -> bool {
139        self.has_color(Color::Blue)
140    }
141    pub fn has_black(self) -> bool {
142        self.has_color(Color::Black)
143    }
144    pub fn has_red(self) -> bool {
145        self.has_color(Color::Red)
146    }
147    pub fn has_green(self) -> bool {
148        self.has_color(Color::Green)
149    }
150
151    pub fn has_any_color(self, mask: u8) -> bool {
152        (self.0 & mask) != 0
153    }
154
155    pub fn has_all_colors(self, mask: u8) -> bool {
156        (self.0 & mask) == mask
157    }
158
159    pub fn contains_all_colors_from(self, other: ColorSet) -> bool {
160        (!self.0 & other.0) == 0
161    }
162
163    pub fn shares_color_with(self, other: ColorSet) -> bool {
164        (self.0 & other.0) != 0
165    }
166
167    pub fn union(self, other: ColorSet) -> ColorSet {
168        ColorSet(self.0 | other.0)
169    }
170
171    pub fn intersection(self, other: ColorSet) -> ColorSet {
172        ColorSet(self.0 & other.0)
173    }
174
175    pub fn inverse(self) -> ColorSet {
176        ColorSet(self.0 ^ 0b11111)
177    }
178
179    pub fn iter(self) -> impl Iterator<Item = Color> {
180        Color::ALL.into_iter().filter(move |c| self.has_color(*c))
181    }
182
183    pub fn from_names(s: &str) -> Self {
184        let mut mask = 0u8;
185        for c in s.chars() {
186            if let Some(color) = Color::from_char(c) {
187                mask |= color.mask();
188            }
189        }
190        if mask == 0 {
191            // Try full name
192            if let Some(color) = Color::from_name(s) {
193                mask = color.mask();
194            }
195        }
196        ColorSet(mask & 0b11111)
197    }
198}
199
200impl std::fmt::Debug for ColorSet {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        if self.is_colorless() {
203            return write!(f, "ColorSet(C)");
204        }
205        write!(f, "ColorSet(")?;
206        for c in self.iter() {
207            write!(f, "{}", c.short_name())?;
208        }
209        write!(f, ")")
210    }
211}
212
213impl std::fmt::Display for ColorSet {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        if self.is_colorless() {
216            return write!(f, "C");
217        }
218        for c in self.iter() {
219            write!(f, "{}", c.short_name())?;
220        }
221        Ok(())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn color_masks() {
231        assert_eq!(Color::White.mask(), 1);
232        assert_eq!(Color::Blue.mask(), 2);
233        assert_eq!(Color::Black.mask(), 4);
234        assert_eq!(Color::Red.mask(), 8);
235        assert_eq!(Color::Green.mask(), 16);
236    }
237
238    #[test]
239    fn color_from_char() {
240        assert_eq!(Color::from_char('W'), Some(Color::White));
241        assert_eq!(Color::from_char('u'), Some(Color::Blue));
242        assert_eq!(Color::from_char('X'), None);
243    }
244
245    #[test]
246    fn colorset_basics() {
247        let cs = ColorSet::from_mask(Color::White.mask() | Color::Blue.mask());
248        assert!(cs.has_white());
249        assert!(cs.has_blue());
250        assert!(!cs.has_black());
251        assert!(cs.is_multicolor());
252        assert_eq!(cs.count_colors(), 2);
253    }
254
255    #[test]
256    fn colorset_all_32_valid() {
257        for i in 0..32u8 {
258            let cs = ColorSet::from_mask(i);
259            assert_eq!(cs.mask(), i);
260        }
261    }
262
263    #[test]
264    fn colorset_inverse() {
265        let wu = ColorSet::from_mask(Color::White.mask() | Color::Blue.mask());
266        let brg = wu.inverse();
267        assert!(brg.has_black());
268        assert!(brg.has_red());
269        assert!(brg.has_green());
270        assert!(!brg.has_white());
271        assert!(!brg.has_blue());
272    }
273}