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