1mod board;
11mod candidates;
12mod masks;
13mod solution;
14mod techniques;
15
16pub use board::Board;
17pub use solution::{Solution, SolvePath, SolveStep};
18pub use techniques::flags::TechniqueFlags;
19
20use crate::error::RustokuError;
21use candidates::Candidates;
22use masks::Masks;
23use rand::prelude::SliceRandom;
24use rand::rng;
25use techniques::TechniquePropagator;
26
27#[derive(Debug, Copy, Clone)]
61pub struct Rustoku {
62 pub board: Board,
64 masks: Masks,
65 candidates_cache: Candidates,
66 techniques: TechniqueFlags,
67}
68
69impl Rustoku {
70 pub fn new(initial_board: Board) -> Result<Self, RustokuError> {
72 let board = initial_board; let mut masks = Masks::new();
74 let mut candidates_cache = Candidates::new();
75
76 for r in 0..9 {
78 for c in 0..9 {
79 let num = board.get(r, c);
80 if num != 0 {
81 if !masks.is_safe(r, c, num) {
82 return Err(RustokuError::DuplicateValues);
83 }
84 masks.add_number(r, c, num);
85 }
86 }
87 }
88
89 for r in 0..9 {
91 for c in 0..9 {
92 if board.is_empty(r, c) {
93 candidates_cache.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
94 }
95 }
96 }
97
98 Ok(Self {
99 board,
100 masks,
101 candidates_cache,
102 techniques: TechniqueFlags::EASY, })
104 }
105
106 pub fn new_from_str(s: &str) -> Result<Self, RustokuError> {
108 let board = Board::try_from(s)?;
109 Self::new(board)
110 }
111
112 pub fn with_techniques(mut self, techniques: TechniqueFlags) -> Self {
114 self.techniques = techniques;
115 self
116 }
117
118 fn find_next_empty_cell(&self) -> Option<(usize, usize)> {
120 let mut min = (10, None); for (r, c) in self.board.iter_empty_cells() {
122 let count = self.candidates_cache.get(r, c).count_ones() as u8;
123 if count < min.0 {
124 min = (count, Some((r, c)));
125 if count == 1 {
126 return min.1;
127 }
128 }
129 }
130 min.1
131 }
132
133 fn place_number(&mut self, r: usize, c: usize, num: u8) {
135 self.board.set(r, c, num);
136 self.masks.add_number(r, c, num);
137 self.candidates_cache
138 .update_affected_cells(r, c, &self.masks, &self.board);
139 }
140
141 fn remove_number(&mut self, r: usize, c: usize, num: u8) {
143 self.board.set(r, c, 0); self.masks.remove_number(r, c, num);
145 self.candidates_cache
146 .update_affected_cells(r, c, &self.masks, &self.board);
147 }
149
150 fn solve_until_recursive(
152 &mut self,
153 solutions: &mut Vec<Solution>,
154 path: &mut SolvePath,
155 bound: usize,
156 ) -> usize {
157 if let Some((r, c)) = self.find_next_empty_cell() {
158 let mut count = 0;
159 let mut nums: Vec<u8> = (1..=9).collect();
160 nums.shuffle(&mut rng());
161
162 for &num in &nums {
163 if self.masks.is_safe(r, c, num) {
164 self.place_number(r, c, num);
165 path.steps.push(SolveStep::Placement {
166 row: r,
167 col: c,
168 value: num,
169 flags: TechniqueFlags::empty(),
170 });
171 count += self.solve_until_recursive(solutions, path, bound);
172 path.steps.pop();
173 self.remove_number(r, c, num);
174
175 if bound > 0 && solutions.len() >= bound {
176 return count;
177 }
178 }
179 }
180 count
181 } else {
182 solutions.push(Solution {
183 board: self.board,
184 solve_path: path.clone(),
185 });
186 1
187 }
188 }
189
190 pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
192 let mut solutions = Vec::new();
193 let mut path = SolvePath::default();
194
195 let mut propagator = TechniquePropagator::new(
196 &mut self.board,
197 &mut self.masks,
198 &mut self.candidates_cache,
199 self.techniques,
200 );
201
202 if !propagator.propagate_constraints(&mut path, 0) {
203 return solutions; }
205
206 self.solve_until_recursive(&mut solutions, &mut path, bound);
207 solutions
208 }
209
210 pub fn solve_any(&mut self) -> Option<Solution> {
212 self.solve_until(1).into_iter().next()
213 }
214
215 pub fn solve_all(&mut self) -> Vec<Solution> {
217 self.solve_until(0)
218 }
219
220 pub fn is_solved(&self) -> bool {
222 self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
223 }
224}
225
226pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
244 if !(17..=81).contains(&num_clues) {
245 return Err(RustokuError::InvalidClueCount);
246 }
247
248 let mut rustoku = Rustoku::new(Board::empty())?;
250 let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
251 let mut board = solution.board;
252
253 let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
255 cells.shuffle(&mut rng());
256
257 let mut clues = 81;
258
259 for &(r, c) in &cells {
261 if clues <= num_clues {
262 break;
263 }
264
265 let original = board.cells[r][c];
266 board.cells[r][c] = 0;
267
268 if Rustoku::new(board)?.solve_until(2).len() != 1 {
269 board.cells[r][c] = original; } else {
271 clues -= 1;
272 }
273 }
274
275 if Rustoku::new(board)?.solve_until(2).len() != 1 {
277 return Err(RustokuError::GenerateFailure);
279 }
280
281 Ok(board)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::core::board::Board;
288 use crate::error::RustokuError;
289 use crate::format::format_line;
290
291 const UNIQUE_PUZZLE: &str =
292 "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
293 const UNIQUE_SOLUTION: &str =
294 "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
295 const TWO_PUZZLE: &str =
296 "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
297 const SIX_PUZZLE: &str =
298 "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
299
300 #[test]
301 fn test_new_with_bytes_and_str() {
302 let board = [
303 [5, 3, 0, 0, 7, 0, 0, 0, 0],
304 [6, 0, 0, 1, 9, 5, 0, 0, 0],
305 [0, 9, 8, 0, 0, 0, 0, 6, 0],
306 [8, 0, 0, 0, 6, 0, 0, 0, 3],
307 [4, 0, 0, 8, 0, 3, 0, 0, 1],
308 [7, 0, 0, 0, 2, 0, 0, 0, 6],
309 [0, 6, 0, 0, 0, 0, 2, 8, 0],
310 [0, 0, 0, 4, 1, 9, 0, 0, 5],
311 [0, 0, 0, 0, 8, 0, 0, 7, 9],
312 ];
313
314 let flat_bytes: [u8; 81] = board
315 .concat()
316 .try_into()
317 .expect("Concat board to bytes failed");
318 let board_str: String = flat_bytes.iter().map(|&b| (b + b'0') as char).collect();
319
320 let board_from_new = Board::new(board);
321 let board_from_bytes = Board::try_from(flat_bytes).expect("Board from flat bytes failed");
322 let board_from_str = Board::try_from(board_str.as_str()).expect("Board from string failed");
323
324 assert_eq!(board_from_new, board_from_bytes);
325 assert_eq!(board_from_new, board_from_str);
326 assert_eq!(board_from_bytes, board_from_str);
327 }
328
329 #[test]
330 fn test_try_from_with_valid_input() {
331 let rustoku = Board::try_from(UNIQUE_PUZZLE);
332 assert!(rustoku.is_ok());
333 }
334
335 #[test]
336 fn test_try_from_with_invalid_length() {
337 let s = "530070000"; let rustoku = Board::try_from(s);
339 assert!(matches!(rustoku, Err(RustokuError::InvalidInputLength)));
340 }
341
342 #[test]
343 fn test_try_from_with_invalid_character() {
344 let s = "53007000060019500009800006080006000340080300170002000606000028000041900500008007X"; let rustoku = Board::try_from(s);
346 assert!(matches!(rustoku, Err(RustokuError::InvalidInputCharacter)));
347 }
348
349 #[test]
350 fn test_try_from_with_duplicate_initial_values() {
351 let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
352 let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
353 let rustoku = Rustoku::new(board);
354 assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
355 }
356
357 #[test]
358 fn test_solve_any_with_solvable_sudoku() {
359 let s = UNIQUE_PUZZLE;
360 let mut rustoku =
361 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
362 let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
363
364 assert_eq!(
365 UNIQUE_SOLUTION,
366 format_line(&solution.board.cells),
367 "Solution does not match the expected result"
368 );
369 }
370
371 #[test]
372 fn test_solve_any_with_unsolvable_sudoku() {
373 let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
374 let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
375 let solution = rustoku.solve_any();
376 assert!(
377 solution.is_none(),
378 "Expected no solution for this unsolvable puzzle"
379 );
380 }
381
382 #[test]
383 fn test_solve_until_with_bound() {
384 let s = UNIQUE_PUZZLE;
385 let mut rustoku =
386 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
387
388 let solutions = rustoku.solve_until(1);
389 assert_eq!(
390 1,
391 solutions.len(),
392 "Expected exactly one solution with bound = 1"
393 );
394
395 let all_solutions = rustoku.solve_until(0);
396 assert_eq!(
397 1,
398 all_solutions.len(),
399 "Expected exactly one solution for this board with bound = 0"
400 );
401
402 assert_eq!(
403 solutions[0].board, all_solutions[0].board,
404 "Solution with bound = 1 does not match the solution with bound = 0"
405 );
406 }
407
408 #[test]
409 fn test_solve_all_with_unique_puzzle() {
410 let s = UNIQUE_PUZZLE;
411 let mut rustoku =
412 Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
413 let solutions = rustoku.solve_all();
414 assert_eq!(
415 1,
416 solutions.len(),
417 "Expected a unique solution for the board"
418 );
419 }
420
421 #[test]
422 fn test_solve_all_with_two_puzzle() {
423 let s = TWO_PUZZLE;
424 let mut rustoku =
425 Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
426 let solutions = rustoku.solve_all();
427 assert_eq!(
428 2,
429 solutions.len(),
430 "Expected two solutions for the given board"
431 );
432 }
433
434 #[test]
435 fn test_solve_all_with_six_puzzle() {
436 let s = SIX_PUZZLE;
437 let mut rustoku =
438 Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
439 let solutions = rustoku.solve_all();
440 assert_eq!(
441 6,
442 solutions.len(),
443 "Expected one solution for the six puzzle"
444 );
445 }
446
447 #[test]
448 fn test_solve_any_with_all_techniques() {
449 let s = UNIQUE_PUZZLE;
450 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
451 let solution = rustoku
452 .with_techniques(TechniqueFlags::all())
453 .solve_any()
454 .expect("Solving with all techniques failed");
455
456 assert_eq!(
457 UNIQUE_SOLUTION,
458 format_line(&solution.board.cells),
459 "Solution does not match the expected result with all techniques"
460 );
461 }
462
463 #[test]
464 fn test_solve_all_with_all_techniques() {
465 let s = TWO_PUZZLE;
466 let rustoku = Rustoku::new_from_str(s)
467 .expect("Rustoku creation failed for multi-solution technique test");
468 let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
469
470 assert_eq!(
471 2,
472 solutions.len(),
473 "Expected two solutions for the given board with all techniques"
474 );
475 }
476
477 #[test]
478 fn test_generate_with_enough_clues() {
479 (20..=80).step_by(20).for_each(|num_clues| {
480 let board = generate_board(num_clues)
481 .unwrap_or_else(|_| panic!("Board generation failed for {} clues", num_clues));
482 let mut rustoku =
483 Rustoku::new(board).expect("Rustoku creation failed from generated board");
484 let clues_count = board
485 .cells
486 .iter()
487 .flatten()
488 .filter(|&&cell| cell != 0)
489 .count();
490 assert!(
491 clues_count >= num_clues,
492 "Expected at least {} clues, but found {} clues",
493 num_clues,
494 clues_count
495 );
496
497 let solutions = rustoku.solve_all();
498 assert_eq!(
499 1,
500 solutions.len(),
501 "Generated puzzle with {} clues should have a unique solution",
502 num_clues
503 );
504 })
505 }
506
507 #[test]
508 fn test_generate_with_too_few_clues() {
509 let num_clues = 16;
510 let result = generate_board(num_clues);
511 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
512 }
513
514 #[test]
515 fn test_generate_with_too_many_clues() {
516 let num_clues = 82;
517 let result = generate_board(num_clues);
518 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
519 }
520
521 #[test]
522 fn test_is_solved_with_valid_solution() {
523 let s = UNIQUE_SOLUTION;
524 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
525 assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
526 }
527
528 #[test]
529 fn test_is_solved_with_unsolved_board() {
530 let s = UNIQUE_PUZZLE;
531 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
532 assert!(!rustoku.is_solved(), "The board should not be valid");
533 }
534}