Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM rust:1 AS base
RUN cargo install cargo-chef
WORKDIR /app

FROM base AS dev
Expand All @@ -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

Expand All @@ -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"]
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
6 changes: 3 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async fn update_table(
difficulty: web::Path<usize>,
) -> 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();
Expand Down Expand Up @@ -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();
Expand Down
94 changes: 67 additions & 27 deletions src/sudoku.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,91 @@
use rand::prelude::SliceRandom;
use rand::Rng;

const SQUARE_SIZE: usize = 3;

pub fn generate(size: usize, difficulty: usize) -> Vec<Vec<usize>> {
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<Vec<usize>> {
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<usize>], row: usize, col: usize, rng: &mut impl Rng) {
let mut nums: Vec<usize> = (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<usize>], 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<usize>], 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
}

// backtracking algorithm
// https://en.wikipedia.org/wiki/Sudoku_solving_algorithms#Backtracking
// inspired by https://gist.github.com/raeffu/8331328

pub fn resolv_backtrack(board: &mut Vec<Vec<usize>>, mut row: usize, mut col: usize) -> bool {
pub fn resolv_backtrack(board: &mut [Vec<usize>], mut row: usize, mut col: usize) -> bool {
if col == board.len() {
col = 0;
row += 1;
Expand Down