diff --git a/Cargo.lock b/Cargo.lock index 2642848..30fdba9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 089a252..e3b19a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ categories = ["games", "mathematics", "science", "algorithms"] [dependencies] crossterm = "0.27.0" +rand = "0.8.5" diff --git a/src/main.rs b/src/main.rs index d1fdfc1..da7c885 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, 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; diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..8695ce6 --- /dev/null +++ b/src/state.rs @@ -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, + }; + } +} diff --git a/src/sudoku/generator.rs b/src/sudoku/generator.rs new file mode 100644 index 0000000..afd08b5 --- /dev/null +++ b/src/sudoku/generator.rs @@ -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::>(); + 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 = (0..9).collect::>(); + let mut cols: Vec = (0..9).collect::>(); + 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 = (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 +} diff --git a/src/sudoku/mod.rs b/src/sudoku/mod.rs new file mode 100644 index 0000000..1fa4966 --- /dev/null +++ b/src/sudoku/mod.rs @@ -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] + } +} diff --git a/src/sudoku/validator.rs b/src/sudoku/validator.rs new file mode 100644 index 0000000..b5fa24e --- /dev/null +++ b/src/sudoku/validator.rs @@ -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 +} diff --git a/src/tests/generator.rs b/src/tests/generator.rs new file mode 100644 index 0000000..1891096 --- /dev/null +++ b/src/tests/generator.rs @@ -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 = 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() + ); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..177cb7c --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,3 @@ +#![cfg(test)] +mod generator; +mod validator; diff --git a/src/tests/validator.rs b/src/tests/validator.rs new file mode 100644 index 0000000..77655f8 --- /dev/null +++ b/src/tests/validator.rs @@ -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)); +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..d6563eb --- /dev/null +++ b/src/ui.rs @@ -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 +where + T: io::Write, +{ + pub data: Vec, + pub ostream: T, + pub width: usize, + pub height: usize, +} + +impl Screen +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) + } +}