1mod board;
11mod candidates;
12mod masks;
13mod solution;
14mod techniques;
15
16pub use board::Board;
17pub use candidates::Candidates;
18pub use masks::Masks;
19pub use solution::{Solution, SolvePath, SolveStep};
20pub use techniques::flags::TechniqueFlags;
21
22use crate::error::RustokuError;
23use rand::prelude::SliceRandom;
24use rand::rng;
25use techniques::TechniquePropagator;
26
27#[derive(Debug, Copy, Clone)]
61pub struct Rustoku {
62 pub board: Board,
64 pub masks: Masks,
66 pub candidates: Candidates,
68 pub techniques: TechniqueFlags,
70}
71
72impl Rustoku {
73 pub fn new(initial_board: Board) -> Result<Self, RustokuError> {
75 let board = initial_board; let mut masks = Masks::new();
77 let mut candidates = Candidates::new();
78
79 for r in 0..9 {
81 for c in 0..9 {
82 let num = board.get(r, c);
83 if num != 0 {
84 if !masks.is_safe(r, c, num) {
85 return Err(RustokuError::DuplicateValues);
86 }
87 masks.add_number(r, c, num);
88 }
89 }
90 }
91
92 for r in 0..9 {
94 for c in 0..9 {
95 if board.is_empty(r, c) {
96 candidates.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
97 }
98 }
99 }
100
101 Ok(Self {
102 board,
103 masks,
104 candidates,
105 techniques: TechniqueFlags::EASY, })
107 }
108
109 pub fn builder() -> RustokuBuilder {
111 RustokuBuilder::new()
112 }
113
114 pub fn new_from_str(s: &str) -> Result<Self, RustokuError> {
116 let board = Board::try_from(s)?;
117 Self::new(board)
118 }
119
120 pub fn with_techniques(mut self, techniques: TechniqueFlags) -> Self {
122 self.techniques = techniques;
123 self
124 }
125
126 fn candidates_from_mask(mask: u16) -> Vec<u8> {
128 let mut nums = Vec::with_capacity(mask.count_ones() as usize);
129 for v in 1..=9u8 {
130 if mask & (1 << (v - 1)) != 0 {
131 nums.push(v);
132 }
133 }
134 nums
135 }
136
137 fn find_next_empty_cell(&self) -> Option<(usize, usize)> {
139 let mut min = (10, None); for (r, c) in self.board.iter_empty_cells() {
141 let count = self.candidates.get(r, c).count_ones() as u8;
142 if count < min.0 {
143 min = (count, Some((r, c)));
144 if count == 1 {
145 return min.1;
146 }
147 }
148 }
149 min.1
150 }
151
152 fn place_number(&mut self, r: usize, c: usize, num: u8) {
154 self.board.set(r, c, num);
155 self.masks.add_number(r, c, num);
156 self.candidates
157 .update_affected_cells_for(r, c, &self.masks, &self.board, Some(num));
158 }
159
160 fn remove_number(&mut self, r: usize, c: usize, num: u8) {
162 self.board.set(r, c, 0); self.masks.remove_number(r, c, num);
164 self.candidates
165 .update_affected_cells(r, c, &self.masks, &self.board);
166 }
168
169 fn solve_until_recursive(
171 &mut self,
172 solutions: &mut Vec<Solution>,
173 path: &mut SolvePath,
174 bound: usize,
175 ) -> usize {
176 let Some((r, c)) = self.find_next_empty_cell() else {
178 solutions.push(Solution {
179 board: self.board,
180 solve_path: path.clone(),
181 });
182 return 1;
183 };
184
185 let mut count = 0;
186 let mask = self.candidates.get(r, c);
188 let mut nums = Self::candidates_from_mask(mask);
189 nums.shuffle(&mut rng());
190
191 for &num in &nums {
192 if !self.masks.is_safe(r, c, num) {
193 continue;
194 }
195
196 self.place_number(r, c, num);
197 let step_number = path.steps.len() as u32;
198 path.steps.push(SolveStep::Placement {
199 row: r,
200 col: c,
201 value: num,
202 flags: TechniqueFlags::empty(),
203 step_number,
204 candidates_eliminated: 0,
205 related_cell_count: 0,
206 difficulty_point: 0,
207 });
208
209 count += self.solve_until_recursive(solutions, path, bound);
210 path.steps.pop();
211 self.remove_number(r, c, num);
212
213 if bound > 0 && solutions.len() >= bound {
215 return count;
216 }
217 }
218
219 count
220 }
221
222 fn techniques_make_valid_changes(&mut self, path: &mut SolvePath) -> bool {
224 let mut propagator = TechniquePropagator::new(
225 &mut self.board,
226 &mut self.masks,
227 &mut self.candidates,
228 self.techniques,
229 );
230 propagator.propagate_constraints(path, 0)
231 }
232
233 pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
235 let mut solutions = Vec::new();
236 let mut path = SolvePath::default();
237
238 if !self.techniques_make_valid_changes(&mut path) {
239 return solutions;
240 }
241
242 self.solve_until_recursive(&mut solutions, &mut path, bound);
243 solutions
244 }
245
246 pub fn solve_any(&mut self) -> Option<Solution> {
248 self.solve_until(1).into_iter().next()
249 }
250
251 pub fn solve_all(&mut self) -> Vec<Solution> {
253 use rayon::prelude::*;
254
255 let mut path = SolvePath::default();
257 if !self.techniques_make_valid_changes(&mut path) {
258 return Vec::new();
259 }
260
261 if let Some((r, c)) = self.find_next_empty_cell() {
263 let mask = self.candidates.get(r, c);
264 let nums = Self::candidates_from_mask(mask);
265
266 let initial_path = path.clone();
267
268 let chunks: Vec<Vec<Solution>> = nums
270 .par_iter()
271 .map(|&num| {
272 let mut cloned = *self; let mut local_solutions: Vec<Solution> = Vec::new();
274 let mut local_path = initial_path.clone();
275
276 cloned.place_number(r, c, num);
278 let step_number = local_path.steps.len() as u32;
279 local_path.steps.push(SolveStep::Placement {
280 row: r,
281 col: c,
282 value: num,
283 flags: TechniqueFlags::empty(),
284 step_number,
285 candidates_eliminated: 0,
286 related_cell_count: 0,
287 difficulty_point: 0,
288 });
289
290 cloned.solve_until_recursive(&mut local_solutions, &mut local_path, 0);
292 local_solutions
293 })
294 .collect();
295
296 let mut solutions = Vec::new();
298 for mut s in chunks {
299 solutions.append(&mut s);
300 }
301 solutions
302 } else {
303 vec![Solution {
305 board: self.board,
306 solve_path: path,
307 }]
308 }
309 }
310
311 pub fn is_solved(&self) -> bool {
313 self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
314 }
315}
316
317pub struct RustokuBuilder {
319 board: Option<Board>,
320 techniques: TechniqueFlags,
321 max_solutions: Option<usize>,
322}
323
324impl RustokuBuilder {
325 pub fn new() -> Self {
327 RustokuBuilder {
328 board: None,
329 techniques: TechniqueFlags::EASY,
330 max_solutions: None,
331 }
332 }
333}
334
335impl Default for RustokuBuilder {
336 fn default() -> Self {
337 Self::new()
338 }
339}
340
341impl RustokuBuilder {
342 pub fn board(mut self, board: Board) -> Self {
344 self.board = Some(board);
345 self
346 }
347
348 pub fn board_from_str(mut self, s: &str) -> Result<Self, RustokuError> {
350 let board = Board::try_from(s)?;
351 self.board = Some(board);
352 Ok(self)
353 }
354
355 pub fn techniques(mut self, techniques: TechniqueFlags) -> Self {
357 self.techniques = techniques;
358 self
359 }
360
361 pub fn max_solutions(mut self, max: usize) -> Self {
363 self.max_solutions = Some(max);
364 self
365 }
366
367 pub fn build(self) -> Result<Rustoku, RustokuError> {
369 let board = self.board.unwrap_or_default();
370 let mut r = Rustoku::new(board)?;
371 r.techniques = self.techniques;
372 Ok(r)
375 }
376}
377
378#[derive(Debug)]
381pub struct Solutions {
382 solver: Rustoku,
383 path: SolvePath,
384 stack: Vec<Frame>,
385 finished: bool,
386}
387
388#[derive(Debug)]
389struct Frame {
390 r: usize,
391 c: usize,
392 nums: Vec<u8>,
393 idx: usize,
394 placed: Option<u8>,
395}
396
397impl Solutions {
398 pub fn from_solver(mut solver: Rustoku) -> Self {
401 let mut path = SolvePath::default();
402 let mut finished = false;
403
404 if !solver.techniques_make_valid_changes(&mut path) {
405 finished = true;
406 }
407
408 let mut stack = Vec::new();
409 if !finished {
410 if let Some((r, c)) = solver.find_next_empty_cell() {
411 let mask = solver.candidates.get(r, c);
412 let mut nums = Rustoku::candidates_from_mask(mask);
413 nums.shuffle(&mut rng());
414 stack.push(Frame {
415 r,
416 c,
417 nums,
418 idx: 0,
419 placed: None,
420 });
421 } else {
422 }
424 }
425
426 Solutions {
427 solver,
428 path,
429 stack,
430 finished,
431 }
432 }
433}
434
435impl Iterator for Solutions {
436 type Item = Solution;
437
438 fn next(&mut self) -> Option<Self::Item> {
439 if self.finished {
440 return None;
441 }
442
443 loop {
444 if self.stack.is_empty() {
446 if let Some((r, c)) = self.solver.find_next_empty_cell() {
447 let mask = self.solver.candidates.get(r, c);
448 let mut nums = Rustoku::candidates_from_mask(mask);
449 nums.shuffle(&mut rng());
450 self.stack.push(Frame {
451 r,
452 c,
453 nums,
454 idx: 0,
455 placed: None,
456 });
457 continue;
458 } else {
459 let sol = Solution {
461 board: self.solver.board,
462 solve_path: self.path.clone(),
463 };
464 self.finished = true;
465 return Some(sol);
466 }
467 }
468
469 let last_idx = self.stack.len() - 1;
470 let frame = &mut self.stack[last_idx];
471
472 if frame.idx >= frame.nums.len() {
474 if let Some(num) = frame.placed {
475 self.solver.remove_number(frame.r, frame.c, num);
477 self.path.steps.pop();
478 frame.placed = None;
479 } else {
480 self.stack.pop();
482 }
483 continue;
484 }
485
486 let num = frame.nums[frame.idx];
487 frame.idx += 1;
488
489 if self.solver.masks.is_safe(frame.r, frame.c, num) {
490 self.solver.place_number(frame.r, frame.c, num);
492 let step_number = self.path.steps.len() as u32;
493 self.path.steps.push(SolveStep::Placement {
494 row: frame.r,
495 col: frame.c,
496 value: num,
497 flags: TechniqueFlags::empty(),
498 step_number,
499 candidates_eliminated: 0,
500 related_cell_count: 0,
501 difficulty_point: 0,
502 });
503 frame.placed = Some(num);
504
505 if let Some((nr, nc)) = self.solver.find_next_empty_cell() {
507 let mask = self.solver.candidates.get(nr, nc);
508 let mut nums2 = Rustoku::candidates_from_mask(mask);
509 nums2.shuffle(&mut rng());
510 self.stack.push(Frame {
511 r: nr,
512 c: nc,
513 nums: nums2,
514 idx: 0,
515 placed: None,
516 });
517 continue;
518 } else {
519 let solution = Solution {
521 board: self.solver.board,
522 solve_path: self.path.clone(),
523 };
524 if let Some(pnum) = frame.placed {
526 self.solver.remove_number(frame.r, frame.c, pnum);
527 self.path.steps.pop();
528 frame.placed = None;
529 }
530 return Some(solution);
531 }
532 }
533 }
535 }
536}
537
538pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
555 if !(17..=81).contains(&num_clues) {
556 return Err(RustokuError::InvalidClueCount);
557 }
558
559 let mut rustoku = Rustoku::new(Board::default())?;
561 let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
562 let mut board = solution.board;
563
564 let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
566 cells.shuffle(&mut rng());
567
568 let mut clues = 81;
569
570 for &(r, c) in &cells {
572 if clues <= num_clues {
573 break;
574 }
575
576 let original = board.cells[r][c];
577 board.cells[r][c] = 0;
578
579 if Rustoku::new(board)?.solve_until(2).len() != 1 {
580 board.cells[r][c] = original; } else {
582 clues -= 1;
583 }
584 }
585
586 if Rustoku::new(board)?.solve_until(2).len() != 1 {
588 return Err(RustokuError::GenerateFailure);
590 }
591
592 Ok(board)
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598 use crate::core::board::Board;
599 use crate::error::RustokuError;
600 use crate::format::format_line;
601
602 const UNIQUE_PUZZLE: &str =
603 "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
604 const UNIQUE_SOLUTION: &str =
605 "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
606 const TWO_PUZZLE: &str =
607 "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
608 const SIX_PUZZLE: &str =
609 "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
610
611 #[test]
612 fn test_new_with_bytes_and_str() {
613 let board = [
614 [5, 3, 0, 0, 7, 0, 0, 0, 0],
615 [6, 0, 0, 1, 9, 5, 0, 0, 0],
616 [0, 9, 8, 0, 0, 0, 0, 6, 0],
617 [8, 0, 0, 0, 6, 0, 0, 0, 3],
618 [4, 0, 0, 8, 0, 3, 0, 0, 1],
619 [7, 0, 0, 0, 2, 0, 0, 0, 6],
620 [0, 6, 0, 0, 0, 0, 2, 8, 0],
621 [0, 0, 0, 4, 1, 9, 0, 0, 5],
622 [0, 0, 0, 0, 8, 0, 0, 7, 9],
623 ];
624
625 let flat_bytes: [u8; 81] = board
626 .concat()
627 .try_into()
628 .expect("Concat board to bytes failed");
629 let board_str: String = flat_bytes.iter().map(|&b| (b + b'0') as char).collect();
630
631 let board_from_new = Board::new(board);
632 let board_from_bytes = Board::try_from(flat_bytes).expect("Board from flat bytes failed");
633 let board_from_str = Board::try_from(board_str.as_str()).expect("Board from string failed");
634
635 assert_eq!(board_from_new, board_from_bytes);
636 assert_eq!(board_from_new, board_from_str);
637 assert_eq!(board_from_bytes, board_from_str);
638 }
639
640 #[test]
641 fn test_try_from_with_valid_input() {
642 let rustoku = Board::try_from(UNIQUE_PUZZLE);
643 assert!(rustoku.is_ok());
644 }
645
646 #[test]
647 fn test_try_from_with_invalid_length() {
648 let s = "530070000"; let rustoku = Board::try_from(s);
650 assert!(matches!(rustoku, Err(RustokuError::InvalidInputLength)));
651 }
652
653 #[test]
654 fn test_try_from_with_invalid_character() {
655 let s = "53007000060019500009800006080006000340080300170002000606000028000041900500008007X"; let rustoku = Board::try_from(s);
657 assert!(matches!(rustoku, Err(RustokuError::InvalidInputCharacter)));
658 }
659
660 #[test]
661 fn test_builder_and_iterator() {
662 let board = Board::try_from(UNIQUE_PUZZLE).expect("valid puzzle");
663 let solver = Rustoku::builder()
664 .board(board)
665 .techniques(TechniqueFlags::all())
666 .build()
667 .expect("builder build");
668
669 let mut sols = Solutions::from_solver(solver);
671 let first = sols.next();
672 assert!(first.is_some());
673 assert!(sols.next().is_none());
675 }
676
677 #[test]
678 fn test_try_from_with_duplicate_initial_values() {
679 let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
680 let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
681 let rustoku = Rustoku::new(board);
682 assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
683 }
684
685 #[test]
686 fn test_solve_any_with_solvable_sudoku() {
687 let s = UNIQUE_PUZZLE;
688 let mut rustoku =
689 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
690 let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
691
692 assert_eq!(
693 UNIQUE_SOLUTION,
694 format_line(&solution.board),
695 "Solution does not match the expected result"
696 );
697 }
698
699 #[test]
700 fn test_solve_any_with_unsolvable_sudoku() {
701 let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
702 let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
703 let solution = rustoku.solve_any();
704 assert!(
705 solution.is_none(),
706 "Expected no solution for this unsolvable puzzle"
707 );
708 }
709
710 #[test]
711 fn test_solve_until_with_bound() {
712 let s = UNIQUE_PUZZLE;
713 let mut rustoku =
714 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
715
716 let solutions = rustoku.solve_until(1);
717 assert_eq!(
718 1,
719 solutions.len(),
720 "Expected exactly one solution with bound = 1"
721 );
722
723 let all_solutions = rustoku.solve_until(0);
724 assert_eq!(
725 1,
726 all_solutions.len(),
727 "Expected exactly one solution for this board with bound = 0"
728 );
729
730 assert_eq!(
731 solutions[0].board, all_solutions[0].board,
732 "Solution with bound = 1 does not match the solution with bound = 0"
733 );
734 }
735
736 #[test]
737 fn test_solve_all_with_unique_puzzle() {
738 let s = UNIQUE_PUZZLE;
739 let mut rustoku =
740 Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
741 let solutions = rustoku.solve_all();
742 assert_eq!(
743 1,
744 solutions.len(),
745 "Expected a unique solution for the board"
746 );
747 }
748
749 #[test]
750 fn test_solve_all_with_two_puzzle() {
751 let s = TWO_PUZZLE;
752 let mut rustoku =
753 Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
754 let solutions = rustoku.solve_all();
755 assert_eq!(
756 2,
757 solutions.len(),
758 "Expected two solutions for the given board"
759 );
760 }
761
762 #[test]
763 fn test_solve_all_with_six_puzzle() {
764 let s = SIX_PUZZLE;
765 let mut rustoku =
766 Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
767 let solutions = rustoku.solve_all();
768 assert_eq!(
769 6,
770 solutions.len(),
771 "Expected one solution for the six puzzle"
772 );
773 }
774
775 #[test]
776 fn test_solve_any_with_all_techniques() {
777 let s = UNIQUE_PUZZLE;
778 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
779 let solution = rustoku
780 .with_techniques(TechniqueFlags::all())
781 .solve_any()
782 .expect("Solving with all techniques failed");
783
784 assert_eq!(
785 UNIQUE_SOLUTION,
786 format_line(&solution.board),
787 "Solution does not match the expected result with all techniques"
788 );
789 }
790
791 #[test]
792 fn test_solve_all_with_all_techniques() {
793 let s = TWO_PUZZLE;
794 let rustoku = Rustoku::new_from_str(s)
795 .expect("Rustoku creation failed for multi-solution technique test");
796 let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
797
798 assert_eq!(
799 2,
800 solutions.len(),
801 "Expected two solutions for the given board with all techniques"
802 );
803 }
804
805 #[test]
806 fn test_generate_with_enough_clues() {
807 (20..=80).step_by(20).for_each(|num_clues| {
808 let board = generate_board(num_clues)
809 .unwrap_or_else(|_| panic!("Board generation failed for {num_clues} clues"));
810 let mut rustoku =
811 Rustoku::new(board).expect("Rustoku creation failed from generated board");
812 let clues_count = board
813 .cells
814 .iter()
815 .flatten()
816 .filter(|&&cell| cell != 0)
817 .count();
818 assert!(
819 clues_count >= num_clues,
820 "Expected at least {num_clues} clues, but found {clues_count} clues"
821 );
822
823 let solutions = rustoku.solve_all();
824 assert_eq!(
825 1,
826 solutions.len(),
827 "Generated puzzle with {num_clues} clues should have a unique solution"
828 );
829 })
830 }
831
832 #[test]
833 fn test_generate_with_too_few_clues() {
834 let num_clues = 16;
835 let result = generate_board(num_clues);
836 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
837 }
838
839 #[test]
840 fn test_generate_with_too_many_clues() {
841 let num_clues = 82;
842 let result = generate_board(num_clues);
843 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
844 }
845
846 #[test]
847 fn test_is_solved_with_valid_solution() {
848 let s = UNIQUE_SOLUTION;
849 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
850 assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
851 }
852
853 #[test]
854 fn test_is_solved_with_unsolved_board() {
855 let s = UNIQUE_PUZZLE;
856 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
857 assert!(!rustoku.is_solved(), "The board should not be valid");
858 }
859
860 struct TechniqueTestCase<'a> {
861 name: &'a str,
862 trigger_string: &'a str,
863 technique_flag: TechniqueFlags,
864 }
865
866 #[test]
867 fn test_each_technique_makes_valid_changes() {
868 let test_cases = vec![
869 TechniqueTestCase {
871 name: "Naked Singles",
872 trigger_string: "385421967194756328627983145571892634839645271246137589462579813918364752753218490",
873 technique_flag: TechniqueFlags::NAKED_SINGLES,
874 },
875 TechniqueTestCase {
877 name: "Hidden Singles",
878 trigger_string: "008007000016083000000000051107290000000000000000046307290000000000860140000300700",
879 technique_flag: TechniqueFlags::HIDDEN_SINGLES,
880 },
881 TechniqueTestCase {
883 name: "Naked Pairs",
884 trigger_string: "700009030000105006400260009002083951007000000005600000000000003100000060000004010",
885 technique_flag: TechniqueFlags::NAKED_PAIRS,
886 },
887 TechniqueTestCase {
889 name: "Hidden Pairs",
890 trigger_string: "000032000000000000007600914096000800005008000030040005050200000700000560904010000",
891 technique_flag: TechniqueFlags::EASY | TechniqueFlags::HIDDEN_PAIRS,
893 },
894 TechniqueTestCase {
896 name: "Locked Candidates",
897 trigger_string: "984000000000500040000000002006097200003002000000000010005060003407051890030009700",
898 technique_flag: TechniqueFlags::LOCKED_CANDIDATES,
899 },
900 TechniqueTestCase {
902 name: "X-Wing",
903 trigger_string: "000000000760003002002640009403900070000004903005000020010560000370090041000000060",
904 technique_flag: TechniqueFlags::XWING,
905 },
906 ];
907
908 for test_case in test_cases {
909 let rustoku = Rustoku::new_from_str(test_case.trigger_string)
910 .unwrap_or_else(|_| panic!("Rustoku creation failed for '{}'", test_case.name));
911 let mut path = SolvePath::default();
912 assert!(
913 rustoku
914 .with_techniques(test_case.technique_flag)
915 .techniques_make_valid_changes(&mut path),
916 "Propagation should not contradict for '{}'",
917 test_case.name
918 );
919 assert!(
920 !path.steps.is_empty(),
921 "Expected at least one placement or elimination for '{}'",
922 test_case.name
923 )
924 }
925 }
926
927 #[test]
930 fn test_naked_singles_places_correct_value() {
931 let s = "385421967194756328627983145571892634839645271246137589462579813918364752753218490";
933 let mut rustoku = Rustoku::new_from_str(s)
934 .unwrap()
935 .with_techniques(TechniqueFlags::NAKED_SINGLES);
936 let mut path = SolvePath::default();
937 rustoku.techniques_make_valid_changes(&mut path);
938
939 let placements: Vec<_> = path
941 .steps
942 .iter()
943 .filter_map(|step| match step {
944 SolveStep::Placement {
945 row, col, value, ..
946 } => Some((*row, *col, *value)),
947 _ => None,
948 })
949 .collect();
950
951 assert!(
952 placements.contains(&(8, 8, 6)),
953 "Expected placement of 6 at (8,8), got {:?}",
954 placements
955 );
956 }
957
958 #[test]
959 fn test_hidden_singles_places_in_correct_cell() {
960 let s = "008007000016083000000000051107290000000000000000046307290000000000860140000300700";
962 let mut rustoku = Rustoku::new_from_str(s)
963 .unwrap()
964 .with_techniques(TechniqueFlags::HIDDEN_SINGLES);
965 let mut path = SolvePath::default();
966 rustoku.techniques_make_valid_changes(&mut path);
967
968 let placements: Vec<_> = path
969 .steps
970 .iter()
971 .filter_map(|step| match step {
972 SolveStep::Placement {
973 row,
974 col,
975 value,
976 flags,
977 ..
978 } if flags.contains(TechniqueFlags::HIDDEN_SINGLES) => Some((*row, *col, *value)),
979 _ => None,
980 })
981 .collect();
982
983 assert!(
984 !placements.is_empty(),
985 "Hidden singles should produce at least one placement"
986 );
987
988 for &(r, c, v) in &placements {
990 assert!((1..=9).contains(&v), "Placed value must be 1-9, got {v}");
991 assert_eq!(
992 rustoku.board.get(r, c),
993 v,
994 "Board cell ({r},{c}) should be {v} after hidden single"
995 );
996 }
997 }
998
999 #[test]
1000 fn test_naked_pairs_eliminates_candidates() {
1001 let s = "700009030000105006400260009002083951007000000005600000000000003100000060000004010";
1003 let mut rustoku = Rustoku::new_from_str(s)
1004 .unwrap()
1005 .with_techniques(TechniqueFlags::NAKED_PAIRS);
1006 let mut path = SolvePath::default();
1007 rustoku.techniques_make_valid_changes(&mut path);
1008
1009 let eliminations: Vec<_> = path
1010 .steps
1011 .iter()
1012 .filter_map(|step| match step {
1013 SolveStep::CandidateElimination {
1014 row,
1015 col,
1016 value,
1017 flags,
1018 ..
1019 } if flags.contains(TechniqueFlags::NAKED_PAIRS) => Some((*row, *col, *value)),
1020 _ => None,
1021 })
1022 .collect();
1023
1024 assert!(
1025 !eliminations.is_empty(),
1026 "Naked pairs should produce at least one candidate elimination"
1027 );
1028
1029 for &(r, c, v) in &eliminations {
1031 let cand_bit = 1u16 << (v - 1);
1032 let remaining = rustoku.candidates.get(r, c);
1033 assert_eq!(
1034 remaining & cand_bit,
1035 0,
1036 "Candidate {v} should be eliminated from ({r},{c})"
1037 );
1038 }
1039 }
1040
1041 #[test]
1042 fn test_hidden_pairs_eliminates_non_pair_candidates() {
1043 let s = "000032000000000000007600914096000800005008000030040005050200000700000560904010000";
1045 let mut rustoku = Rustoku::new_from_str(s)
1046 .unwrap()
1047 .with_techniques(TechniqueFlags::EASY | TechniqueFlags::HIDDEN_PAIRS);
1048 let mut path = SolvePath::default();
1049 rustoku.techniques_make_valid_changes(&mut path);
1050
1051 let eliminations: Vec<_> = path
1052 .steps
1053 .iter()
1054 .filter_map(|step| match step {
1055 SolveStep::CandidateElimination {
1056 row,
1057 col,
1058 value,
1059 flags,
1060 ..
1061 } if flags.contains(TechniqueFlags::HIDDEN_PAIRS) => Some((*row, *col, *value)),
1062 _ => None,
1063 })
1064 .collect();
1065
1066 assert!(
1067 !eliminations.is_empty(),
1068 "Hidden pairs should produce at least one candidate elimination"
1069 );
1070
1071 for &(r, c, v) in &eliminations {
1073 let cand_bit = 1u16 << (v - 1);
1074 let remaining = rustoku.candidates.get(r, c);
1075 assert_eq!(
1076 remaining & cand_bit,
1077 0,
1078 "Candidate {v} should be eliminated from ({r},{c}) by hidden pair"
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn test_locked_candidates_eliminates_outside_box() {
1085 let s = "984000000000500040000000002006097200003002000000000010005060003407051890030009700";
1087 let mut rustoku = Rustoku::new_from_str(s)
1088 .unwrap()
1089 .with_techniques(TechniqueFlags::LOCKED_CANDIDATES);
1090 let mut path = SolvePath::default();
1091 rustoku.techniques_make_valid_changes(&mut path);
1092
1093 let eliminations: Vec<_> = path
1094 .steps
1095 .iter()
1096 .filter_map(|step| match step {
1097 SolveStep::CandidateElimination {
1098 row,
1099 col,
1100 value,
1101 flags,
1102 ..
1103 } if flags.contains(TechniqueFlags::LOCKED_CANDIDATES) => {
1104 Some((*row, *col, *value))
1105 }
1106 _ => None,
1107 })
1108 .collect();
1109
1110 assert!(
1111 !eliminations.is_empty(),
1112 "Locked candidates should produce at least one candidate elimination"
1113 );
1114
1115 for &(r, c, v) in &eliminations {
1116 let cand_bit = 1u16 << (v - 1);
1117 let remaining = rustoku.candidates.get(r, c);
1118 assert_eq!(
1119 remaining & cand_bit,
1120 0,
1121 "Candidate {v} should be eliminated from ({r},{c}) by locked candidates"
1122 );
1123 }
1124 }
1125
1126 #[test]
1127 fn test_xwing_eliminates_from_correct_lines() {
1128 let s = "000000000760003002002640009403900070000004903005000020010560000370090041000000060";
1130 let mut rustoku = Rustoku::new_from_str(s)
1131 .unwrap()
1132 .with_techniques(TechniqueFlags::XWING);
1133 let mut path = SolvePath::default();
1134 rustoku.techniques_make_valid_changes(&mut path);
1135
1136 let eliminations: Vec<_> = path
1137 .steps
1138 .iter()
1139 .filter_map(|step| match step {
1140 SolveStep::CandidateElimination {
1141 row,
1142 col,
1143 value,
1144 flags,
1145 ..
1146 } if flags.contains(TechniqueFlags::XWING) => Some((*row, *col, *value)),
1147 _ => None,
1148 })
1149 .collect();
1150
1151 assert!(
1152 !eliminations.is_empty(),
1153 "X-Wing should produce at least one candidate elimination"
1154 );
1155
1156 for &(r, c, v) in &eliminations {
1157 let cand_bit = 1u16 << (v - 1);
1158 let remaining = rustoku.candidates.get(r, c);
1159 assert_eq!(
1160 remaining & cand_bit,
1161 0,
1162 "Candidate {v} should be eliminated from ({r},{c}) by X-Wing"
1163 );
1164 }
1165 }
1166
1167 #[test]
1168 fn test_all_techniques_produce_valid_solution() {
1169 let puzzles = vec![
1171 "385421967194756328627983145571892634839645271246137589462579813918364752753218490",
1172 "008007000016083000000000051107290000000000000000046307290000000000860140000300700",
1173 "700009030000105006400260009002083951007000000005600000000000003100000060000004010",
1174 "000032000000000000007600914096000800005008000030040005050200000700000560904010000",
1175 "984000000000500040000000002006097200003002000000000010005060003407051890030009700",
1176 "000000000760003002002640009403900070000004903005000020010560000370090041000000060",
1177 ];
1178
1179 for puzzle in puzzles {
1180 let mut rustoku = Rustoku::new_from_str(puzzle)
1181 .unwrap()
1182 .with_techniques(TechniqueFlags::all());
1183 let solution = rustoku.solve_any();
1184 assert!(
1185 solution.is_some(),
1186 "Puzzle should be solvable with all techniques: {puzzle}"
1187 );
1188 let solved = solution.unwrap().board;
1189 let check = Rustoku::new(solved).unwrap();
1190 assert!(
1191 check.is_solved(),
1192 "Solution must be valid for puzzle: {puzzle}"
1193 );
1194 }
1195 }
1196
1197 #[test]
1198 fn test_techniques_do_not_alter_given_clues() {
1199 let puzzles = vec![
1201 (
1202 "Naked Singles",
1203 "385421967194756328627983145571892634839645271246137589462579813918364752753218490",
1204 TechniqueFlags::NAKED_SINGLES,
1205 ),
1206 (
1207 "Hidden Singles",
1208 "008007000016083000000000051107290000000000000000046307290000000000860140000300700",
1209 TechniqueFlags::HIDDEN_SINGLES,
1210 ),
1211 (
1212 "Naked Pairs",
1213 "700009030000105006400260009002083951007000000005600000000000003100000060000004010",
1214 TechniqueFlags::NAKED_PAIRS,
1215 ),
1216 ];
1217
1218 for (name, puzzle, flag) in puzzles {
1219 let original = Board::try_from(puzzle).unwrap();
1220 let mut rustoku = Rustoku::new_from_str(puzzle).unwrap().with_techniques(flag);
1221 let mut path = SolvePath::default();
1222 rustoku.techniques_make_valid_changes(&mut path);
1223
1224 for r in 0..9 {
1225 for c in 0..9 {
1226 let orig_val = original.get(r, c);
1227 if orig_val != 0 {
1228 assert_eq!(
1229 rustoku.board.get(r, c),
1230 orig_val,
1231 "{name}: clue at ({r},{c}) was overwritten"
1232 );
1233 }
1234 }
1235 }
1236 }
1237 }
1238}