Skip to main content

manabrew_engine/phase/
mod.rs

1//! Phase module — turn structure, phase handling, and untap logic.
2//!
3//! Mirrors Java's `forge.game.phase` package.
4
5pub mod extra_phase;
6pub mod extra_turn;
7pub mod phase_handler;
8pub mod phase_type;
9pub mod untap;
10
11use std::collections::HashMap;
12
13use forge_foundation::PhaseType;
14use serde::{Deserialize, Serialize};
15
16use crate::ids::{CardId, PlayerId};
17
18// Re-exports
19pub use extra_phase::ExtraPhase;
20pub use extra_turn::ExtraTurn;
21pub use phase_handler::PhaseHandler;
22
23/// A phase command — a deferred action to execute at a phase boundary.
24/// Mirrors Java's `GameCommand` callbacks stored in `Phase`.
25///
26/// In Java, these are `Runnable`-like objects that modify game state.
27/// In Rust, we represent them as an enum of known command types,
28/// since we can't store closures in serializable state.
29#[derive(Debug, Clone)]
30pub enum PhaseCommand {
31    /// Remove a continuous effect by its ID.
32    RemoveEffect(CardId),
33    /// Restore a card's controller to its owner.
34    RestoreController(CardId),
35    /// Remove granted keywords from a card.
36    RemoveGrantedKeywords(CardId),
37    /// Generic cleanup marker.
38    Cleanup(CardId),
39}
40
41/// Phase instance — stores commands that execute at phase boundaries.
42///
43/// Mirrors Java's `Phase` class. Each `Phase` in Java holds lists of
44/// `GameCommand` callbacks for "at <phase>", "until <phase>", and
45/// per-player "until <player's> next <phase>" effects.
46#[derive(Debug, Clone, Default)]
47pub struct Phase {
48    #[allow(dead_code)]
49    phase_type: Option<PhaseType>,
50    /// Commands to execute "at" this phase.
51    at: Vec<PhaseCommand>,
52    /// Commands to execute "until" this phase (remove effects).
53    until: Vec<PhaseCommand>,
54    /// Per-player commands to execute "until <player's> next <phase>".
55    until_map: HashMap<PlayerId, Vec<PhaseCommand>>,
56    /// Per-player commands registered for end-of-phase execution.
57    until_end_map: HashMap<PlayerId, Vec<PhaseCommand>>,
58    /// Per-player commands staged to be moved to until_end_map.
59    register_map: HashMap<PlayerId, Vec<PhaseCommand>>,
60}
61
62impl Phase {
63    pub fn new(phase_type: PhaseType) -> Self {
64        Phase {
65            phase_type: Some(phase_type),
66            ..Default::default()
67        }
68    }
69
70    /// Clear all commands from this phase.
71    /// Mirrors Java's `Phase.clearCommands()`.
72    pub fn clear_commands(&mut self) {
73        self.at.clear();
74        self.until.clear();
75        self.until_map.clear();
76        self.until_end_map.clear();
77        self.register_map.clear();
78    }
79
80    /// Add a command to execute "at" this phase.
81    /// Mirrors Java's `Phase.addAt()`.
82    pub fn add_at(&mut self, cmd: PhaseCommand) {
83        self.at.insert(0, cmd);
84    }
85
86    /// Execute all "at" commands, draining the list.
87    /// Mirrors Java's `Phase.executeAt()`.
88    pub fn execute_at(&mut self) -> Vec<PhaseCommand> {
89        std::mem::take(&mut self.at)
90    }
91
92    /// Add a command to execute "until" this phase (global or per-player).
93    /// When called without a player, adds to the global until list.
94    /// When called with a player, adds to the per-player until map.
95    /// Mirrors Java's `Phase.addUntil()` (both overloads).
96    pub fn add_until(&mut self, player: Option<PlayerId>, cmd: PhaseCommand) {
97        if let Some(p) = player {
98            self.until_map.entry(p).or_default().insert(0, cmd);
99        } else {
100            self.until.insert(0, cmd);
101        }
102    }
103
104    /// Execute "until" commands, draining the list.
105    /// When called without a player, executes global until commands.
106    /// When called with a player, executes per-player until commands.
107    /// Mirrors Java's `Phase.executeUntil()` (both overloads).
108    pub fn execute_until(&mut self, player: Option<PlayerId>) -> Vec<PhaseCommand> {
109        if let Some(p) = player {
110            self.until_map.remove(&p).unwrap_or_default()
111        } else {
112            std::mem::take(&mut self.until)
113        }
114    }
115
116    /// Register a command for end-of-phase execution for a player.
117    /// Mirrors Java's `Phase.registerUntilEnd()`.
118    pub fn register_until_end(&mut self, player: PlayerId, cmd: PhaseCommand) {
119        self.register_map.entry(player).or_default().insert(0, cmd);
120    }
121
122    /// Add a command to the end-of-phase map for a player.
123    /// Mirrors Java's `Phase.addUntilEnd()`.
124    pub fn add_until_end(&mut self, player: PlayerId, cmd: PhaseCommand) {
125        self.until_end_map.entry(player).or_default().insert(0, cmd);
126    }
127
128    /// Move registered commands to the until-end map.
129    /// Mirrors Java's `Phase.registerUntilEndCommand()`.
130    pub fn register_until_end_command(&mut self, player: PlayerId) {
131        if let Some(cmds) = self.register_map.remove(&player) {
132            self.until_end_map.insert(player, cmds);
133        }
134    }
135
136    /// Execute end-of-phase commands for a player.
137    /// Mirrors Java's `Phase.executeUntilEndOfPhase()`.
138    pub fn execute_until_end_of_phase(&mut self, player: PlayerId) -> Vec<PhaseCommand> {
139        self.until_end_map.remove(&player).unwrap_or_default()
140    }
141}
142
143/// Tracks the current turn state: whose turn, which phase, turn number.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct TurnState {
146    pub turn_number: u32,
147    pub active_player: PlayerId,
148    pub phase: PhaseType,
149    pub priority_player: PlayerId,
150    pub num_players: u32,
151
152    // Combat tracking
153    pub combat_attackers_declared: bool,
154    pub combat_blockers_declared: bool,
155    /// Authoritative blocker -> attacker assignments for the current combat.
156    pub combat_block_assignments: Vec<(CardId, CardId)>,
157
158    // Per-turn flags
159    pub drawn_for_turn: bool,
160}
161
162impl TurnState {
163    pub fn new(active_player: PlayerId, num_players: u32) -> Self {
164        TurnState {
165            turn_number: 1,
166            active_player,
167            phase: PhaseType::Untap,
168            priority_player: active_player,
169            num_players,
170            combat_attackers_declared: false,
171            combat_blockers_declared: false,
172            combat_block_assignments: vec![],
173            drawn_for_turn: false,
174        }
175    }
176
177    /// Advance to the next phase. Returns true if the turn ended (wrapped to Untap).
178    pub fn advance_phase(&mut self) -> bool {
179        let next = self.phase.next();
180        let turn_ended = next == PhaseType::Untap && self.phase == PhaseType::Cleanup;
181        self.phase = next;
182
183        if turn_ended {
184            self.turn_number += 1;
185            self.combat_attackers_declared = false;
186            self.combat_blockers_declared = false;
187            self.combat_block_assignments.clear();
188            self.drawn_for_turn = false;
189        }
190
191        // Reset combat flags when entering combat
192        if self.phase == PhaseType::CombatBegin {
193            self.combat_attackers_declared = false;
194            self.combat_blockers_declared = false;
195            self.combat_block_assignments.clear();
196        }
197
198        turn_ended
199    }
200
201    /// Advance to the next player's turn (for multiplayer).
202    pub fn next_player_turn(&mut self, player_order: &[PlayerId]) {
203        if let Some(pos) = player_order.iter().position(|&p| p == self.active_player) {
204            let next = (pos + 1) % player_order.len();
205            self.active_player = player_order[next];
206            self.priority_player = self.active_player;
207            self.turn_number += 1;
208            self.combat_attackers_declared = false;
209            self.combat_blockers_declared = false;
210            self.combat_block_assignments.clear();
211            self.drawn_for_turn = false;
212        }
213    }
214
215    /// Advance to the next turn, consuming an extra turn if available.
216    /// Returns `Some((player, skip_untap))` if the advancing player needs
217    /// their skip_next_untap flag set on PlayerState; `None` otherwise.
218    /// Mirrors Java's `PhaseHandler.handleNextTurn()`.
219    pub fn advance_turn(
220        &mut self,
221        extra_turns: &mut std::collections::VecDeque<ExtraTurn>,
222        player_order: &[PlayerId],
223    ) -> Option<(PlayerId, bool)> {
224        if let Some(extra_turn) = extra_turns.pop_front() {
225            let player = extra_turn.player;
226            self.active_player = player;
227            self.priority_player = player;
228            self.turn_number += 1;
229            self.combat_attackers_declared = false;
230            self.combat_blockers_declared = false;
231            self.combat_block_assignments.clear();
232            self.drawn_for_turn = false;
233            if extra_turn.skip_untap {
234                Some((player, true))
235            } else {
236                None
237            }
238        } else {
239            self.next_player_turn(player_order);
240            None
241        }
242    }
243
244    pub fn is_main_phase(&self) -> bool {
245        self.phase.is_main()
246    }
247
248    pub fn is_combat(&self) -> bool {
249        self.phase.is_combat()
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn advance_phases() {
259        let mut ts = TurnState::new(PlayerId(0), 2);
260        assert_eq!(ts.phase, PhaseType::Untap);
261
262        ts.advance_phase();
263        assert_eq!(ts.phase, PhaseType::Upkeep);
264
265        ts.advance_phase();
266        assert_eq!(ts.phase, PhaseType::Draw);
267    }
268
269    #[test]
270    fn turn_wraps() {
271        let mut ts = TurnState::new(PlayerId(0), 2);
272        assert_eq!(ts.turn_number, 1);
273
274        // Advance through all phases
275        loop {
276            let ended = ts.advance_phase();
277            if ended {
278                break;
279            }
280        }
281        assert_eq!(ts.turn_number, 2);
282        assert_eq!(ts.phase, PhaseType::Untap);
283    }
284
285    #[test]
286    fn phase_commands() {
287        let mut phase = Phase::new(PhaseType::Upkeep);
288        phase.add_at(PhaseCommand::Cleanup(CardId(1)));
289        phase.add_until(None, PhaseCommand::RemoveEffect(CardId(2)));
290
291        let at_cmds = phase.execute_at();
292        assert_eq!(at_cmds.len(), 1);
293
294        let until_cmds = phase.execute_until(None);
295        assert_eq!(until_cmds.len(), 1);
296
297        // After execution, lists should be empty
298        assert!(phase.execute_at().is_empty());
299        assert!(phase.execute_until(None).is_empty());
300    }
301
302    #[test]
303    fn phase_per_player_commands() {
304        let mut phase = Phase::new(PhaseType::Cleanup);
305        let p0 = PlayerId(0);
306        let p1 = PlayerId(1);
307
308        phase.add_until(Some(p0), PhaseCommand::Cleanup(CardId(1)));
309        phase.add_until(Some(p1), PhaseCommand::Cleanup(CardId(2)));
310
311        let p0_cmds = phase.execute_until(Some(p0));
312        assert_eq!(p0_cmds.len(), 1);
313
314        // p1 commands should still be there
315        let p1_cmds = phase.execute_until(Some(p1));
316        assert_eq!(p1_cmds.len(), 1);
317    }
318}