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