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)]
10pub struct Board {
11    /// Each cell can contain a number from 1 to 9, or be empty (is 0).
12    pub 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    /// Creates an empty Rustoku board, where all cells are initialized to 0.
23    pub fn empty() -> Self {
24        Board {
25            cells: [[0u8; 9]; 9],
26        }
27    }
28
29    /// Gets a value from the board at the specified row and column.
30    pub fn get(&self, r: usize, c: usize) -> u8 {
31        self.cells[r][c]
32    }
33
34    /// Sets a value in the board at the specified row and column.
35    pub fn set(&mut self, r: usize, c: usize, value: u8) {
36        self.cells[r][c] = value;
37    }
38
39    /// Checks if a cell at the specified row and column is empty (contains 0).
40    pub fn is_empty(&self, r: usize, c: usize) -> bool {
41        self.cells[r][c] == 0
42    }
43
44    /// Iterates over all cells in the board, yielding their row and column indices.
45    pub fn iter_cells(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
46        (0..9).flat_map(move |r| (0..9).map(move |c| (r, c)))
47    }
48
49    /// Iterates over empty cells in the board, yielding their row and column indices.
50    pub fn iter_empty_cells(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
51        (0..9).flat_map(move |r| {
52            (0..9).filter_map(move |c| {
53                if self.is_empty(r, c) {
54                    Some((r, c))
55                } else {
56                    None
57                }
58            })
59        })
60    }
61}
62
63impl TryFrom<[u8; 81]> for Board {
64    type Error = RustokuError;
65
66    fn try_from(bytes: [u8; 81]) -> Result<Self, Self::Error> {
67        let mut board = [[0u8; 9]; 9];
68        for i in 0..81 {
69            // Validate that numbers are within 0-9 if you want strictness here,
70            // though Rustoku::new will validate initial state safety.
71            if bytes[i] > 9 {
72                return Err(RustokuError::InvalidInputCharacter); // Or a more specific error
73            }
74            board[i / 9][i % 9] = bytes[i];
75        }
76        Ok(Board::new(board)) // Construct the board
77    }
78}
79
80impl TryFrom<&str> for Board {
81    type Error = RustokuError;
82
83    fn try_from(s: &str) -> Result<Self, Self::Error> {
84        if s.len() != 81 {
85            return Err(RustokuError::InvalidInputLength);
86        }
87        let mut bytes = [0u8; 81];
88        for (i, ch) in s.bytes().enumerate() {
89            match ch {
90                b'0'..=b'9' => bytes[i] = ch - b'0',
91                b'.' | b'_' => bytes[i] = 0, // Treat '.' and '_' as empty cells
92                _ => return Err(RustokuError::InvalidInputCharacter),
93            }
94        }
95        // Now use the TryFrom<[u8; 81]> for board
96        bytes.try_into()
97    }
98}