Skip to main content

manabrew_engine/phase/
phase_handler.rs

1//! PhaseHandler — manages game turn and phase progression.
2//!
3//! Mirrors Java's `PhaseHandler.java`.
4//! The core turn state machine lives in `crate::game_loop::GameLoop`;
5//! this module provides the phase-specific state tracking that the
6//! game loop delegates to.
7
8use std::collections::HashMap;
9
10use forge_foundation::PhaseType;
11
12use crate::ids::PlayerId;
13
14use super::extra_phase::ExtraPhase;
15use super::extra_turn::ExtraTurn;
16
17/// Tracks phase-specific state for a game.
18/// Mirrors Java's `PhaseHandler` fields.
19#[derive(Debug, Clone)]
20pub struct PhaseHandler {
21    phase: Option<PhaseType>,
22    turn: i32,
23
24    /// Stack of extra turns (LIFO — most recent on top).
25    extra_turns: Vec<ExtraTurn>,
26    /// Extra phases keyed by the phase they follow.
27    extra_phases: HashMap<PhaseType, Vec<ExtraPhase>>,
28
29    pub n_upkeeps_this_turn: i32,
30    pub n_upkeeps_this_game: i32,
31    pub n_combats_this_turn: i32,
32    pub n_mains_this_turn: i32,
33    pub n_end_of_turns_this_turn: i32,
34    pub planar_dice_special_action_this_turn: i32,
35
36    player_turn: Option<PlayerId>,
37    player_previous_turn: Option<PlayerId>,
38
39    priority_player: Option<PlayerId>,
40    first_priority: Option<PlayerId>,
41
42    pub skip_damage_steps: bool,
43    pub repeat_cleanup: bool,
44    pub give_priority_to_player: bool,
45}
46
47impl PhaseHandler {
48    pub fn new() -> Self {
49        PhaseHandler {
50            phase: None,
51            turn: 0,
52            extra_turns: Vec::new(),
53            extra_phases: HashMap::new(),
54            n_upkeeps_this_turn: 0,
55            n_upkeeps_this_game: 0,
56            n_combats_this_turn: 0,
57            n_mains_this_turn: 0,
58            n_end_of_turns_this_turn: 0,
59            planar_dice_special_action_this_turn: 0,
60            player_turn: None,
61            player_previous_turn: None,
62            priority_player: None,
63            first_priority: None,
64            skip_damage_steps: false,
65            repeat_cleanup: false,
66            give_priority_to_player: false,
67        }
68    }
69
70    pub fn get_phase(&self) -> Option<PhaseType> {
71        self.phase
72    }
73
74    pub fn set_phase(&mut self, phase: PhaseType) {
75        self.phase = Some(phase);
76    }
77
78    pub fn get_turn(&self) -> i32 {
79        self.turn
80    }
81
82    pub fn is_player_turn(&self, player: PlayerId) -> bool {
83        self.player_turn == Some(player)
84    }
85
86    pub fn get_player_turn(&self) -> Option<PlayerId> {
87        self.player_turn
88    }
89
90    pub fn set_player_turn(&mut self, player: PlayerId) {
91        self.player_turn = Some(player);
92        self.set_priority(player);
93    }
94
95    pub fn get_previous_player_turn(&self) -> Option<PlayerId> {
96        self.player_previous_turn
97    }
98
99    pub fn get_priority_player(&self) -> Option<PlayerId> {
100        self.priority_player
101    }
102
103    pub fn set_priority(&mut self, player: PlayerId) {
104        self.first_priority = Some(player);
105        self.priority_player = Some(player);
106    }
107
108    pub fn reset_priority(&mut self) {
109        if let Some(player) = self.player_turn {
110            self.set_priority(player);
111        }
112    }
113
114    pub fn is_first_combat(&self) -> bool {
115        self.n_combats_this_turn == 1
116    }
117
118    pub fn get_num_combat(&self) -> i32 {
119        self.n_combats_this_turn
120    }
121
122    pub fn get_num_upkeep(&self) -> i32 {
123        self.n_upkeeps_this_turn
124    }
125
126    pub fn is_first_upkeep(&self) -> bool {
127        self.phase == Some(PhaseType::Upkeep) && self.n_upkeeps_this_turn == 0
128    }
129
130    pub fn is_first_upkeep_this_game(&self) -> bool {
131        self.phase == Some(PhaseType::Upkeep) && self.n_upkeeps_this_game == 0
132    }
133
134    pub fn get_num_main(&self) -> i32 {
135        self.n_mains_this_turn
136    }
137
138    pub fn before_first_post_combat_main_end(&self) -> bool {
139        self.n_mains_this_turn
140            <= if self.phase == Some(PhaseType::Main2) {
141                2
142            } else {
143                1
144            }
145    }
146
147    pub fn skipped_declare_blockers(&self) -> bool {
148        self.skip_damage_steps
149    }
150
151    pub fn get_num_end_of_turn(&self) -> i32 {
152        self.n_end_of_turns_this_turn
153    }
154
155    pub fn is(&self, phase: PhaseType) -> bool {
156        self.phase == Some(phase)
157    }
158
159    pub fn is_phase_player(&self, phase: PhaseType, player: PlayerId) -> bool {
160        self.phase == Some(phase) && self.player_turn == Some(player)
161    }
162
163    /// Add an extra turn for the given player.
164    /// Mirrors Java's `PhaseHandler.addExtraTurn()`.
165    pub fn add_extra_turn(&mut self, player: PlayerId, player_order: &[PlayerId]) -> &ExtraTurn {
166        // Use a stack: if empty, push the normal next turn first
167        if self.extra_turns.is_empty() {
168            if let Some(current) = self.player_turn {
169                let next = next_player_after(current, player_order);
170                self.extra_turns.push(ExtraTurn::new(next));
171            }
172        }
173        self.extra_turns.push(ExtraTurn::new(player));
174        self.extra_turns.last().unwrap()
175    }
176
177    /// Add extra phase(s) after the given phase.
178    /// Mirrors Java's `PhaseHandler.addExtraPhase()`.
179    pub fn add_extra_phase(
180        &mut self,
181        after_phase: PhaseType,
182        extra_phase_list: &[PhaseType],
183        next_phase: PhaseType,
184    ) {
185        for (i, &extra) in extra_phase_list.iter().enumerate() {
186            let entry = self.extra_phases.entry(extra).or_default();
187            if i < extra_phase_list.len() - 1 {
188                entry.push(ExtraPhase::new(extra_phase_list[i + 1]));
189            } else {
190                entry.push(ExtraPhase::new(next_phase));
191            }
192        }
193        let after_entry = self.extra_phases.entry(after_phase).or_default();
194        after_entry.push(ExtraPhase::new(extra_phase_list[0]));
195    }
196
197    pub fn get_next_turn(&self, player_order: &[PlayerId]) -> Option<PlayerId> {
198        if let Some(last) = self.extra_turns.last() {
199            Some(last.get_player())
200        } else {
201            self.player_turn.map(|p| next_player_after(p, player_order))
202        }
203    }
204
205    /// Reset the phase handler for a game restart (e.g. Karn Liberated).
206    /// Mirrors Java's `PhaseHandler.restart()`.
207    pub fn restart(&mut self) {
208        self.extra_phases.clear();
209        self.extra_turns.clear();
210        self.turn = 0;
211    }
212
213    /// Called when the stack resolves — re-enable priority.
214    /// Mirrors Java's `PhaseHandler.onStackResolved()`.
215    pub fn on_stack_resolved(&mut self) {
216        self.give_priority_to_player = true;
217    }
218
219    pub fn get_planar_dice_special_action_this_turn(&self) -> i32 {
220        self.planar_dice_special_action_this_turn
221    }
222
223    pub fn inc_planar_dice_special_action_this_turn(&mut self) {
224        self.planar_dice_special_action_this_turn += 1;
225    }
226
227    /// Get the continuous extra turn count for a player.
228    /// Mirrors Java's `PhaseHandler.getExtraTurnForPlayer()`.
229    pub fn get_extra_turn_for_player(&self, player: PlayerId) -> i32 {
230        if self.extra_turns.len() < 2 {
231            return 0;
232        }
233        let mut count = 0;
234        // Skip the first element (bottom of stack = normal turn)
235        for et in self.extra_turns.iter().skip(1) {
236            if et.get_player() != player {
237                break;
238            }
239            count += 1;
240        }
241        count
242    }
243
244    /// Advance to the next turn. Returns the player who gets the next turn.
245    /// Mirrors Java's `PhaseHandler.handleNextTurn()`.
246    pub fn handle_next_turn(&mut self, player_order: &[PlayerId]) -> PlayerId {
247        self.player_previous_turn = self.player_turn;
248
249        let next = if let Some(extra) = self.extra_turns.pop() {
250            let player = extra.get_player();
251            // Register skip_untap if needed
252            if extra.is_skip_untap() {
253                // Caller should handle this
254            }
255            player
256        } else {
257            self.player_turn
258                .map(|p| next_player_after(p, player_order))
259                .unwrap_or(player_order[0])
260        };
261
262        self.turn += 1;
263        self.extra_phases.clear();
264        self.n_upkeeps_this_turn = 0;
265        self.n_combats_this_turn = 0;
266        self.n_mains_this_turn = 0;
267        self.n_end_of_turns_this_turn = 0;
268        self.planar_dice_special_action_this_turn = 0;
269
270        self.set_player_turn(next);
271        next
272    }
273
274    pub fn end_combat_phase_by_effect(&mut self) {
275        self.phase = Some(PhaseType::CombatEnd);
276    }
277
278    pub fn end_turn_by_effect(&mut self) {
279        self.extra_phases.clear();
280        self.phase = Some(PhaseType::Cleanup);
281    }
282
283    pub fn debug_print_state(&self, has_priority: bool) -> String {
284        format!(
285            "{:?}'s {:?} [{}P] {:?}",
286            self.player_turn,
287            self.phase,
288            if has_priority { "+" } else { "-" },
289            self.priority_player
290        )
291    }
292
293    /// Returns true if a combat is currently in progress.
294    /// Mirrors Java's `PhaseHandler.inCombat()`.
295    pub fn in_combat(&self) -> bool {
296        self.phase.is_some_and(|p| p.is_combat())
297    }
298
299    /// End the current combat.
300    /// Mirrors Java's `PhaseHandler.endCombat()`.
301    pub fn end_combat(&mut self) {
302        // The actual combat cleanup (removing attackers/blockers) is handled
303        // by the game loop's CombatState. This resets phase-level tracking.
304        self.skip_damage_steps = false;
305    }
306
307    /// Set up the first turn of the game for the given player.
308    /// Mirrors Java's `PhaseHandler.setupFirstTurn()`.
309    pub fn setup_first_turn(&mut self, goes_first: PlayerId) {
310        self.set_player_turn(goes_first);
311        self.set_phase(PhaseType::Untap);
312        self.turn = 1;
313        self.give_priority_to_player = false;
314    }
315
316    /// Start the first turn and enter the main game loop.
317    /// Mirrors Java's `PhaseHandler.startFirstTurn()`.
318    /// In Rust, the actual loop is driven by `GameLoop::run_turn_state_machine()`.
319    pub fn start_first_turn(&mut self, goes_first: PlayerId) {
320        self.setup_first_turn(goes_first);
321    }
322
323    /// Dev mode: advance to a target phase, running phase transitions.
324    /// Mirrors Java's `PhaseHandler.devAdvanceToPhase()`.
325    pub fn dev_advance_to_phase(&mut self, target_phase: PhaseType) -> bool {
326        while let Some(current) = self.phase {
327            if !current.is_before(target_phase) {
328                break;
329            }
330            self.phase = Some(current.next());
331        }
332        true
333    }
334
335    /// Dev mode: set the phase and player directly.
336    /// Mirrors Java's `PhaseHandler.devModeSet()`.
337    pub fn dev_mode_set(
338        &mut self,
339        phase: Option<PhaseType>,
340        player: Option<PlayerId>,
341        end_combat: bool,
342        cturn: i32,
343    ) {
344        if let Some(p) = phase {
345            self.set_phase(p);
346        }
347        if let Some(pl) = player {
348            self.set_player_turn(pl);
349        }
350        self.turn = cturn;
351        if end_combat {
352            self.end_combat();
353        }
354    }
355}
356
357impl Default for PhaseHandler {
358    fn default() -> Self {
359        Self::new()
360    }
361}
362
363/// Get the next player in turn order after the given player.
364fn next_player_after(current: PlayerId, player_order: &[PlayerId]) -> PlayerId {
365    if let Some(pos) = player_order.iter().position(|&p| p == current) {
366        let next = (pos + 1) % player_order.len();
367        player_order[next]
368    } else {
369        player_order[0]
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn new_phase_handler() {
379        let ph = PhaseHandler::new();
380        assert_eq!(ph.get_phase(), None);
381        assert_eq!(ph.get_turn(), 0);
382    }
383
384    #[test]
385    fn set_phase() {
386        let mut ph = PhaseHandler::new();
387        ph.set_phase(PhaseType::Upkeep);
388        assert_eq!(ph.get_phase(), Some(PhaseType::Upkeep));
389        assert!(ph.is(PhaseType::Upkeep));
390    }
391
392    #[test]
393    fn extra_turn_count() {
394        let mut ph = PhaseHandler::new();
395        let p0 = PlayerId(0);
396        let p1 = PlayerId(1);
397        ph.set_player_turn(p0);
398        ph.add_extra_turn(p0, &[p0, p1]);
399        assert_eq!(ph.get_extra_turn_for_player(p0), 1);
400        assert_eq!(ph.get_extra_turn_for_player(p1), 0);
401    }
402}