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: 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 = 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.set(r, c, masks.compute_candidates_mask_for_cell(r, c));
94 }
95 }
96 }
97
98 Ok(Self {
99 board,
100 masks,
101 candidates,
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.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
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
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 fn techniques_make_valid_changes(&mut self, path: &mut SolvePath) -> bool {
192 let mut propagator = TechniquePropagator::new(
193 &mut self.board,
194 &mut self.masks,
195 &mut self.candidates,
196 self.techniques,
197 );
198 propagator.propagate_constraints(path, 0)
199 }
200
201 pub fn solve_until(&mut self, bound: usize) -> Vec<Solution> {
203 let mut solutions = Vec::new();
204 let mut path = SolvePath::default();
205
206 if !self.techniques_make_valid_changes(&mut path) {
207 return solutions;
208 }
209
210 self.solve_until_recursive(&mut solutions, &mut path, bound);
211 solutions
212 }
213
214 pub fn solve_any(&mut self) -> Option<Solution> {
216 self.solve_until(1).into_iter().next()
217 }
218
219 pub fn solve_all(&mut self) -> Vec<Solution> {
221 self.solve_until(0)
222 }
223
224 pub fn is_solved(&self) -> bool {
226 self.board.cells.iter().flatten().all(|&val| val != 0) && Rustoku::new(self.board).is_ok()
227 }
228}
229
230pub fn generate_board(num_clues: usize) -> Result<Board, RustokuError> {
248 if !(17..=81).contains(&num_clues) {
249 return Err(RustokuError::InvalidClueCount);
250 }
251
252 let mut rustoku = Rustoku::new(Board::default())?;
254 let solution = rustoku.solve_any().ok_or(RustokuError::DuplicateValues)?;
255 let mut board = solution.board;
256
257 let mut cells: Vec<(usize, usize)> = board.iter_cells().collect();
259 cells.shuffle(&mut rng());
260
261 let mut clues = 81;
262
263 for &(r, c) in &cells {
265 if clues <= num_clues {
266 break;
267 }
268
269 let original = board.cells[r][c];
270 board.cells[r][c] = 0;
271
272 if Rustoku::new(board)?.solve_until(2).len() != 1 {
273 board.cells[r][c] = original; } else {
275 clues -= 1;
276 }
277 }
278
279 if Rustoku::new(board)?.solve_until(2).len() != 1 {
281 return Err(RustokuError::GenerateFailure);
283 }
284
285 Ok(board)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::core::board::Board;
292 use crate::error::RustokuError;
293 use crate::format::format_line;
294
295 const UNIQUE_PUZZLE: &str =
296 "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
297 const UNIQUE_SOLUTION: &str =
298 "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
299 const TWO_PUZZLE: &str =
300 "295743861431865900876192543387459216612387495549216738763504189928671354154938600";
301 const SIX_PUZZLE: &str =
302 "295743001431865900876192543387459216612387495549216738763500000000000000000000000";
303
304 #[test]
305 fn test_new_with_bytes_and_str() {
306 let board = [
307 [5, 3, 0, 0, 7, 0, 0, 0, 0],
308 [6, 0, 0, 1, 9, 5, 0, 0, 0],
309 [0, 9, 8, 0, 0, 0, 0, 6, 0],
310 [8, 0, 0, 0, 6, 0, 0, 0, 3],
311 [4, 0, 0, 8, 0, 3, 0, 0, 1],
312 [7, 0, 0, 0, 2, 0, 0, 0, 6],
313 [0, 6, 0, 0, 0, 0, 2, 8, 0],
314 [0, 0, 0, 4, 1, 9, 0, 0, 5],
315 [0, 0, 0, 0, 8, 0, 0, 7, 9],
316 ];
317
318 let flat_bytes: [u8; 81] = board
319 .concat()
320 .try_into()
321 .expect("Concat board to bytes failed");
322 let board_str: String = flat_bytes.iter().map(|&b| (b + b'0') as char).collect();
323
324 let board_from_new = Board::new(board);
325 let board_from_bytes = Board::try_from(flat_bytes).expect("Board from flat bytes failed");
326 let board_from_str = Board::try_from(board_str.as_str()).expect("Board from string failed");
327
328 assert_eq!(board_from_new, board_from_bytes);
329 assert_eq!(board_from_new, board_from_str);
330 assert_eq!(board_from_bytes, board_from_str);
331 }
332
333 #[test]
334 fn test_try_from_with_valid_input() {
335 let rustoku = Board::try_from(UNIQUE_PUZZLE);
336 assert!(rustoku.is_ok());
337 }
338
339 #[test]
340 fn test_try_from_with_invalid_length() {
341 let s = "530070000"; let rustoku = Board::try_from(s);
343 assert!(matches!(rustoku, Err(RustokuError::InvalidInputLength)));
344 }
345
346 #[test]
347 fn test_try_from_with_invalid_character() {
348 let s = "53007000060019500009800006080006000340080300170002000606000028000041900500008007X"; let rustoku = Board::try_from(s);
350 assert!(matches!(rustoku, Err(RustokuError::InvalidInputCharacter)));
351 }
352
353 #[test]
354 fn test_try_from_with_duplicate_initial_values() {
355 let s = "530070000600195000098000060800060003400803001700020006060000280000419005500080079";
356 let board = Board::try_from(s).expect("Board parsing failed before duplicate check");
357 let rustoku = Rustoku::new(board);
358 assert!(matches!(rustoku, Err(RustokuError::DuplicateValues)));
359 }
360
361 #[test]
362 fn test_solve_any_with_solvable_sudoku() {
363 let s = UNIQUE_PUZZLE;
364 let mut rustoku =
365 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
366 let solution = rustoku.solve_any().expect("Solving solvable puzzle failed");
367
368 assert_eq!(
369 UNIQUE_SOLUTION,
370 format_line(&solution.board),
371 "Solution does not match the expected result"
372 );
373 }
374
375 #[test]
376 fn test_solve_any_with_unsolvable_sudoku() {
377 let s = "078002609030008020002000083000000040043090000007300090200001036001840902050003007";
378 let mut rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed");
379 let solution = rustoku.solve_any();
380 assert!(
381 solution.is_none(),
382 "Expected no solution for this unsolvable puzzle"
383 );
384 }
385
386 #[test]
387 fn test_solve_until_with_bound() {
388 let s = UNIQUE_PUZZLE;
389 let mut rustoku =
390 Rustoku::new_from_str(s).expect("Rustoku creation failed from puzzle string");
391
392 let solutions = rustoku.solve_until(1);
393 assert_eq!(
394 1,
395 solutions.len(),
396 "Expected exactly one solution with bound = 1"
397 );
398
399 let all_solutions = rustoku.solve_until(0);
400 assert_eq!(
401 1,
402 all_solutions.len(),
403 "Expected exactly one solution for this board with bound = 0"
404 );
405
406 assert_eq!(
407 solutions[0].board, all_solutions[0].board,
408 "Solution with bound = 1 does not match the solution with bound = 0"
409 );
410 }
411
412 #[test]
413 fn test_solve_all_with_unique_puzzle() {
414 let s = UNIQUE_PUZZLE;
415 let mut rustoku =
416 Rustoku::new_from_str(s).expect("Rustoku creation failed from unique puzzle string");
417 let solutions = rustoku.solve_all();
418 assert_eq!(
419 1,
420 solutions.len(),
421 "Expected a unique solution for the board"
422 );
423 }
424
425 #[test]
426 fn test_solve_all_with_two_puzzle() {
427 let s = TWO_PUZZLE;
428 let mut rustoku =
429 Rustoku::new_from_str(s).expect("Rustoku creation failed from two puzzle string");
430 let solutions = rustoku.solve_all();
431 assert_eq!(
432 2,
433 solutions.len(),
434 "Expected two solutions for the given board"
435 );
436 }
437
438 #[test]
439 fn test_solve_all_with_six_puzzle() {
440 let s = SIX_PUZZLE;
441 let mut rustoku =
442 Rustoku::new_from_str(s).expect("Rustoku creation failed from six puzzle string");
443 let solutions = rustoku.solve_all();
444 assert_eq!(
445 6,
446 solutions.len(),
447 "Expected one solution for the six puzzle"
448 );
449 }
450
451 #[test]
452 fn test_solve_any_with_all_techniques() {
453 let s = UNIQUE_PUZZLE;
454 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for technique test");
455 let solution = rustoku
456 .with_techniques(TechniqueFlags::all())
457 .solve_any()
458 .expect("Solving with all techniques failed");
459
460 assert_eq!(
461 UNIQUE_SOLUTION,
462 format_line(&solution.board),
463 "Solution does not match the expected result with all techniques"
464 );
465 }
466
467 #[test]
468 fn test_solve_all_with_all_techniques() {
469 let s = TWO_PUZZLE;
470 let rustoku = Rustoku::new_from_str(s)
471 .expect("Rustoku creation failed for multi-solution technique test");
472 let solutions = rustoku.with_techniques(TechniqueFlags::all()).solve_all();
473
474 assert_eq!(
475 2,
476 solutions.len(),
477 "Expected two solutions for the given board with all techniques"
478 );
479 }
480
481 #[test]
482 fn test_generate_with_enough_clues() {
483 (20..=80).step_by(20).for_each(|num_clues| {
484 let board = generate_board(num_clues)
485 .unwrap_or_else(|_| panic!("Board generation failed for {} clues", num_clues));
486 let mut rustoku =
487 Rustoku::new(board).expect("Rustoku creation failed from generated board");
488 let clues_count = board
489 .cells
490 .iter()
491 .flatten()
492 .filter(|&&cell| cell != 0)
493 .count();
494 assert!(
495 clues_count >= num_clues,
496 "Expected at least {} clues, but found {} clues",
497 num_clues,
498 clues_count
499 );
500
501 let solutions = rustoku.solve_all();
502 assert_eq!(
503 1,
504 solutions.len(),
505 "Generated puzzle with {} clues should have a unique solution",
506 num_clues
507 );
508 })
509 }
510
511 #[test]
512 fn test_generate_with_too_few_clues() {
513 let num_clues = 16;
514 let result = generate_board(num_clues);
515 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
516 }
517
518 #[test]
519 fn test_generate_with_too_many_clues() {
520 let num_clues = 82;
521 let result = generate_board(num_clues);
522 assert!(matches!(result, Err(RustokuError::InvalidClueCount)));
523 }
524
525 #[test]
526 fn test_is_solved_with_valid_solution() {
527 let s = UNIQUE_SOLUTION;
528 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for solved check");
529 assert!(rustoku.is_solved(), "The Sudoku puzzle should be solved");
530 }
531
532 #[test]
533 fn test_is_solved_with_unsolved_board() {
534 let s = UNIQUE_PUZZLE;
535 let rustoku = Rustoku::new_from_str(s).expect("Rustoku creation failed for unsolved check");
536 assert!(!rustoku.is_solved(), "The board should not be valid");
537 }
538
539 struct TechniqueTestCase<'a> {
540 name: &'a str,
541 trigger_string: &'a str,
542 technique_flag: TechniqueFlags,
543 }
544
545 #[test]
546 fn test_each_technique_makes_valid_changes() {
547 let test_cases = vec![
548 TechniqueTestCase {
550 name: "Naked Singles",
551 trigger_string: "385421967194756328627983145571892634839645271246137589462579813918364752753218490",
552 technique_flag: TechniqueFlags::NAKED_SINGLES,
553 },
554 TechniqueTestCase {
556 name: "Hidden Singles",
557 trigger_string: "008007000016083000000000051107290000000000000000046307290000000000860140000300700",
558 technique_flag: TechniqueFlags::HIDDEN_SINGLES,
559 },
560 TechniqueTestCase {
562 name: "Naked Pairs",
563 trigger_string: "700009030000105006400260009002083951007000000005600000000000003100000060000004010",
564 technique_flag: TechniqueFlags::NAKED_PAIRS,
565 },
566 TechniqueTestCase {
568 name: "Hidden Pairs",
569 trigger_string: "000032000000000000007600914096000800005008000030040005050200000700000560904010000",
570 technique_flag: TechniqueFlags::HIDDEN_PAIRS,
571 },
572 TechniqueTestCase {
574 name: "Locked Candidates",
575 trigger_string: "984000000000500040000000002006097200003002000000000010005060003407051890030009700",
576 technique_flag: TechniqueFlags::LOCKED_CANDIDATES,
577 },
578 TechniqueTestCase {
580 name: "X-Wing",
581 trigger_string: "000000000760003002002640009403900070000004903005000020010560000370090041000000060",
582 technique_flag: TechniqueFlags::XWING,
583 },
584 ];
585
586 for test_case in test_cases {
587 let rustoku = Rustoku::new_from_str(test_case.trigger_string)
588 .expect(&format!("Rustoku creation failed for '{}'", test_case.name));
589 assert!(
590 rustoku
591 .with_techniques(test_case.technique_flag)
592 .techniques_make_valid_changes(&mut SolvePath::default()),
593 "The board should change for '{}'",
594 test_case.name
595 )
596 }
597 }
598}