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::{Difficulty, TechniqueFlags};
21
22use crate::error::RustokuError;
23use rand::prelude::SliceRandom;
24use rand::rng;
25use std::collections::HashSet;
26use techniques::TechniquePropagator;
27
28#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
30pub enum Symmetry {
31 #[default]
33 None,
34 Rotational180,
36 Rotational90,
38 MirrorVertical,
40 MirrorHorizontal,
42 MirrorDiagonal,
44}
45
46impl Symmetry {
47 pub fn get_partners(&self, r: usize, c: usize) -> Vec<(usize, usize)> {
49 let mut partners = HashSet::new();
50 partners.insert((r, c));
51
52 match self {
53 Symmetry::None => {}
54 Symmetry::Rotational180 => {
55 partners.insert((8 - r, 8 - c));
56 }
57 Symmetry::Rotational90 => {
58 partners.insert((c, 8 - r));
59 partners.insert((8 - r, 8 - c));
60 partners.insert((8 - c, r));
61 }
62 Symmetry::MirrorVertical => {
63 partners.insert((r, 8 - c));
64 }
65 Symmetry::MirrorHorizontal => {
66 partners.insert((8 - r, c));
67 }
68 Symmetry::MirrorDiagonal => {
69 partners.insert((c, r));
70 }
71 }
72
73 partners.into_iter().collect()
74 }
75}
76
77#[derive(Debug, Clone, Copy)]
95pub struct BoardGenerator {
96 num_clues: usize,
97 symmetry: Symmetry,
98 difficulty: Option<Difficulty>,
99 max_attempts: usize,
100}
101
102impl Default for BoardGenerator {
103 fn default() -> Self {
104 Self {
105 num_clues: 30,
106 symmetry: Symmetry::None,
107 difficulty: None,
108 max_attempts: 1,
109 }
110 }
111}
112
113impl BoardGenerator {
114 pub fn new() -> Self {
116 Self::default()
117 }
118
119 pub fn clues(mut self, num_clues: usize) -> Self {
121 self.num_clues = num_clues;
122 self
123 }
124
125 pub fn symmetry(mut self, symmetry: Symmetry) -> Self {
127 self.symmetry = symmetry;
128 self
129 }
130
131 pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
136 self.difficulty = Some(difficulty);
137 self
138 }
139
140 pub fn max_attempts(mut self, max_attempts: usize) -> Self {
142 self.max_attempts = max_attempts;
143 self
144 }
145
146 pub fn generate(&self) -> Result<Board, RustokuError> {
148 if let Some(target_difficulty) = self.difficulty {
149 self.generate_with_difficulty(target_difficulty)
150 } else {
151 self.generate_single()
152 }
153 }
154
155 fn generate_single(&self) -> Result<Board, RustokuError> {
156 if !(17..=81).contains(&self.num_clues) {
157 return Err(RustokuError::InvalidClueCount);
158 }
159
160 let mut rustoku = Rustoku::new(Board::default())?;
162 let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
163 let mut board = solution.board;
164
165 let mut visited = [[false; 9]; 9];
167 let mut groups = Vec::new();
168
169 let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
171 cells.shuffle(&mut rng());
172
173 for (r, c) in cells {
174 if !visited[r][c] {
175 let partners = self.symmetry.get_partners(r, c);
176 for &(pr, pc) in &partners {
177 visited[pr][pc] = true;
178 }
179 groups.push(partners);
180 }
181 }
182
183 groups.shuffle(&mut rng());
185
186 let mut clues = 81;
187
188 for group in groups {
190 if clues <= self.num_clues {
191 break;
192 }
193
194 let mut group_clues = Vec::new();
196 for &(r, c) in &group {
197 let val = board.cells[r][c];
198 if val != 0 {
199 group_clues.push((r, c, val));
200 }
201 }
202
203 if group_clues.is_empty() {
204 continue;
205 }
206
207 for &(r, c, _) in &group_clues {
209 board.cells[r][c] = 0;
210 }
211
212 if Rustoku::new(board)?.solve_until(2).len() != 1 {
213 for &(r, c, val) in &group_clues {
215 board.cells[r][c] = val;
216 }
217 } else {
218 clues -= group_clues.len();
219 }
220 }
221
222 if Rustoku::new(board)?.solve_until(2).len() != 1 {
224 return Err(RustokuError::GenerateFailure);
225 }
226
227 Ok(board)
228 }
229
230 fn generate_with_difficulty(
231 &self,
232 target_difficulty: Difficulty,
233 ) -> Result<Board, RustokuError> {
234 use rand::RngExt;
235
236 for _ in 0..self.max_attempts {
237 let clues = match target_difficulty {
239 Difficulty::Easy => rand::rng().random_range(34..=42),
240 Difficulty::Medium => rand::rng().random_range(28..=34),
241 Difficulty::Hard => rand::rng().random_range(22..=28),
242 Difficulty::Expert => rand::rng().random_range(17..=22),
243 };
244
245 let mut sub_generator = *self;
247 sub_generator.num_clues = clues;
248 sub_generator.difficulty = None; if let Ok(board) = sub_generator.generate_single() {
251 let mut rustoku = Rustoku::builder()
252 .board(board)
253 .techniques(TechniqueFlags::all())
254 .build()?;
255
256 let solutions = rustoku.solve_all();
258 if solutions.len() == 1 {
259 let solution = &solutions[0];
260
261 let mut max_difficulty = Difficulty::Easy;
263 let mut required_guessing = false;
264
265 for step in &solution.solve_path.steps {
266 match step {
267 SolveStep::Placement { flags, .. } => {
268 if flags.is_empty() {
269 required_guessing = true;
270 break;
271 }
272 let step_diff = flags.difficulty();
273 if step_diff > max_difficulty {
274 max_difficulty = step_diff;
275 }
276 }
277 SolveStep::CandidateElimination { flags, .. } => {
278 if !flags.is_empty() {
279 let step_diff = flags.difficulty();
280 if step_diff > max_difficulty {
281 max_difficulty = step_diff;
282 }
283 }
284 }
285 }
286 }
287
288 if !required_guessing && max_difficulty == target_difficulty {
289 return Ok(board);
290 }
291 }
292 }
293 }
294
295 Err(RustokuError::GenerateFailure)
296 }
297}
298
299#[derive(Debug, Copy, Clone)]
333pub struct Rustoku {
334 pub board: Board,
336 pub masks: Masks,
338 pub candidates: Candidates,
340 pub techniques: TechniqueFlags,
342}
343
344impl Rustoku {
345 pub fn new(initial_board: Board) -> Result<Self, RustokuError> {
347 let board = initial_board; let mut masks = Masks::new();
349 let mut candidates = Candidates::new();
350
351 for r in 0..9 {
353 for c in 0..9 {
354 let num = board.get(r, c);
355 if num != 0 {
356 if !masks.is_safe(r, c, num) {
357 return Err(RustokuError::DuplicateValues);
358 }
359 masks.add_number(r, c, num);
360 }
361 }
362 }
363
364 for r in 0..9 {
366 for c in 0..9 {
367 if board.is_empty(r, c) {
368 candidates.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
369 }
370 }
371 }
372
373 Ok(Self {
374 board,
375 masks,
376 candidates,
377 techniques: TechniqueFlags::EASY, })
379 }
380
381 pub fn builder() -> RustokuBuilder {
383 RustokuBuilder::new()
384 }
385
386 pub fn new_from_str(s: &str) -> Result<Self, RustokuError> {
388 let board = Board::try_from(s)?;
389 Self::new(board)
390 }
391
392 pub fn with_techniques(mut self, techniques: TechniqueFlags) -> Self {
394 self.techniques = techniques;
395 self
396 }
397
398 pub(crate) fn candidate_grid_snapshot(&self) -> Vec<Vec<Vec<u8>>> {
399 (0..9)
400 .map(|r| {
401 (0..9)
402 .map(|c| {
403 if self.board.get(r, c) != 0 {
404 vec![]
405 } else {
406 self.candidates.get_candidates(r, c)
407 }
408 })
409 .collect()
410 })
411 .collect()
412 }
413
414 pub(crate) fn apply_trace_step(&mut self, step: &SolveStep) {
415 match *step {
416 SolveStep::Placement {
417 row, col, value, ..
418 } => {
419 self.place_number(row, col, value);
420 }
421 SolveStep::CandidateElimination {
422 row, col, value, ..
423 } => {
424 let initial_mask = self.candidates.get(row, col);
425 let refined_mask = initial_mask & !(1 << (value - 1));
426 self.candidates.set(row, col, refined_mask);
427 }
428 }
429 }
430
431 fn candidates_from_mask(mask: u16) -> Vec<u8> {
433 let mut nums = Vec::with_capacity(mask.count_ones() as usize);
434 for v in 1..=9u8 {
435 if mask & (1 << (v - 1)) != 0 {
436 nums.push(v);
437 }
438 }
439 nums
440 }
441
442 #[inline]
444 fn find_next_empty_cell(&self) -> Option<(usize, usize)> {
445 let mut min = (10, None); for (r, c) in self.board.iter_empty_cells() {
447 let count = self.candidates.get(r, c).count_ones() as u8;
448 if count < min.0 {
449 min = (count, Some((r, c)));
450 if count == 1 {
451 return min.1;
452 }
453 }
454 }
455 min.1
456 }
457
458 #[inline]
460 fn place_number(&mut self, r: usize, c: usize, num: u8) {
461 self.board.set(r, c, num);
462 self.masks.add_number(r, c, num);
463 self.candidates
464 .update_affected_cells_for(r, c, &self.masks, &self.board, Some(num));
465 }
466
467 #[inline]
469 fn remove_number(&mut self, r: usize, c: usize, num: u8) {
470 self.board.set(r, c, 0); self.masks.remove_number(r, c, num);
472 self.candidates
473 .update_affected_cells(r, c, &self.masks, &self.board);
474 }
476
477 fn solve_until_recursive(
479 &mut self,
480 solutions: &mut Vec<Solution>,
481 path: &mut SolvePath,
482 bound: usize,
483 ) -> usize {
484 let Some((r, c)) = self.find_next_empty_cell() else {
486 solutions.push(Solution {
487 board: self.board,
488 solve_path: path.clone(),
489 });
490 return 1;
491 };
492
493 let mut count = 0;
494 let mask = self.candidates.get(r, c);
496 let mut nums = Self::candidates_from_mask(mask);
497 nums.shuffle(&mut rng());
498
499 for &num in &nums {
500 if !self.masks.is_safe(r, c, num) {
501 continue;
502 }
503
504 self.place_number(r, c, num);
505 let step_number = path.steps.len() as u32;
506 path.steps.push(SolveStep::Placement {
507 row: r,
508 col: c,
509 value: num,
510 flags: TechniqueFlags::empty(),
511 step_number,
512 candidates_eliminated: 0,
513 related_cell_count: 0,
514 difficulty_point: 0,
515 });
516
517 count += self.solve_until_recursive(solutions, path, bound);
518 path.steps.pop();
519 self.remove_number(r, c, num);
520
521 if bound > 0 && solutions.len() >= bound {
523 return count;
524 }
525 }
526
527 count
528 }
529
530 fn techniques_make_valid_changes(&mut self, path: &mut SolvePath) -> bool {
532 let mut propagator = TechniquePropagator::new(
533 &mut self.board,
534 &mut self.masks,
535 &mut self.candidates,
536 self.techniques,
537 );
538 propagator.propagate_constraints(path, 0)
539 }
540
541 pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
543 let mut solutions = Vec::new();
544 let mut path = SolvePath::default();
545
546 if !self.techniques_make_valid_changes(&mut path) {
547 return solutions;
548 }
549
550 self.solve_until_recursive(&mut solutions, &mut path, bound);
551 solutions
552 }
553
554 pub fn solve_any(&mut self) -> Option<Solution> {
556 self.solve_until(1).into_iter().next()
557 }
558
559 pub fn solve_all(&mut self) -> Vec<Solution> {
561 use rayon::prelude::*;
562
563 let mut path = SolvePath::default();
565 if !self.techniques_make_valid_changes(&mut path) {
566 return Vec::new();
567 }
568
569 if let Some((r, c)) = self.find_next_empty_cell() {
571 let mask = self.candidates.get(r, c);
572 let nums = Self::candidates_from_mask(mask);
573
574 let initial_path = path.clone();
575
576 let chunks: Vec<Vec<Solution>> = nums
578 .par_iter()
579 .map(|&num| {
580 let mut cloned = *self; let mut local_solutions: Vec<Solution> = Vec::new();
582 let mut local_path = initial_path.clone();
583
584 cloned.place_number(r, c, num);
586 let step_number = local_path.steps.len() as u32;
587 local_path.steps.push(SolveStep::Placement {
588 row: r,
589 col: c,
590 value: num,
591 flags: TechniqueFlags::empty(),
592 step_number,
593 candidates_eliminated: 0,
594 related_cell_count: 0,
595 difficulty_point: 0,
596 });
597
598 cloned.solve_until_recursive(&mut local_solutions, &mut local_path, 0);
600 local_solutions
601 })
602 .collect();
603
604 let mut solutions = Vec::new();
606 for mut s in chunks {
607 solutions.append(&mut s);
608 }
609 solutions
610 } else {
611 vec![Solution {
613 board: self.board,
614 solve_path: path,
615 }]
616 }
617 }
618
619 pub fn is_solved(&self) -> bool {
621 self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
622 }
623}
624
625pub struct RustokuBuilder {
627 board: Option<Board>,
628 techniques: TechniqueFlags,
629 max_solutions: Option<usize>,
630}
631
632impl RustokuBuilder {
633 pub fn new() -> Self {
635 RustokuBuilder {
636 board: None,
637 techniques: TechniqueFlags::EASY,
638 max_solutions: None,
639 }
640 }
641}
642
643impl Default for RustokuBuilder {
644 fn default() -> Self {
645 Self::new()
646 }
647}
648
649impl RustokuBuilder {
650 pub fn board(mut self, board: Board) -> Self {
652 self.board = Some(board);
653 self
654 }
655
656 pub fn board_from_str(mut self, s: &str) -> Result<Self, RustokuError> {
658 let board = Board::try_from(s)?;
659 self.board = Some(board);
660 Ok(self)
661 }
662
663 pub fn techniques(mut self, techniques: TechniqueFlags) -> Self {
665 self.techniques = techniques;
666 self
667 }
668
669 pub fn max_solutions(mut self, max: usize) -> Self {
671 self.max_solutions = Some(max);
672 self
673 }
674
675 pub fn build(self) -> Result<Rustoku, RustokuError> {
677 let board = self.board.unwrap_or_default();
678 let mut r = Rustoku::new(board)?;
679 r.techniques = self.techniques;
680 Ok(r)
683 }
684}
685
686#[derive(Debug)]
689pub struct Solutions {
690 solver: Rustoku,
691 path: SolvePath,
692 stack: Vec<Frame>,
693 finished: bool,
694}
695
696#[derive(Debug)]
697struct Frame {
698 r: usize,
699 c: usize,
700 nums: Vec<u8>,
701 idx: usize,
702 placed: Option<u8>,
703}
704
705impl Solutions {
706 pub fn from_solver(mut solver: Rustoku) -> Self {
709 let mut path = SolvePath::default();
710 let mut finished = false;
711
712 if !solver.techniques_make_valid_changes(&mut path) {
713 finished = true;
714 }
715
716 let mut stack = Vec::new();
717 if !finished {
718 if let Some((r, c)) = solver.find_next_empty_cell() {
719 let mask = solver.candidates.get(r, c);
720 let mut nums = Rustoku::candidates_from_mask(mask);
721 nums.shuffle(&mut rng());
722 stack.push(Frame {
723 r,
724 c,
725 nums,
726 idx: 0,
727 placed: None,
728 });
729 } else {
730 }
732 }
733
734 Solutions {
735 solver,
736 path,
737 stack,
738 finished,
739 }
740 }
741}
742
743impl Iterator for Solutions {
744 type Item = Solution;
745
746 fn next(&mut self) -> Option<Self::Item> {
747 if self.finished {
748 return None;
749 }
750
751 loop {
752 if self.stack.is_empty() {
754 if let Some((r, c)) = self.solver.find_next_empty_cell() {
755 let mask = self.solver.candidates.get(r, c);
756 let mut nums = Rustoku::candidates_from_mask(mask);
757 nums.shuffle(&mut rng());
758 self.stack.push(Frame {
759 r,
760 c,
761 nums,
762 idx: 0,
763 placed: None,
764 });
765 continue;
766 } else {
767 let sol = Solution {
769 board: self.solver.board,
770 solve_path: self.path.clone(),
771 };
772 self.finished = true;
773 return Some(sol);
774 }
775 }
776
777 let last_idx = self.stack.len() - 1;
778 let frame = &mut self.stack[last_idx];
779
780 if frame.idx >= frame.nums.len() {
782 if let Some(num) = frame.placed {
783 self.solver.remove_number(frame.r, frame.c, num);
785 self.path.steps.pop();
786 frame.placed = None;
787 } else {
788 self.stack.pop();
790 }
791 continue;
792 }
793
794 let num = frame.nums[frame.idx];
795 frame.idx += 1;
796
797 if self.solver.masks.is_safe(frame.r, frame.c, num) {
798 self.solver.place_number(frame.r, frame.c, num);
800 let step_number = self.path.steps.len() as u32;
801 self.path.steps.push(SolveStep::Placement {
802 row: frame.r,
803 col: frame.c,
804 value: num,
805 flags: TechniqueFlags::empty(),
806 step_number,
807 candidates_eliminated: 0,
808 related_cell_count: 0,
809 difficulty_point: 0,
810 });
811 frame.placed = Some(num);
812
813 if let Some((nr, nc)) = self.solver.find_next_empty_cell() {
815 let mask = self.solver.candidates.get(nr, nc);
816 let mut nums2 = Rustoku::candidates_from_mask(mask);
817 nums2.shuffle(&mut rng());
818 self.stack.push(Frame {
819 r: nr,
820 c: nc,
821 nums: nums2,
822 idx: 0,
823 placed: None,
824 });
825 continue;
826 } else {
827 let solution = Solution {
829 board: self.solver.board,
830 solve_path: self.path.clone(),
831 };
832 if let Some(pnum) = frame.placed {
834 self.solver.remove_number(frame.r, frame.c, pnum);
835 self.path.steps.pop();
836 frame.placed = None;
837 }
838 return Some(solution);
839 }
840 }
841 }
843 }
844}
845
846pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
865 BoardGenerator::new().clues(num_clues).generate()
866}
867
868pub fn generate_board_by_difficulty(
872 difficulty: Difficulty,
873 max_attempts: usize,
874) -> Result<Board, RustokuError> {
875 BoardGenerator::new()
876 .difficulty(difficulty)
877 .max_attempts(max_attempts)
878 .generate()
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884 use crate::core::board::Board;
885 use crate::core::techniques::flags::Difficulty;
886 use crate::error::RustokuError;
887 use crate::format::format_line;
888
889 const UNIQUE_PUZZLE: &str =
890 "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
891 const UNIQUE_SOLUTION: &str =
892 "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
893 const TWO_PUZZLE: &str =
894 "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
895 const SIX_PUZZLE: &str =
896 "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
897
898 #[test]
899 fn test_builder_and_iterator() {
900 let board = Board::try_from(UNIQUE_PUZZLE).expect("valid puzzle");
901 let solver = Rustoku::builder()
902 .board(board)
903 .techniques(TechniqueFlags::all())
904 .build()
905 .expect("builder build");
906
907 let mut sols = Solutions::from_solver(solver);
909 let first = sols.next();
910 assert!(first.is_some());
911 assert!(sols.next().is_none());
913 }
914
915 #[test]
916 fn test_try_from_with_duplicate_initial_values() {
917 let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
918 let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
919 let rustoku = Rustoku::new(board);
920 assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
921 }
922
923 #[test]
924 fn test_solve_any_with_solvable_sudoku() {
925 let s = UNIQUE_PUZZLE;
926 let mut rustoku =
927 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
928 let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
929
930 assert_eq!(
931 UNIQUE_SOLUTION,
932 format_line(&solution.board),
933 "Solution does not match the expected result"
934 );
935 }
936
937 #[test]
938 fn test_solve_any_with_unsolvable_sudoku() {
939 let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
940 let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
941 let solution = rustoku.solve_any();
942 assert!(
943 solution.is_none(),
944 "Expected no solution for this unsolvable puzzle"
945 );
946 }
947
948 #[test]
949 fn test_solve_until_with_bound() {
950 let s = UNIQUE_PUZZLE;
951 let mut rustoku =
952 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
953
954 let solutions = rustoku.solve_until(1);
955 assert_eq!(
956 1,
957 solutions.len(),
958 "Expected exactly one solution with bound = 1"
959 );
960
961 let all_solutions = rustoku.solve_until(0);
962 assert_eq!(
963 1,
964 all_solutions.len(),
965 "Expected exactly one solution for this board with bound = 0"
966 );
967
968 assert_eq!(
969 solutions[0].board, all_solutions[0].board,
970 "Solution with bound = 1 does not match the solution with bound = 0"
971 );
972 }
973
974 #[test]
975 fn test_solve_all_with_unique_puzzle() {
976 let s = UNIQUE_PUZZLE;
977 let mut rustoku =
978 Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
979 let solutions = rustoku.solve_all();
980 assert_eq!(
981 1,
982 solutions.len(),
983 "Expected a unique solution for the board"
984 );
985 }
986
987 #[test]
988 fn test_solve_all_with_two_puzzle() {
989 let s = TWO_PUZZLE;
990 let mut rustoku =
991 Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
992 let solutions = rustoku.solve_all();
993 assert_eq!(
994 2,
995 solutions.len(),
996 "Expected two solutions for the given board"
997 );
998 }
999
1000 #[test]
1001 fn test_solve_all_with_six_puzzle() {
1002 let s = SIX_PUZZLE;
1003 let mut rustoku =
1004 Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
1005 let solutions = rustoku.solve_all();
1006 assert_eq!(
1007 6,
1008 solutions.len(),
1009 "Expected one solution for the six puzzle"
1010 );
1011 }
1012
1013 #[test]
1014 fn test_solve_any_with_all_techniques() {
1015 let s = UNIQUE_PUZZLE;
1016 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
1017 let solution = rustoku
1018 .with_techniques(TechniqueFlags::all())
1019 .solve_any()
1020 .expect("Solving with all techniques failed");
1021
1022 assert_eq!(
1023 UNIQUE_SOLUTION,
1024 format_line(&solution.board),
1025 "Solution does not match the expected result with all techniques"
1026 );
1027 }
1028
1029 #[test]
1030 fn test_solve_all_with_all_techniques() {
1031 let s = TWO_PUZZLE;
1032 let rustoku = Rustoku::new_from_str(s)
1033 .expect("Rustoku creation failed for multi-solution technique test");
1034 let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
1035
1036 assert_eq!(
1037 2,
1038 solutions.len(),
1039 "Expected two solutions for the given board with all techniques"
1040 );
1041 }
1042
1043 #[test]
1044 fn test_generate_with_enough_clues() {
1045 (20..=80).step_by(20).for_each(|num_clues| {
1046 let board = generate_board(num_clues)
1047 .expect("Board generation failed - check clue count is between 17 and 81");
1048 let mut rustoku =
1049 Rustoku::new(board).expect("Rustoku creation failed from generated board");
1050 let clues_count = board
1051 .cells
1052 .iter()
1053 .flatten()
1054 .filter(|&&cell| cell != 0)
1055 .count();
1056 assert!(
1057 clues_count >= num_clues,
1058 "Expected at least {num_clues} clues, but found {clues_count} clues"
1059 );
1060
1061 let solutions = rustoku.solve_all();
1062 assert_eq!(
1063 1,
1064 solutions.len(),
1065 "Generated puzzle with {num_clues} clues should have a unique solution"
1066 );
1067 })
1068 }
1069
1070 #[test]
1071 fn test_generate_with_too_few_clues() {
1072 let num_clues = 16;
1073 let result = generate_board(num_clues);
1074 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
1075 }
1076
1077 #[test]
1078 fn test_generate_with_too_many_clues() {
1079 let num_clues = 82;
1080 let result = generate_board(num_clues);
1081 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
1082 }
1083
1084 #[test]
1085 fn test_is_solved_with_valid_solution() {
1086 let s = UNIQUE_SOLUTION;
1087 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
1088 assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
1089 }
1090
1091 #[test]
1092 fn test_is_solved_with_unsolved_board() {
1093 let s = UNIQUE_PUZZLE;
1094 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
1095 assert!(!rustoku.is_solved(), "The board should not be valid");
1096 }
1097
1098 #[test]
1099 fn test_generate_by_difficulty_easy() {
1100 let board = generate_board_by_difficulty(Difficulty::Easy, 100)
1101 .expect("Failed to generate an Easy puzzle within 100 attempts");
1102
1103 let mut rustoku = Rustoku::builder()
1104 .board(board)
1105 .techniques(TechniqueFlags::all())
1106 .build()
1107 .unwrap();
1108
1109 let solutions = rustoku.solve_all();
1110 assert_eq!(solutions.len(), 1);
1111
1112 let mut required_guessing = false;
1113 let mut max_difficulty = Difficulty::Easy;
1114
1115 for step in &solutions[0].solve_path.steps {
1116 match step {
1117 crate::core::solution::SolveStep::Placement { flags, .. } => {
1118 if flags.is_empty() {
1119 required_guessing = true;
1120 }
1121 if flags.difficulty() > max_difficulty {
1122 max_difficulty = flags.difficulty();
1123 }
1124 }
1125 crate::core::solution::SolveStep::CandidateElimination { flags, .. } => {
1126 if flags.difficulty() > max_difficulty {
1127 max_difficulty = flags.difficulty();
1128 }
1129 }
1130 }
1131 }
1132
1133 assert!(
1134 !required_guessing,
1135 "Easy puzzle should not require guessing"
1136 );
1137 assert_eq!(
1138 max_difficulty,
1139 Difficulty::Easy,
1140 "Puzzle exceeded target difficulty"
1141 );
1142 }
1143
1144 #[test]
1145 fn test_generate_by_difficulty_hard() {
1146 let board = generate_board_by_difficulty(Difficulty::Hard, 1000)
1147 .expect("Failed to generate a Hard puzzle within 1000 attempts");
1148
1149 let mut rustoku = Rustoku::builder()
1150 .board(board)
1151 .techniques(TechniqueFlags::all())
1152 .build()
1153 .unwrap();
1154
1155 let solutions = rustoku.solve_all();
1156 assert_eq!(solutions.len(), 1);
1157
1158 let mut required_guessing = false;
1159 let mut max_difficulty = Difficulty::Easy;
1160
1161 for step in &solutions[0].solve_path.steps {
1162 match step {
1163 crate::core::solution::SolveStep::Placement { flags, .. } => {
1164 if flags.is_empty() {
1165 required_guessing = true;
1166 }
1167 if flags.difficulty() > max_difficulty {
1168 max_difficulty = flags.difficulty();
1169 }
1170 }
1171 crate::core::solution::SolveStep::CandidateElimination { flags, .. } => {
1172 if flags.difficulty() > max_difficulty {
1173 max_difficulty = flags.difficulty();
1174 }
1175 }
1176 }
1177 }
1178
1179 assert!(
1180 !required_guessing,
1181 "Hard puzzle should not require guessing"
1182 );
1183 assert_eq!(
1184 max_difficulty,
1185 Difficulty::Hard,
1186 "Puzzle exceeded target difficulty"
1187 );
1188 }
1189}