add sudoku generator & validator

This commit is contained in:
markichnich 2023-08-21 15:26:12 +02:00
parent a9395dc55f
commit 808abac4d8
11 changed files with 570 additions and 149 deletions

48
Cargo.lock generated
View File

@ -51,6 +51,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "getrandom"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "libc"
version = "0.2.147"
@ -108,6 +119,42 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
@ -128,6 +175,7 @@ name = "shdoku"
version = "0.1.0"
dependencies = [
"crossterm",
"rand",
]
[[package]]

View File

@ -10,3 +10,4 @@ categories = ["games", "mathematics", "science", "algorithms"]
[dependencies]
crossterm = "0.27.0"
rand = "0.8.5"

View File

@ -1,170 +1,65 @@
extern crate crossterm;
use crossterm::{
cursor::{MoveTo, SetCursorStyle},
event::{poll, read, Event::*, KeyCode::*},
execute, queue,
terminal::{disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All},
};
use crossterm::event::{poll, read, Event::*, KeyCode::*};
use std::{
io::{self, stdout, Stdout, Write},
time::Duration,
};
extern crate rand;
mod state;
use state::*;
mod sudoku;
use sudoku::*;
mod ui;
use ui::*;
use std::{io, time::Duration};
fn main() -> io::Result<()> {
let (width, height) = size()?;
let (mut width, mut height) = (width as usize, height as usize);
let mut screen = vec![String::with_capacity(width); height];
let modifiable = [
[false, false, true, true, false, true, true, true, true],
[false, true, true, false, false, false, true, true, true],
[true, false, false, true, true, true, true, false, true],
[false, true, true, true, false, true, true, true, false],
[false, true, true, false, true, false, true, true, false],
[false, true, true, true, false, true, true, true, false],
[true, false, true, true, true, true, false, false, true],
[true, true, true, false, false, false, true, true, false],
[true, true, true, true, false, true, true, false, false],
];
let mut board = [
[5, 3, 0, 0, 7, 0, 0, 0, 0],
[6, 0, 0, 1, 9, 5, 0, 0, 0],
[0, 9, 8, 0, 0, 0, 0, 6, 0],
[8, 0, 0, 0, 6, 0, 0, 0, 3],
[4, 0, 0, 8, 0, 3, 0, 0, 1],
[7, 0, 0, 0, 2, 0, 0, 0, 6],
[0, 6, 0, 0, 0, 0, 2, 8, 0],
[0, 0, 0, 4, 1, 9, 0, 0, 5],
[0, 0, 0, 0, 8, 0, 0, 7, 9],
];
let mut cur_row = 0;
let mut cur_col = 0;
let mut stdout = stdout();
enable_raw_mode()?;
let mut screen = Screen::init(io::stdout());
let mut state = State::init(Difficulty::Medium);
loop {
let (w, h) = size()?;
(width, height) = (w as usize, h as usize);
if poll(Duration::from_millis(250))? {
if let Ok(Key(k)) = read() {
match k.code {
Char('q') => break,
Char('h') => cur_col = (cur_col + 8) % 9,
Char('j') => cur_row = (cur_row + 1) % 9,
Char('k') => cur_row = (cur_row + 8) % 9,
Char('l') => cur_col = (cur_col + 1) % 9,
Char('H') => cur_col = (cur_col + 6) % 9,
Char('J') => cur_row = (cur_row + 3) % 9,
Char('K') => cur_row = (cur_row + 6) % 9,
Char('L') => cur_col = (cur_col + 3) % 9,
Char('x') => board[cur_row][cur_col] = 0,
Char(num) if ('1'..='9').contains(&num) => {
if modifiable[cur_row][cur_col] {
board[cur_row][cur_col] = num as u8 - b'0'
Char('h') => state.move_cursor(Dir::Left),
Char('j') => state.move_cursor(Dir::Down),
Char('k') => state.move_cursor(Dir::Up),
Char('l') => state.move_cursor(Dir::Right),
Char('H') => state.move_cursor(Dir::FarLeft),
Char('J') => state.move_cursor(Dir::FarDown),
Char('K') => state.move_cursor(Dir::FarUp),
Char('L') => state.move_cursor(Dir::FarRight),
Char('x') => {
if state.current_cell_modifiable() {
*state.current_cell() = 0;
}
}
Char(num) if ('1'..='9').contains(&num) => {
if state.current_cell_modifiable() {
*state.current_cell() = num as u8 - b'0';
}
if is_solution(&state.board) {
screen.deinit()?;
println!("you win");
break;
}
}
Char('q') => {
screen.deinit()?;
break;
}
_ => {}
}
}
}
render_to_screen(&mut screen, width, height, board);
draw_screen(&mut stdout, &screen)?;
place_cursor(&mut stdout, cur_row, cur_col, width, height)?;
screen.update_dimensions()?;
screen.render(state.board);
screen.draw(state.cur_row, state.cur_col)?;
}
disable_raw_mode()?;
Ok(())
}
fn place_cursor(
stdout: &mut Stdout,
cur_row: usize,
cur_col: usize,
width: usize,
height: usize,
) -> io::Result<()> {
let (x, y) = ((width / 2 - 14) as u16, (height / 2 - 7) as u16);
let (x, y) = (x + 2, y + 1);
let (x, y) = (x + 2 * cur_col as u16, y + cur_row as u16);
let x = match cur_col {
0..=2 => x,
3..=5 => x + 3,
_ => x + 6,
};
let y = match cur_row {
0..=2 => y,
3..=5 => y + 1,
_ => y + 2,
};
execute!(stdout, MoveTo(x, y), SetCursorStyle::SteadyBlock)
}
fn draw_screen(stdout: &mut Stdout, screen: &[String]) -> io::Result<()> {
queue!(stdout, Clear(All))?;
let s: String = screen.join("\r\n");
write!(stdout, "{}", s)?;
stdout.flush()?;
Ok(())
}
fn render_to_screen(screen: &mut Vec<String>, cols: usize, rows: usize, board: [[u8; 9]; 9]) {
let mut lines = Vec::new();
lines.push("┌────────┬────────┬────────┐".to_string().chars().collect());
for (row, row_slice) in board.iter().enumerate() {
match row {
3 | 6 => {
lines.push("├────────┼────────┼────────┤".to_string().chars().collect());
}
_ => {}
}
let mut line = String::new();
for (col, cur_field) in row_slice.iter().enumerate() {
match col {
0 => {
line.push_str("");
}
3 | 6 => {
line.push_str("");
}
_ => {}
}
line.push(match cur_field {
0 => ' ',
n => (b'0' + n) as char,
});
line.push(' ');
}
line.push_str("");
lines.push(line);
}
lines.push("└────────┴────────┴────────┘".to_string().chars().collect());
let pad_hori = cols / 2 + lines[0].chars().count() / 2;
let pad_vert = rows - lines.len();
let pad_top = pad_vert / 2;
let pad_bot = pad_vert - pad_top;
let mut new_screen = Vec::new();
for _ in 0..pad_top {
new_screen.push(String::new());
}
for line in lines {
let padded = format!("{: >width$}", line, width = pad_hori);
new_screen.push(padded);
}
for _ in 0..pad_bot {
new_screen.push(String::new());
}
*screen = new_screen;
}
mod tests;

72
src/state.rs Normal file
View File

@ -0,0 +1,72 @@
use crate::sudoku::*;
use crate::Dir::*;
pub enum Dir {
Up,
Down,
Left,
Right,
FarUp,
FarDown,
FarLeft,
FarRight,
}
pub struct State {
pub board: Board,
pub modifiable: [[bool; 9]; 9],
pub cur_row: usize,
pub cur_col: usize,
}
impl State {
pub fn init(difficulty: Difficulty) -> Self {
let board = generate_sudoku(difficulty);
let modifiable = State::get_modifiables(board);
Self {
board,
modifiable,
cur_row: 4,
cur_col: 4,
}
}
fn get_modifiables(board: Board) -> [[bool; 9]; 9] {
let mut modifiable = [[false; 9]; 9];
for (board_row, modifiable_row) in board.iter().zip(modifiable.iter_mut()) {
for (board_cell, modifiable_flag) in board_row.iter().zip(modifiable_row.iter_mut()) {
if *board_cell == 0 {
*modifiable_flag = true;
}
}
}
modifiable
}
pub fn current_cell_modifiable(&self) -> bool {
self.modifiable[self.cur_row][self.cur_col]
}
pub fn current_cell(&mut self) -> &mut u8 {
&mut self.board[self.cur_row][self.cur_col]
}
pub fn move_cursor(&mut self, direction: Dir) {
self.cur_col = match direction {
Left => (self.cur_col + 8) % 9,
Right => (self.cur_col + 1) % 9,
FarLeft => (self.cur_col + 6) % 9,
FarRight => (self.cur_col + 3) % 9,
_ => self.cur_col,
};
self.cur_row = match direction {
Up => (self.cur_row + 8) % 9,
Down => (self.cur_row + 1) % 9,
FarUp => (self.cur_row + 6) % 9,
FarDown => (self.cur_row + 3) % 9,
_ => self.cur_row,
};
}
}

127
src/sudoku/generator.rs Normal file
View File

@ -0,0 +1,127 @@
// TODO: remove when done
#![cfg_attr(debug_assertions, allow(unused))]
use crate::generator::Difficulty::*;
use crate::rand::{seq::SliceRandom, thread_rng};
use crate::sudoku::{Board, New};
/// categories of difficulty, indicating how many
/// empty spaces will be on a sudoku board.
#[allow(unused)]
pub enum Difficulty {
Easy,
Medium,
Hard,
Extreme,
Custom(usize),
}
impl Difficulty {
/// the number of cells to be deleted from a filled
/// sudoku board of the given `Difficulty`
pub fn removal_count(&self) -> usize {
match self {
Easy => 35,
Medium => 45,
Hard => 52,
Extreme => 62,
Custom(x) => *x,
}
}
}
/// generate a random, unsolved sudoku board with a given `Difficulty`.
pub fn generate_sudoku(difficulty: Difficulty) -> Board {
let mut board = Board::new();
while solve_random(&mut board).is_err() {}
let removal_count = difficulty.removal_count();
let mut remove_positions = (0..81).map(|i| (i / 9, i % 9)).collect::<Vec<_>>();
remove_positions.shuffle(&mut thread_rng());
for position in remove_positions.into_iter().take(removal_count) {
let (row, col) = position;
board[row][col] = 0;
}
board
}
/// TODO: currently empty cells needs to be recreated
/// on each recursive call, and are randomized anew
/// each time, which is an unnecessary overhead.
///
/// solves a sudoku, randomizing which empty cell of equal
/// mrv () to choose next.
/// this is used to generate random, fully solvable sudokus.
fn solve_random(board: &mut Board) -> Result<(), ()> {
let mut empty_cells: Vec<(usize, usize)> = Vec::new();
let mut rows: Vec<usize> = (0..9).collect::<Vec<_>>();
let mut cols: Vec<usize> = (0..9).collect::<Vec<_>>();
rows.shuffle(&mut thread_rng());
cols.shuffle(&mut thread_rng());
for &r in &rows {
for &c in &cols {
if board[r][c] == 0 {
empty_cells.push((r, c));
}
}
}
if empty_cells.is_empty() {
return Ok(());
}
// choose an empty cell with minimum remaining values heuristic
empty_cells.sort_by_key(|&(r, c)| {
let mut possibilities = 0;
for x in 1..=9 {
if valid_move(board, r, c, x) {
possibilities += 1;
}
}
possibilities
});
let (r, c) = empty_cells[0];
let mut values: Vec<u8> = (1..=9).filter(|&x| valid_move(board, r, c, x)).collect();
// sort possible values by least constraining value heuristic
values.sort_by_key(|&x| {
let mut constraints = 0;
for &(row, col) in &empty_cells {
if valid_move(board, row, col, x) {
constraints += 1;
}
}
constraints
});
for x in values {
board[r][c] = x;
match solve_random(board) {
Err(_) => board[r][c] = 0, // reset value after backtrack
_ => return Ok(()), // done
}
}
Err(()) // no valid solution for cell, backtrack
}
/// check if placing value `x` in the cell located at `row`, `col`
/// is a valid move on the given `board`.
fn valid_move(board: &Board, row: usize, col: usize, x: u8) -> bool {
for i in 0..9 {
if board[row][i] == x
|| board[i][col] == x
|| board[row / 3 * 3 + i / 3][col / 3 * 3 + i % 3] == x
{
return false;
}
}
true
}

17
src/sudoku/mod.rs Normal file
View File

@ -0,0 +1,17 @@
pub mod generator;
pub mod validator;
pub use generator::*;
pub use validator::*;
pub type Board = [[u8; 9]; 9];
trait New {
fn new() -> Self;
}
impl New for Board {
fn new() -> Self {
[[0u8; 9]; 9]
}
}

35
src/sudoku/validator.rs Normal file
View File

@ -0,0 +1,35 @@
// TODO: remove when done
#![cfg_attr(debug_assertions, allow(unused))]
use crate::sudoku::Board;
pub fn is_solution(sudoku: &Board) -> bool {
for i in 0..9 {
let mut row_set = 0u16;
let mut col_set = 0u16;
for j in 0..9 {
if sudoku[i][j] == 0 || sudoku[i][j] > 9 {
return false;
}
let (tmp_row, tmp_col) = (row_set, col_set);
row_set ^= 1 << sudoku[i][j];
col_set ^= 1 << sudoku[j][i];
if row_set < tmp_row || col_set < tmp_col {
return false;
}
}
}
for i in 0..9 {
let mut block_set = 0u16;
for j in 0..9 {
let tmp = block_set;
block_set ^= 1 << sudoku[i / 3 * 3 + i % 3][j / 3 * 3 + j % 3];
if block_set < tmp {
return false;
}
}
}
true
}

32
src/tests/generator.rs Normal file
View File

@ -0,0 +1,32 @@
use crate::{generate_sudoku, Board, Difficulty};
#[test]
fn generated_sudoku_uniqueness() {
use std::collections::HashSet;
use std::time::{Duration, Instant};
let iterations = 100;
let mut uniques: HashSet<Board> = HashSet::new();
let mut total_time = Duration::new(0, 0);
for i in 0..iterations {
let inb4 = Instant::now();
uniques.insert(generate_sudoku(Difficulty::Custom(0)));
let dt = Instant::now() - inb4;
total_time += dt;
println!("iteration {: >5} took {:.5} s", i, dt.as_secs_f64());
}
println!(
"total time taken to generate {} sudoku boards: {}",
iterations,
total_time.as_secs_f64()
);
assert!(
uniques.len() == iterations,
"expected {} generated sudokus to all be unique, but got {} duplicates.",
iterations,
iterations - uniques.len()
);
}

3
src/tests/mod.rs Normal file
View File

@ -0,0 +1,3 @@
#![cfg(test)]
mod generator;
mod validator;

49
src/tests/validator.rs Normal file
View File

@ -0,0 +1,49 @@
use crate::is_solution;
#[test]
fn valid() {
let board = [
[7, 6, 9, 5, 3, 8, 1, 2, 4],
[2, 4, 3, 7, 1, 9, 6, 5, 8],
[8, 5, 1, 4, 6, 2, 9, 7, 3],
[4, 8, 6, 9, 7, 5, 3, 1, 2],
[5, 3, 7, 6, 2, 1, 4, 8, 9],
[1, 9, 2, 8, 4, 3, 7, 6, 5],
[6, 1, 8, 3, 5, 4, 2, 9, 7],
[9, 7, 4, 2, 8, 6, 5, 3, 1],
[3, 2, 5, 1, 9, 7, 8, 4, 6],
];
assert!(is_solution(&board));
}
#[test]
fn invalid_wrong() {
let board = [
[7, 6, 9, 5, 3, 8, 1, 2, 4],
[2, 4, 3, 7, 1, 9, 6, 5, 8],
[8, 5, 1, 4, 6, 2, 9, 7, 3],
[4, 8, 6, 9, 7, 5, 3, 1, 2],
[5, 3, 7, 6, 2, 1, 4, 8, 9],
[1, 9, 2, 8, 4, 3, 7, 6, 5],
[6, 1, 8, 3, 5, 4, 2, 9, 7],
[9, 7, 4, 2, 8, 6, 5, 3, 1],
[3, 2, 5, 1, 9, 7, 8, 4, 9],
];
assert!(!is_solution(&board));
}
#[test]
fn invalid_unfinished() {
let board = [
[3, 1, 5, 8, 4, 7, 6, 2, 9],
[4, 7, 8, 2, 9, 6, 3, 5, 0],
[2, 9, 6, 3, 5, 1, 7, 8, 4],
[7, 4, 2, 9, 6, 8, 5, 1, 3],
[6, 8, 9, 5, 1, 3, 4, 7, 2],
[5, 0, 1, 4, 7, 2, 8, 9, 6],
[1, 2, 4, 6, 8, 5, 9, 3, 7],
[8, 6, 3, 7, 2, 9, 0, 4, 5],
[9, 5, 7, 1, 3, 4, 2, 6, 8],
];
assert!(!is_solution(&board));
}

142
src/ui.rs Normal file
View File

@ -0,0 +1,142 @@
use std::io;
use crossterm::{
cursor::{MoveTo, SetCursorStyle},
execute, queue,
terminal::{
disable_raw_mode, enable_raw_mode, size, Clear, ClearType::All, EnterAlternateScreen,
LeaveAlternateScreen,
},
};
pub struct Screen<T>
where
T: io::Write,
{
pub data: Vec<String>,
pub ostream: T,
pub width: usize,
pub height: usize,
}
impl<T> Screen<T>
where
T: io::Write,
{
pub fn init(ostream: T) -> Self {
let (width, height) = size().unwrap();
let mut screen = Screen {
data: vec![String::with_capacity(width as usize); height as usize],
ostream,
width: width as usize,
height: height as usize,
};
enable_raw_mode().expect("[-]: Error: ui::init: Failed to enable raw mode.");
queue!(screen.ostream, EnterAlternateScreen)
.expect("[-]: Error: ui::init: Failed to enter alternate screen.");
screen
}
pub fn deinit(&mut self) -> io::Result<()> {
disable_raw_mode()?;
execute!(self.ostream, LeaveAlternateScreen)?;
Ok(())
}
pub fn update_dimensions(&mut self) -> io::Result<()> {
let (width, height) = size()?;
self.width = width as usize;
self.height = height as usize;
Ok(())
}
pub fn clear(&mut self) -> io::Result<()> {
queue!(self.ostream, Clear(All))
}
pub fn draw(&mut self, row: usize, col: usize) -> io::Result<()> {
self.draw_screen()?;
self.place_cursor(row, col)?;
self.ostream.flush()?;
Ok(())
}
pub fn draw_screen(&mut self) -> io::Result<()> {
self.clear()?;
let s: String = self.data.join("\r\n");
write!(self.ostream, "{}", s)?;
Ok(())
}
pub fn render(&mut self, board: [[u8; 9]; 9]) {
let mut lines = Vec::new();
lines.push("┌────────┬────────┬────────┐".to_string().chars().collect());
for (row, row_slice) in board.iter().enumerate() {
match row {
3 | 6 => {
lines.push("├────────┼────────┼────────┤".to_string().chars().collect());
}
_ => {}
}
let mut line = String::new();
for (col, cur_cell) in row_slice.iter().enumerate() {
match col {
0 => line.push_str(""),
3 | 6 => line.push_str(""),
_ => {}
}
line.push(match cur_cell {
0 => ' ',
n => (b'0' + n) as char,
});
line.push(' ');
}
line.push_str("");
lines.push(line);
}
lines.push("└────────┴────────┴────────┘".to_string().chars().collect());
let pad_hori = self.width / 2 + lines[0].chars().count() / 2;
let pad_vert = self.height - lines.len();
let pad_top = pad_vert / 2;
let pad_bot = pad_vert - pad_top;
let mut new_data = Vec::new();
for _ in 0..pad_top {
new_data.push(String::new());
}
for line in lines {
let padded = format!("{: >width$}", line, width = pad_hori);
new_data.push(padded);
}
for _ in 0..pad_bot {
new_data.push(String::new());
}
self.data = new_data;
}
pub fn place_cursor(&mut self, row: usize, col: usize) -> io::Result<()> {
let (x, y) = (
(self.width / 2 - 14) as u16,
(self.height / 2 - 7 + (self.height & 1)) as u16,
);
let (x, y) = (x + 2, y + 1);
let (x, y) = (x + 2 * col as u16, y + row as u16);
let x = match col {
0..=2 => x,
3..=5 => x + 3,
_ => x + 6,
};
let y = match row {
0..=2 => y,
3..=5 => y + 1,
_ => y + 2,
};
queue!(self.ostream, MoveTo(x, y), SetCursorStyle::SteadyBlock)
}
}