Skip to main content

hexo_engine/
turn.rs

1use crate::types::Player;
2
3/// Tracks whose turn it is and how many moves remain in the current turn.
4///
5/// P1's first placement is hardcoded at (0,0), so the game effectively
6/// starts with P2 to move with 2 moves remaining.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum TurnState {
9    P1Turn { moves_left: u8 },
10    P2Turn { moves_left: u8 },
11    GameOver,
12}
13
14impl TurnState {
15    /// Returns the player whose turn it currently is, or None if the game is over.
16    pub fn current_player(self) -> Option<Player> {
17        match self {
18            TurnState::P1Turn { .. } => Some(Player::P1),
19            TurnState::P2Turn { .. } => Some(Player::P2),
20            TurnState::GameOver => None,
21        }
22    }
23
24    /// Returns how many moves are left in this turn, or None if the game is over.
25    pub fn moves_remaining(self) -> Option<u8> {
26        match self {
27            TurnState::P1Turn { moves_left } => Some(moves_left),
28            TurnState::P2Turn { moves_left } => Some(moves_left),
29            TurnState::GameOver => None,
30        }
31    }
32
33    /// Advance the state after a stone placement.
34    ///
35    /// If `winner` is true, transitions to `GameOver`.
36    /// Otherwise, decrements `moves_left`; when it reaches 0, switches to the
37    /// other player with `moves_left = 2`.
38    pub fn advance(self, winner: bool) -> TurnState {
39        if winner {
40            return TurnState::GameOver;
41        }
42        match self {
43            TurnState::P1Turn { moves_left: 2 } => TurnState::P1Turn { moves_left: 1 },
44            TurnState::P1Turn { moves_left: 1 } => TurnState::P2Turn { moves_left: 2 },
45            TurnState::P1Turn { moves_left: _ } => unreachable!("invalid moves_left for P1Turn"),
46            TurnState::P2Turn { moves_left: 2 } => TurnState::P2Turn { moves_left: 1 },
47            TurnState::P2Turn { moves_left: 1 } => TurnState::P1Turn { moves_left: 2 },
48            TurnState::P2Turn { moves_left: _ } => unreachable!("invalid moves_left for P2Turn"),
49            TurnState::GameOver => TurnState::GameOver,
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    // --- current_player() ---
59
60    #[test]
61    fn current_player_p1_turn() {
62        let state = TurnState::P1Turn { moves_left: 2 };
63        assert_eq!(state.current_player(), Some(Player::P1));
64    }
65
66    #[test]
67    fn current_player_p2_turn() {
68        let state = TurnState::P2Turn { moves_left: 2 };
69        assert_eq!(state.current_player(), Some(Player::P2));
70    }
71
72    #[test]
73    fn current_player_game_over() {
74        assert_eq!(TurnState::GameOver.current_player(), None);
75    }
76
77    // --- moves_remaining() ---
78
79    #[test]
80    fn moves_remaining_start_of_turn() {
81        let state = TurnState::P2Turn { moves_left: 2 };
82        assert_eq!(state.moves_remaining(), Some(2));
83    }
84
85    #[test]
86    fn moves_remaining_after_first_placement() {
87        let state = TurnState::P2Turn { moves_left: 1 };
88        assert_eq!(state.moves_remaining(), Some(1));
89    }
90
91    #[test]
92    fn moves_remaining_game_over() {
93        assert_eq!(TurnState::GameOver.moves_remaining(), None);
94    }
95
96    // --- advance(winner) normal flow ---
97
98    #[test]
99    fn advance_decrements_moves_left_from_2_to_1() {
100        let state = TurnState::P2Turn { moves_left: 2 };
101        let next = state.advance(false);
102        assert_eq!(next, TurnState::P2Turn { moves_left: 1 });
103    }
104
105    #[test]
106    fn advance_switches_player_when_moves_left_reaches_0() {
107        let state = TurnState::P2Turn { moves_left: 1 };
108        let next = state.advance(false);
109        assert_eq!(next, TurnState::P1Turn { moves_left: 2 });
110    }
111
112    #[test]
113    fn advance_winner_mid_turn_transitions_to_game_over() {
114        let state = TurnState::P2Turn { moves_left: 2 };
115        let next = state.advance(true);
116        assert_eq!(next, TurnState::GameOver);
117    }
118
119    #[test]
120    fn advance_winner_on_last_move_transitions_to_game_over() {
121        let state = TurnState::P1Turn { moves_left: 1 };
122        let next = state.advance(true);
123        assert_eq!(next, TurnState::GameOver);
124    }
125
126    #[test]
127    fn advance_game_over_returns_game_over() {
128        let next = TurnState::GameOver.advance(false);
129        assert_eq!(next, TurnState::GameOver);
130    }
131
132    // --- Full cycle: P2(2) -> P2(1) -> P1(2) -> P1(1) -> P2(2) ---
133
134    #[test]
135    fn full_turn_cycle_p2_to_p1_to_p2() {
136        let s0 = TurnState::P2Turn { moves_left: 2 };
137        assert_eq!(s0.current_player(), Some(Player::P2));
138        assert_eq!(s0.moves_remaining(), Some(2));
139
140        let s1 = s0.advance(false);
141        assert_eq!(s1, TurnState::P2Turn { moves_left: 1 });
142        assert_eq!(s1.current_player(), Some(Player::P2));
143        assert_eq!(s1.moves_remaining(), Some(1));
144
145        let s2 = s1.advance(false);
146        assert_eq!(s2, TurnState::P1Turn { moves_left: 2 });
147        assert_eq!(s2.current_player(), Some(Player::P1));
148        assert_eq!(s2.moves_remaining(), Some(2));
149
150        let s3 = s2.advance(false);
151        assert_eq!(s3, TurnState::P1Turn { moves_left: 1 });
152        assert_eq!(s3.current_player(), Some(Player::P1));
153        assert_eq!(s3.moves_remaining(), Some(1));
154
155        let s4 = s3.advance(false);
156        assert_eq!(s4, TurnState::P2Turn { moves_left: 2 });
157        assert_eq!(s4.current_player(), Some(Player::P2));
158        assert_eq!(s4.moves_remaining(), Some(2));
159    }
160
161    #[test]
162    fn advance_p1_decrements_from_2_to_1() {
163        let state = TurnState::P1Turn { moves_left: 2 };
164        let next = state.advance(false);
165        assert_eq!(next, TurnState::P1Turn { moves_left: 1 });
166    }
167}