use ab_glyph::FontRef;
use image::RgbImage;
use imageproc::drawing::draw_text_mut;
use thiserror::Error;
use crate::{
structures::dancing_links::{DancingLinksError, DancingMatrix},
util::{BLACK_PIXEL, ROBOTO_MEDIUM, WHITE_PIXEL},
RgbBuffer,
};
const GRID_SIZE: usize = 9;
const SUDOKU_CONSTRAINTS: [[usize; 9]; 324] = const {
let mut constraints = [[0; 9]; 324];
let mut index = 0;
while index < 81 {
let mut inner_index = 0;
while inner_index < 9 {
constraints[index][inner_index] = index * 9 + inner_index;
constraints[index + 81][inner_index] = (index % 9) + inner_index * 9 + (index / 9) * 81;
constraints[index + 162][inner_index] = index + inner_index * 81;
constraints[index + 243][inner_index] = (index % 9)
+ (inner_index % 3) * 9
+ (inner_index / 3) * 81
+ ((index % 27) / 9) * 27
+ (index / 27) * 243;
inner_index += 1;
}
index += 1;
}
constraints
};
#[derive(Debug, Error)]
pub enum SudokuError {
#[error("invalid integer `{0}`, must be integer 1-9 (use `0` for an empty space)")]
InvalidInteger(char),
#[error("sudoku must by 9 x 9, got {0} entries")]
InvalidSize(usize),
#[error("sudoku has no solution")]
NoSolution,
}
pub fn parse_sudoku(puzzle: &str) -> Result<Vec<u8>, SudokuError> {
let puzzle = puzzle
.chars()
.map(|char| {
char.to_digit(10)
.map(|x| x as u8)
.ok_or(SudokuError::InvalidInteger(char))
})
.collect::<Result<Vec<u8>, SudokuError>>()?;
if puzzle.len() != GRID_SIZE * GRID_SIZE {
return Err(SudokuError::InvalidSize(puzzle.len()));
}
Ok(puzzle)
}
pub fn solve_sudoku(puzzle: &[u8]) -> Result<Vec<u8>, SudokuError> {
if puzzle.len() != GRID_SIZE * GRID_SIZE {
return Err(SudokuError::InvalidSize(puzzle.len()));
}
let mut matrix = DancingMatrix::new(
SUDOKU_CONSTRAINTS
.iter()
.map(|constraint| constraint.iter()),
);
for (index, &value) in puzzle.iter().enumerate() {
if value == 0 {
continue;
}
let result = matrix.add_solution(index * 9 + (value as usize) - 1);
if matches!(result, Err(DancingLinksError::InvalidRow)) {
return Err(SudokuError::NoSolution);
}
}
let mut solution = matrix.solve().map_err(|_| SudokuError::NoSolution)?;
solution.sort_unstable();
Ok(solution.iter().map(|num| (num % 9) as u8 + 1).collect())
}
pub fn print_sudoku(puzzle: &[u8]) -> Result<RgbBuffer, SudokuError> {
if puzzle.len() != GRID_SIZE * GRID_SIZE {
return Err(SudokuError::InvalidSize(puzzle.len()));
}
const IMAGE_SIZE: u32 = GRID_SIZE as u32 * 100;
let mut image = RgbImage::from_pixel(IMAGE_SIZE, IMAGE_SIZE, WHITE_PIXEL);
let font = FontRef::try_from_slice(ROBOTO_MEDIUM).expect("Font should be valid");
for grid_pos in 0..(GRID_SIZE as u32) {
for line_coord in 0..IMAGE_SIZE {
let grid_coord = grid_pos * 100;
if grid_pos % 3 == 0 && grid_pos != 0 && grid_pos != GRID_SIZE as u32 - 1 {
image.put_pixel(line_coord, grid_coord + 1, BLACK_PIXEL);
image.put_pixel(line_coord, grid_coord - 1, BLACK_PIXEL);
image.put_pixel(grid_coord + 1, line_coord, BLACK_PIXEL);
image.put_pixel(grid_coord - 1, line_coord, BLACK_PIXEL);
}
image.put_pixel(line_coord, grid_coord, BLACK_PIXEL);
image.put_pixel(grid_coord, line_coord, BLACK_PIXEL);
}
}
for (i, &number) in puzzle.iter().enumerate() {
if number == 0 {
continue;
}
let x = (i % GRID_SIZE) * 100 + 25;
let y = (i / GRID_SIZE) * 100 + 5;
draw_text_mut(
&mut image,
BLACK_PIXEL,
x as i32,
y as i32,
100.0,
&font,
&number.to_string(),
);
}
Ok(image)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use image::ImageFormat;
fn test_parse(string: &str, expected: Vec<u8>) {
let actual = super::parse_sudoku(string).expect("should be ok");
assert_eq!(actual, expected);
}
fn test_solve(puzzle: &[u8], expected: &[u8]) {
let acutal = super::solve_sudoku(puzzle).expect("should be ok");
assert_eq!(acutal, expected);
}
fn test_print(puzzle: Vec<u8>, expected: &[u8]) {
let mut actual = Vec::new();
super::print_sudoku(&puzzle)
.expect("should be ok")
.write_to(&mut Cursor::new(&mut actual), ImageFormat::Png)
.expect("should be ok");
assert_eq!(actual, expected);
}
const EASY_STRING: &str =
"415830090003009104002150006900783000200000381500012400004900063380500040009307500";
const EASY_UNSOLVED: [u8; 81] = [
4, 1, 5, 8, 3, 0, 0, 9, 0, 0, 0, 3, 0, 0, 9, 1, 0, 4, 0, 0, 2, 1, 5, 0, 0, 0, 6, 9, 0, 0,
7, 8, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 3, 8, 1, 5, 0, 0, 0, 1, 2, 4, 0, 0, 0, 0, 4, 9, 0, 0,
0, 6, 3, 3, 8, 0, 5, 0, 0, 0, 4, 0, 0, 0, 9, 3, 0, 7, 5, 0, 0,
];
const EASY_SOLVED: [u8; 81] = [
4, 1, 5, 8, 3, 6, 2, 9, 7, 8, 6, 3, 2, 7, 9, 1, 5, 4, 7, 9, 2, 1, 5, 4, 8, 3, 6, 9, 4, 1,
7, 8, 3, 6, 2, 5, 2, 7, 6, 4, 9, 5, 3, 8, 1, 5, 3, 8, 6, 1, 2, 4, 7, 9, 1, 5, 4, 9, 2, 8,
7, 6, 3, 3, 8, 7, 5, 6, 1, 9, 4, 2, 6, 2, 9, 3, 4, 7, 5, 1, 8,
];
const EASY_UNSOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/unsolved/easy.png");
const EASY_SOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/solved/easy.png");
#[test]
fn parse_easy() {
test_parse(EASY_STRING, EASY_UNSOLVED.to_vec());
}
#[test]
fn miri_solve_easy() {
test_solve(&EASY_UNSOLVED, &EASY_SOLVED);
}
#[test]
fn print_easy() {
test_print(EASY_UNSOLVED.to_vec(), EASY_UNSOLVED_IMAGE);
test_print(EASY_SOLVED.to_vec(), EASY_SOLVED_IMAGE);
}
const MEDIUM_STRING: &str =
"500000300009000027400105009200000070000006000006049000300027900080600000000034012";
const MEDIUM_UNSOLVED: [u8; 81] = [
5, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 9, 0, 0, 0, 0, 2, 7, 4, 0, 0, 1, 0, 5, 0, 0, 9, 2, 0, 0,
0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 6, 0, 4, 9, 0, 0, 0, 3, 0, 0, 0, 2, 7,
9, 0, 0, 0, 8, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 1, 2,
];
const MEDIUM_SOLVED: [u8; 81] = [
5, 7, 8, 9, 6, 2, 3, 4, 1, 6, 1, 9, 4, 8, 3, 5, 2, 7, 4, 2, 3, 1, 7, 5, 8, 6, 9, 2, 9, 4,
3, 1, 8, 6, 7, 5, 8, 3, 7, 2, 5, 6, 1, 9, 4, 1, 5, 6, 7, 4, 9, 2, 3, 8, 3, 4, 1, 5, 2, 7,
9, 8, 6, 7, 8, 2, 6, 9, 1, 4, 5, 3, 9, 6, 5, 8, 3, 4, 7, 1, 2,
];
const MEDIUM_UNSOLVED_IMAGE: &[u8] =
include_bytes!("../../../tests/sudoku/unsolved/medium.png");
const MEDIUM_SOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/solved/medium.png");
#[test]
fn parse_medium() {
test_parse(MEDIUM_STRING, MEDIUM_UNSOLVED.to_vec());
}
#[test]
fn miri_solve_medium() {
test_solve(&MEDIUM_UNSOLVED, &MEDIUM_SOLVED);
}
#[test]
fn print_medium() {
test_print(MEDIUM_UNSOLVED.to_vec(), MEDIUM_UNSOLVED_IMAGE);
test_print(MEDIUM_SOLVED.to_vec(), MEDIUM_SOLVED_IMAGE);
}
const HARD1_STRING: &str =
"000030400900400300300000072009005000800010000700600529000100700601050008040000010";
const HARD1_UNSOLVED: [u8; 81] = [
0, 0, 0, 0, 3, 0, 4, 0, 0, 9, 0, 0, 4, 0, 0, 3, 0, 0, 3, 0, 0, 0, 0, 0, 0, 7, 2, 0, 0, 9,
0, 0, 5, 0, 0, 0, 8, 0, 0, 0, 1, 0, 0, 0, 0, 7, 0, 0, 6, 0, 0, 5, 2, 9, 0, 0, 0, 1, 0, 0,
7, 0, 0, 6, 0, 1, 0, 5, 0, 0, 0, 8, 0, 4, 0, 0, 0, 0, 0, 1, 0,
];
const HARD1_SOLVED: [u8; 81] = [
1, 7, 2, 5, 3, 8, 4, 9, 6, 9, 8, 6, 4, 7, 2, 3, 5, 1, 3, 5, 4, 9, 6, 1, 8, 7, 2, 4, 6, 9,
3, 2, 5, 1, 8, 7, 8, 2, 5, 7, 1, 9, 6, 3, 4, 7, 1, 3, 6, 8, 4, 5, 2, 9, 2, 9, 8, 1, 4, 3,
7, 6, 5, 6, 3, 1, 2, 5, 7, 9, 4, 8, 5, 4, 7, 8, 9, 6, 2, 1, 3,
];
const HARD1_UNSOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/unsolved/hard1.png");
const HARD1_SOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/solved/hard1.png");
#[test]
fn parse_hard1() {
test_parse(HARD1_STRING, HARD1_UNSOLVED.to_vec());
}
#[test]
fn miri_solve_hard1() {
test_solve(&HARD1_UNSOLVED, &HARD1_SOLVED);
}
#[test]
fn print_hard1() {
test_print(HARD1_UNSOLVED.to_vec(), HARD1_UNSOLVED_IMAGE);
test_print(HARD1_SOLVED.to_vec(), HARD1_SOLVED_IMAGE);
}
const HARD2_STRING: &str =
"800000000003600000070090200050007000000045700000100030001000068008500010090000400";
const HARD2_UNSOLVED: [u8; 81] = [
8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 0, 0, 0, 0, 0, 0, 7, 0, 0, 9, 0, 2, 0, 0, 0, 5, 0,
0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 4, 5, 7, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0,
0, 6, 8, 0, 0, 8, 5, 0, 0, 0, 1, 0, 0, 9, 0, 0, 0, 0, 4, 0, 0,
];
const HARD2_SOLVED: [u8; 81] = [
8, 1, 2, 7, 5, 3, 6, 4, 9, 9, 4, 3, 6, 8, 2, 1, 7, 5, 6, 7, 5, 4, 9, 1, 2, 8, 3, 1, 5, 4,
2, 3, 7, 8, 9, 6, 3, 6, 9, 8, 4, 5, 7, 2, 1, 2, 8, 7, 1, 6, 9, 5, 3, 4, 5, 2, 1, 9, 7, 4,
3, 6, 8, 4, 3, 8, 5, 2, 6, 9, 1, 7, 7, 9, 6, 3, 1, 8, 4, 5, 2,
];
const HARD2_UNSOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/unsolved/hard2.png");
const HARD2_SOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/solved/hard2.png");
#[test]
fn parse_hard2() {
test_parse(HARD2_STRING, HARD2_UNSOLVED.to_vec());
}
#[test]
fn miri_solve_hard2() {
test_solve(&HARD2_UNSOLVED, &HARD2_SOLVED);
}
#[test]
fn print_hard2() {
test_print(HARD2_UNSOLVED.to_vec(), HARD2_UNSOLVED_IMAGE);
test_print(HARD2_SOLVED.to_vec(), HARD2_SOLVED_IMAGE);
}
const HARD3_STRING: &str =
"120300000400000300003050000004200500000080009060005070001500200000090060000007008";
const HARD3_UNSOLVED: [u8; 81] = [
1, 2, 0, 3, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 3, 0, 5, 0, 0, 0, 0, 0, 0, 4,
2, 0, 0, 5, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 9, 0, 6, 0, 0, 0, 5, 0, 7, 0, 0, 0, 1, 5, 0, 0,
2, 0, 0, 0, 0, 0, 0, 9, 0, 0, 6, 0, 0, 0, 0, 0, 0, 7, 0, 0, 8,
];
const HARD3_SOLVED: [u8; 81] = [
1, 2, 5, 3, 7, 4, 8, 9, 6, 4, 7, 9, 6, 1, 8, 3, 2, 5, 6, 8, 3, 9, 5, 2, 7, 1, 4, 7, 1, 4,
2, 6, 9, 5, 8, 3, 5, 3, 2, 7, 8, 1, 6, 4, 9, 9, 6, 8, 4, 3, 5, 1, 7, 2, 8, 9, 1, 5, 4, 6,
2, 3, 7, 2, 5, 7, 8, 9, 3, 4, 6, 1, 3, 4, 6, 1, 2, 7, 9, 5, 8,
];
const HARD3_UNSOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/unsolved/hard3.png");
const HARD3_SOLVED_IMAGE: &[u8] = include_bytes!("../../../tests/sudoku/solved/hard3.png");
#[test]
fn parse_hard3() {
test_parse(HARD3_STRING, HARD3_UNSOLVED.to_vec());
}
#[test]
fn miri_solve_hard3() {
test_solve(&HARD3_UNSOLVED, &HARD3_SOLVED);
}
#[test]
fn print_hard3() {
test_print(HARD3_UNSOLVED.to_vec(), HARD3_UNSOLVED_IMAGE);
test_print(HARD3_SOLVED.to_vec(), HARD3_SOLVED_IMAGE);
}
const IMPOSSIBLE_STRING: &str =
"731000008000500042400009700020304000005000400000180006000708005090020100006090000";
const IMPOSSIBLE_UNSOLVED: [u8; 81] = [
7, 3, 1, 0, 0, 0, 0, 0, 8, 0, 0, 0, 5, 0, 0, 0, 4, 2, 4, 0, 0, 0, 0, 9, 7, 0, 0, 0, 2, 0,
3, 0, 4, 0, 0, 0, 0, 0, 5, 0, 0, 0, 4, 0, 0, 0, 0, 0, 1, 8, 0, 0, 0, 6, 0, 0, 0, 7, 0, 8,
0, 0, 5, 0, 9, 0, 0, 2, 0, 1, 0, 0, 0, 0, 6, 0, 9, 0, 0, 0, 0,
];
const IMPOSSIBLE_UNSOLVED_IMAGE: &[u8] =
include_bytes!("../../../tests/sudoku/unsolved/impossible.png");
#[test]
fn parse_impossible() {
test_parse(IMPOSSIBLE_STRING, IMPOSSIBLE_UNSOLVED.to_vec());
}
#[test]
fn miri_solve_impossible() {
let actual = super::solve_sudoku(&IMPOSSIBLE_UNSOLVED).expect_err("should be Err");
assert!(matches!(actual, super::SudokuError::NoSolution));
}
#[test]
fn print_impossible() {
test_print(IMPOSSIBLE_UNSOLVED.to_vec(), IMPOSSIBLE_UNSOLVED_IMAGE);
}
}