use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use anyhow::Context;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::problem::{
PuzLit, VarValPair,
parse::{EdgeSide, LtAxis, PuzzleParse, ShowRole},
solver::PuzzleSolver,
};
fn clue_matrix_to_layers(clues: &[Vec<i64>]) -> Vec<Vec<String>> {
let max_clues = clues
.iter()
.map(|row| row.iter().filter(|&&v| v > 0).count())
.max()
.unwrap_or(0);
if max_clues == 0 {
return Vec::new();
}
let mut layers: Vec<Vec<String>> = vec![vec![String::new(); clues.len()]; max_clues];
for (r, row) in clues.iter().enumerate() {
let non_zero: Vec<i64> = row.iter().copied().filter(|&v| v > 0).collect();
let offset = max_clues - non_zero.len();
for (k, v) in non_zero.into_iter().enumerate() {
layers[offset + k][r] = v.to_string();
}
}
layers
}
fn read_2d_option_i64(
pp: &PuzzleParse,
known: &BTreeSet<PuzLit>,
name: &str,
) -> anyhow::Result<Vec<Vec<Option<i64>>>> {
if pp.eprime.has_param(name) {
return pp.eprime.param_vec_vec_option_i64(name);
}
if !pp.eprime.vars.contains(name) && !pp.eprime.auxvars.contains(name) {
anyhow::bail!("$#SHOW name '{name}' is neither a parameter nor a find/aux variable");
}
let dims = pp
.get_matrix_indices(name)
.with_context(|| format!("variable '{name}' has no shape information for rendering"))?;
if dims.len() != 2 {
anyhow::bail!(
"$#SHOW '{name}': expected 2-D matrix, got {} dimensions",
dims.len()
);
}
let h = dims[0].max(0) as usize;
let w = dims[1].max(0) as usize;
let mut grid = vec![vec![None; w]; h];
for lit in known {
if !lit.sign() || lit.var().name() != name {
continue;
}
let lit_var = lit.var();
let idx = lit_var.indices();
if idx.len() != 2 {
continue;
}
let r = (idx[0] - 1) as usize;
let c = (idx[1] - 1) as usize;
if r < h && c < w {
grid[r][c] = Some(lit.val());
}
}
Ok(grid)
}
fn read_2d_i64(
pp: &PuzzleParse,
known: &BTreeSet<PuzLit>,
name: &str,
) -> anyhow::Result<Vec<Vec<i64>>> {
if pp.eprime.has_param(name) {
return pp.eprime.param_vec_vec_i64(name);
}
let m = read_2d_option_i64(pp, known, name)?;
Ok(m.into_iter()
.map(|row| row.into_iter().map(|c| c.unwrap_or(0)).collect())
.collect())
}
fn read_1d_i64(pp: &PuzzleParse, known: &BTreeSet<PuzLit>, name: &str) -> anyhow::Result<Vec<i64>> {
if pp.eprime.has_param(name) {
return pp.eprime.param_vec_i64(name);
}
if !pp.eprime.vars.contains(name) && !pp.eprime.auxvars.contains(name) {
anyhow::bail!("$#SHOW name '{name}' is neither a parameter nor a find/aux variable");
}
let dims = pp
.get_matrix_indices(name)
.with_context(|| format!("variable '{name}' has no shape information for rendering"))?;
if dims.len() != 1 {
anyhow::bail!(
"$#SHOW '{name}': expected 1-D vector, got {} dimensions",
dims.len()
);
}
let n = dims[0].max(0) as usize;
let mut out = vec![0; n];
for lit in known {
if !lit.sign() || lit.var().name() != name {
continue;
}
let lit_var = lit.var();
let idx = lit_var.indices();
if idx.len() != 1 {
continue;
}
let i = (idx[0] - 1) as usize;
if i < n {
out[i] = lit.val();
}
}
Ok(out)
}
fn read_scalar_i64(pp: &PuzzleParse, known: &BTreeSet<PuzLit>, name: &str) -> anyhow::Result<i64> {
if pp.eprime.has_param(name) {
return pp.eprime.param_i64(name);
}
for lit in known {
if lit.sign() && lit.var().name() == name && lit.var().indices().is_empty() {
return Ok(lit.val());
}
}
anyhow::bail!(
"$#SHOW name '{name}': scalar has no value (not a parameter, and no \
known equality literal found)"
)
}
fn read_2d_string(
pp: &PuzzleParse,
known: &BTreeSet<PuzLit>,
name: &str,
) -> anyhow::Result<Vec<Vec<String>>> {
if pp.eprime.has_param(name) {
return pp.eprime.param_vec_vec_string(name);
}
let m = read_2d_i64(pp, known, name)?;
Ok(m.into_iter()
.map(|row| row.into_iter().map(|c| c.to_string()).collect())
.collect())
}
fn read_edge_labels(
pp: &PuzzleParse,
known: &BTreeSet<PuzLit>,
name: &str,
) -> anyhow::Result<Option<Vec<Vec<String>>>> {
let dims = if pp.eprime.has_param(name) {
pp.eprime.param_vec_vec_i64(name).ok().map(|_| 2)
} else {
pp.get_matrix_indices(name).map(|d| d.len())
};
if let Some(2) = dims {
let m = read_2d_i64(pp, known, name)?;
let layers = clue_matrix_to_layers(&m);
return Ok(if layers.is_empty() {
None
} else {
Some(layers)
});
}
let blank_if_neg_one = |s: String| -> String { if s == "-1" { String::new() } else { s } };
if pp.eprime.has_param(name) {
let labels = pp.eprime.param_vec_string(name)?;
return Ok(Some(vec![
labels.into_iter().map(blank_if_neg_one).collect(),
]));
}
let v = read_1d_i64(pp, known, name)?;
Ok(Some(vec![
v.iter().map(|x| blank_if_neg_one(x.to_string())).collect(),
]))
}
fn build_constraint_shapes(
solver: &PuzzleSolver,
constraint_num: &HashMap<String, usize>,
allowed_names: &HashSet<String>,
) -> Vec<ConstraintShape> {
let mut shapes: Vec<ConstraintShape> = Vec::with_capacity(constraint_num.len());
for (name, &idx) in constraint_num.iter() {
let scope = solver.puzzleparse().constraint_scope(name);
let cells: BTreeSet<[i64; 2]> = scope
.iter()
.filter(|p| allowed_names.contains(p.var().name()))
.filter_map(|p| {
let i = p.var().indices();
if i.len() == 2 && i[0] >= 1 && i[1] >= 1 {
Some([i[0] - 1, i[1] - 1])
} else {
None
}
})
.collect();
if cells.is_empty() {
continue;
}
let cells: Vec<[i64; 2]> = cells.into_iter().collect();
let kind = detect_constraint_shape_kind(&cells);
shapes.push(ConstraintShape {
idx,
kind,
cells,
stagger: 0,
});
}
shapes.sort_by_key(|s| s.idx);
let mut row_groups: BTreeMap<i64, Vec<usize>> = BTreeMap::new();
let mut col_groups: BTreeMap<i64, Vec<usize>> = BTreeMap::new();
for (vec_pos, shape) in shapes.iter().enumerate() {
match shape.kind {
ConstraintShapeKind::Row => {
row_groups
.entry(shape.cells[0][0])
.or_default()
.push(vec_pos);
}
ConstraintShapeKind::Col => {
col_groups
.entry(shape.cells[0][1])
.or_default()
.push(vec_pos);
}
_ => {}
}
}
for indices in row_groups.values().chain(col_groups.values()) {
for (slot, &vec_pos) in indices.iter().enumerate() {
shapes[vec_pos].stagger = stagger_slot(slot);
}
}
shapes
}
fn stagger_slot(n: usize) -> i32 {
let half = (n as i32 + 1) / 2;
if n.is_multiple_of(2) { half } else { -half }
}
fn detect_constraint_shape_kind(cells: &[[i64; 2]]) -> ConstraintShapeKind {
if cells.is_empty() {
return ConstraintShapeKind::Region;
}
let same_row = cells.iter().all(|c| c[0] == cells[0][0]);
if same_row {
return ConstraintShapeKind::Row;
}
let same_col = cells.iter().all(|c| c[1] == cells[0][1]);
if same_col {
return ConstraintShapeKind::Col;
}
if cells.len() == 2 {
return ConstraintShapeKind::Pair;
}
ConstraintShapeKind::Region
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct ConstraintInstance {
pub description: String,
pub cells: Vec<[i64; 2]>,
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct Puzzle {
pub kind: String,
pub width: i64,
pub height: i64,
pub start_grid: Option<Vec<Vec<Option<i64>>>>,
pub solution_grid: Option<Vec<Vec<Option<i64>>>>,
pub cages: Option<Vec<Vec<Option<i64>>>>,
#[serde(default)]
pub region_tint: Option<Vec<Vec<Option<i64>>>>,
pub top_labels: Option<Vec<Vec<String>>>,
pub bottom_labels: Option<Vec<Vec<String>>>,
pub left_labels: Option<Vec<Vec<String>>>,
pub right_labels: Option<Vec<Vec<String>>>,
pub thermometers: Option<Vec<Vec<[i64; 2]>>>,
pub less_than: Option<Vec<[i64; 4]>>,
pub cage_sums: Option<Vec<i64>>,
pub info: Option<Vec<String>>,
pub constraint_classes: Option<BTreeMap<String, Vec<ConstraintInstance>>>,
#[serde(default)]
pub decorations: Vec<String>,
}
impl Puzzle {
pub fn new_from_puzzle(problem: &PuzzleParse) -> anyhow::Result<Puzzle> {
Self::new_from_puzzle_and_known(problem, &BTreeSet::new())
}
pub fn new_from_puzzle_and_known(
problem: &PuzzleParse,
known: &BTreeSet<PuzLit>,
) -> anyhow::Result<Puzzle> {
let kind = problem.eprime.kind.clone().unwrap_or("Unknown".to_string());
let main_show = problem
.eprime
.show
.iter()
.find(|d| d.role == ShowRole::Main)
.ok_or_else(|| {
anyhow::anyhow!(
"model has no `$#SHOW <var> main` directive — required for rendering"
)
})?;
let dims = problem
.get_matrix_indices(&main_show.var)
.with_context(|| format!("main var '{}' has no shape information", main_show.var))?;
anyhow::ensure!(
dims.len() == 2,
"main var '{}' must be a 2-D matrix for rendering, got {} dimensions",
main_show.var,
dims.len()
);
let height = dims[0];
let width = dims[1];
let mut start_grid = None;
let mut cages = None;
let mut region_tint = None;
let mut top_labels = None;
let mut bottom_labels = None;
let mut left_labels = None;
let mut right_labels = None;
let show = &problem.eprime.show;
let find_role = |pred: &dyn Fn(&ShowRole) -> bool| -> Option<&str> {
show.iter().find(|d| pred(&d.role)).map(|d| d.var.as_str())
};
let edge_param = |side: EdgeSide| -> Option<String> {
find_role(&|r| matches!(r, ShowRole::Edge { side: s } if *s == side))
.map(str::to_string)
};
if let Some(p) = edge_param(EdgeSide::Top) {
top_labels = read_edge_labels(problem, known, &p)?;
}
if let Some(p) = edge_param(EdgeSide::Left) {
left_labels = read_edge_labels(problem, known, &p)?;
}
if let Some(p) = edge_param(EdgeSide::Bottom) {
bottom_labels = read_edge_labels(problem, known, &p)?;
}
if let Some(p) = edge_param(EdgeSide::Right) {
right_labels = read_edge_labels(problem, known, &p)?;
}
if let Some(p) = find_role(&|r| matches!(r, ShowRole::SideLabels)) {
let side_labels = read_2d_string(problem, known, p)?;
left_labels = Some(vec![side_labels[0].clone()]);
top_labels = Some(vec![side_labels[1].clone()]);
right_labels = Some(vec![side_labels[2].clone()]);
bottom_labels = Some(vec![side_labels[3].clone()]);
}
if let Some(p) = find_role(&|r| matches!(r, ShowRole::Givens)) {
start_grid = Some(read_2d_option_i64(problem, known, p)?);
}
if let Some(p) = find_role(&|r| matches!(r, ShowRole::Cages)) {
cages = Some(read_2d_option_i64(problem, known, p)?);
}
if let Some(p) = find_role(&|r| matches!(r, ShowRole::RegionTint)) {
let raw = read_2d_option_i64(problem, known, p)?;
region_tint = Some(
raw.into_iter()
.map(|row| {
row.into_iter()
.map(|cell| match cell {
Some(k) if k >= 1 => Some(k),
_ => None,
})
.collect()
})
.collect(),
);
}
let mut thermometers = None;
let mut less_than = None;
let mut cage_sums = None;
if let Some(d) = show
.iter()
.find(|d| matches!(d.role, ShowRole::Thermometers { .. }))
{
let ShowRole::Thermometers { step: step_name } = &d.role else {
unreachable!()
};
let step = read_scalar_i64(problem, known, step_name)?;
let therms_raw = read_2d_i64(problem, known, &d.var)?;
let mut therm_paths: BTreeMap<i64, BTreeMap<i64, [i64; 2]>> = BTreeMap::new();
for (col_0, col_data) in therms_raw.iter().enumerate() {
for (row_0, &val) in col_data.iter().enumerate() {
if val == 0 {
continue;
}
let therm_id = val / step;
let pos = val % step;
therm_paths
.entry(therm_id)
.or_default()
.insert(pos, [row_0 as i64, col_0 as i64]);
}
}
let paths: Vec<Vec<[i64; 2]>> = therm_paths
.into_values()
.map(|path| path.into_values().collect())
.collect();
if !paths.is_empty() {
thermometers = Some(paths);
}
}
let mut pairs: Vec<[i64; 4]> = Vec::new();
if let Some(p) = find_role(&|r| matches!(r, ShowRole::LessThan)) {
let lt_raw = read_2d_i64(problem, known, p)?;
pairs.extend(
lt_raw
.iter()
.map(|v| [v[0] - 1, v[1] - 1, v[2] - 1, v[3] - 1]),
);
}
for d in show.iter() {
let axis = match d.role {
ShowRole::LessThanGrid { axis } => axis,
_ => continue,
};
let m = read_2d_i64(problem, known, &d.var)?;
for (r, row) in m.iter().enumerate() {
for (c, &v) in row.iter().enumerate() {
let (r, c) = (r as i64, c as i64);
let pair = match (axis, v) {
(LtAxis::Horizontal, 1) => [r, c, r, c + 1],
(LtAxis::Horizontal, 2) => [r, c + 1, r, c],
(LtAxis::Vertical, 1) => [r, c, r + 1, c],
(LtAxis::Vertical, 2) => [r + 1, c, r, c],
(_, 0) => continue,
(_, other) => anyhow::bail!(
"$#SHOW {} less_than_grid {axis:?}: cell [{r},{c}] \
has unexpected value {other} (expected 0, 1, or 2)",
d.var
),
};
pairs.push(pair);
}
}
}
if !pairs.is_empty() {
less_than = Some(pairs);
}
if let Some(p) = find_role(&|r| matches!(r, ShowRole::CageSums)) {
cage_sums = Some(read_1d_i64(problem, known, p)?);
}
let info = if problem.eprime.info.is_empty() {
None
} else {
Some(problem.eprime.info.clone())
};
const MAX_INSTANCES_PER_CLASS: usize = 50;
let constraint_classes = if problem.constraints.is_empty() {
None
} else {
let mut classes: BTreeMap<String, Vec<ConstraintInstance>> = BTreeMap::new();
for (lit, description) in problem.constraints.iter() {
if let Some(puzlits) = problem.direct.invlitmap.get(lit)
&& let Some(puzlit) = puzlits.iter().find(|p| p.val() == 1)
{
let class = puzlit.var().name().clone();
let entries = classes.entry(class.clone()).or_default();
if entries.len() >= MAX_INSTANCES_PER_CLASS {
continue;
}
let scope = problem.constraint_scope(description);
let cells: Vec<[i64; 2]> = scope
.iter()
.filter(|vvp| vvp.var().indices().len() == 2)
.map(|vvp| {
let idx = vvp.var().indices();
[idx[0] - 1, idx[1] - 1]
})
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
if cells.is_empty() {
continue;
}
entries.push(ConstraintInstance {
description: description.clone(),
cells,
});
}
}
if classes.is_empty() {
None
} else {
Some(classes)
}
};
Ok(Puzzle {
kind,
width,
height,
start_grid,
solution_grid: None,
cages,
region_tint,
top_labels,
bottom_labels,
left_labels,
right_labels,
thermometers,
less_than,
cage_sums,
info,
constraint_classes,
decorations: problem.eprime.decs.clone(),
})
}
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct StateLit {
pub val: i64,
pub classes: Option<BTreeSet<String>>,
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct State {
pub knowledge_grid: Option<Vec<Vec<Option<Vec<StateLit>>>>>,
pub statements: Option<Vec<Statement>>,
pub description: Option<String>,
#[serde(default)]
pub blocked_cells: Option<Vec<[i64; 2]>>,
#[serde(default)]
pub constraint_shapes: Option<Vec<ConstraintShape>>,
#[serde(default)]
pub verbose: Option<Vec<VerboseSection>>,
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct VerboseSection {
pub title: String,
pub body: String,
}
#[derive(Clone, Copy, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum ConstraintShapeKind {
Row,
Col,
Pair,
Region,
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct ConstraintShape {
pub idx: usize,
pub kind: ConstraintShapeKind,
pub cells: Vec<[i64; 2]>,
pub stagger: i32,
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct Statement {
pub content: String,
pub classes: Vec<String>,
}
#[derive(Clone, PartialOrd, Ord, Hash, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct Problem {
pub puzzle: Puzzle,
pub state: Option<State>,
}
pub struct DescriptionStatement {
pub result: String,
pub constraints: Vec<String>,
pub name: Option<String>,
pub fingerprint: Option<String>,
}
impl DescriptionStatement {
pub fn new(result: String, constraints: Vec<String>) -> Self {
Self {
result,
constraints,
name: None,
fingerprint: None,
}
}
}
impl Problem {
pub fn new_from_puzzle(problem: &PuzzleParse) -> anyhow::Result<Problem> {
let puzzle = Puzzle::new_from_puzzle(problem)?;
Ok(Problem {
puzzle,
state: None,
})
}
pub fn new_from_puzzle_and_state(
solver: &PuzzleSolver,
tosolve: &BTreeSet<VarValPair>,
known: &BTreeSet<PuzLit>,
deduced_lits: &BTreeSet<PuzLit>,
comments: &str,
) -> anyhow::Result<Problem> {
Self::new_from_puzzle_and_mus(solver, tosolve, known, deduced_lits, &[], comments, false)
}
pub fn new_from_puzzle_and_mus(
solver: &PuzzleSolver,
tosolve: &BTreeSet<VarValPair>,
known: &BTreeSet<PuzLit>,
deduced_lits: &BTreeSet<PuzLit>,
deduction_list: &[DescriptionStatement],
comments: &str,
hide_untouched_candidates: bool,
) -> anyhow::Result<Problem> {
let puzzle = Puzzle::new_from_puzzle_and_known(solver.puzzleparse(), known)?;
let main_show = solver
.puzzleparse()
.eprime
.show
.iter()
.find(|d| d.role == ShowRole::Main)
.ok_or_else(|| {
anyhow::anyhow!(
"model has no `$#SHOW <var> main` directive — required for rendering"
)
})?;
let allowed_names: HashSet<String> = std::iter::once(main_show.var.clone()).collect();
let mut knowledgegrid: Vec<Vec<Option<Vec<StateLit>>>> =
vec![
vec![None; usize::try_from(puzzle.width).context("width is negative")?];
usize::try_from(puzzle.height).context("height is negative")?
];
let mut constraint_num: HashMap<String, usize> = HashMap::new();
let mut constraint_tags: HashMap<VarValPair, BTreeSet<String>> = HashMap::new();
for deduction in deduction_list {
for constraint in &deduction.constraints {
if !constraint_num.contains_key(constraint) {
let len = constraint_num.len();
constraint_num.insert(constraint.clone(), len);
let scope = solver.puzzleparse().constraint_scope(constraint);
for p in scope {
let tags = constraint_tags.entry(p).or_default();
tags.insert(format!("highlight_con{len}"));
tags.insert("js_highlighter".to_string());
}
}
}
}
let all_lits = solver.puzzleparse().all_var_varvals();
let touched_cells: BTreeSet<(usize, usize)> = if hide_untouched_candidates {
known
.iter()
.filter_map(|p| {
let var = p.varval().var().clone();
if !allowed_names.contains(var.name()) {
return None;
}
let idx = var.indices();
if idx.len() != 2 {
return None;
}
let i = usize::try_from(idx[0]).ok()?.checked_sub(1)?;
let j = usize::try_from(idx[1]).ok()?.checked_sub(1)?;
Some((i, j))
})
.collect()
} else {
BTreeSet::new()
};
for l in all_lits {
if !(tosolve.contains(&l) || known.contains(&PuzLit::new_eq(l.clone()))) {
continue;
}
if !allowed_names.contains(l.var().name()) {
continue;
}
let index = l.var().indices().clone();
assert_eq!(index.len(), 2);
let i = usize::try_from(index[0]).context("negative index 0?")?;
let j = usize::try_from(index[1]).context("negative index 1?")?;
if hide_untouched_candidates && !touched_cells.contains(&(i - 1, j - 1)) {
continue;
}
assert!(i > 0, "Variables should be 1-indexed");
assert!(j > 0, "Variables should be 1-indexed");
let i = i - 1;
let j = j - 1;
let mut tags = BTreeSet::new();
if let Some(val) = constraint_tags.get(&l) {
tags.extend(val.clone());
tags.insert("litinmus".to_string());
}
if deduced_lits.contains(&PuzLit::new_eq(l.clone())) {
tags.insert("litpos".to_string());
tags.insert("highlight_".to_string() + &l.to_css_string());
tags.insert("js_highlighter".to_string());
}
if deduced_lits.contains(&PuzLit::new_neq(l.clone())) {
tags.insert("litneg".to_string());
tags.insert("highlight_".to_string() + &l.to_css_string());
tags.insert("js_highlighter".to_string());
}
if known.contains(&PuzLit::new_eq(l.clone())) {
tags.insert("litknown".to_string());
}
tags.insert(format!("var-{}", l.var().name()));
if knowledgegrid[i][j].is_none() {
knowledgegrid[i][j] = Some(vec![]);
}
knowledgegrid[i][j].as_mut().unwrap().push(StateLit {
val: l.val(),
classes: Some(tags),
});
}
let mut statements = Vec::new();
for deduction in deduction_list {
let mut header = String::new();
if let Some(name) = &deduction.name {
header.push_str(&format!(
"<div class=\"technique-name\">{}</div>",
tera::escape_html(name)
));
}
statements.push(Statement {
content: format!("{header}{}", deduction.result),
classes: vec!["deduction".to_string()],
});
for constraint in &deduction.constraints {
let num = constraint_num.get(constraint).unwrap();
statements.push(Statement {
content: tera::escape_html(constraint),
classes: vec![
format!("highlight_con{}", num),
"js_highlighter".to_string(),
],
});
}
}
let constraint_shapes = build_constraint_shapes(solver, &constraint_num, &allowed_names);
let height = usize::try_from(puzzle.height).unwrap_or(0);
let width = usize::try_from(puzzle.width).unwrap_or(0);
let blocked: Vec<[i64; 2]> = (0..height)
.flat_map(|r| (0..width).map(move |c| (r, c)))
.filter(|&(r, c)| {
knowledgegrid[r][c].is_none()
&& puzzle
.start_grid
.as_ref()
.is_none_or(|sg| sg[r][c].is_none())
})
.map(|(r, c)| [r as i64, c as i64])
.collect();
let blocked_cells = if blocked.is_empty() {
None
} else {
Some(blocked)
};
let state = State {
knowledge_grid: Some(knowledgegrid),
statements: Some(statements),
description: Some(comments.to_owned()),
blocked_cells,
constraint_shapes: if constraint_shapes.is_empty() {
None
} else {
Some(constraint_shapes)
},
verbose: None,
};
Ok(Problem {
puzzle,
state: Some(state),
})
}
pub fn new_from_puzzle_and_difficulty(
solver: &PuzzleSolver,
tosolve: &BTreeSet<VarValPair>,
known: &BTreeSet<PuzLit>,
complexity: &BTreeMap<VarValPair, usize>,
description: &str,
hide_untouched_candidates: bool,
) -> anyhow::Result<Problem> {
let puzzle = Puzzle::new_from_puzzle_and_known(solver.puzzleparse(), known)?;
let main_show = solver
.puzzleparse()
.eprime
.show
.iter()
.find(|d| d.role == ShowRole::Main)
.ok_or_else(|| {
anyhow::anyhow!(
"model has no `$#SHOW <var> main` directive — required for rendering"
)
})?;
let allowed_names: HashSet<String> = std::iter::once(main_show.var.clone()).collect();
let mut knowledgegrid: Vec<Vec<Option<Vec<StateLit>>>> =
vec![
vec![None; usize::try_from(puzzle.width).context("width is negative")?];
usize::try_from(puzzle.height).context("height is negative")?
];
let all_lits = solver.puzzleparse().all_var_varvals();
let complexity_vals: BTreeSet<_> = complexity.values().collect();
let touched_cells: BTreeSet<(usize, usize)> = if hide_untouched_candidates {
known
.iter()
.filter_map(|p| {
let var = p.varval().var().clone();
if !allowed_names.contains(var.name()) {
return None;
}
let idx = var.indices();
if idx.len() != 2 {
return None;
}
let i = usize::try_from(idx[0]).ok()?.checked_sub(1)?;
let j = usize::try_from(idx[1]).ok()?.checked_sub(1)?;
Some((i, j))
})
.collect()
} else {
BTreeSet::new()
};
for l in all_lits {
if !(tosolve.contains(&l) || known.contains(&PuzLit::new_eq(l.clone()))) {
continue;
}
if !allowed_names.contains(l.var().name()) {
continue;
}
let index = l.var().indices().clone();
assert_eq!(index.len(), 2);
let i = usize::try_from(index[0]).context("negative index 0?")?;
let j = usize::try_from(index[1]).context("negative index 1?")?;
assert!(i > 0, "Variables should be 1-indexed");
assert!(j > 0, "Variables should be 1-indexed");
let i = i - 1;
let j = j - 1;
if hide_untouched_candidates && !touched_cells.contains(&(i, j)) {
continue;
}
let mut tags = BTreeSet::new();
if let Some(val) = complexity.get(&l) {
let i = complexity_vals.iter().position(|&v| v == val).unwrap_or(0);
tags.insert(format!("highlight_con{i}"));
tags.insert("js_highlighter".to_string());
}
if known.contains(&PuzLit::new_eq(l.clone())) {
tags.insert("litknown".to_string());
}
tags.insert(format!("var-{}", l.var().name()));
if knowledgegrid[i][j].is_none() {
knowledgegrid[i][j] = Some(vec![]);
}
knowledgegrid[i][j].as_mut().unwrap().push(StateLit {
val: l.val(),
classes: Some(tags),
});
}
let statements = complexity_vals
.iter()
.enumerate()
.map(|(i, consize)| Statement {
content: format!("MUS size {consize}"),
classes: vec![format!("highlight_con{}", i), "js_highlighter".to_string()],
})
.collect_vec();
let height = usize::try_from(puzzle.height).unwrap_or(0);
let width = usize::try_from(puzzle.width).unwrap_or(0);
let blocked: Vec<[i64; 2]> = (0..height)
.flat_map(|r| (0..width).map(move |c| (r, c)))
.filter(|&(r, c)| {
knowledgegrid[r][c].is_none()
&& puzzle
.start_grid
.as_ref()
.is_none_or(|sg| sg[r][c].is_none())
})
.map(|(r, c)| [r as i64, c as i64])
.collect();
let blocked_cells = if blocked.is_empty() {
None
} else {
Some(blocked)
};
let state = State {
knowledge_grid: Some(knowledgegrid),
statements: Some(statements),
description: Some(description.to_owned()),
blocked_cells,
constraint_shapes: None,
verbose: None,
};
Ok(Problem {
puzzle,
state: Some(state),
})
}
}
#[cfg(test)]
mod tests {
use test_log::test;
use crate::json::Puzzle;
use crate::problem::util::test_utils::build_puzzleparse;
#[test]
fn detect_kind_classifies_layouts() {
use super::{ConstraintShapeKind as K, detect_constraint_shape_kind};
assert_eq!(detect_constraint_shape_kind(&[[3, 1], [3, 5]]), K::Row);
assert_eq!(
detect_constraint_shape_kind(&[[3, 1], [3, 2], [3, 9]]),
K::Row
);
assert_eq!(detect_constraint_shape_kind(&[[1, 4], [7, 4]]), K::Col);
assert_eq!(detect_constraint_shape_kind(&[[1, 1], [3, 5]]), K::Pair);
assert_eq!(
detect_constraint_shape_kind(&[[1, 1], [1, 2], [2, 1]]),
K::Region
);
assert_eq!(detect_constraint_shape_kind(&[[2, 2]]), K::Row);
}
#[test]
fn stagger_slot_pattern() {
use super::stagger_slot;
assert_eq!(stagger_slot(0), 0);
assert_eq!(stagger_slot(1), -1);
assert_eq!(stagger_slot(2), 1);
assert_eq!(stagger_slot(3), -2);
assert_eq!(stagger_slot(4), 2);
}
#[test]
fn givens_role_reads_from_known_puzlits_for_find_var() -> anyhow::Result<()> {
use crate::problem::PuzLit;
use crate::problem::PuzVar;
use crate::problem::VarValPair;
use crate::problem::parse::{ShowDirective, ShowRole};
use std::collections::BTreeSet;
let mut puz = build_puzzleparse("./tst/binairo.eprime", "./tst/binairo-1.param");
puz.eprime.show = vec![
ShowDirective {
var: "grid".to_string(),
role: ShowRole::Main,
},
ShowDirective {
var: "grid".to_string(),
role: ShowRole::Givens,
},
];
let mut known: BTreeSet<PuzLit> = BTreeSet::new();
known.insert(PuzLit::new_eq(VarValPair::new(
&PuzVar::new("grid", vec![1, 1]),
1,
)));
let p = Puzzle::new_from_puzzle_and_known(&puz, &known)?;
let sg = p
.start_grid
.expect("givens role should populate start_grid");
assert_eq!(sg[0][0], Some(1), "known grid[1,1]=1 should appear");
assert_eq!(sg[0][1], None);
Ok(())
}
#[test]
fn test_parse_essence_binairo() -> anyhow::Result<()> {
let puz = build_puzzleparse("./tst/binairo.eprime", "./tst/binairo-1.param");
let p = Puzzle::new_from_puzzle(&puz)?;
assert_eq!(p.kind, "Binairo");
assert_eq!(p.width, 6);
assert_eq!(p.height, 6);
Ok(())
}
#[test]
fn test_puzzle_dimensions_match_param() -> anyhow::Result<()> {
let puz = build_puzzleparse("./tst/binairo.eprime", "./tst/binairo-1.param");
let p = Puzzle::new_from_puzzle(&puz)?;
assert_eq!(p.width, 6, "binairo n=6 should give width=6");
assert_eq!(p.height, 6, "binairo n=6 should give height=6");
Ok(())
}
#[test]
fn test_puzzle_start_grid_present() -> anyhow::Result<()> {
let puz = build_puzzleparse("./tst/binairo.eprime", "./tst/binairo-1.param");
let p = Puzzle::new_from_puzzle(&puz)?;
assert!(p.start_grid.is_some());
let sg = p.start_grid.unwrap();
assert_eq!(sg.len() as i64, p.height);
assert_eq!(sg[0].len() as i64, p.width);
Ok(())
}
#[test]
fn test_clue_matrix_to_layers_nonogram_5x5() {
use crate::json::clue_matrix_to_layers;
let row_clues = vec![
vec![5, 0, 0],
vec![1, 3, 0],
vec![3, 1, 0],
vec![2, 2, 0],
vec![1, 1, 1],
];
let layers = clue_matrix_to_layers(&row_clues);
assert_eq!(layers.len(), 3);
assert_eq!(layers[0], vec!["", "", "", "", "1"]);
assert_eq!(layers[1], vec!["", "1", "3", "2", "1"]);
assert_eq!(layers[2], vec!["5", "3", "1", "2", "1"]);
}
#[test]
fn test_clue_matrix_to_layers_trims_empty_depth() {
use crate::json::clue_matrix_to_layers;
let clues = vec![vec![3, 0, 0, 0], vec![1, 2, 0, 0]];
let layers = clue_matrix_to_layers(&clues);
assert_eq!(layers.len(), 2);
assert_eq!(layers[0], vec!["", "1"]);
assert_eq!(layers[1], vec!["3", "2"]);
}
#[test]
fn test_clue_matrix_to_layers_all_zeros() {
use crate::json::clue_matrix_to_layers;
let clues = vec![vec![0, 0], vec![0, 0]];
assert!(clue_matrix_to_layers(&clues).is_empty());
}
#[test]
fn test_puzzle_minesweeper_has_no_start_grid() -> anyhow::Result<()> {
let puz = build_puzzleparse("./tst/minesweeper.eprime", "./tst/minesweeperPrinted.param");
let p = Puzzle::new_from_puzzle(&puz)?;
let all_empty = p
.start_grid
.as_ref()
.is_none_or(|sg| sg.iter().all(|row| row.iter().all(|c| c.is_none())));
assert!(all_empty, "minesweeper should have no fixed start cells");
Ok(())
}
#[test]
fn test_puzzle_kakuro_non_square_dimensions() -> anyhow::Result<()> {
let puz = build_puzzleparse("./tst/kakuro.eprime", "./tst/kakuro-non-square.param");
let p = Puzzle::new_from_puzzle(&puz)?;
assert_eq!(
p.height, 2,
"kakuro: puzzle.height must follow main var index[0] domain (2), not the `height` param (3)"
);
assert_eq!(
p.width, 3,
"kakuro: puzzle.width must follow main var index[1] domain (3), not the `width` param (2)"
);
Ok(())
}
#[test]
fn test_puzzle_minesweeper_non_square_dimensions() -> anyhow::Result<()> {
let puz = build_puzzleparse(
"./tst/minesweeper.eprime",
"./tst/minesweeper-non-square.param",
);
let p = Puzzle::new_from_puzzle(&puz)?;
assert_eq!(p.width, 3, "minesweeper: width param=3 is the col count");
assert_eq!(p.height, 2, "minesweeper: height param=2 is the row count");
Ok(())
}
#[test]
fn test_problem_kakuro_non_square_no_panic() -> anyhow::Result<()> {
use crate::problem::PuzLit;
use crate::problem::solver::PuzzleSolver;
use std::collections::BTreeSet;
use std::sync::Arc;
let pp = Arc::new(build_puzzleparse(
"./tst/kakuro.eprime",
"./tst/kakuro-non-square.param",
));
let mut solver = PuzzleSolver::new(pp)?;
let varlits = solver.get_provable_varlits().clone();
let tosolve: BTreeSet<_> = varlits
.iter()
.flat_map(|x| solver.lit_to_puzlit(x))
.map(PuzLit::varval)
.collect();
let known = BTreeSet::new();
let deduced = BTreeSet::new();
let problem =
super::Problem::new_from_puzzle_and_state(&solver, &tosolve, &known, &deduced, "test")?;
let kg = problem
.state
.as_ref()
.and_then(|s| s.knowledge_grid.as_ref())
.expect("state should have a knowledge grid");
assert_eq!(kg.len(), 2, "outer (row) length must be 2");
assert_eq!(kg[0].len(), 3, "inner (col) length must be 3");
Ok(())
}
}