From 3e6a9a193305bae474200f973bc60b6d181195a4 Mon Sep 17 00:00:00 2001 From: Antony DAVID Date: Mon, 24 Jun 2024 13:55:29 +0200 Subject: [PATCH 1/4] feat: generate random boards --- Dockerfile | 4 +++- README.md | 4 ++++ src/main.rs | 23 ++++++++++++++++++- src/sudoku.rs | 61 ++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 77 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index c86149c..e52ef54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM rust:1 AS base +RUN cargo install cargo-chef WORKDIR /app FROM base AS dev @@ -7,7 +8,6 @@ COPY . . CMD ["cargo", "watch", "-x", "run"] FROM base AS planner -RUN cargo install cargo-chef COPY . . RUN cargo chef prepare --recipe-path recipe.json @@ -21,4 +21,6 @@ RUN cargo build --release --bin sudoku-rust FROM debian:stable-slim AS prod WORKDIR /app COPY --from=build /app/target/release/sudoku-rust /usr/local/bin +COPY --from=build /app/styles /app/styles +COPY --from=build /app/templates /app/templates ENTRYPOINT ["/usr/local/bin/sudoku-rust"] diff --git a/README.md b/README.md index e4826f8..6fda856 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ Build the docker image Run the docker container ```bash + # with live reload + docker run --rm --name sudoku-rust -it -p 8000:8000 -v $(pwd):/app sudoku-rust + + # release optimized docker run --name sudoku-rust -it -p 8000:8000 sudoku-rust ``` diff --git a/src/main.rs b/src/main.rs index 61ac76d..7d3280d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,7 @@ async fn update_table( difficulty: web::Path, ) -> impl Responder { let difficulty = difficulty.into_inner(); - let board = sudoku::generate(BOARD_SIZE, difficulty); + let board = sudoku::generate_board(BOARD_SIZE, difficulty); app_state.set_board(board.clone()); let mut context = Context::new(); @@ -128,4 +128,25 @@ mod tests { } assert!(resolv_backtrack(&mut board.clone(), 0, 0)); } + + #[test] + fn board_invalid() { + const BOARD_SIZE: usize = 9; + let board = generate(BOARD_SIZE, 1); + assert_eq!(board.len(), 9); + + let mut hm = std::collections::HashMap::new(); + for row in board.iter().take(BOARD_SIZE).enumerate() { + for value in row.1.iter().take(BOARD_SIZE) { + if hm.contains_key(value) { + panic!("Invalid board"); + } + if *value != 0 { + hm.insert(*value, true); + } + } + hm.clear(); + } + assert!(!resolv_backtrack(&mut board.clone(), 0, 0)); + } } diff --git a/src/sudoku.rs b/src/sudoku.rs index 39690d2..cd4265f 100644 --- a/src/sudoku.rs +++ b/src/sudoku.rs @@ -1,27 +1,62 @@ use rand::Rng; +use rand::prelude::SliceRandom; const SQUARE_SIZE: usize = 3; -pub fn generate(size: usize, difficulty: usize) -> Vec> { +pub fn generate_board(size: usize, difficulty: usize) -> Vec> { let mut board = vec![vec![0; size]; size]; let mut rng = rand::thread_rng(); - let luck: f64 = match difficulty { - 1 => 0.4, - 2 => 0.5, - 3 => 0.6, - _ => 0.4, - }; - resolv_backtrack(&mut board, 0, 0); // generate a valid board - for i in board.iter_mut().take(size) { - for j in 0..size { - if rng.gen_bool(luck) { - (*i)[j] = 0; - } + // Fill the diagonal blocks, this is the "seed" + for i in (0..size).step_by((size as f64).sqrt() as usize) { + fill_block(&mut board, i, i, &mut rng); + } + + // Solve the board + let res = resolv_backtrack(&mut board, 0, 0); + if !res { + return generate_board(size, difficulty); + } + + let keep: usize = match difficulty { + // Easy keep 50% of the numbers + 1 => ((board.len() as f64 * board.len() as f64) * 0.5) as usize, + // Medium keep 40% of the numbers + 2 => ((board.len() as f64 * board.len() as f64) * 0.4) as usize, + // Hard keep 30% of the numbers + 3 => ((board.len() as f64 * board.len() as f64) * 0.3) as usize, + // Maximum difficulty keep 17 numbers + 4 => 17, + _ => ((board.len() as f64 * board.len() as f64) * 0.5) as usize, + }; + let mut counter = board.len() as usize * board.len() as usize; + + while counter > keep { + let row = rng.gen_range(0..size); + let col = rng.gen_range(0..size); + if board[row][col] != 0 { + board[row][col] = 0; + counter -= 1; } } + board } + +// Fill a square block with random numbers +fn fill_block(board: &mut Vec>, row: usize, col: usize, rng: &mut impl Rng) { + let mut nums: Vec = (1..=board.len()).collect(); + nums.shuffle(rng); + + for i in 0..SQUARE_SIZE { + for j in 0..SQUARE_SIZE { + board[row + i][col + j] = nums[i * SQUARE_SIZE + j]; + } + } +} + + +// Check if a number is valid in a cell (row, col) pub fn is_num_valid(board: &[Vec], row: usize, col: usize, num: usize) -> bool { for i in 0..board.len() { if board[row][i] == num || board[i][col] == num { From 362251911c61444c716b4fa2f93899fa98736804 Mon Sep 17 00:00:00 2001 From: Antony David Date: Mon, 24 Jun 2024 19:44:01 +0200 Subject: [PATCH 2/4] fix: fmt & clippy --- src/sudoku.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sudoku.rs b/src/sudoku.rs index cd4265f..28b266f 100644 --- a/src/sudoku.rs +++ b/src/sudoku.rs @@ -1,5 +1,5 @@ -use rand::Rng; use rand::prelude::SliceRandom; +use rand::Rng; const SQUARE_SIZE: usize = 3; @@ -30,7 +30,7 @@ pub fn generate_board(size: usize, difficulty: usize) -> Vec> { _ => ((board.len() as f64 * board.len() as f64) * 0.5) as usize, }; let mut counter = board.len() as usize * board.len() as usize; - + while counter > keep { let row = rng.gen_range(0..size); let col = rng.gen_range(0..size); @@ -44,7 +44,7 @@ pub fn generate_board(size: usize, difficulty: usize) -> Vec> { } // Fill a square block with random numbers -fn fill_block(board: &mut Vec>, row: usize, col: usize, rng: &mut impl Rng) { +fn fill_block(board: &mut [Vec], row: usize, col: usize, rng: &mut impl Rng) { let mut nums: Vec = (1..=board.len()).collect(); nums.shuffle(rng); @@ -55,7 +55,6 @@ fn fill_block(board: &mut Vec>, row: usize, col: usize, rng: &mut imp } } - // Check if a number is valid in a cell (row, col) pub fn is_num_valid(board: &[Vec], row: usize, col: usize, num: usize) -> bool { for i in 0..board.len() { From a9a71cd1db67aa8aa6405c4771515c3d43bd6415 Mon Sep 17 00:00:00 2001 From: Antony David Date: Mon, 24 Jun 2024 19:49:57 +0200 Subject: [PATCH 3/4] fix: test --- src/main.rs | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7d3280d..05884da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,12 +106,12 @@ async fn main() -> std::io::Result<()> { #[cfg(test)] mod tests { - use crate::sudoku::{generate, resolv_backtrack}; + use crate::sudoku::{generate_board, resolv_backtrack}; #[test] fn board_valid() { const BOARD_SIZE: usize = 9; - let board = generate(BOARD_SIZE, 1); + let board = generate_board(BOARD_SIZE, 1); assert_eq!(board.len(), 9); let mut hm = std::collections::HashMap::new(); @@ -128,25 +128,4 @@ mod tests { } assert!(resolv_backtrack(&mut board.clone(), 0, 0)); } - - #[test] - fn board_invalid() { - const BOARD_SIZE: usize = 9; - let board = generate(BOARD_SIZE, 1); - assert_eq!(board.len(), 9); - - let mut hm = std::collections::HashMap::new(); - for row in board.iter().take(BOARD_SIZE).enumerate() { - for value in row.1.iter().take(BOARD_SIZE) { - if hm.contains_key(value) { - panic!("Invalid board"); - } - if *value != 0 { - hm.insert(*value, true); - } - } - hm.clear(); - } - assert!(!resolv_backtrack(&mut board.clone(), 0, 0)); - } } From 7ffdea5f1bb0204486bd50e6bdfb805b481dad8d Mon Sep 17 00:00:00 2001 From: Antony David Date: Mon, 24 Jun 2024 22:23:18 +0200 Subject: [PATCH 4/4] perf: generate board faster --- src/sudoku.rs | 96 +++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/src/sudoku.rs b/src/sudoku.rs index 28b266f..c7dd308 100644 --- a/src/sudoku.rs +++ b/src/sudoku.rs @@ -4,43 +4,24 @@ use rand::Rng; const SQUARE_SIZE: usize = 3; pub fn generate_board(size: usize, difficulty: usize) -> Vec> { - let mut board = vec![vec![0; size]; size]; - let mut rng = rand::thread_rng(); + loop { + let mut board = vec![vec![0; size]; size]; + let mut rng = rand::thread_rng(); - // Fill the diagonal blocks, this is the "seed" - for i in (0..size).step_by((size as f64).sqrt() as usize) { - fill_block(&mut board, i, i, &mut rng); - } - - // Solve the board - let res = resolv_backtrack(&mut board, 0, 0); - if !res { - return generate_board(size, difficulty); - } - - let keep: usize = match difficulty { - // Easy keep 50% of the numbers - 1 => ((board.len() as f64 * board.len() as f64) * 0.5) as usize, - // Medium keep 40% of the numbers - 2 => ((board.len() as f64 * board.len() as f64) * 0.4) as usize, - // Hard keep 30% of the numbers - 3 => ((board.len() as f64 * board.len() as f64) * 0.3) as usize, - // Maximum difficulty keep 17 numbers - 4 => 17, - _ => ((board.len() as f64 * board.len() as f64) * 0.5) as usize, - }; - let mut counter = board.len() as usize * board.len() as usize; + // Fill the diagonal blocks + for i in (0..size).step_by((size as f64).sqrt() as usize) { + fill_block(&mut board, i, i, &mut rng); + } - while counter > keep { - let row = rng.gen_range(0..size); - let col = rng.gen_range(0..size); - if board[row][col] != 0 { - board[row][col] = 0; - counter -= 1; + // Solve the board + if !resolv_backtrack(&mut board, 0, 0) { + continue; } - } - board + // Remove numbers while maintaining a unique solution + remove_num(&mut board, difficulty, &mut rng); + return board; + } } // Fill a square block with random numbers @@ -55,23 +36,48 @@ fn fill_block(board: &mut [Vec], row: usize, col: usize, rng: &mut impl R } } +fn remove_num(board: &mut [Vec], difficulty: usize, rng: &mut impl Rng) -> bool { + let size = board.len(); + let to_remove = match difficulty { + 1 => (size * size) / 2, + 2 => (size * size * 3) / 5, + 3 => (size * size * 7) / 10, + 4 => size * size - 17, + _ => (size * size) / 2, + }; + + let mut positions: Vec<(usize, usize)> = (0..size) + .flat_map(|r| (0..size).map(move |c| (r, c))) + .collect(); + positions.shuffle(rng); + + for _ in 0..to_remove { + if let Some((row, col)) = positions.pop() { + board[row][col] = 0; + } + } + + true +} + // Check if a number is valid in a cell (row, col) pub fn is_num_valid(board: &[Vec], row: usize, col: usize, num: usize) -> bool { - for i in 0..board.len() { - if board[row][i] == num || board[i][col] == num { - return false; - } + let size = board.len(); + + if (0..size).any(|i| board[row][i] == num || board[i][col] == num) { + return false; } let sub_row = (row / SQUARE_SIZE) * SQUARE_SIZE; let sub_col = (col / SQUARE_SIZE) * SQUARE_SIZE; - for i in 0..SQUARE_SIZE { - for j in 0..SQUARE_SIZE { - if board[sub_row + i][sub_col + j] == num { - return false; - } - } - } + + board.iter().skip(sub_row).take(SQUARE_SIZE).any(|row| { + row.iter() + .skip(sub_col) + .take(SQUARE_SIZE) + .any(|&cell| cell == num) + }); + true } @@ -79,7 +85,7 @@ pub fn is_num_valid(board: &[Vec], row: usize, col: usize, num: usize) -> // https://en.wikipedia.org/wiki/Sudoku_solving_algorithms#Backtracking // inspired by https://gist.github.com/raeffu/8331328 -pub fn resolv_backtrack(board: &mut Vec>, mut row: usize, mut col: usize) -> bool { +pub fn resolv_backtrack(board: &mut [Vec], mut row: usize, mut col: usize) -> bool { if col == board.len() { col = 0; row += 1;