1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5pub enum PhaseType {
6 Untap,
7 Upkeep,
8 Draw,
9 Main1,
10 CombatBegin,
11 CombatDeclareAttackers,
12 CombatDeclareBlockers,
13 CombatFirstStrikeDamage,
14 CombatDamage,
15 CombatEnd,
16 Main2,
17 EndOfTurn,
18 Cleanup,
19}
20
21impl PhaseType {
22 pub const TURN_ORDER: [PhaseType; 13] = [
24 PhaseType::Untap,
25 PhaseType::Upkeep,
26 PhaseType::Draw,
27 PhaseType::Main1,
28 PhaseType::CombatBegin,
29 PhaseType::CombatDeclareAttackers,
30 PhaseType::CombatDeclareBlockers,
31 PhaseType::CombatFirstStrikeDamage,
32 PhaseType::CombatDamage,
33 PhaseType::CombatEnd,
34 PhaseType::Main2,
35 PhaseType::EndOfTurn,
36 PhaseType::Cleanup,
37 ];
38
39 pub const BEGINNING_PHASE: [PhaseType; 3] =
41 [PhaseType::Untap, PhaseType::Upkeep, PhaseType::Draw];
42
43 pub const COMBAT_PHASE: [PhaseType; 6] = [
44 PhaseType::CombatBegin,
45 PhaseType::CombatDeclareAttackers,
46 PhaseType::CombatDeclareBlockers,
47 PhaseType::CombatFirstStrikeDamage,
48 PhaseType::CombatDamage,
49 PhaseType::CombatEnd,
50 ];
51
52 pub fn is_main(self) -> bool {
53 matches!(self, PhaseType::Main1 | PhaseType::Main2)
54 }
55
56 pub fn is_combat(self) -> bool {
57 matches!(
58 self,
59 PhaseType::CombatBegin
60 | PhaseType::CombatDeclareAttackers
61 | PhaseType::CombatDeclareBlockers
62 | PhaseType::CombatFirstStrikeDamage
63 | PhaseType::CombatDamage
64 | PhaseType::CombatEnd
65 )
66 }
67
68 pub fn index(self) -> usize {
70 Self::TURN_ORDER.iter().position(|&p| p == self).unwrap()
71 }
72
73 pub fn next(self) -> PhaseType {
75 let idx = self.index();
76 Self::TURN_ORDER[(idx + 1) % Self::TURN_ORDER.len()]
77 }
78
79 pub fn is_before(self, other: PhaseType) -> bool {
80 self.index() < other.index()
81 }
82
83 pub fn is_after(self, other: PhaseType) -> bool {
84 self.index() > other.index()
85 }
86
87 pub fn from_step_string(s: &str) -> Option<Self> {
89 match s {
90 "untap" => Some(PhaseType::Untap),
91 "upkeep" => Some(PhaseType::Upkeep),
92 "draw" => Some(PhaseType::Draw),
93 "main1" => Some(PhaseType::Main1),
94 "begin_combat" => Some(PhaseType::CombatBegin),
95 "declare_attackers" => Some(PhaseType::CombatDeclareAttackers),
96 "declare_blockers" => Some(PhaseType::CombatDeclareBlockers),
97 "first_strike_damage" => Some(PhaseType::CombatFirstStrikeDamage),
98 "combat_damage" => Some(PhaseType::CombatDamage),
99 "end_combat" => Some(PhaseType::CombatEnd),
100 "main2" => Some(PhaseType::Main2),
101 "end" => Some(PhaseType::EndOfTurn),
102 "cleanup" => Some(PhaseType::Cleanup),
103 _ => None,
104 }
105 }
106
107 pub fn script_name(self) -> &'static str {
109 match self {
110 PhaseType::Untap => "Untap",
111 PhaseType::Upkeep => "Upkeep",
112 PhaseType::Draw => "Draw",
113 PhaseType::Main1 => "Main1",
114 PhaseType::CombatBegin => "BeginCombat",
115 PhaseType::CombatDeclareAttackers => "Declare Attackers",
116 PhaseType::CombatDeclareBlockers => "Declare Blockers",
117 PhaseType::CombatFirstStrikeDamage => "First Strike Damage",
118 PhaseType::CombatDamage => "Combat Damage",
119 PhaseType::CombatEnd => "EndCombat",
120 PhaseType::Main2 => "Main2",
121 PhaseType::EndOfTurn => "End of Turn",
122 PhaseType::Cleanup => "Cleanup",
123 }
124 }
125
126 pub fn from_script_name(s: &str) -> Option<Self> {
127 let s = s.trim();
128 if s.eq_ignore_ascii_case("EndStep") || s.eq_ignore_ascii_case("EndOfTurnStep") {
129 return Some(PhaseType::EndOfTurn);
130 }
131 for &phase in &Self::TURN_ORDER {
132 if phase.script_name().eq_ignore_ascii_case(s)
133 || format!("{:?}", phase).eq_ignore_ascii_case(s)
134 {
135 return Some(phase);
136 }
137 }
138 if s.eq_ignore_ascii_case("Main") {
140 return Some(PhaseType::Main1);
141 }
142 None
143 }
144
145 pub fn parse_range(values: &str) -> Vec<Self> {
146 let mut result: Vec<Self> = Vec::new();
147 let mut push = |phase: PhaseType, result: &mut Vec<Self>| {
148 if !result.contains(&phase) {
149 result.push(phase);
150 }
151 };
152 for s in values.split(',') {
153 let s = s.trim();
154 if let Some(idx) = s.find("->") {
155 let from = Self::from_script_name(&s[..idx]);
156 let to_str = s[idx + 2..].trim();
157 let to = if to_str.is_empty() {
158 Some(PhaseType::Cleanup)
159 } else {
160 Self::from_script_name(to_str)
161 };
162 if let (Some(from), Some(to)) = (from, to) {
163 let mut in_range = false;
164 for &phase in &Self::TURN_ORDER {
165 if phase == from {
166 in_range = true;
167 }
168 if in_range {
169 push(phase, &mut result);
170 }
171 if phase == to {
172 break;
173 }
174 }
175 }
176 } else if s.eq_ignore_ascii_case("Main") {
177 push(PhaseType::Main1, &mut result);
178 push(PhaseType::Main2, &mut result);
179 } else if let Some(phase) = Self::from_script_name(s) {
180 push(phase, &mut result);
181 }
182 }
183 result
184 }
185}
186
187impl std::fmt::Display for PhaseType {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 write!(f, "{}", self.script_name())
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn turn_order() {
199 assert_eq!(PhaseType::Untap.next(), PhaseType::Upkeep);
200 assert_eq!(PhaseType::Cleanup.next(), PhaseType::Untap);
201 }
202
203 #[test]
204 fn is_before_after() {
205 assert!(PhaseType::Untap.is_before(PhaseType::Draw));
206 assert!(PhaseType::Main2.is_after(PhaseType::Main1));
207 }
208
209 #[test]
210 fn script_names() {
211 assert_eq!(
212 PhaseType::from_script_name("BeginCombat"),
213 Some(PhaseType::CombatBegin)
214 );
215 assert_eq!(
216 PhaseType::from_script_name("End of Turn"),
217 Some(PhaseType::EndOfTurn)
218 );
219 }
220}