#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum CellState {
#[default]
Empty,
Filled,
Crossed,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GameStatus {
Playing,
Won,
}
pub type Clue = Vec<u8>;
pub struct NonogramGame {
pub width: usize,
pub height: usize,
solution: Vec<bool>,
pub cells: Vec<CellState>,
pub row_clues: Vec<Clue>,
pub col_clues: Vec<Clue>,
pub status: GameStatus,
undo_stack: Vec<Vec<CellState>>,
redo_stack: Vec<Vec<CellState>>,
}
impl NonogramGame {
pub fn from_grid(grid: Vec<Vec<bool>>) -> Self {
let height = grid.len();
assert!(height > 0, "solution grid must have at least one row");
let width = grid[0].len();
assert!(width > 0, "solution rows must have at least one column");
assert!(
grid.iter().all(|row| row.len() == width),
"solution rows must all have the same width"
);
let solution: Vec<bool> = grid.into_iter().flatten().collect();
Self::from_solution(width, height, solution)
}
pub fn random(width: usize, height: usize, density: f32, seed: u64) -> Self {
assert!(width > 0 && height > 0);
let density = density.clamp(0.05, 0.95);
let mut rng = fastrand::Rng::with_seed(seed);
loop {
let solution: Vec<bool> = (0..width * height).map(|_| rng.f32() < density).collect();
let filled = solution.iter().filter(|&&v| v).count();
if filled > 0 && filled < solution.len() {
return Self::from_solution(width, height, solution);
}
}
}
fn from_solution(width: usize, height: usize, solution: Vec<bool>) -> Self {
let row_clues = (0..height)
.map(|y| Self::line_clue(&solution[y * width..(y + 1) * width]))
.collect();
let col_clues = (0..width)
.map(|x| {
let col: Vec<bool> = (0..height).map(|y| solution[y * width + x]).collect();
Self::line_clue(&col)
})
.collect();
let mut game = Self {
width,
height,
solution,
cells: vec![CellState::Empty; width * height],
row_clues,
col_clues,
status: GameStatus::Playing,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
};
game.cross_out_empty_lines();
game
}
pub fn reset(&mut self) {
self.cells = vec![CellState::Empty; self.width * self.height];
self.status = GameStatus::Playing;
self.undo_stack.clear();
self.redo_stack.clear();
self.cross_out_empty_lines();
}
#[inline]
pub fn idx(&self, x: usize, y: usize) -> usize {
y * self.width + x
}
pub fn is_solution_filled(&self, x: usize, y: usize) -> bool {
self.solution[self.idx(x, y)]
}
pub fn toggle_fill(&mut self, x: usize, y: usize) {
let new_state = if self.cells[self.idx(x, y)] == CellState::Filled {
CellState::Empty
} else {
CellState::Filled
};
self.set_cell(x, y, new_state);
}
pub fn toggle_cross(&mut self, x: usize, y: usize) {
let new_state = if self.cells[self.idx(x, y)] == CellState::Crossed {
CellState::Empty
} else {
CellState::Crossed
};
self.set_cell(x, y, new_state);
}
pub fn clear_cell(&mut self, x: usize, y: usize) {
self.set_cell(x, y, CellState::Empty);
}
pub fn fill(&mut self, x: usize, y: usize) {
self.set_cell(x, y, CellState::Filled);
}
pub fn cross(&mut self, x: usize, y: usize) {
self.set_cell(x, y, CellState::Crossed);
}
fn set_cell(&mut self, x: usize, y: usize, new_state: CellState) {
let idx = self.idx(x, y);
if self.cells[idx] == new_state {
return;
}
self.undo_stack.push(self.cells.clone());
self.redo_stack.clear();
self.cells[idx] = new_state;
self.auto_cross_completed_lines(x, y);
self.check_win();
}
fn auto_cross_completed_lines(&mut self, x: usize, y: usize) {
self.cross_row_if_satisfied(y);
self.cross_col_if_satisfied(x);
}
fn cross_out_empty_lines(&mut self) {
for y in 0..self.height {
self.cross_row_if_satisfied(y);
}
for x in 0..self.width {
self.cross_col_if_satisfied(x);
}
}
fn cross_row_if_satisfied(&mut self, y: usize) {
if self.row_satisfied(y) {
for x in 0..self.width {
let idx = self.idx(x, y);
if self.cells[idx] == CellState::Empty {
self.cells[idx] = CellState::Crossed;
}
}
}
}
fn cross_col_if_satisfied(&mut self, x: usize) {
if self.col_satisfied(x) {
for y in 0..self.height {
let idx = self.idx(x, y);
if self.cells[idx] == CellState::Empty {
self.cells[idx] = CellState::Crossed;
}
}
}
}
pub fn row_satisfied(&self, y: usize) -> bool {
let line: Vec<bool> = (0..self.width)
.map(|x| self.cells[self.idx(x, y)] == CellState::Filled)
.collect();
Self::line_clue(&line) == self.row_clues[y]
}
pub fn col_satisfied(&self, x: usize) -> bool {
let line: Vec<bool> = (0..self.height)
.map(|y| self.cells[self.idx(x, y)] == CellState::Filled)
.collect();
Self::line_clue(&line) == self.col_clues[x]
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn undo(&mut self) {
if let Some(prev) = self.undo_stack.pop() {
self.redo_stack
.push(std::mem::replace(&mut self.cells, prev));
self.check_win();
}
}
pub fn redo(&mut self) {
if let Some(next) = self.redo_stack.pop() {
self.undo_stack
.push(std::mem::replace(&mut self.cells, next));
self.check_win();
}
}
fn check_win(&mut self) {
let won = (0..self.height).all(|y| self.row_satisfied(y))
&& (0..self.width).all(|x| self.col_satisfied(x));
self.status = if won {
GameStatus::Won
} else {
GameStatus::Playing
};
}
fn line_clue(line: &[bool]) -> Clue {
let mut clue = Vec::new();
let mut run = 0u8;
for &filled in line {
if filled {
run += 1;
} else if run > 0 {
clue.push(run);
run = 0;
}
}
if run > 0 {
clue.push(run);
}
if clue.is_empty() {
clue.push(0);
}
clue
}
pub fn random_logical(width: usize, height: usize, density: f32, seed: u64) -> Self {
assert!(width > 0 && height > 0);
let density = density.clamp(0.05, 0.95);
let mut rng = fastrand::Rng::with_seed(seed);
let max_attempts = 10_000;
for _ in 0..max_attempts {
let solution: Vec<bool> = (0..width * height).map(|_| rng.f32() < density).collect();
let filled = solution.iter().filter(|&&v| v).count();
if filled == 0 || filled == solution.len() {
continue;
}
let game = Self::from_solution(width, height, solution);
if is_logically_solvable(width, height, &game.row_clues, &game.col_clues) {
return game;
}
}
panic!(
"could not generate a logically solvable puzzle after \
{max_attempts} attempts"
);
}
}
fn solve_line(line: &[Option<bool>], clue: &[u8]) -> Vec<Option<bool>> {
let n = line.len();
if clue.len() == 1 && clue[0] == 0 {
return vec![Some(false); n];
}
let mut filled_count = vec![0u32; n];
let mut total = 0u32;
#[allow(clippy::too_many_arguments)]
fn backtrack(
line: &[Option<bool>],
clue: &[u8],
line_pos: usize,
clue_idx: usize,
n: usize,
filled: &mut [bool],
filled_count: &mut [u32],
total: &mut u32,
) {
if clue_idx == clue.len() {
if line[line_pos..].iter().all(|&c| c != Some(true)) {
*total += 1;
for (i, &f) in filled.iter().enumerate() {
if f {
filled_count[i] += 1;
}
}
}
return;
}
let run_len = clue[clue_idx] as usize;
let remaining_runs = &clue[clue_idx + 1..];
let remaining_run_sum: usize = remaining_runs.iter().map(|&r| r as usize).sum();
let remaining_gaps = remaining_runs.len().saturating_sub(1);
let remaining_min = remaining_run_sum + remaining_gaps;
let max_start = n.saturating_sub(remaining_min + run_len);
let gap = if remaining_runs.is_empty() { 0 } else { 1 };
let mut start = line_pos;
while start <= max_start {
if line[line_pos..start].contains(&Some(true)) {
break;
}
if line[start..start + run_len].contains(&Some(false)) {
start += 1;
continue;
}
for cell in filled[start..start + run_len].iter_mut() {
*cell = true;
}
backtrack(
line,
clue,
(start + run_len + gap).min(n),
clue_idx + 1,
n,
filled,
filled_count,
total,
);
for cell in filled[start..start + run_len].iter_mut() {
*cell = false;
}
start += 1;
}
}
let mut filled = vec![false; n];
backtrack(
line,
clue,
0,
0,
n,
&mut filled,
&mut filled_count,
&mut total,
);
if total == 0 {
return line.to_vec();
}
let mut result = vec![None; n];
for i in 0..n {
if line[i].is_some() {
result[i] = line[i];
} else if filled_count[i] == 0 {
result[i] = Some(false);
} else if filled_count[i] == total {
result[i] = Some(true);
}
}
result
}
pub fn is_logically_solvable(
width: usize,
height: usize,
row_clues: &[Clue],
col_clues: &[Clue],
) -> bool {
let mut grid = vec![None; width * height];
loop {
let old = grid.clone();
for y in 0..height {
let line: Vec<Option<bool>> = (0..width).map(|x| grid[y * width + x]).collect();
let solved = solve_line(&line, &row_clues[y]);
for x in 0..width {
grid[y * width + x] = solved[x];
}
}
for x in 0..width {
let line: Vec<Option<bool>> = (0..height).map(|y| grid[y * width + x]).collect();
let solved = solve_line(&line, &col_clues[x]);
for y in 0..height {
grid[y * width + x] = solved[y];
}
}
if grid == old {
break;
}
}
grid.iter().all(|&c| c.is_some())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_clue_examples() {
assert_eq!(NonogramGame::line_clue(&[false, false, false]), vec![0]);
assert_eq!(
NonogramGame::line_clue(&[false, true, true, false, true]),
vec![2, 1]
);
assert_eq!(NonogramGame::line_clue(&[true, true, true]), vec![3]);
assert_eq!(
NonogramGame::line_clue(&[true, false, true, false, true]),
vec![1, 1, 1]
);
}
#[test]
fn from_grid_computes_clues() {
let grid = vec![
vec![false, true, false],
vec![true, true, true],
vec![false, true, false],
];
let game = NonogramGame::from_grid(grid);
assert_eq!(game.row_clues, vec![vec![1], vec![3], vec![1]]);
assert_eq!(game.col_clues, vec![vec![1], vec![3], vec![1]]);
}
#[test]
#[should_panic(expected = "same width")]
fn from_grid_rejects_ragged_rows() {
NonogramGame::from_grid(vec![vec![true, false], vec![true]]);
}
#[test]
fn row_and_col_satisfied() {
let grid = vec![vec![true, false], vec![false, true]];
let mut game = NonogramGame::from_grid(grid);
assert!(!game.row_satisfied(0));
game.toggle_fill(0, 0);
assert!(game.row_satisfied(0));
assert!(game.col_satisfied(0));
assert!(!game.row_satisfied(1));
}
#[test]
fn crossed_cells_do_not_count_against_satisfaction_or_win() {
let grid = vec![vec![true, false]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
game.toggle_cross(1, 0); assert_eq!(game.status, GameStatus::Won);
}
#[test]
fn completing_a_row_auto_crosses_its_remaining_cells() {
let grid = vec![vec![true, true, false], vec![false, false, true]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
assert_eq!(game.cells[game.idx(2, 0)], CellState::Empty);
game.toggle_fill(1, 0);
assert!(game.row_satisfied(0));
assert_eq!(game.cells[game.idx(2, 0)], CellState::Crossed);
}
#[test]
fn completing_a_column_auto_crosses_its_remaining_cells() {
let grid = vec![vec![true, false], vec![true, false], vec![false, true]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
assert_eq!(game.cells[game.idx(0, 2)], CellState::Empty);
game.toggle_fill(0, 1);
assert!(game.col_satisfied(0));
assert_eq!(game.cells[game.idx(0, 2)], CellState::Crossed);
}
#[test]
fn auto_crossed_cells_do_not_count_against_satisfaction_or_win() {
let grid = vec![vec![true, false]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
assert_eq!(game.status, GameStatus::Won);
}
#[test]
fn undo_reverts_auto_crossing_along_with_the_completing_move() {
let grid = vec![vec![true, false], vec![false, true]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
game.undo();
assert_eq!(game.cells[game.idx(1, 0)], CellState::Empty);
}
#[test]
fn un_satisfying_a_line_later_leaves_its_auto_crosses_in_place() {
let grid = vec![vec![true, false], vec![false, true]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
game.toggle_fill(0, 0); assert!(!game.row_satisfied(0));
assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
}
#[test]
fn empty_line_is_crossed_out_from_the_start() {
let grid = vec![vec![true, false], vec![false, false]];
let game = NonogramGame::from_grid(grid);
assert_eq!(game.row_clues[1], vec![0]);
assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
assert_eq!(game.cells[game.idx(1, 1)], CellState::Crossed);
assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
}
#[test]
fn reset_reapplies_the_empty_line_crossing() {
let grid = vec![vec![true, false], vec![false, false]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
game.reset();
assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
}
#[test]
fn wrongly_crossed_required_cell_blocks_win() {
let grid = vec![vec![true, false]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_cross(0, 0); assert_eq!(game.status, GameStatus::Playing);
game.toggle_fill(1, 0); assert_eq!(game.status, GameStatus::Playing);
}
#[test]
fn undo_redo_round_trip() {
let grid = vec![vec![true, true]];
let mut game = NonogramGame::from_grid(grid);
assert!(!game.can_undo());
game.toggle_fill(0, 0);
game.toggle_fill(1, 0);
assert_eq!(game.status, GameStatus::Won);
assert!(game.can_undo());
game.undo();
assert_eq!(game.status, GameStatus::Playing);
assert_eq!(game.cells[1], CellState::Empty);
assert!(game.can_redo());
game.redo();
assert_eq!(game.status, GameStatus::Won);
}
#[test]
fn reset_keeps_puzzle_clears_progress() {
let grid = vec![vec![true]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(0, 0);
assert_eq!(game.status, GameStatus::Won);
let row_clues_before = game.row_clues.clone();
game.reset();
assert_eq!(game.status, GameStatus::Playing);
assert!(game.cells.iter().all(|&c| c == CellState::Empty));
assert!(!game.can_undo());
assert_eq!(game.row_clues, row_clues_before);
}
#[test]
fn random_is_deterministic_per_seed() {
let a = NonogramGame::random(10, 10, 0.4, 42);
let b = NonogramGame::random(10, 10, 0.4, 42);
assert_eq!(a.row_clues, b.row_clues);
assert_eq!(a.col_clues, b.col_clues);
}
#[test]
fn col_satisfied_after_filling_whole_column() {
let rows: [u16; 10] = [15, 15, 63, 63, 255, 255, 1023, 1023, 1023, 1023];
let grid: Vec<Vec<bool>> = rows
.iter()
.map(|&row| (0..10).map(|x| (row >> x) & 1 != 0).collect())
.collect();
let mut game = NonogramGame::from_grid(grid);
assert_eq!(game.col_clues[0], vec![10]);
for y in 0..10 {
game.toggle_fill(0, y);
}
for y in 0..10 {
assert_eq!(game.cells[game.idx(0, y)], CellState::Filled);
}
assert!(
game.col_satisfied(0),
"column 0 should be satisfied after filling all 10 of its cells"
);
}
#[test]
fn alternate_valid_solution_also_wins() {
let grid = vec![vec![true, false], vec![false, true]];
let mut game = NonogramGame::from_grid(grid);
game.toggle_fill(1, 0);
game.toggle_fill(0, 1);
assert_eq!(game.status, GameStatus::Won);
}
#[test]
fn random_avoids_degenerate_puzzles() {
let game = NonogramGame::random(4, 4, 0.0, 7);
let filled = (0..4)
.flat_map(|y| (0..4).map(move |x| (x, y)))
.filter(|&(x, y)| game.is_solution_filled(x, y))
.count();
assert!(filled > 0 && filled < 16);
}
#[test]
fn solve_line_determines_overlap() {
let line = vec![None; 5];
let clue = vec![3];
let result = solve_line(&line, &clue);
assert_eq!(result, vec![None, None, Some(true), None, None]);
}
#[test]
fn solve_line_all_empty_on_zero_clue() {
let line = vec![None; 4];
let clue = vec![0];
let result = solve_line(&line, &clue);
assert_eq!(result, vec![Some(false); 4]);
}
#[test]
fn solve_line_respects_known_filled_cell() {
let line = vec![None, Some(true), None, None, None];
let clue = vec![3];
let result = solve_line(&line, &clue);
assert_eq!(
result,
vec![None, Some(true), Some(true), None, Some(false)]
);
}
#[test]
fn solve_line_respects_known_empty_cell() {
let line = vec![None, None, Some(false), None, None];
let clue = vec![3];
let result = solve_line(&line, &clue);
assert_eq!(result, line);
}
#[test]
fn solve_line_multi_run() {
let line = vec![None; 7];
let clue = vec![2, 1];
let result = solve_line(&line, &clue);
assert_eq!(result.len(), 7);
}
#[test]
fn is_logically_solvable_returns_true_for_simple_puzzle() {
let solution = vec![
vec![false, true, false],
vec![true, true, true],
vec![false, true, false],
];
let game = NonogramGame::from_grid(solution);
assert!(is_logically_solvable(
game.width,
game.height,
&game.row_clues,
&game.col_clues,
));
}
#[test]
fn is_logically_solvable_returns_false_for_ambiguous_puzzle() {
let solution = vec![vec![true, false], vec![false, true]];
let game = NonogramGame::from_grid(solution);
assert!(!is_logically_solvable(
game.width,
game.height,
&game.row_clues,
&game.col_clues,
));
}
#[test]
fn random_logical_is_line_solvable() {
let game = NonogramGame::random_logical(8, 8, 0.5, 42);
assert!(is_logically_solvable(
game.width,
game.height,
&game.row_clues,
&game.col_clues,
));
}
#[test]
fn random_logical_rejects_unsolvable_seed() {
for seed in 0..10 {
let game = NonogramGame::random_logical(6, 6, 0.4, seed);
assert!(is_logically_solvable(
game.width,
game.height,
&game.row_clues,
&game.col_clues,
));
}
}
}