Skip to main content

rustoku_lib/core/
candidates.rs

1use super::board::Board;
2use super::masks::Masks;
3
4/// Represents the candidates cache for a Rustoku puzzle.
5///
6/// This struct holds a 9x9 grid of candidate masks for each cell in the Rustoku board.
7/// Each cell's candidates are represented as a bitmask, where each bit corresponds to a number
8/// from 1 to 9. A bit set to 1 indicates that the corresponding number is a candidate for that cell.
9/// This struct provides methods to get and set candidate masks for specific cells, as well as to
10/// update the candidates based on the current state of the board and masks.
11#[derive(Debug, Copy, Clone, PartialEq, Eq)]
12pub struct Candidates {
13    cache: [[u16; 9]; 9],
14}
15
16impl Candidates {
17    pub(super) fn new() -> Self {
18        Candidates { cache: [[0; 9]; 9] }
19    }
20
21    /// Returns the candidate mask for a specific cell in the cache.
22    #[inline]
23    pub(super) fn get(&self, r: usize, c: usize) -> u16 {
24        self.cache[r][c]
25    }
26
27    /// Returns the actual candidate numbers (1-9) for a specific cell.
28    pub fn get_candidates(&self, r: usize, c: usize) -> Vec<u8> {
29        let mask = self.get(r, c);
30        let mut candidates = Vec::new();
31        for i in 0..9 {
32            // Check if the i-th bit is set (representing number i+1)
33            if (mask >> i) & 1 == 1 {
34                candidates.push((i + 1) as u8);
35            }
36        }
37        candidates
38    }
39
40    /// Sets the candidate mask for a specific cell in the cache.
41    #[inline]
42    pub(super) fn set(&mut self, r: usize, c: usize, mask: u16) {
43        self.cache[r][c] = mask;
44    }
45
46    /// Update affected cells in the cache based on the current state of the board and masks.
47    /// Used for both placement and removal.
48    pub(super) fn update_affected_cells(
49        &mut self,
50        r: usize,
51        c: usize,
52        masks: &Masks,
53        board: &Board,
54    ) {
55        self.update_affected_cells_for(r, c, masks, board, None);
56    }
57
58    /// Update affected cells, optionally filtering by a placed number.
59    /// When `placed_num` is Some, only recomputes cells that had that number as a candidate
60    /// (plus the placed cell itself and box cells). This is an optimization for placements.
61    /// When `placed_num` is None, recomputes all affected cells (needed for removals).
62    pub(super) fn update_affected_cells_for(
63        &mut self,
64        r: usize,
65        c: usize,
66        masks: &Masks,
67        board: &Board,
68        placed_num: Option<u8>,
69    ) {
70        // Invalidate/update cache for the target cell
71        if board.is_empty(r, c) {
72            // Removal case: recompute candidates for the now-empty cell
73            self.cache[r][c] = masks.compute_candidates_mask_for_cell(r, c);
74        } else {
75            // Placement case: no candidates for a filled cell
76            self.cache[r][c] = 0;
77        }
78
79        let num_bit = placed_num.map(|n| 1u16 << (n - 1));
80
81        // Update cache for affected row and column
82        for i in 0..9 {
83            if board.is_empty(r, i) && i != c {
84                // Skip if we know the placed number and this cell didn't have it as a candidate
85                if let Some(bit) = num_bit
86                    && self.cache[r][i] & bit == 0
87                {
88                    continue;
89                }
90                self.cache[r][i] = masks.compute_candidates_mask_for_cell(r, i);
91            }
92            if board.is_empty(i, c) && i != r {
93                if let Some(bit) = num_bit
94                    && self.cache[i][c] & bit == 0
95                {
96                    continue;
97                }
98                self.cache[i][c] = masks.compute_candidates_mask_for_cell(i, c);
99            }
100        }
101
102        // Update box cells (excluding cells already handled by row/col above)
103        let box_idx = Masks::get_box_idx(r, c);
104        let start_row = (box_idx / 3) * 3;
105        let start_col = (box_idx % 3) * 3;
106        for r_offset in 0..3 {
107            for c_offset in 0..3 {
108                let cur_r = start_row + r_offset;
109                let cur_c = start_col + c_offset;
110                // Skip the placed cell and cells already updated in the row/col pass
111                if (cur_r == r) || (cur_c == c) {
112                    continue;
113                }
114                if board.is_empty(cur_r, cur_c) {
115                    if let Some(bit) = num_bit
116                        && self.cache[cur_r][cur_c] & bit == 0
117                    {
118                        continue;
119                    }
120                    self.cache[cur_r][cur_c] = masks.compute_candidates_mask_for_cell(cur_r, cur_c);
121                }
122            }
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_get_candidates_empty_mask() {
133        let candidates = Candidates::new(); // Starts with all 0s
134        let r = 0;
135        let c = 0;
136        let cands: Vec<u8> = candidates.get_candidates(r, c);
137        assert_eq!(cands, Vec::<u8>::new());
138    }
139
140    #[test]
141    fn test_get_candidates_full_mask() {
142        let mut candidates = Candidates::new();
143        let r = 0;
144        let c = 0;
145        // All bits from 0 to 8 set (representing numbers 1 to 9)
146        // 0b00000001_11111111 = 511 (binary)
147        let full_mask = (1 << 9) - 1; // Or 0b111111111
148        candidates.set(r, c, full_mask);
149        let cands = candidates.get_candidates(r, c);
150        assert_eq!(cands, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
151    }
152
153    #[test]
154    fn test_get_candidates_single_candidate() {
155        let mut candidates = Candidates::new();
156        let r = 1;
157        let c = 2;
158
159        // Test for candidate 1 (bit 0)
160        candidates.set(r, c, 1 << 0); // Mask: 0b000000001
161        let cands_1 = candidates.get_candidates(r, c);
162        assert_eq!(cands_1, vec![1]);
163
164        // Test for candidate 5 (bit 4)
165        candidates.set(r, c, 1 << 4); // Mask: 0b000010000
166        let cands_5 = candidates.get_candidates(r, c);
167        assert_eq!(cands_5, vec![5]);
168
169        // Test for candidate 9 (bit 8)
170        candidates.set(r, c, 1 << 8); // Mask: 0b100000000
171        let cands_9 = candidates.get_candidates(r, c);
172        assert_eq!(cands_9, vec![9]);
173    }
174
175    #[test]
176    fn test_get_candidates_multiple_candidates() {
177        let mut candidates = Candidates::new();
178        let r = 3;
179        let c = 4;
180
181        // Candidates: 2, 4, 7
182        // Bit positions: 1, 3, 6
183        // Mask: (1 << 1) | (1 << 3) | (1 << 6)
184        // Mask: 0b01001010 = 2 + 8 + 64 = 74
185        let mask = (1 << 1) | (1 << 3) | (1 << 6); // 0b01001010
186        candidates.set(r, c, mask);
187        let cands = candidates.get_candidates(r, c);
188        assert_eq!(cands, vec![2, 4, 7]);
189
190        // Candidates: 1, 9
191        // Bit positions: 0, 8
192        let mask_1_9 = (1 << 0) | (1 << 8); // 0b100000001
193        candidates.set(r, c, mask_1_9);
194        let cands_1_9 = candidates.get_candidates(r, c);
195        assert_eq!(cands_1_9, vec![1, 9]);
196    }
197
198    #[test]
199    fn test_get_candidates_different_cells() {
200        let mut candidates = Candidates::new();
201
202        // Set candidates for (0,0)
203        candidates.set(0, 0, (1 << 0) | (1 << 2)); // Candidates 1, 3
204        // Set candidates for (8,8)
205        candidates.set(8, 8, (1 << 5) | (1 << 7)); // Candidates 6, 8
206
207        assert_eq!(candidates.get_candidates(0, 0), vec![1, 3]);
208        assert_eq!(candidates.get_candidates(8, 8), vec![6, 8]);
209        assert_eq!(candidates.get_candidates(0, 1), Vec::<u8>::new()); // Unset cell
210    }
211}