add sudoku generator & validator
This commit is contained in:
parent
a9395dc55f
commit
808abac4d8
48
Cargo.lock
generated
48
Cargo.lock
generated
@ -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]]
|
||||
|
||||
@ -10,3 +10,4 @@ categories = ["games", "mathematics", "science", "algorithms"]
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.27.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
193
src/main.rs
193
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<String>, 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;
|
||||
|
||||
72
src/state.rs
Normal file
72
src/state.rs
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
127
src/sudoku/generator.rs
Normal file
127
src/sudoku/generator.rs
Normal file
@ -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::<Vec<_>>();
|
||||
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<usize> = (0..9).collect::<Vec<_>>();
|
||||
let mut cols: Vec<usize> = (0..9).collect::<Vec<_>>();
|
||||
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<u8> = (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
|
||||
}
|
||||
17
src/sudoku/mod.rs
Normal file
17
src/sudoku/mod.rs
Normal file
@ -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]
|
||||
}
|
||||
}
|
||||
35
src/sudoku/validator.rs
Normal file
35
src/sudoku/validator.rs
Normal file
@ -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
|
||||
}
|
||||
32
src/tests/generator.rs
Normal file
32
src/tests/generator.rs
Normal file
@ -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<Board> = 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()
|
||||
);
|
||||
}
|
||||
3
src/tests/mod.rs
Normal file
3
src/tests/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
#![cfg(test)]
|
||||
mod generator;
|
||||
mod validator;
|
||||
49
src/tests/validator.rs
Normal file
49
src/tests/validator.rs
Normal file
@ -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));
|
||||
}
|
||||
142
src/ui.rs
Normal file
142
src/ui.rs
Normal file
@ -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<T>
|
||||
where
|
||||
T: io::Write,
|
||||
{
|
||||
pub data: Vec<String>,
|
||||
pub ostream: T,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl<T> Screen<T>
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user