Skip to main content

forge_foundation/
phase.rs

1use serde::{Deserialize, Serialize};
2
3/// Turn phases/steps. Mirrors Java `PhaseType`.
4#[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    /// The full turn sequence in order.
23    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    /// Phase groups for grouping related steps.
40    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    /// Index in the turn order (0-12).
69    pub fn index(self) -> usize {
70        Self::TURN_ORDER.iter().position(|&p| p == self).unwrap()
71    }
72
73    /// Get the next phase in the turn sequence. Wraps from Cleanup -> Untap.
74    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    /// Script-compatible name used in card definition files.
88    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        // "Main" matches both main phases — return Main1 as default
119        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}