use core::panic;
use std::fmt::Debug;
use std::hash::Hash;
use std::{collections::HashMap, hash::Hasher};
use crate::{
geometry::{Geometry, GridKind, Outline, Rect, Shape, Square, Tri, TriCoord},
grid_solve::{self, LineStatus, SolveOptions},
import::{solution_to_puzzle, solution_to_tri_puzzle, solution_to_triano_puzzle},
};
use serde::{Deserialize, Serialize};
pub type Palette = HashMap<Color, ColorInfo>;
pub trait Clue: Clone + Copy + Debug + PartialEq + Eq + Hash + Send {
fn style() -> ClueStyle;
fn must_be_separated_from(&self, next: &Self) -> bool;
fn len(&self) -> usize;
fn color_at(&self, idx: usize) -> Color;
fn color_ranges(&self) -> Vec<(Color, std::ops::Range<usize>)>;
fn to_string(&self, palette: &Palette) -> String;
fn html_color(&self, palette: &Palette) -> String;
fn html_text(&self, palette: &Palette) -> String;
fn express<'a>(&self, palette: &'a Palette) -> Vec<(&'a ColorInfo, Option<u16>)>;
}
impl Debug for Nono {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}]{}", self.color.0, self.count)
}
}
#[derive(PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
pub struct Nono {
pub color: Color,
pub count: u16,
}
impl Clue for Nono {
fn style() -> ClueStyle {
ClueStyle::Nono
}
fn must_be_separated_from(&self, next: &Self) -> bool {
self.color == next.color
}
fn len(&self) -> usize {
self.count as usize
}
fn color_at(&self, _: usize) -> Color {
self.color
}
fn color_ranges(&self) -> Vec<(Color, std::ops::Range<usize>)> {
vec![(self.color, 0..self.count as usize)]
}
fn to_string(&self, palette: &Palette) -> String {
format!("{}{}", palette[&self.color].ch, self.count)
}
fn html_color(&self, palette: &Palette) -> String {
let (r, g, b) = palette[&self.color].rgb;
format!("color:rgb({},{},{})", r, g, b)
}
fn html_text(&self, _: &Palette) -> String {
format!("{}", self.count)
}
fn express<'a>(&self, palette: &'a Palette) -> Vec<(&'a ColorInfo, Option<u16>)> {
vec![(&palette[&self.color], Some(self.count))]
}
}
#[derive(PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
pub struct Triano {
pub front_cap: Option<Color>,
pub body_len: u16,
pub body_color: Color,
pub back_cap: Option<Color>,
}
impl Clue for Triano {
fn style() -> ClueStyle {
ClueStyle::Triano
}
fn len(&self) -> usize {
self.body_len as usize
+ self.front_cap.is_some() as usize
+ self.back_cap.is_some() as usize
}
fn color_at(&self, idx: usize) -> Color {
match (idx, self.front_cap, self.back_cap) {
(0, Some(c), _) => c,
(idx, _, Some(c)) if idx == self.len() - 1 => c,
_ => self.body_color,
}
}
fn must_be_separated_from(&self, next: &Self) -> bool {
self.body_color == next.body_color && self.back_cap.is_none() && next.front_cap.is_none()
}
fn color_ranges(&self) -> Vec<(Color, std::ops::Range<usize>)> {
let mut segments = vec![];
if let Some(front_cap) = self.front_cap {
segments.push((front_cap, 1));
}
if self.body_len > 0 {
segments.push((self.body_color, self.body_len as usize));
}
if let Some(back_cap) = self.back_cap {
segments.push((back_cap, 1));
}
let mut res: Vec<(Color, std::ops::Range<usize>)> = vec![];
let mut idx = 0;
for (color, len) in segments {
match res.last_mut() {
Some((last_color, range)) if *last_color == color => range.end += len,
_ => res.push((color, idx..idx + len)),
}
idx += len;
}
res
}
fn to_string(&self, palette: &Palette) -> String {
let mut res = String::new();
if let Some(front_cap) = self.front_cap {
res.push(palette[&front_cap].ch);
}
res.push(palette[&self.body_color].ch);
res.push_str(&self.body_len.to_string());
if let Some(back_cap) = self.back_cap {
res.push(palette[&back_cap].ch);
}
res
}
fn html_color(&self, palette: &Palette) -> String {
let (r, g, b) = palette[&self.body_color].rgb;
format!("color:rgb({},{},{})", r, g, b)
}
fn html_text(&self, palette: &Palette) -> String {
let mut res = String::new();
if let Some(front_cap) = self.front_cap {
let color_info = &palette[&front_cap];
res.push(color_info.ch);
}
res.push_str(&self.body_len.to_string());
if let Some(back_cap) = self.back_cap {
let color_info = &palette[&back_cap];
res.push(color_info.ch);
}
res
}
fn express<'a>(&self, palette: &'a Palette) -> Vec<(&'a ColorInfo, Option<u16>)> {
let mut res = vec![];
if let Some(front_cap) = self.front_cap {
res.push((&palette[&front_cap], None));
}
if self.body_len > 0 {
res.push((&palette[&self.body_color], Some(self.body_len)));
}
if let Some(back_cap) = self.back_cap {
res.push((&palette[&back_cap], None));
}
res
}
}
impl Debug for Triano {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(front_cap) = self.front_cap {
write!(f, "[{}]", front_cap.0)?;
}
write!(f, "[{}]{}", self.body_color.0, self.body_len)?;
if let Some(back_cap) = self.back_cap {
write!(f, "[{}]", back_cap.0)?;
}
Ok(())
}
}
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Color(pub u8);
pub static BACKGROUND: Color = Color(0);
pub static UNSOLVED: Color = Color(255);
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize, PartialOrd, Ord)]
pub struct Corner {
pub upper: bool,
pub left: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)]
pub struct ColorInfo {
pub ch: char,
pub name: String,
pub rgb: (u8, u8, u8),
pub color: Color,
#[serde(skip_serializing_if = "Option::is_none")]
pub corner: Option<Corner>,
}
impl Ord for ColorInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let (sr, sg, sb) = self.rgb;
let (or, og, ob) = other.rgb;
self.corner
.cmp(&other.corner)
.then((sr as u16 + sg as u16 + sb as u16).cmp(&(or as u16 + og as u16 + ob as u16)))
.then(self.rgb.cmp(&other.rgb))
.then(self.ch.cmp(&other.ch))
.then(self.name.cmp(&other.name))
.then(self.color.cmp(&other.color))
.then(sr.cmp(&or))
.then(sg.cmp(&og))
.then(sb.cmp(&ob))
}
}
impl PartialOrd for ColorInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl ColorInfo {
pub fn default_bg() -> ColorInfo {
ColorInfo {
ch: ' ',
name: "white".to_string(),
rgb: (255, 255, 255),
color: BACKGROUND,
corner: None,
}
}
pub fn default_fg(color: Color) -> ColorInfo {
ColorInfo {
ch: '#',
name: "black".to_string(),
rgb: (0, 0, 0),
color,
corner: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Solution<K: GridKind> {
pub clue_style: ClueStyle,
pub palette: HashMap<Color, ColorInfo>, pub geometry: Geometry<K>,
pub cells: Vec<Color>,
}
pub type PartialSolution = Vec<crate::line_solve::Cell>;
impl<K: GridKind> Solution<K> {
pub fn new(
clue_style: ClueStyle,
palette: HashMap<Color, ColorInfo>,
geometry: Geometry<K>,
cells: Vec<Color>,
) -> Solution<K> {
assert_eq!(cells.len(), geometry.cell_count());
Solution {
clue_style,
palette,
geometry,
cells,
}
}
pub fn get(&self, coord: K::Coord) -> Option<Color> {
self.geometry.cell(coord).map(|c| self.cells[c as usize])
}
pub fn to_partial(&self) -> PartialSolution {
self.cells
.iter()
.map(|color| {
if *color == UNSOLVED {
crate::line_solve::Cell::new_anything()
} else {
crate::line_solve::Cell::from_color(*color)
}
})
.collect()
}
pub fn resized(&self, side: K::Side, delta: i32) -> Option<Solution<K>> {
let new_geometry = self.geometry.resized(side, delta)?;
let mut cells = vec![BACKGROUND; new_geometry.cell_count()];
for old_cell in 0..self.geometry.cell_count() as u32 {
if let Some(new_cell) = new_geometry.cell(self.geometry.coord(old_cell)) {
cells[new_cell as usize] = self.cells[old_cell as usize];
}
}
Some(Solution {
clue_style: self.clue_style,
palette: self.palette.clone(),
geometry: new_geometry,
cells,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Puzzle<C: Clue, K: GridKind> {
pub palette: HashMap<Color, ColorInfo>, pub geometry: Geometry<K>,
pub lines: Vec<Vec<C>>,
}
impl<C: Clue, K: GridKind> Hash for Puzzle<C, K> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.lines.hash(state);
}
}
impl<C: Clue> Puzzle<C, Square> {
pub fn square(
palette: HashMap<Color, ColorInfo>,
rows: Vec<Vec<C>>,
cols: Vec<Vec<C>>,
) -> Puzzle<C, Square> {
let geometry = Geometry::new(Rect {
width: cols.len(),
height: rows.len(),
});
let mut lines = rows;
lines.extend(cols);
Puzzle {
palette,
geometry,
lines,
}
}
#[cfg(test)]
pub fn single_lane(
palette: HashMap<Color, ColorInfo>,
len: usize,
clues: Vec<C>,
) -> Puzzle<C, Square> {
Puzzle {
palette,
geometry: Geometry::<Square>::single_lane(len),
lines: vec![clues],
}
}
pub fn row_clues(&self) -> &[Vec<C>] {
&self.lines[self.geometry.lane_map().family(0)]
}
pub fn col_clues(&self) -> &[Vec<C>] {
&self.lines[self.geometry.lane_map().family(1)]
}
}
impl<C: Clue> Puzzle<C, Tri> {
pub fn triangular(
palette: HashMap<Color, ColorInfo>,
outline: Outline,
lines: Vec<Vec<C>>,
) -> Puzzle<C, Tri> {
let geometry = Geometry::new(outline);
assert_eq!(
lines.len(),
geometry.lane_map().lane_count(),
"wrong number of clue lists for this outline"
);
Puzzle {
palette,
geometry,
lines,
}
}
}
impl<C: Clue, K: GridKind> Puzzle<C, K> {
pub fn lane_map(&self) -> &crate::geometry::LaneMap {
self.geometry.lane_map()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum DynPuzzle {
SquareNono(Puzzle<Nono, Square>),
SquareTriano(Puzzle<Triano, Square>),
TriNono(Puzzle<Nono, Tri>),
}
impl From<Puzzle<Nono, Square>> for DynPuzzle {
fn from(p: Puzzle<Nono, Square>) -> DynPuzzle {
DynPuzzle::SquareNono(p)
}
}
impl From<Puzzle<Triano, Square>> for DynPuzzle {
fn from(p: Puzzle<Triano, Square>) -> DynPuzzle {
DynPuzzle::SquareTriano(p)
}
}
impl From<Puzzle<Nono, Tri>> for DynPuzzle {
fn from(p: Puzzle<Nono, Tri>) -> DynPuzzle {
DynPuzzle::TriNono(p)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DynSolution {
Square(Solution<Square>),
Tri(Solution<Tri>),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum DynCoord {
Square((usize, usize)),
Tri(TriCoord),
}
#[macro_export]
macro_rules! with_puzzle {
($dyn_puzzle:expr, |$p:ident| $body:expr) => {
match $dyn_puzzle {
$crate::puzzle::DynPuzzle::SquareNono($p) => $body,
$crate::puzzle::DynPuzzle::SquareTriano($p) => $body,
$crate::puzzle::DynPuzzle::TriNono($p) => $body,
}
};
}
#[macro_export]
macro_rules! with_solution {
($dyn_solution:expr, |$s:ident| $body:expr) => {
match $dyn_solution {
$crate::puzzle::DynSolution::Square($s) => $body,
$crate::puzzle::DynSolution::Tri($s) => $body,
}
};
}
pub trait PuzzleDynOps {
fn palette(&self) -> &HashMap<Color, ColorInfo>;
fn family_lane_counts(&self) -> Vec<usize>;
fn extent(&self) -> crate::layout::Vec2;
fn solve(
&self,
options: &crate::grid_solve::SolveOptions,
) -> anyhow::Result<crate::grid_solve::Report>;
fn partial_solve(
&self,
partial: &mut PartialSolution,
options: &crate::grid_solve::SolveOptions,
) -> anyhow::Result<crate::grid_solve::Report>;
fn plain_solve(&self) -> anyhow::Result<crate::grid_solve::Report> {
self.solve(&SolveOptions::default())
}
fn analyze_lines(&self, partial: &PartialSolution) -> Vec<Vec<LineStatus>>;
fn settle_solution(&self, partial: &mut PartialSolution) -> anyhow::Result<()>;
}
impl<C: Clue, K: GridKind> PuzzleDynOps for Puzzle<C, K> {
fn palette(&self) -> &HashMap<Color, ColorInfo> {
&self.palette
}
fn family_lane_counts(&self) -> Vec<usize> {
let lanes = self.geometry.lane_map();
(0..lanes.family_count())
.map(|f| lanes.family(f).len())
.collect()
}
fn extent(&self) -> crate::layout::Vec2 {
self.geometry.extent()
}
fn partial_solve(
&self,
partial: &mut PartialSolution,
options: &crate::grid_solve::SolveOptions,
) -> anyhow::Result<crate::grid_solve::Report> {
grid_solve::line_logic_solve(self, &mut None, options, partial)
}
fn solve(&self, options: &SolveOptions) -> anyhow::Result<crate::grid_solve::Report> {
let mut partial =
vec![crate::line_solve::Cell::new(&self.palette); self.geometry.cell_count()];
grid_solve::line_logic_solve(self, &mut None, options, &mut partial)
}
fn analyze_lines(&self, partial: &PartialSolution) -> Vec<Vec<LineStatus>> {
grid_solve::analyze_lines(self, partial)
}
fn settle_solution(&self, partial: &mut PartialSolution) -> anyhow::Result<()> {
grid_solve::settle_solution(self, partial)
}
}
impl PuzzleDynOps for DynPuzzle {
fn palette(&self) -> &HashMap<Color, ColorInfo> {
with_puzzle!(self, |p| p.palette())
}
fn family_lane_counts(&self) -> Vec<usize> {
with_puzzle!(self, |p| p.family_lane_counts())
}
fn extent(&self) -> crate::layout::Vec2 {
with_puzzle!(self, |p| p.extent())
}
fn partial_solve(
&self,
partial: &mut PartialSolution,
options: &crate::grid_solve::SolveOptions,
) -> anyhow::Result<crate::grid_solve::Report> {
with_puzzle!(self, |p| p.partial_solve(partial, options))
}
fn solve(
&self,
options: &crate::grid_solve::SolveOptions,
) -> anyhow::Result<crate::grid_solve::Report> {
with_puzzle!(self, |p| p.solve(options))
}
fn analyze_lines(&self, partial: &PartialSolution) -> Vec<Vec<LineStatus>> {
with_puzzle!(self, |p| p.analyze_lines(partial))
}
fn settle_solution(&self, partial: &mut PartialSolution) -> anyhow::Result<()> {
with_puzzle!(self, |p| p.settle_solution(partial))
}
}
impl DynPuzzle {
pub fn shape(&self) -> Shape {
with_puzzle!(self, |p| p.geometry.shape())
}
pub fn dims_label(&self) -> String {
with_puzzle!(self, |p| p.geometry.dims_label())
}
pub fn clue_lines(&self) -> usize {
with_puzzle!(self, |p| p.lines.len())
}
pub fn as_square_nono(&self) -> Option<&Puzzle<Nono, Square>> {
match self {
DynPuzzle::SquareNono(p) => Some(p),
_ => None,
}
}
pub fn as_square_triano(&self) -> Option<&Puzzle<Triano, Square>> {
match self {
DynPuzzle::SquareTriano(p) => Some(p),
_ => None,
}
}
pub fn as_tri_nono(&self) -> Option<&Puzzle<Nono, Tri>> {
match self {
DynPuzzle::TriNono(p) => Some(p),
_ => None,
}
}
}
impl DynSolution {
pub fn palette(&self) -> &HashMap<Color, ColorInfo> {
with_solution!(self, |s| &s.palette)
}
pub fn palette_mut(&mut self) -> &mut HashMap<Color, ColorInfo> {
with_solution!(self, |s| &mut s.palette)
}
pub fn cells(&self) -> &[Color] {
with_solution!(self, |s| &s.cells)
}
pub fn cells_mut(&mut self) -> &mut Vec<Color> {
with_solution!(self, |s| &mut s.cells)
}
pub fn clue_style(&self) -> ClueStyle {
with_solution!(self, |s| s.clue_style)
}
pub fn shape(&self) -> Shape {
with_solution!(self, |s| s.geometry.shape())
}
pub fn dims_label(&self) -> String {
with_solution!(self, |s| s.geometry.dims_label())
}
pub fn lane_map(&self) -> &crate::geometry::LaneMap {
with_solution!(self, |s| s.geometry.lane_map())
}
pub fn to_partial(&self) -> PartialSolution {
with_solution!(self, |s| s.to_partial())
}
pub fn to_puzzle(&self) -> DynPuzzle {
match self {
DynSolution::Square(s) => s.to_puzzle(),
DynSolution::Tri(s) => s.to_puzzle(),
}
}
pub fn quality_check(&self) -> Vec<String> {
with_solution!(self, |s| s.quality_check())
}
pub fn extent(&self) -> crate::layout::Vec2 {
with_solution!(self, |s| s.geometry.extent())
}
pub fn cell_of(&self, coord: DynCoord) -> Option<u32> {
match (self, coord) {
(DynSolution::Square(s), DynCoord::Square(c)) => s.geometry.cell(c),
(DynSolution::Tri(s), DynCoord::Tri(c)) => s.geometry.cell(c),
_ => None,
}
}
pub fn cell_at(&self, p: crate::layout::Point) -> Option<DynCoord> {
match self {
DynSolution::Square(s) => s.geometry.cell_at(p).map(DynCoord::Square),
DynSolution::Tri(s) => s.geometry.cell_at(p).map(DynCoord::Tri),
}
}
pub fn neighbor_cells(&self, cell: u32) -> Vec<u32> {
with_solution!(self, |s| s.geometry.neighbor_cells(cell).collect())
}
pub fn cell_shape(&self, cell: u32) -> crate::layout::CellShape {
with_solution!(self, |s| s.geometry.cell_shape(cell))
}
pub fn cell_origin(&self, cell: u32) -> crate::layout::Point {
with_solution!(self, |s| s.geometry.cell_origin(cell))
}
pub fn snap_translation(&self, v: crate::layout::Vec2) -> (i32, i32) {
with_solution!(self, |s| s.geometry.snap_translation(v))
}
pub fn translate_cell(&self, cell: u32, steps: (i32, i32)) -> Option<u32> {
with_solution!(self, |s| s.geometry.translate_cell(cell, steps))
}
pub fn arm_directions(&self) -> &'static [crate::layout::Vec2] {
with_solution!(self, |s| s.geometry.arm_directions())
}
pub fn gutters(
&self,
) -> &[(
Option<crate::geometry::ClueSet>,
Vec<crate::layout::GutterLane>,
)] {
with_solution!(self, |s| s.geometry.gutters())
}
pub fn guides(&self) -> &[crate::layout::Guide] {
with_solution!(self, |s| s.geometry.guides())
}
pub fn runs_at_cell(&self, cell: u32) -> Vec<(usize, usize)> {
with_solution!(self, |s| {
let target = s.cells[cell as usize];
s.geometry.runs(cell, |c| s.cells[c as usize] == target)
})
}
pub fn blocks_at_cell(&self, cell: u32) -> Vec<(usize, usize)> {
with_solution!(self, |s| {
let target = s.cells[cell as usize];
s.geometry
.memberships(cell)
.iter()
.zip(s.geometry.runs(cell, |c| s.cells[c as usize] == target))
.map(|(m, (back, fwd))| (m.lane as usize, back + fwd + 1))
.collect()
})
}
pub fn as_square(&self) -> Option<&Solution<Square>> {
match self {
DynSolution::Square(s) => Some(s),
DynSolution::Tri(_) => None,
}
}
pub fn as_square_mut(&mut self) -> Option<&mut Solution<Square>> {
match self {
DynSolution::Square(s) => Some(s),
DynSolution::Tri(_) => None,
}
}
pub fn as_tri(&self) -> Option<&Solution<Tri>> {
match self {
DynSolution::Tri(s) => Some(s),
DynSolution::Square(_) => None,
}
}
}
pub struct DynSolveCache {
nono_cache: Option<crate::grid_solve::LineCache<Nono>>,
triano_cache: Option<crate::grid_solve::LineCache<Triano>>,
}
impl Default for DynSolveCache {
fn default() -> Self {
Self::new()
}
}
impl DynSolveCache {
pub fn new() -> Self {
DynSolveCache {
nono_cache: Some(HashMap::new()),
triano_cache: Some(HashMap::new()),
}
}
pub fn solve(&mut self, p: &DynPuzzle) -> anyhow::Result<crate::grid_solve::Report> {
let options = crate::grid_solve::SolveOptions::default();
match p {
DynPuzzle::SquareNono(p) => crate::grid_solve::solve(p, &mut self.nono_cache, &options),
DynPuzzle::TriNono(p) => crate::grid_solve::solve(p, &mut self.nono_cache, &options),
DynPuzzle::SquareTriano(p) => {
crate::grid_solve::solve(p, &mut self.triano_cache, &options)
}
}
}
}
impl<K: GridKind> std::ops::Index<K::Coord> for Solution<K> {
type Output = Color;
fn index(&self, coord: K::Coord) -> &Color {
let cell = self
.geometry
.cell(coord)
.unwrap_or_else(|| panic!("{coord:?} is outside the puzzle"));
&self.cells[cell as usize]
}
}
impl<K: GridKind> std::ops::IndexMut<K::Coord> for Solution<K> {
fn index_mut(&mut self, coord: K::Coord) -> &mut Color {
let cell = self
.geometry
.cell(coord)
.unwrap_or_else(|| panic!("{coord:?} is outside the puzzle"));
&mut self.cells[cell as usize]
}
}
impl Solution<Square> {
pub fn from_columns(
clue_style: ClueStyle,
palette: HashMap<Color, ColorInfo>,
grid: Vec<Vec<Color>>,
) -> Solution<Square> {
let width = grid.len();
let height = grid.first().map(|c| c.len()).unwrap_or(0);
let mut cells = vec![BACKGROUND; width * height];
for (x, col) in grid.iter().enumerate() {
assert_eq!(col.len(), height, "ragged grid");
for (y, color) in col.iter().enumerate() {
cells[y * width + x] = *color;
}
}
Solution::new(
clue_style,
palette,
Geometry::new(Rect { width, height }),
cells,
)
}
pub fn to_columns(&self) -> Vec<Vec<Color>> {
(0..self.x_size())
.map(|x| (0..self.y_size()).map(|y| self[(x, y)]).collect())
.collect()
}
pub fn x_size(&self) -> usize {
self.geometry.dims().width
}
pub fn y_size(&self) -> usize {
self.geometry.dims().height
}
}
impl<K: GridKind> Solution<K> {
pub fn quality_check(&self) -> Vec<String> {
let mut problems = vec![];
let cell_count = self.cells.len();
let lane_count = self.geometry.lane_map().lane_count();
let bg_squares_found: usize = self.cells.iter().filter(|c| **c == BACKGROUND).count();
if bg_squares_found < lane_count {
problems.push(format!(
"{} is a very small number of background squares",
bg_squares_found
));
}
if (cell_count - bg_squares_found) < lane_count {
problems.push(format!(
"{} is a very small number of foreground squares",
cell_count - bg_squares_found
));
}
let num_colors = self.palette.len();
if num_colors > 10 {
problems.push(format!(
"{} colors detected; that's probably too many.",
num_colors
))
}
for (color_key, color) in &self.palette {
for (color_key2, color2) in &self.palette {
if color_key >= color_key2 {
continue;
}
if color.corner != color2.corner && color.rgb == color2.rgb {
continue; }
let (r, g, b) = color.rgb;
let (r2, g2, b2) = color2.rgb;
if (r2 as i16 - r as i16).abs()
+ (g2 as i16 - g as i16).abs()
+ (b2 as i16 - b as i16).abs()
< 30
{
problems.push(format!(
"very similar colors found: {:?} (\"{}\") and {:?} (\"{}\")",
color.rgb, color.name, color2.rgb, color2.name
));
}
}
}
problems
}
pub fn runs_at(&self, coord: K::Coord) -> Vec<(usize, usize)> {
let Some(cell) = self.geometry.cell(coord) else {
return vec![];
};
let target = self.cells[cell as usize];
self.geometry
.runs(cell, |c| self.cells[c as usize] == target)
}
}
impl Solution<Square> {
pub fn blank_bw(x_size: usize, y_size: usize) -> Solution<Square> {
Solution::new(
ClueStyle::Nono,
HashMap::from([
(BACKGROUND, ColorInfo::default_bg()),
(Color(1), ColorInfo::default_fg(Color(1))),
]),
Geometry::new(Rect {
width: x_size,
height: y_size,
}),
vec![BACKGROUND; x_size * y_size],
)
}
pub fn to_puzzle(&self) -> DynPuzzle {
match self.clue_style {
ClueStyle::Nono => solution_to_puzzle(self).into(),
ClueStyle::Triano => solution_to_triano_puzzle(self).into(),
}
}
pub fn count_contiguous(&self, x: usize, y: usize) -> (usize, usize, usize, usize) {
let runs = self.runs_at((x, y));
let (left, right) = runs[0];
let (up, down) = runs[1];
(up, down, left, right)
}
}
impl Solution<Tri> {
pub fn to_puzzle(&self) -> DynPuzzle {
assert_eq!(
self.clue_style,
ClueStyle::Nono,
"a triddler can't have trianogram clues"
);
solution_to_tri_puzzle(self).into()
}
}
#[derive(Clone, Copy, Debug, clap::ValueEnum, Default, PartialEq, Eq)]
pub enum NonogramFormat {
#[default]
Image,
Webpbn,
Olsak,
CharGrid,
Woven,
Html,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ClueStyle {
#[default]
Nono,
Triano,
}
pub fn infer_format(path: &str, format_arg: Option<NonogramFormat>) -> NonogramFormat {
if let Some(format) = format_arg {
return format;
}
let ext = path.rsplit_once('.').map(|x| x.1);
match ext {
Some("png") | Some("bmp") | Some("gif") => NonogramFormat::Image,
Some("xml") | Some("pbn") => NonogramFormat::Webpbn,
Some("g") => NonogramFormat::Olsak,
Some("html") => NonogramFormat::Html,
Some("txt") => NonogramFormat::CharGrid,
Some("woven") => NonogramFormat::Woven,
_ => NonogramFormat::CharGrid,
}
}
#[derive(Clone, Debug)]
pub struct Document {
p: Option<DynPuzzle>,
s: Option<DynSolution>,
pub file: String,
pub title: String,
pub description: String,
pub author: String,
pub id: String,
pub license: String,
}
impl Document {
pub fn quality_check(&mut self) -> Vec<String> {
let mut problems = vec![];
if self.author.is_empty() {
problems.push("missing author".to_string());
}
if let Ok(solution) = self.solution() {
problems.extend(solution.quality_check());
}
let puzzle = self.puzzle();
match puzzle.plain_solve() {
Ok(report) => {
if report.cells_left > 0 {
problems.push("puzzle is not solveable with line-logic".to_string());
}
}
Err(_) => {
problems.push("puzzle is self-contradictory".to_string());
}
}
problems
}
pub fn new(
puzzle: Option<DynPuzzle>,
solution: Option<DynSolution>,
file: String,
title: Option<String>,
description: Option<String>,
author: Option<String>,
id: Option<String>,
license: Option<String>,
) -> Document {
assert!(puzzle.is_some() || solution.is_some());
Document {
p: puzzle,
s: solution,
file,
title: title.unwrap_or_default(),
description: description.unwrap_or_default(),
author: author.unwrap_or_default(),
id: id.unwrap_or_default(),
license: license.unwrap_or_default(),
}
}
#[allow(dead_code)] pub fn file(&self) -> &str {
&self.file
}
pub fn get_or_make_up_title(&self) -> anyhow::Result<String> {
if !self.title.is_empty() {
return Ok(self.title.clone());
}
let mut hasher = std::hash::DefaultHasher::new();
if let Some(solution) = self.try_solution() {
for color in solution.cells() {
color.hash(&mut hasher);
}
} else {
let puzzle = self.try_puzzle().unwrap();
puzzle.hash(&mut hasher);
}
let hash = hasher.finish().to_le_bytes();
Ok(mnemonic::to_string(&hash[0..4]))
}
#[allow(dead_code)]
pub fn try_puzzle(&self) -> Option<&DynPuzzle> {
self.p.as_ref()
}
pub fn has_complete_solution(&mut self) -> anyhow::Result<bool> {
let solution = self.solution()?;
for &color in solution.cells() {
if color == UNSOLVED {
return Ok(false);
}
}
Ok(true)
}
pub fn dims_label(&self) -> String {
match (&self.s, &self.p) {
(Some(s), _) => s.dims_label(),
(_, Some(p)) => p.dims_label(),
_ => String::new(),
}
}
pub fn puzzle(&mut self) -> &DynPuzzle {
if self.p.is_none() {
self.p = Some(self.s.as_ref().unwrap().to_puzzle());
}
self.p.as_ref().unwrap()
}
pub fn try_solution(&self) -> Option<&DynSolution> {
self.s.as_ref()
}
pub fn solution(&mut self) -> anyhow::Result<&DynSolution> {
if self.s.is_none() {
self.s = Some(self.p.as_ref().unwrap().plain_solve()?.solution)
}
Ok(self.s.as_ref().unwrap())
}
pub fn solution_mut(&mut self) -> &mut DynSolution {
if self.s.is_none() {
self.s = Some(self.p.as_ref().unwrap().plain_solve().unwrap().solution)
}
self.p = None; self.s.as_mut().unwrap()
}
pub fn take_solution(self) -> anyhow::Result<DynSolution> {
match self.s {
Some(s) => Ok(s),
None => self.p.unwrap().plain_solve().map(|r| r.solution),
}
}
pub fn square_solution_mut(&mut self) -> Option<&mut Solution<Square>> {
self.solution_mut().as_square_mut()
}
pub fn from_puzzle(puzzle: DynPuzzle, file: String) -> Self {
Self {
p: Some(puzzle),
s: None,
file,
title: "".to_string(),
description: "".to_string(),
author: "".to_string(),
id: "".to_string(),
license: "".to_string(),
}
}
pub fn from_solution(solution: DynSolution, file: String) -> Self {
Self {
p: None,
s: Some(solution),
file,
title: "".to_string(),
description: "".to_string(),
author: "".to_string(),
id: "".to_string(),
license: "".to_string(),
}
}
}
#[cfg(test)]
mod block_tests {
#[test]
fn blocks_are_the_rosette_arms_plus_one() {
for path in ["examples/png/apron.png", "examples/triddler/blob.g"] {
let mut doc = crate::import::load_path(&path.into(), None).unwrap();
let picture = doc.solution_mut();
for cell in 0..picture.cells().len() as u32 {
let runs = picture.runs_at_cell(cell);
let blocks = picture.blocks_at_cell(cell);
assert_eq!(runs.len(), blocks.len(), "{path}: one entry per family");
for (family, ((back, fwd), (lane, len))) in runs.iter().zip(&blocks).enumerate() {
assert_eq!(*len, back + fwd + 1, "{path}: cell {cell}, family {family}");
let lane = picture.lane_map().lane(*lane);
assert_eq!(
lane.family, family,
"{path}: cell {cell} lanes out of order"
);
assert!(
lane.cells.contains(&cell),
"{path}: cell {cell} isn't on the lane its block was measured along"
);
assert!(
*len <= lane.cells.len(),
"{path}: block longer than its lane"
);
}
}
}
}
}
#[cfg(test)]
mod resize_tests {
use super::*;
use crate::geometry::Side;
fn blank_tri(side_len: i32) -> Solution<Tri> {
let geometry = Geometry::new(Outline::hexagon(side_len));
let cell_count = geometry.cell_count();
Solution::new(
ClueStyle::Nono,
HashMap::from([
(BACKGROUND, ColorInfo::default_bg()),
(Color(1), ColorInfo::default_fg(Color(1))),
]),
geometry,
vec![BACKGROUND; cell_count],
)
}
#[test]
fn resizing_carries_colors_over_by_coordinate() {
let mut sol = blank_tri(2);
let coord = sol.geometry.coord(0);
sol.cells[0] = Color(1);
for side in Side::all() {
let bigger = sol
.resized(side, 1)
.expect("growing a small hexagon should never empty it");
assert_eq!(
bigger.get(coord),
Some(Color(1)),
"growing {side:?} lost the painted cell's color"
);
let old_coords: std::collections::HashSet<TriCoord> = (0..sol.geometry.cell_count()
as u32)
.map(|c| sol.geometry.coord(c))
.collect();
let mut saw_new_cell = false;
for cell in 0..bigger.geometry.cell_count() as u32 {
if !old_coords.contains(&bigger.geometry.coord(cell)) {
saw_new_cell = true;
assert_eq!(
bigger.cells[cell as usize], BACKGROUND,
"growing {side:?} should start new cells as background"
);
}
}
assert!(saw_new_cell, "growing {side:?} added no cells");
}
}
#[test]
fn shrinking_drops_out_of_bounds_cells_without_panicking() {
let sol = blank_tri(3);
for side in Side::all() {
if let Some(smaller) = sol.resized(side, -1) {
assert!(smaller.cells.len() < sol.cells.len());
}
}
}
#[test]
fn shrinking_to_nothing_is_refused() {
let mut sol = blank_tri(1);
let mut refused = false;
for _ in 0..10 {
match sol.resized(Side::Top, -1) {
Some(next) => sol = next,
None => {
refused = true;
break;
}
}
}
assert!(refused, "shrinking never refused");
}
}