From 2d7596f753e40aa417e3d8cc4febd626c2c9dba6 Mon Sep 17 00:00:00 2001 From: LordBlackhawk Date: Sat, 2 Dec 2023 21:01:32 +0100 Subject: [PATCH] added possibility to choose random number generator explicitly from outside (#11) --- CHANGELOG.md | 7 ++++++- Cargo.toml | 2 +- src/board/canonicalization.rs | 2 +- src/board/sudoku.rs | 29 ++++++++++++++++++++++++++--- src/generator.rs | 22 +++++++++++++--------- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b30ae9e..a6da106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,12 @@ Unreleased - `generate_unique_with_symmetry` -> `generate_with_symmetry` - `generate_unique_from` -> `generate_from` - `generate_unique_with_symmetry_from` -> `generate_with_symmetry_from` -* Add `Sudoku::shuffled`. +* Add new APIs: + * `Sudoku::shuffled` + * two functions for generating sudokus with a user-chosen RNG: + * `generate_solved_with_rng` + * `generate_with_symmetry_and_rng_from` +* * Improved errors for `Sudoku` methods. Errors now implement `std::error::Error` and none of them return `Result` anymore. Moved `parse_errors` module to `errors`. diff --git a/Cargo.toml b/Cargo.toml index ca1036b..5841238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ authors = ["Emerentius"] edition = "2018" [dependencies] -rand = "0.7.3" +rand = "0.8.5" serde = { version = "1.0.80", optional = true } crunchy = "0.2.1" thiserror = "1.0.21" diff --git a/src/board/canonicalization.rs b/src/board/canonicalization.rs index 949b10f..66c3f44 100644 --- a/src/board/canonicalization.rs +++ b/src/board/canonicalization.rs @@ -66,7 +66,7 @@ impl Transformation { let mut digits = [1, 2, 3, 4, 5, 6, 7, 8, 9]; // manual top-down Fisher-Yates shuffle. Needs only 1 ranged random num rather than 9 - let mut permutation = rng.gen_range(0, 362_880u32); // 9! + let mut permutation = rng.gen_range(0..362_880u32); // 9! for n_choices in (2..10).rev() { let num = permutation % n_choices; permutation /= n_choices; diff --git a/src/board/sudoku.rs b/src/board/sudoku.rs index e1933a6..5a1c3b4 100644 --- a/src/board/sudoku.rs +++ b/src/board/sudoku.rs @@ -1,4 +1,5 @@ use rand::seq::SliceRandom; +use rand::Rng; use crate::consts::*; use crate::errors::{BlockParseError, InvalidEntry, LineParseError, NotEnoughRows}; @@ -168,7 +169,12 @@ impl Symmetry { impl Sudoku { /// Generate a random, solved sudoku pub fn generate_solved() -> Self { - SudokuGenerator::generate_solved() + Sudoku::generate_solved_with_rng(&mut rand::thread_rng()) + } + + /// Generate a random, solved sudoku. All random numbers are drawn from the given random number generator `rng`. + pub fn generate_solved_with_rng(rng: &mut R) -> Self { + SudokuGenerator::generate_solved(rng) } /// Generate a random, uniquely solvable sudoku with 180° rotational symmetry. @@ -205,7 +211,24 @@ impl Sudoku { /// Most puzzles generated by this from solved sudokus are easy. /// /// If the source `sudoku` is invalid or has multiple solutions, it will be returned as is. - pub fn generate_with_symmetry_from(mut sudoku: Sudoku, symmetry: Symmetry) -> Self { + pub fn generate_with_symmetry_from(sudoku: Sudoku, symmetry: Symmetry) -> Self { + Sudoku::generate_with_symmetry_and_rng_from(sudoku, symmetry, &mut rand::thread_rng()) + } + + /// Generate a random, uniqely solvable sudoku + /// that has the same solution as the given `sudoku` by removing the contents of some of its cells + /// whilst upholding the `symmetry`. If the input sudoku is partially filled without the desired + /// symmetry, the output may not have it either. + /// All random numbers are drawn from the given random number generator `rng`. + /// The puzzles are minimal in that no cell can be removed without losing uniquess of solution. + /// Most puzzles generated by this from solved sudokus are easy. + /// + /// If the source `sudoku` is invalid or has multiple solutions, it will be returned as is. + pub fn generate_with_symmetry_and_rng_from( + mut sudoku: Sudoku, + symmetry: Symmetry, + rng: &mut R, + ) -> Self { // this function is following // the approach outlined here: https://stackoverflow.com/a/7280517 // @@ -219,7 +242,7 @@ impl Sudoku { .iter_mut() .enumerate() .for_each(|(cell, place)| *place = cell); - cell_order.shuffle(&mut rand::thread_rng()); + cell_order.shuffle(rng); // With symmetries, many cells are equivalent. // If we've already visited one cell in a symmetry class, we can skip ahead diff --git a/src/generator.rs b/src/generator.rs index b947eeb..f5bb4a4 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -22,7 +22,7 @@ pub(crate) struct SudokuGenerator { impl SudokuGenerator { #[inline] - pub fn new() -> SudokuGenerator { + pub fn new() -> Self { SudokuGenerator { grid: Sudoku([0; N_CELLS]), n_solved_cells: 0, @@ -208,10 +208,10 @@ impl SudokuGenerator { } #[inline(always)] - fn find_good_random_guess(&mut self) -> Candidate { + fn find_good_random_guess(&mut self, rng: &mut R) -> Candidate { let best_cell = self.find_cell_min_poss(); let poss_digits = self.cell_poss_digits[best_cell]; - let choice = rand::thread_rng().gen_range(0, poss_digits.len()); + let choice = rng.gen_range(0..poss_digits.len()); let digit = poss_digits.into_iter().nth(choice as usize).unwrap(); Candidate { digit, @@ -236,7 +236,11 @@ impl SudokuGenerator { } // for generation of random, filled sudokus - fn randomized_solve_one(mut self, stack: &mut Vec) -> Result { + fn randomized_solve_one( + mut self, + rng: &mut R, + stack: &mut Vec, + ) -> Result { // insert and deduce in a loop // do a random guess when no more deductions are found // backtrack on error (via recursion) @@ -251,9 +255,9 @@ impl SudokuGenerator { continue; } - let entry = self.find_good_random_guess(); + let entry = self.find_good_random_guess(rng); stack.push(entry); - if let filled_sudoku @ Ok(_) = self.clone().randomized_solve_one(stack) { + if let filled_sudoku @ Ok(_) = self.clone().randomized_solve_one(rng, stack) { return filled_sudoku; } stack.clear(); @@ -262,12 +266,12 @@ impl SudokuGenerator { } } - pub fn generate_solved() -> Sudoku { + pub fn generate_solved(rng: &mut R) -> Sudoku { // fill first row with a permutation of 1...9 // not necessary, but ~15% faster let mut stack = Vec::with_capacity(N_CELLS); let mut perm = [1, 2, 3, 4, 5, 6, 7, 8, 9]; - perm.shuffle(&mut rand::thread_rng()); + perm.shuffle(rng); stack.extend( (0..9) @@ -275,6 +279,6 @@ impl SudokuGenerator { .map(|(cell, &digit)| Candidate::new(cell, digit)), ); - Self::new().randomized_solve_one(&mut stack).unwrap() + Self::new().randomized_solve_one(rng, &mut stack).unwrap() } }