Skip to main content

rustoku_lib/core/
board.rs

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