#[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();
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(),
}
}
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();
}
#[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.check_win();
}
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 = self
.cells
.iter()
.zip(self.solution.iter())
.all(|(cell, &solution_filled)| (*cell == CellState::Filled) == solution_filled);
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
}
}
#[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 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, false]];
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 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);
}
}