Skip to main content

rustoku_lib/core/
board.rs

1use crate::error::RustokuError;
2
3/// Raw 9x9 board with some useful helpers.
4///
5/// There are multiple ways to create a `Board`:
6/// - Using a 2D array of `u8` with dimensions 9x9
7/// - Using a 1D array of `u8` with length 81
8/// - Using a string representation with length 81
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub struct Board {
11    /// Each cell can contain a number from 1 to 9, or be empty (is 0).
12    pub(crate) cells: [[u8; 9]; 9],
13}
14
15impl Board {
16    pub fn new(initial_board: [[u8; 9]; 9]) -> Self {
17        Board {
18            cells: initial_board,
19        }
20    }
21
22    /// Gets a value from the board at the specified row and column.
23    pub fn get(&self, r: usize, c: usize) -> u8 {
24        self.cells[r][c]
25    }
26
27    /// Sets a value in the board at the specified row and column.
28    pub(super) fn set(&mut self, r: usize, c: usize, value: u8) {
29        self.cells[r][c] = value;
30    }
31
32    /// Checks if a cell at the specified row and column is empty (contains 0).
33    pub fn is_empty(&self, r: usize, c: usize) -> bool {
34        self.cells[r][c] == 0
35    }
36
37    /// Iterates over all cells in the board, yielding their row and column indices.
38    pub fn iter_cells(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
39        (0..9).flat_map(move |r| (0..9).map(move |c| (r, c)))
40    }
41
42    /// Iterates over empty cells in the board, yielding their row and column indices.
43    pub fn iter_empty_cells(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
44        (0..9).flat_map(move |r| {
45            (0..9).filter_map(move |c| {
46                if self.is_empty(r, c) {
47                    Some((r, c))
48                } else {
49                    None
50                }
51            })
52        })
53    }
54}
55
56impl TryFrom<[u8; 81]> for Board {
57    type Error = RustokuError;
58
59    fn try_from(bytes: [u8; 81]) -> Result<Self, Self::Error> {
60        let mut board = [[0u8; 9]; 9];
61        for i in 0..81 {
62            // Validate that numbers are within 0-9 if you want strictness here,
63            // though Rustoku::new will validate initial state safety.
64            if bytes[i] > 9 {
65                return Err(RustokuError::InvalidInputCharacter); // Or a more specific error
66            }
67            board[i / 9][i % 9] = bytes[i];
68        }
69        Ok(Board::new(board)) // Construct the board
70    }
71}
72
73impl TryFrom<&str> for Board {
74    type Error = RustokuError;
75
76    fn try_from(s: &str) -> Result<Self, Self::Error> {
77        if s.len() != 81 {
78            return Err(RustokuError::InvalidInputLength);
79        }
80        let mut bytes = [0u8; 81];
81        for (i, ch) in s.bytes().enumerate() {
82            match ch {
83                b'0'..=b'9' => bytes[i] = ch - b'0',
84                b'.' | b'_' => bytes[i] = 0, // Treat '.' and '_' as empty cells
85                _ => return Err(RustokuError::InvalidInputCharacter),
86            }
87        }
88        // Now use the TryFrom<[u8; 81]> for board
89        bytes.try_into()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::error::RustokuError;
97
98    const UNIQUE_PUZZLE: &str =
99        "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
100
101    #[test]
102    fn test_new_with_bytes_and_str() {
103        let board = [
104            [5, 3, 0, 0, 7, 0, 0, 0, 0],
105            [6, 0, 0, 1, 9, 5, 0, 0, 0],
106            [0, 9, 8, 0, 0, 0, 0, 6, 0],
107            [8, 0, 0, 0, 6, 0, 0, 0, 3],
108            [4, 0, 0, 8, 0, 3, 0, 0, 1],
109            [7, 0, 0, 0, 2, 0, 0, 0, 6],
110            [0, 6, 0, 0, 0, 0, 2, 8, 0],
111            [0, 0, 0, 4, 1, 9, 0, 0, 5],
112            [0, 0, 0, 0, 8, 0, 0, 7, 9],
113        ];
114
115        let flat_bytes: [u8; 81] = board
116            .concat()
117            .try_into()
118            .expect("Concat board to bytes failed");
119        let board_str: String = flat_bytes.iter().map(|&b| (b + b'0') as char).collect();
120
121        let board_from_new = Board::new(board);
122        let board_from_bytes = Board::try_from(flat_bytes).expect("Board from flat bytes failed");
123        let board_from_str = Board::try_from(board_str.as_str()).expect("Board from string failed");
124
125        assert_eq!(board_from_new, board_from_bytes);
126        assert_eq!(board_from_new, board_from_str);
127        assert_eq!(board_from_bytes, board_from_str);
128    }
129
130    #[test]
131    fn test_try_from_with_valid_input() {
132        let rustoku = Board::try_from(UNIQUE_PUZZLE);
133        assert!(rustoku.is_ok());
134    }
135
136    #[test]
137    fn test_try_from_with_invalid_length() {
138        let s = "530070000"; // Too short
139        let rustoku = Board::try_from(s);
140        assert!(matches!(rustoku, Err(RustokuError::InvalidInputLength)));
141    }
142
143    #[test]
144    fn test_try_from_with_invalid_character() {
145        let s = "53007000060019500009800006080006000340080300170002000606000028000041900500008007X"; // 'X'
146        let rustoku = Board::try_from(s);
147        assert!(matches!(rustoku, Err(RustokuError::InvalidInputCharacter)));
148    }
149}