#![deny(missing_docs)]
#![cfg_attr(
test,
allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::print_stderr
)
)]
pub(crate) mod cage;
pub(crate) mod csp;
pub(crate) mod fill;
mod generate;
pub(crate) mod grid;
mod latin_square;
pub(crate) mod mdd;
pub(crate) mod memo;
pub(crate) mod operator;
pub(crate) mod polyomino;
pub(crate) mod puzzle;
pub(crate) mod regin;
pub(crate) mod solutions;
pub(crate) mod table;
pub(crate) mod tuples;
pub use cage::{Cage, Operation};
pub use fill::Fill;
pub use generate::generate;
pub use latin_square::generate_latin_square;
pub use polyomino::{Cell, Polyomino};
pub use puzzle::{CageOperator, Puzzle, operators_for};
pub fn init_debug_logging() {
if std::env::var("MATHDOKU_DEBUG").as_deref() == Ok("1") {
let _ = env_logger::builder()
.filter_module("mathdoku", log::LevelFilter::Debug)
.try_init();
}
}
pub type N = u8;
pub type T = u32;
pub type Operator = CageOperator;
pub type Target = T;
#[derive(Debug)]
pub enum Error {
InvalidGridSize(usize),
DisconnectedPolyomino,
MissingCell(Cell),
MissingPolyomino(Polyomino),
CageConflict(Polyomino),
InfeasibleCage(Polyomino, u64),
InvalidCageFill(Polyomino, Fill),
EmptyFills,
InvalidCellCageIndex(usize),
InvalidCellValue(Cell, N),
InvalidValue(N),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidGridSize(n) => write!(f, "invalid grid size: {n}"),
Self::DisconnectedPolyomino => write!(f, "cells do not form a connected polyomino"),
Self::MissingCell(cell) => write!(f, "cell not in grid or polyomino: {cell}"),
Self::InvalidCageFill(poly, fill) => {
write!(f, "invalid fill {fill} for cage {poly:?}")
}
Self::EmptyFills => write!(f, "no candidate fills for cage"),
Self::InvalidCellCageIndex(i) => write!(f, "cell cage index out of bounds: {i}"),
Self::InvalidCellValue(cell, n) => {
write!(f, "value {n} not a candidate for cell {cell}")
}
Self::MissingPolyomino(poly) => write!(f, "polyomino not in puzzle grid: {poly:?}"),
Self::CageConflict(poly) => {
write!(f, "cage overlaps existing cage: {poly:?}")
}
Self::InfeasibleCage(poly, target) => {
write!(f, "no valid assignments for cage {poly:?} target {target}")
}
Self::InvalidValue(v) => write!(f, "value {v} is outside the valid range 1..=9"),
}
}
}
impl std::error::Error for Error {}
#[cfg(test)]
mod tests {
use super::*;
use crate::fill::Fill;
use crate::polyomino::{Cell, Polyomino};
#[test]
fn error_display_invalid_grid_size() {
assert_eq!(
Error::InvalidGridSize(0).to_string(),
"invalid grid size: 0"
);
}
#[test]
fn error_display_missing_cell() {
assert_eq!(
Error::MissingCell(Cell(2, 3)).to_string(),
"cell not in grid or polyomino: (2, 3)"
);
}
#[test]
fn error_display_empty_fills() {
assert_eq!(Error::EmptyFills.to_string(), "no candidate fills for cage");
}
#[test]
fn error_display_invalid_cell_value() {
assert_eq!(
Error::InvalidCellValue(Cell(1, 1), 5).to_string(),
"value 5 not a candidate for cell (1, 1)"
);
}
#[test]
fn error_display_invalid_cell_cage_index() {
assert_eq!(
Error::InvalidCellCageIndex(3).to_string(),
"cell cage index out of bounds: 3"
);
}
#[test]
fn error_display_disconnected_polyomino() {
assert_eq!(
Error::DisconnectedPolyomino.to_string(),
"cells do not form a connected polyomino"
);
}
#[test]
fn error_display_invalid_cage_fill() {
let poly = Polyomino::from([Cell(1, 1)]).unwrap();
let fill = Fill::from(&[1, 2]);
assert!(
Error::InvalidCageFill(poly, fill)
.to_string()
.contains("invalid fill")
);
}
#[test]
fn error_display_missing_polyomino() {
let poly = Polyomino::from([Cell(1, 1)]).unwrap();
assert!(
Error::MissingPolyomino(poly)
.to_string()
.contains("polyomino not in puzzle grid")
);
}
#[test]
fn error_display_cage_conflict() {
let poly = Polyomino::from([Cell(1, 1)]).unwrap();
assert!(
Error::CageConflict(poly)
.to_string()
.contains("cage overlaps existing cage")
);
}
#[test]
fn error_display_infeasible_cage() {
let poly = Polyomino::from([Cell(1, 1)]).unwrap();
let msg = Error::InfeasibleCage(poly, 42).to_string();
assert!(msg.contains("no valid assignments"));
assert!(msg.contains("42"));
}
#[test]
fn error_display_invalid_value() {
assert_eq!(
Error::InvalidValue(10).to_string(),
"value 10 is outside the valid range 1..=9"
);
}
#[test]
fn init_debug_logging_env_branch_and_debug_paths() {
let _ = env_logger::builder()
.filter_module("mathdoku", log::LevelFilter::Debug)
.target(env_logger::Target::Pipe(Box::new(std::io::sink())))
.try_init();
unsafe { std::env::set_var("MATHDOKU_DEBUG", "1") };
init_debug_logging();
unsafe { std::env::remove_var("MATHDOKU_DEBUG") };
let p = Puzzle::new(4).unwrap();
let add = Polyomino::from([Cell(1, 1), Cell(1, 2)]).unwrap();
let p = p.insert(&add, CageOperator::Add, 5).unwrap().unwrap();
let sub = Polyomino::from([Cell(2, 1), Cell(3, 1)]).unwrap();
let p = p.insert(&sub, CageOperator::Subtract, 1).unwrap().unwrap();
let given = Polyomino::from([Cell(4, 4)]).unwrap();
let p = p.insert(&given, CageOperator::Given, 2).unwrap().unwrap();
assert!(p.fixpoint().is_some());
}
}