1#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
5pub enum CellState {
6 #[default]
8 Empty,
9 Filled,
11 Crossed,
14}
15
16#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub enum GameStatus {
19 Playing,
21 Won,
24}
25
26pub type Clue = Vec<u8>;
31
32pub struct NonogramGame {
40 pub width: usize,
42 pub height: usize,
44 solution: Vec<bool>,
49 pub cells: Vec<CellState>,
51 pub row_clues: Vec<Clue>,
53 pub col_clues: Vec<Clue>,
55 pub status: GameStatus,
57 undo_stack: Vec<Vec<CellState>>,
58 redo_stack: Vec<Vec<CellState>>,
59}
60
61impl NonogramGame {
62 pub fn from_grid(grid: Vec<Vec<bool>>) -> Self {
70 let height = grid.len();
71 assert!(height > 0, "solution grid must have at least one row");
72 let width = grid[0].len();
73 assert!(width > 0, "solution rows must have at least one column");
74 assert!(
75 grid.iter().all(|row| row.len() == width),
76 "solution rows must all have the same width"
77 );
78 let solution: Vec<bool> = grid.into_iter().flatten().collect();
79 Self::from_solution(width, height, solution)
80 }
81
82 pub fn random(width: usize, height: usize, density: f32, seed: u64) -> Self {
94 assert!(width > 0 && height > 0);
95 let density = density.clamp(0.05, 0.95);
96 let mut rng = fastrand::Rng::with_seed(seed);
97 loop {
98 let solution: Vec<bool> = (0..width * height).map(|_| rng.f32() < density).collect();
99 let filled = solution.iter().filter(|&&v| v).count();
100 if filled > 0 && filled < solution.len() {
101 return Self::from_solution(width, height, solution);
102 }
103 }
104 }
105
106 fn from_solution(width: usize, height: usize, solution: Vec<bool>) -> Self {
107 let row_clues = (0..height)
108 .map(|y| Self::line_clue(&solution[y * width..(y + 1) * width]))
109 .collect();
110 let col_clues = (0..width)
111 .map(|x| {
112 let col: Vec<bool> = (0..height).map(|y| solution[y * width + x]).collect();
113 Self::line_clue(&col)
114 })
115 .collect();
116 let mut game = Self {
117 width,
118 height,
119 solution,
120 cells: vec![CellState::Empty; width * height],
121 row_clues,
122 col_clues,
123 status: GameStatus::Playing,
124 undo_stack: Vec::new(),
125 redo_stack: Vec::new(),
126 };
127 game.cross_out_empty_lines();
128 game
129 }
130
131 pub fn reset(&mut self) {
135 self.cells = vec![CellState::Empty; self.width * self.height];
136 self.status = GameStatus::Playing;
137 self.undo_stack.clear();
138 self.redo_stack.clear();
139 self.cross_out_empty_lines();
140 }
141
142 #[inline]
143 pub fn idx(&self, x: usize, y: usize) -> usize {
144 y * self.width + x
145 }
146
147 pub fn is_solution_filled(&self, x: usize, y: usize) -> bool {
149 self.solution[self.idx(x, y)]
150 }
151
152 pub fn toggle_fill(&mut self, x: usize, y: usize) {
155 let new_state = if self.cells[self.idx(x, y)] == CellState::Filled {
156 CellState::Empty
157 } else {
158 CellState::Filled
159 };
160 self.set_cell(x, y, new_state);
161 }
162
163 pub fn toggle_cross(&mut self, x: usize, y: usize) {
166 let new_state = if self.cells[self.idx(x, y)] == CellState::Crossed {
167 CellState::Empty
168 } else {
169 CellState::Crossed
170 };
171 self.set_cell(x, y, new_state);
172 }
173
174 pub fn clear_cell(&mut self, x: usize, y: usize) {
176 self.set_cell(x, y, CellState::Empty);
177 }
178
179 pub fn fill(&mut self, x: usize, y: usize) {
184 self.set_cell(x, y, CellState::Filled);
185 }
186
187 pub fn cross(&mut self, x: usize, y: usize) {
190 self.set_cell(x, y, CellState::Crossed);
191 }
192
193 fn set_cell(&mut self, x: usize, y: usize, new_state: CellState) {
194 let idx = self.idx(x, y);
195 if self.cells[idx] == new_state {
196 return;
197 }
198 self.undo_stack.push(self.cells.clone());
199 self.redo_stack.clear();
200 self.cells[idx] = new_state;
201 self.auto_cross_completed_lines(x, y);
202 self.check_win();
203 }
204
205 fn auto_cross_completed_lines(&mut self, x: usize, y: usize) {
211 self.cross_row_if_satisfied(y);
212 self.cross_col_if_satisfied(x);
213 }
214
215 fn cross_out_empty_lines(&mut self) {
222 for y in 0..self.height {
223 self.cross_row_if_satisfied(y);
224 }
225 for x in 0..self.width {
226 self.cross_col_if_satisfied(x);
227 }
228 }
229
230 fn cross_row_if_satisfied(&mut self, y: usize) {
231 if self.row_satisfied(y) {
232 for x in 0..self.width {
233 let idx = self.idx(x, y);
234 if self.cells[idx] == CellState::Empty {
235 self.cells[idx] = CellState::Crossed;
236 }
237 }
238 }
239 }
240
241 fn cross_col_if_satisfied(&mut self, x: usize) {
242 if self.col_satisfied(x) {
243 for y in 0..self.height {
244 let idx = self.idx(x, y);
245 if self.cells[idx] == CellState::Empty {
246 self.cells[idx] = CellState::Crossed;
247 }
248 }
249 }
250 }
251
252 pub fn row_satisfied(&self, y: usize) -> bool {
256 let line: Vec<bool> = (0..self.width)
257 .map(|x| self.cells[self.idx(x, y)] == CellState::Filled)
258 .collect();
259 Self::line_clue(&line) == self.row_clues[y]
260 }
261
262 pub fn col_satisfied(&self, x: usize) -> bool {
265 let line: Vec<bool> = (0..self.height)
266 .map(|y| self.cells[self.idx(x, y)] == CellState::Filled)
267 .collect();
268 Self::line_clue(&line) == self.col_clues[x]
269 }
270
271 pub fn can_undo(&self) -> bool {
272 !self.undo_stack.is_empty()
273 }
274
275 pub fn can_redo(&self) -> bool {
276 !self.redo_stack.is_empty()
277 }
278
279 pub fn undo(&mut self) {
280 if let Some(prev) = self.undo_stack.pop() {
281 self.redo_stack
282 .push(std::mem::replace(&mut self.cells, prev));
283 self.check_win();
284 }
285 }
286
287 pub fn redo(&mut self) {
288 if let Some(next) = self.redo_stack.pop() {
289 self.undo_stack
290 .push(std::mem::replace(&mut self.cells, next));
291 self.check_win();
292 }
293 }
294
295 fn check_win(&mut self) {
296 let won = (0..self.height).all(|y| self.row_satisfied(y))
297 && (0..self.width).all(|x| self.col_satisfied(x));
298 self.status = if won {
299 GameStatus::Won
300 } else {
301 GameStatus::Playing
302 };
303 }
304
305 fn line_clue(line: &[bool]) -> Clue {
309 let mut clue = Vec::new();
310 let mut run = 0u8;
311 for &filled in line {
312 if filled {
313 run += 1;
314 } else if run > 0 {
315 clue.push(run);
316 run = 0;
317 }
318 }
319 if run > 0 {
320 clue.push(run);
321 }
322 if clue.is_empty() {
323 clue.push(0);
324 }
325 clue
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn line_clue_examples() {
335 assert_eq!(NonogramGame::line_clue(&[false, false, false]), vec![0]);
336 assert_eq!(
337 NonogramGame::line_clue(&[false, true, true, false, true]),
338 vec![2, 1]
339 );
340 assert_eq!(NonogramGame::line_clue(&[true, true, true]), vec![3]);
341 assert_eq!(
342 NonogramGame::line_clue(&[true, false, true, false, true]),
343 vec![1, 1, 1]
344 );
345 }
346
347 #[test]
348 fn from_grid_computes_clues() {
349 let grid = vec![
350 vec![false, true, false],
351 vec![true, true, true],
352 vec![false, true, false],
353 ];
354 let game = NonogramGame::from_grid(grid);
355 assert_eq!(game.row_clues, vec![vec![1], vec![3], vec![1]]);
356 assert_eq!(game.col_clues, vec![vec![1], vec![3], vec![1]]);
357 }
358
359 #[test]
360 #[should_panic(expected = "same width")]
361 fn from_grid_rejects_ragged_rows() {
362 NonogramGame::from_grid(vec![vec![true, false], vec![true]]);
363 }
364
365 #[test]
366 fn row_and_col_satisfied() {
367 let grid = vec![vec![true, false], vec![false, true]];
368 let mut game = NonogramGame::from_grid(grid);
369 assert!(!game.row_satisfied(0));
370 game.toggle_fill(0, 0);
371 assert!(game.row_satisfied(0));
372 assert!(game.col_satisfied(0));
373 assert!(!game.row_satisfied(1));
374 }
375
376 #[test]
377 fn crossed_cells_do_not_count_against_satisfaction_or_win() {
378 let grid = vec![vec![true, false]];
379 let mut game = NonogramGame::from_grid(grid);
380 game.toggle_fill(0, 0);
381 game.toggle_cross(1, 0); assert_eq!(game.status, GameStatus::Won);
383 }
384
385 #[test]
386 fn completing_a_row_auto_crosses_its_remaining_cells() {
387 let grid = vec![vec![true, true, false], vec![false, false, true]];
388 let mut game = NonogramGame::from_grid(grid);
389 game.toggle_fill(0, 0);
390 assert_eq!(game.cells[game.idx(2, 0)], CellState::Empty);
391 game.toggle_fill(1, 0);
392 assert!(game.row_satisfied(0));
393 assert_eq!(game.cells[game.idx(2, 0)], CellState::Crossed);
394 }
395
396 #[test]
397 fn completing_a_column_auto_crosses_its_remaining_cells() {
398 let grid = vec![vec![true, false], vec![true, false], vec![false, true]];
399 let mut game = NonogramGame::from_grid(grid);
400 game.toggle_fill(0, 0);
401 assert_eq!(game.cells[game.idx(0, 2)], CellState::Empty);
402 game.toggle_fill(0, 1);
403 assert!(game.col_satisfied(0));
404 assert_eq!(game.cells[game.idx(0, 2)], CellState::Crossed);
405 }
406
407 #[test]
408 fn auto_crossed_cells_do_not_count_against_satisfaction_or_win() {
409 let grid = vec![vec![true, false]];
410 let mut game = NonogramGame::from_grid(grid);
411 game.toggle_fill(0, 0);
412 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
413 assert_eq!(game.status, GameStatus::Won);
414 }
415
416 #[test]
417 fn undo_reverts_auto_crossing_along_with_the_completing_move() {
418 let grid = vec![vec![true, false], vec![false, true]];
419 let mut game = NonogramGame::from_grid(grid);
420 game.toggle_fill(0, 0);
421 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
422 game.undo();
423 assert_eq!(game.cells[game.idx(1, 0)], CellState::Empty);
424 }
425
426 #[test]
427 fn un_satisfying_a_line_later_leaves_its_auto_crosses_in_place() {
428 let grid = vec![vec![true, false], vec![false, true]];
429 let mut game = NonogramGame::from_grid(grid);
430 game.toggle_fill(0, 0);
431 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
432 game.toggle_fill(0, 0); assert!(!game.row_satisfied(0));
434 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
435 }
436
437 #[test]
438 fn empty_line_is_crossed_out_from_the_start() {
439 let grid = vec![vec![true, false], vec![false, false]];
440 let game = NonogramGame::from_grid(grid);
441 assert_eq!(game.row_clues[1], vec![0]);
442 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
443 assert_eq!(game.cells[game.idx(1, 1)], CellState::Crossed);
444 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
446 }
447
448 #[test]
449 fn reset_reapplies_the_empty_line_crossing() {
450 let grid = vec![vec![true, false], vec![false, false]];
451 let mut game = NonogramGame::from_grid(grid);
452 game.toggle_fill(0, 0);
453 game.reset();
454 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
455 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
456 }
457
458 #[test]
459 fn wrongly_crossed_required_cell_blocks_win() {
460 let grid = vec![vec![true, false]];
461 let mut game = NonogramGame::from_grid(grid);
462 game.toggle_cross(0, 0); assert_eq!(game.status, GameStatus::Playing);
464 game.toggle_fill(1, 0); assert_eq!(game.status, GameStatus::Playing);
466 }
467
468 #[test]
469 fn undo_redo_round_trip() {
470 let grid = vec![vec![true, true]];
471 let mut game = NonogramGame::from_grid(grid);
472 assert!(!game.can_undo());
473 game.toggle_fill(0, 0);
474 game.toggle_fill(1, 0);
475 assert_eq!(game.status, GameStatus::Won);
476 assert!(game.can_undo());
477 game.undo();
478 assert_eq!(game.status, GameStatus::Playing);
479 assert_eq!(game.cells[1], CellState::Empty);
480 assert!(game.can_redo());
481 game.redo();
482 assert_eq!(game.status, GameStatus::Won);
483 }
484
485 #[test]
486 fn reset_keeps_puzzle_clears_progress() {
487 let grid = vec![vec![true]];
488 let mut game = NonogramGame::from_grid(grid);
489 game.toggle_fill(0, 0);
490 assert_eq!(game.status, GameStatus::Won);
491 let row_clues_before = game.row_clues.clone();
492 game.reset();
493 assert_eq!(game.status, GameStatus::Playing);
494 assert!(game.cells.iter().all(|&c| c == CellState::Empty));
495 assert!(!game.can_undo());
496 assert_eq!(game.row_clues, row_clues_before);
497 }
498
499 #[test]
500 fn random_is_deterministic_per_seed() {
501 let a = NonogramGame::random(10, 10, 0.4, 42);
502 let b = NonogramGame::random(10, 10, 0.4, 42);
503 assert_eq!(a.row_clues, b.row_clues);
504 assert_eq!(a.col_clues, b.col_clues);
505 }
506
507 #[test]
508 fn col_satisfied_after_filling_whole_column() {
509 let rows: [u16; 10] = [15, 15, 63, 63, 255, 255, 1023, 1023, 1023, 1023];
512 let grid: Vec<Vec<bool>> = rows
513 .iter()
514 .map(|&row| (0..10).map(|x| (row >> x) & 1 != 0).collect())
515 .collect();
516 let mut game = NonogramGame::from_grid(grid);
517 assert_eq!(game.col_clues[0], vec![10]);
518 for y in 0..10 {
519 game.toggle_fill(0, y);
520 }
521 for y in 0..10 {
522 assert_eq!(game.cells[game.idx(0, y)], CellState::Filled);
523 }
524 assert!(
525 game.col_satisfied(0),
526 "column 0 should be satisfied after filling all 10 of its cells"
527 );
528 }
529
530 #[test]
531 fn alternate_valid_solution_also_wins() {
532 let grid = vec![vec![true, false], vec![false, true]];
536 let mut game = NonogramGame::from_grid(grid);
537 game.toggle_fill(1, 0);
538 game.toggle_fill(0, 1);
539 assert_eq!(game.status, GameStatus::Won);
540 }
541
542 #[test]
543 fn random_avoids_degenerate_puzzles() {
544 let game = NonogramGame::random(4, 4, 0.0, 7);
547 let filled = (0..4)
548 .flat_map(|y| (0..4).map(move |x| (x, y)))
549 .filter(|&(x, y)| game.is_solution_filled(x, y))
550 .count();
551 assert!(filled > 0 && filled < 16);
552 }
553}