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..05884da 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(); @@ -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(); diff --git a/src/sudoku.rs b/src/sudoku.rs index 39690d2..c7dd308 100644 --- a/src/sudoku.rs +++ b/src/sudoku.rs @@ -1,43 +1,83 @@ +use rand::prelude::SliceRandom; use rand::Rng; const SQUARE_SIZE: usize = 3; -pub fn generate(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, +pub fn generate_board(size: usize, difficulty: usize) -> Vec> { + loop { + let mut board = vec![vec![0; size]; size]; + let mut rng = rand::thread_rng(); + + // 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); + } + + // Solve the board + if !resolv_backtrack(&mut board, 0, 0) { + continue; + } + + // Remove numbers while maintaining a unique solution + remove_num(&mut board, difficulty, &mut rng); + return 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]; + } + } +} + +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, }; - 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; - } + 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; } } - board + + 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 } @@ -45,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;