diff --git a/src/main.rs b/src/main.rs index 88c7086..973ee0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,10 @@ use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; use lazy_static::lazy_static; -use rand::Rng; use std::sync::Mutex; use tera::{Context, Tera}; +mod sudoku; const BOARD_SIZE: usize = 9; -const SQUARE_SIZE: usize = 3; - struct Sudoku { pub board: Mutex>>, } @@ -47,7 +45,7 @@ async fn update_table( difficulty: web::Path, ) -> impl Responder { let difficulty = difficulty.into_inner(); - let board = generate(BOARD_SIZE, difficulty); + let board = sudoku::generate(BOARD_SIZE, difficulty); app_state.set_board(board.clone()); let mut context = Context::new(); @@ -62,7 +60,7 @@ async fn update_table( async fn solve_table(tera: web::Data, data: web::Data) -> impl Responder { let mut board = data.board.lock().unwrap().clone(); - resolv_backtrack(&mut board, 0, 0); + sudoku::resolv_backtrack(&mut board, 0, 0); data.set_board(board.clone()); let mut context = Context::new(); @@ -106,74 +104,28 @@ async fn main() -> std::io::Result<()> { .await } -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 => luck = 0.4, - 2 => luck = 0.45, - 3 => luck = 0.5, - _ => luck = 0.4, - } - resolv_backtrack(&mut board, 0, 0); // generate a valid board - for i in 0..size { - for j in 0..size { - if rng.gen_bool(luck) { - board[i][j] = 0; +#[cfg(test)] +mod tests { + use crate::{sudoku::generate, sudoku::resolv_backtrack}; + + #[test] + fn board_valid() { + const BOARD_SIZE: usize = 9; + let board = generate(BOARD_SIZE, 1); + assert_eq!(board.len(), 9); + + let mut hm = std::collections::HashMap::new(); + for i in 0..BOARD_SIZE { + for j in 0..BOARD_SIZE { + if hm.contains_key(&board[i][j]) { + assert!(false); + } + if board[i][j] != 0 { + hm.insert(board[i][j], true); + } } + hm.clear(); } + assert_eq!(resolv_backtrack(&mut board.clone(), 0, 0), true); } - board -} -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 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; - } - } - } - 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>, mut row: usize, mut col: usize) -> bool { - if col == board.len() { - col = 0; - row += 1; - if row == board.len() { - // end of board - return true; - } - } - - if board[row][col] != 0 { - return resolv_backtrack(board, row, col + 1); - } - - for num in 1..=board.len() { - if is_num_valid(board, row, col, num) { - board[row][col] = num; - if resolv_backtrack(board, row, col + 1) { - // found a number - return true; - } - // backtrack - board[row][col] = 0; - } - } - - false } diff --git a/src/sudoku.rs b/src/sudoku.rs new file mode 100644 index 0000000..32a5b59 --- /dev/null +++ b/src/sudoku.rs @@ -0,0 +1,75 @@ +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 => luck = 0.4, + 2 => luck = 0.5, + 3 => luck = 0.6, + _ => luck = 0.4, + } + resolv_backtrack(&mut board, 0, 0); // generate a valid board + for i in 0..size { + for j in 0..size { + if rng.gen_bool(luck) { + board[i][j] = 0; + } + } + } + board +} +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 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; + } + } + } + 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>, mut row: usize, mut col: usize) -> bool { + if col == board.len() { + col = 0; + row += 1; + if row == board.len() { + // end of board + return true; + } + } + + if board[row][col] != 0 { + return resolv_backtrack(board, row, col + 1); + } + + for num in 1..=board.len() { + if is_num_valid(board, row, col, num) { + board[row][col] = num; + if resolv_backtrack(board, row, col + 1) { + // found a number + return true; + } + // backtrack + board[row][col] = 0; + } + } + + false +} diff --git a/styles/main.css b/styles/main.css index b9d7dd9..5395375 100644 --- a/styles/main.css +++ b/styles/main.css @@ -2,8 +2,18 @@ box-sizing: border-box; } +body { + display: flex; + flex-direction: column; + margin: auto; + min-height: 100vh; + background-color: #131516 !important; +} + h1 { text-align: center; + color: #dd4814; + margin-top: 3rem; } table { @@ -17,17 +27,18 @@ td.sudoku-case[data-value='0'] { } td { - border: 1px solid black; + border: 1px solid #8d8272; width: 40px; height: 40px; font-size: 24px; font-weight: bold; - background-color: #fff; + background-color: #191b1c; + color: #eee; cursor: pointer; } td:hover { - background-color: #eee; + background-color: #2c2f30; } .button-container { @@ -40,9 +51,10 @@ td:hover { padding: 10px; font-size: 20px; font-weight: bold; - color: white; + color: #eee; border: none; cursor: pointer; + border-radius: 4px; } .easy-button { @@ -64,9 +76,10 @@ td:hover { font-size: 20px; font-weight: bold; background-color: #4caf50; - color: white; + color: #eee; border: none; cursor: pointer; + border-radius: 4px; } .generate-button:hover { @@ -83,38 +96,56 @@ td:hover { opacity: 0.8; } +footer { + color: #eee; + padding: 20px; + text-align: center; + font-size: 14px; + margin-top: auto; + background-color: #191b1c !important; +} + +footer a { + color: #dd4814; + text-decoration: none; +} + +footer a:hover { + text-decoration: underline; +} + tr:nth-child(3n + 1) td:nth-child(3n + 1) { - border-top: 3px solid black; - border-left: 3px solid black; + border-top: 3px solid #8d8272; + border-left: 3px solid #8d8272; } tr:nth-child(3n + 1) td:nth-child(3n + 2) { - border-top: 3px solid black; + border-top: 3px solid #8d8272; } tr:nth-child(3n + 1) td:nth-child(3n + 3) { - border-top: 3px solid black; - border-right: 3px solid black; + border-top: 3px solid #8d8272; + border-right: 3px solid #8d8272; } tr:nth-child(3n + 2) td:nth-child(3n + 1) { - border-left: 3px solid black; + border-left: 3px solid #8d8272; } tr:nth-child(3n + 2) td:nth-child(3n + 3) { - border-right: 3px solid black; + border-right: 3px solid #8d8272; } tr:nth-child(3n + 3) td:nth-child(3n + 1) { - border-bottom: 3px solid black; - border-left: 3px solid black; + border-bottom: 3px solid #8d8272; + border-left: 3px solid #8d8272; } tr:nth-child(3n + 3) td:nth-child(3n + 2) { - border-bottom: 3px solid black; + border-bottom: 3px solid #8d8272; } tr:nth-child(3n + 3) td:nth-child(3n + 3) { - border-bottom: 3px solid black; - border-right: 3px solid black; + border-bottom: 3px solid #8d8272; + border-right: 3px solid #8d8272; } diff --git a/templates/layout/footer.html b/templates/layout/footer.html new file mode 100644 index 0000000..99d5d58 --- /dev/null +++ b/templates/layout/footer.html @@ -0,0 +1,4 @@ +
+

Made in rust with Actix.

+

Copyright © Jayllyz

+
diff --git a/templates/pages/index.html b/templates/pages/index.html index ad8699f..d538892 100644 --- a/templates/pages/index.html +++ b/templates/pages/index.html @@ -4,7 +4,7 @@ {{ title }} - +
@@ -31,5 +31,6 @@

{{ title }}

+ {% include "layout/footer.html" %}