forge_foundation/
phase.rs1use 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 script_name(self) -> &'static str {
89 match self {
90 PhaseType::Untap => "Untap",
91 PhaseType::Upkeep => "Upkeep",
92 PhaseType::Draw => "Draw",
93 PhaseType::Main1 => "Main1",
94 PhaseType::CombatBegin => "BeginCombat",
95 PhaseType::CombatDeclareAttackers => "Declare Attackers",
96 PhaseType::CombatDeclareBlockers => "Declare Blockers",
97 PhaseType::CombatFirstStrikeDamage => "First Strike Damage",
98 PhaseType::CombatDamage => "Combat Damage",
99 PhaseType::CombatEnd => "EndCombat",
100 PhaseType::Main2 => "Main2",
101 PhaseType::EndOfTurn => "End of Turn",
102 PhaseType::Cleanup => "Cleanup",
103 }
104 }
105
106 pub fn from_script_name(s: &str) -> Option<Self> {
107 let s = s.trim();
108 if s.eq_ignore_ascii_case("EndStep") || s.eq_ignore_ascii_case("EndOfTurnStep") {
109 return Some(PhaseType::EndOfTurn);
110 }
111 for &phase in &Self::TURN_ORDER {
112 if phase.script_name().eq_ignore_ascii_case(s)
113 || format!("{phase:?}").eq_ignore_ascii_case(s)
114 {
115 return Some(phase);
116 }
117 }
118 if s.eq_ignore_ascii_case("Main") {
120 return Some(PhaseType::Main1);
121 }
122 None
123 }
124
125 pub fn parse_range(values: &str) -> Vec<Self> {
126 let mut result: Vec<Self> = Vec::new();
127 let push = |phase: PhaseType, result: &mut Vec<Self>| {
128 if !result.contains(&phase) {
129 result.push(phase);
130 }
131 };
132 for s in values.split(',') {
133 let s = s.trim();
134 if let Some(idx) = s.find("->") {
135 let from = Self::from_script_name(&s[..idx]);
136 let to_str = s[idx + 2..].trim();
137 let to = if to_str.is_empty() {
138 Some(PhaseType::Cleanup)
139 } else {
140 Self::from_script_name(to_str)
141 };
142 if let (Some(from), Some(to)) = (from, to) {
143 let mut in_range = false;
144 for &phase in &Self::TURN_ORDER {
145 if phase == from {
146 in_range = true;
147 }
148 if in_range {
149 push(phase, &mut result);
150 }
151 if phase == to {
152 break;
153 }
154 }
155 }
156 } else if s.eq_ignore_ascii_case("Main") {
157 push(PhaseType::Main1, &mut result);
158 push(PhaseType::Main2, &mut result);
159 } else if let Some(phase) = Self::from_script_name(s) {
160 push(phase, &mut result);
161 }
162 }
163 result
164 }
165}
166
167impl std::fmt::Display for PhaseType {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 write!(f, "{}", self.script_name())
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn turn_order() {
179 assert_eq!(PhaseType::Untap.next(), PhaseType::Upkeep);
180 assert_eq!(PhaseType::Cleanup.next(), PhaseType::Untap);
181 }
182
183 #[test]
184 fn is_before_after() {
185 assert!(PhaseType::Untap.is_before(PhaseType::Draw));
186 assert!(PhaseType::Main2.is_after(PhaseType::Main1));
187 }
188
189 #[test]
190 fn script_names() {
191 assert_eq!(
192 PhaseType::from_script_name("BeginCombat"),
193 Some(PhaseType::CombatBegin)
194 );
195 assert_eq!(
196 PhaseType::from_script_name("End of Turn"),
197 Some(PhaseType::EndOfTurn)
198 );
199 }
200}