Skip to content

Commit

Permalink
added possibility to choose random number generator explicitly from o…
Browse files Browse the repository at this point in the history
…utside (#11)
  • Loading branch information
LordBlackhawk committed Dec 2, 2023
1 parent 7b9a39b commit 2d7596f
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 15 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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<T, ()>` anymore.
Moved `parse_errors` module to `errors`.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/board/canonicalization.rs
Expand Up @@ -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;
Expand Down
29 changes: 26 additions & 3 deletions 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};
Expand Down Expand Up @@ -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<R: Rng + ?Sized>(rng: &mut R) -> Self {
SudokuGenerator::generate_solved(rng)
}

/// Generate a random, uniquely solvable sudoku with 180° rotational symmetry.
Expand Down Expand Up @@ -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<R: Rng + ?Sized>(
mut sudoku: Sudoku,
symmetry: Symmetry,
rng: &mut R,
) -> Self {
// this function is following
// the approach outlined here: https://stackoverflow.com/a/7280517
//
Expand All @@ -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
Expand Down
22 changes: 13 additions & 9 deletions src/generator.rs
Expand Up @@ -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,
Expand Down Expand Up @@ -208,10 +208,10 @@ impl SudokuGenerator {
}

#[inline(always)]
fn find_good_random_guess(&mut self) -> Candidate {
fn find_good_random_guess<R: Rng + ?Sized>(&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,
Expand All @@ -236,7 +236,11 @@ impl SudokuGenerator {
}

// for generation of random, filled sudokus
fn randomized_solve_one(mut self, stack: &mut Vec<Candidate>) -> Result<Sudoku, Unsolvable> {
fn randomized_solve_one<R: Rng + ?Sized>(
mut self,
rng: &mut R,
stack: &mut Vec<Candidate>,
) -> Result<Sudoku, Unsolvable> {
// insert and deduce in a loop
// do a random guess when no more deductions are found
// backtrack on error (via recursion)
Expand All @@ -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();
Expand All @@ -262,19 +266,19 @@ impl SudokuGenerator {
}
}

pub fn generate_solved() -> Sudoku {
pub fn generate_solved<R: Rng + ?Sized>(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)
.zip(perm.iter())
.map(|(cell, &digit)| Candidate::new(cell, digit)),
);

Self::new().randomized_solve_one(&mut stack).unwrap()
Self::new().randomized_solve_one(rng, &mut stack).unwrap()
}
}

0 comments on commit 2d7596f

Please sign in to comment.