Skip to main content

hexo_engine/
win.rs

1use crate::board::Board;
2use crate::types::{Coord, Player, WIN_AXES};
3
4/// Counts consecutive stones belonging to `player` starting from `coord`
5/// (exclusive), stepping in direction `dir`.
6/// Stops at the first empty cell or opponent stone.
7fn count_consecutive(board: &Board, coord: Coord, dir: Coord, player: Player, max: u8) -> u8 {
8    let mut count = 0u8;
9    let mut cur = (coord.0 + dir.0, coord.1 + dir.1);
10    while count < max {
11        match board.get(cur) {
12            Some(p) if p == player => {
13                count += 1;
14                cur = (cur.0 + dir.0, cur.1 + dir.1);
15            }
16            _ => break,
17        }
18    }
19    count
20}
21
22/// Returns true if placing at `coord` for `player` creates a run of
23/// at least `win_length` stones along any of the three hex axes.
24pub fn check_win(board: &Board, coord: Coord, player: Player, win_length: u8) -> bool {
25    let max = win_length - 1; // max consecutive needed in any one direction
26    for &(dq, dr) in &WIN_AXES {
27        let fwd = count_consecutive(board, coord, (dq, dr), player, max);
28        let bwd = count_consecutive(board, coord, (-dq, -dr), player, max.saturating_sub(fwd));
29        if 1 + fwd + bwd >= win_length {
30            return true;
31        }
32    }
33    false
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    /// Build a board from a list of (coord, player) pairs.
41    /// Note: Board::new() always places P1 at (0,0), so we start from a
42    /// scratch-like board by working with coordinates that don't collide,
43    /// or we accept the implicit P1 stone at (0,0) and work around it.
44    /// For isolation we build a fresh board and place exactly the stones we want.
45    fn build_board(stones: &[(Coord, Player)]) -> Board {
46        // Board::new() places P1 at (0,0). We create a clean slate by using a
47        // helper that bypasses that — but Board::new() is the only constructor,
48        // so we use it and then explicitly include (0,0) in our test layouts
49        // (placed by new()) or use coordinates that don't include (0,0).
50        // Simplest: just call new() and place the extra stones, being careful
51        // not to double-place (0,0).
52        let mut board = Board::new();
53        for &(coord, player) in stones {
54            if coord == (0, 0) {
55                // (0,0) is already P1; only skip if it's the same player.
56                // If it differs this is a test-setup error.
57                debug_assert_eq!(player, Player::P1, "Board::new puts P1 at origin");
58                continue;
59            }
60            board.place(coord, player).expect("test: duplicate stone in build_board");
61        }
62        board
63    }
64
65    // ---- count_consecutive tests ----
66
67    #[test]
68    fn count_consecutive_empty_returns_zero() {
69        let board = build_board(&[]);
70        // Walking right from (0,0) — the cell to the right (1,0) is empty.
71        let n = count_consecutive(&board, (0, 0), (1, 0), Player::P1, u8::MAX);
72        assert_eq!(n, 0);
73    }
74
75    #[test]
76    fn count_consecutive_three_in_a_row() {
77        // P1 stones at (1,0), (2,0), (3,0); start from (0,0) going right.
78        let board = build_board(&[
79            ((0, 0), Player::P1),
80            ((1, 0), Player::P1),
81            ((2, 0), Player::P1),
82            ((3, 0), Player::P1),
83        ]);
84        let n = count_consecutive(&board, (0, 0), (1, 0), Player::P1, u8::MAX);
85        assert_eq!(n, 3);
86    }
87
88    #[test]
89    fn count_consecutive_stops_at_opponent() {
90        // P1 at (1,0), P2 at (2,0).
91        let board = build_board(&[
92            ((0, 0), Player::P1),
93            ((1, 0), Player::P1),
94            ((2, 0), Player::P2),
95            ((3, 0), Player::P1),
96        ]);
97        let n = count_consecutive(&board, (0, 0), (1, 0), Player::P1, u8::MAX);
98        assert_eq!(n, 1); // only (1,0) before opponent
99    }
100
101    #[test]
102    fn count_consecutive_stops_at_empty_gap() {
103        // P1 at (1,0), gap at (2,0), P1 at (3,0).
104        let board = build_board(&[
105            ((0, 0), Player::P1),
106            ((1, 0), Player::P1),
107            ((3, 0), Player::P1),
108        ]);
109        let n = count_consecutive(&board, (0, 0), (1, 0), Player::P1, u8::MAX);
110        assert_eq!(n, 1); // stops at gap (2,0)
111    }
112
113    // ---- check_win tests ----
114
115    #[test]
116    fn check_win_single_stone_no_win() {
117        // Board::new() only has P1 at (0,0); win_length=6 → no win.
118        let board = build_board(&[((0, 0), Player::P1)]);
119        assert!(!check_win(&board, (0, 0), Player::P1, 6));
120    }
121
122    #[test]
123    fn check_win_six_in_a_row_axis_q() {
124        // 6 P1 stones along (1,0) axis: (0,0)..(5,0)
125        let board = build_board(&[
126            ((0, 0), Player::P1),
127            ((1, 0), Player::P1),
128            ((2, 0), Player::P1),
129            ((3, 0), Player::P1),
130            ((4, 0), Player::P1),
131            ((5, 0), Player::P1),
132        ]);
133        assert!(check_win(&board, (3, 0), Player::P1, 6));
134    }
135
136    #[test]
137    fn check_win_six_in_a_row_axis_r() {
138        // 6 P1 stones along (0,1) axis: (10,0)..(10,5)
139        let board = build_board(&[
140            ((10, 0), Player::P1),
141            ((10, 1), Player::P1),
142            ((10, 2), Player::P1),
143            ((10, 3), Player::P1),
144            ((10, 4), Player::P1),
145            ((10, 5), Player::P1),
146        ]);
147        assert!(check_win(&board, (10, 2), Player::P1, 6));
148    }
149
150    #[test]
151    fn check_win_six_in_a_row_axis_diagonal() {
152        // 6 P1 stones along (1,-1) axis: (10,0),(11,-1),(12,-2),(13,-3),(14,-4),(15,-5)
153        let board = build_board(&[
154            ((10, 0), Player::P1),
155            ((11, -1), Player::P1),
156            ((12, -2), Player::P1),
157            ((13, -3), Player::P1),
158            ((14, -4), Player::P1),
159            ((15, -5), Player::P1),
160        ]);
161        assert!(check_win(&board, (12, -2), Player::P1, 6));
162    }
163
164    #[test]
165    fn check_win_five_is_not_enough_for_six() {
166        // Only 5 P1 stones along q axis.
167        let board = build_board(&[
168            ((0, 0), Player::P1),
169            ((1, 0), Player::P1),
170            ((2, 0), Player::P1),
171            ((3, 0), Player::P1),
172            ((4, 0), Player::P1),
173        ]);
174        assert!(!check_win(&board, (2, 0), Player::P1, 6));
175    }
176
177    #[test]
178    fn check_win_seven_also_wins() {
179        // 7 P1 stones along q axis; win_length=6 → still wins (AC-10c).
180        let board = build_board(&[
181            ((0, 0), Player::P1),
182            ((1, 0), Player::P1),
183            ((2, 0), Player::P1),
184            ((3, 0), Player::P1),
185            ((4, 0), Player::P1),
186            ((5, 0), Player::P1),
187            ((6, 0), Player::P1),
188        ]);
189        assert!(check_win(&board, (3, 0), Player::P1, 6));
190    }
191
192    #[test]
193    fn check_win_wrong_player_no_win() {
194        // 6 P1 stones; asking for P2 win → false (REQ-13 / AC-13).
195        let board = build_board(&[
196            ((0, 0), Player::P1),
197            ((1, 0), Player::P1),
198            ((2, 0), Player::P1),
199            ((3, 0), Player::P1),
200            ((4, 0), Player::P1),
201            ((5, 0), Player::P1),
202        ]);
203        assert!(!check_win(&board, (5, 0), Player::P2, 6));
204    }
205
206    #[test]
207    fn check_win_four_in_a_row_curriculum_variant() {
208        // 4-in-a-row variant (AC-19b): 4 P1 stones along q axis.
209        let board = build_board(&[
210            ((0, 0), Player::P1),
211            ((1, 0), Player::P1),
212            ((2, 0), Player::P1),
213            ((3, 0), Player::P1),
214        ]);
215        assert!(check_win(&board, (2, 0), Player::P1, 4));
216    }
217
218    #[test]
219    fn check_win_three_not_enough_for_four() {
220        // 3 P1 stones; win_length=4 → no win (AC-10b for 4-variant).
221        let board = build_board(&[
222            ((0, 0), Player::P1),
223            ((1, 0), Player::P1),
224            ((2, 0), Player::P1),
225        ]);
226        assert!(!check_win(&board, (1, 0), Player::P1, 4));
227    }
228}