Skip to main content

hexo_engine/
board.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use crate::types::{Coord, Player};
5
6/// Sparse board storage: maps placed coordinates to their owning player.
7#[derive(Clone)]
8pub struct Board {
9    stones: HashMap<Coord, Player>,
10}
11
12/// Error returned when attempting to place a stone on an occupied cell.
13#[derive(Debug, PartialEq, Eq)]
14pub enum PlaceError {
15    CellOccupied,
16}
17
18impl fmt::Display for PlaceError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            PlaceError::CellOccupied => write!(f, "cell is already occupied"),
22        }
23    }
24}
25
26impl std::error::Error for PlaceError {}
27
28impl Board {
29    /// Creates a new board with P1's opening stone at the origin (0, 0).
30    pub fn new() -> Self {
31        let mut stones = HashMap::new();
32        stones.insert((0, 0), Player::P1);
33        Board { stones }
34    }
35
36    /// Places `player`'s stone at `coord`.
37    ///
38    /// Returns `Err(PlaceError::CellOccupied)` if the cell already has a stone.
39    pub fn place(&mut self, coord: Coord, player: Player) -> Result<(), PlaceError> {
40        if self.stones.contains_key(&coord) {
41            return Err(PlaceError::CellOccupied);
42        }
43        self.stones.insert(coord, player);
44        Ok(())
45    }
46
47    /// Returns the player whose stone occupies `coord`, or `None` if empty.
48    pub fn get(&self, coord: Coord) -> Option<Player> {
49        self.stones.get(&coord).copied()
50    }
51
52    /// Returns a reference to the underlying stone map.
53    pub fn stones(&self) -> &HashMap<Coord, Player> {
54        &self.stones
55    }
56
57    /// Returns the total number of stones on the board.
58    pub fn stone_count(&self) -> usize {
59        self.stones.len()
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn new_has_p1_at_origin() {
69        let board = Board::new();
70        assert_eq!(board.get((0, 0)), Some(Player::P1));
71    }
72
73    #[test]
74    fn place_on_empty_cell_succeeds() {
75        let mut board = Board::new();
76        let result = board.place((1, 0), Player::P2);
77        assert!(result.is_ok());
78        assert_eq!(board.get((1, 0)), Some(Player::P2));
79    }
80
81    #[test]
82    fn place_on_occupied_cell_fails() {
83        let mut board = Board::new();
84        // (0,0) is already occupied by P1 from new()
85        let result = board.place((0, 0), Player::P2);
86        assert_eq!(result, Err(PlaceError::CellOccupied));
87    }
88
89    #[test]
90    fn get_on_empty_cell_returns_none() {
91        let board = Board::new();
92        assert_eq!(board.get((99, 99)), None);
93    }
94
95    #[test]
96    fn stones_returns_all_placed_stones() {
97        let mut board = Board::new();
98        board.place((1, 0), Player::P2).unwrap();
99        let stones = board.stones();
100        assert_eq!(stones.len(), 2);
101        assert_eq!(stones.get(&(0, 0)), Some(&Player::P1));
102        assert_eq!(stones.get(&(1, 0)), Some(&Player::P2));
103    }
104
105    #[test]
106    fn clone_is_independent() {
107        let mut original = Board::new();
108        let mut cloned = original.clone();
109
110        // Mutate the clone
111        cloned.place((5, 5), Player::P2).unwrap();
112
113        // Original should be unaffected
114        assert_eq!(original.get((5, 5)), None);
115
116        // Mutate the original
117        original.place((3, 3), Player::P1).unwrap();
118
119        // Clone should be unaffected
120        assert_eq!(cloned.get((3, 3)), None);
121    }
122}