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 pub fn random_logical(width: usize, height: usize, density: f32, seed: u64) -> Self {
337 assert!(width > 0 && height > 0);
338 let density = density.clamp(0.05, 0.95);
339 let mut rng = fastrand::Rng::with_seed(seed);
340 let max_attempts = 10_000;
341
342 for _ in 0..max_attempts {
343 let solution: Vec<bool> = (0..width * height).map(|_| rng.f32() < density).collect();
344 let filled = solution.iter().filter(|&&v| v).count();
345 if filled == 0 || filled == solution.len() {
346 continue;
347 }
348
349 let game = Self::from_solution(width, height, solution);
350
351 if is_logically_solvable(width, height, &game.row_clues, &game.col_clues) {
352 return game;
353 }
354 }
355
356 panic!(
357 "could not generate a logically solvable puzzle after \
358 {max_attempts} attempts"
359 );
360 }
361}
362
363fn solve_line(line: &[Option<bool>], clue: &[u8]) -> Vec<Option<bool>> {
370 let n = line.len();
371
372 if clue.len() == 1 && clue[0] == 0 {
373 return vec![Some(false); n];
374 }
375
376 let mut filled_count = vec![0u32; n];
377 let mut total = 0u32;
378
379 #[allow(clippy::too_many_arguments)]
380 fn backtrack(
381 line: &[Option<bool>],
382 clue: &[u8],
383 line_pos: usize,
384 clue_idx: usize,
385 n: usize,
386 filled: &mut [bool],
387 filled_count: &mut [u32],
388 total: &mut u32,
389 ) {
390 if clue_idx == clue.len() {
391 if line[line_pos..].iter().all(|&c| c != Some(true)) {
392 *total += 1;
393 for (i, &f) in filled.iter().enumerate() {
394 if f {
395 filled_count[i] += 1;
396 }
397 }
398 }
399 return;
400 }
401
402 let run_len = clue[clue_idx] as usize;
403 let remaining_runs = &clue[clue_idx + 1..];
404 let remaining_run_sum: usize = remaining_runs.iter().map(|&r| r as usize).sum();
405 let remaining_gaps = remaining_runs.len().saturating_sub(1);
406 let remaining_min = remaining_run_sum + remaining_gaps;
407
408 let max_start = n.saturating_sub(remaining_min + run_len);
409
410 let gap = if remaining_runs.is_empty() { 0 } else { 1 };
412
413 let mut start = line_pos;
414 while start <= max_start {
415 if line[line_pos..start].contains(&Some(true)) {
416 break;
417 }
418 if line[start..start + run_len].contains(&Some(false)) {
419 start += 1;
420 continue;
421 }
422 for cell in filled[start..start + run_len].iter_mut() {
423 *cell = true;
424 }
425 backtrack(
426 line,
427 clue,
428 (start + run_len + gap).min(n),
429 clue_idx + 1,
430 n,
431 filled,
432 filled_count,
433 total,
434 );
435 for cell in filled[start..start + run_len].iter_mut() {
436 *cell = false;
437 }
438 start += 1;
439 }
440 }
441
442 let mut filled = vec![false; n];
443 backtrack(
444 line,
445 clue,
446 0,
447 0,
448 n,
449 &mut filled,
450 &mut filled_count,
451 &mut total,
452 );
453
454 if total == 0 {
455 return line.to_vec();
456 }
457
458 let mut result = vec![None; n];
459 for i in 0..n {
460 if line[i].is_some() {
461 result[i] = line[i];
462 } else if filled_count[i] == 0 {
463 result[i] = Some(false);
464 } else if filled_count[i] == total {
465 result[i] = Some(true);
466 }
467 }
468 result
469}
470
471pub fn is_logically_solvable(
475 width: usize,
476 height: usize,
477 row_clues: &[Clue],
478 col_clues: &[Clue],
479) -> bool {
480 let mut grid = vec![None; width * height];
481
482 loop {
483 let old = grid.clone();
484
485 for y in 0..height {
486 let line: Vec<Option<bool>> = (0..width).map(|x| grid[y * width + x]).collect();
487 let solved = solve_line(&line, &row_clues[y]);
488 for x in 0..width {
489 grid[y * width + x] = solved[x];
490 }
491 }
492
493 for x in 0..width {
494 let line: Vec<Option<bool>> = (0..height).map(|y| grid[y * width + x]).collect();
495 let solved = solve_line(&line, &col_clues[x]);
496 for y in 0..height {
497 grid[y * width + x] = solved[y];
498 }
499 }
500
501 if grid == old {
502 break;
503 }
504 }
505
506 grid.iter().all(|&c| c.is_some())
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn line_clue_examples() {
515 assert_eq!(NonogramGame::line_clue(&[false, false, false]), vec![0]);
516 assert_eq!(
517 NonogramGame::line_clue(&[false, true, true, false, true]),
518 vec![2, 1]
519 );
520 assert_eq!(NonogramGame::line_clue(&[true, true, true]), vec![3]);
521 assert_eq!(
522 NonogramGame::line_clue(&[true, false, true, false, true]),
523 vec![1, 1, 1]
524 );
525 }
526
527 #[test]
528 fn from_grid_computes_clues() {
529 let grid = vec![
530 vec![false, true, false],
531 vec![true, true, true],
532 vec![false, true, false],
533 ];
534 let game = NonogramGame::from_grid(grid);
535 assert_eq!(game.row_clues, vec![vec![1], vec![3], vec![1]]);
536 assert_eq!(game.col_clues, vec![vec![1], vec![3], vec![1]]);
537 }
538
539 #[test]
540 #[should_panic(expected = "same width")]
541 fn from_grid_rejects_ragged_rows() {
542 NonogramGame::from_grid(vec![vec![true, false], vec![true]]);
543 }
544
545 #[test]
546 fn row_and_col_satisfied() {
547 let grid = vec![vec![true, false], vec![false, true]];
548 let mut game = NonogramGame::from_grid(grid);
549 assert!(!game.row_satisfied(0));
550 game.toggle_fill(0, 0);
551 assert!(game.row_satisfied(0));
552 assert!(game.col_satisfied(0));
553 assert!(!game.row_satisfied(1));
554 }
555
556 #[test]
557 fn crossed_cells_do_not_count_against_satisfaction_or_win() {
558 let grid = vec![vec![true, false]];
559 let mut game = NonogramGame::from_grid(grid);
560 game.toggle_fill(0, 0);
561 game.toggle_cross(1, 0); assert_eq!(game.status, GameStatus::Won);
563 }
564
565 #[test]
566 fn completing_a_row_auto_crosses_its_remaining_cells() {
567 let grid = vec![vec![true, true, false], vec![false, false, true]];
568 let mut game = NonogramGame::from_grid(grid);
569 game.toggle_fill(0, 0);
570 assert_eq!(game.cells[game.idx(2, 0)], CellState::Empty);
571 game.toggle_fill(1, 0);
572 assert!(game.row_satisfied(0));
573 assert_eq!(game.cells[game.idx(2, 0)], CellState::Crossed);
574 }
575
576 #[test]
577 fn completing_a_column_auto_crosses_its_remaining_cells() {
578 let grid = vec![vec![true, false], vec![true, false], vec![false, true]];
579 let mut game = NonogramGame::from_grid(grid);
580 game.toggle_fill(0, 0);
581 assert_eq!(game.cells[game.idx(0, 2)], CellState::Empty);
582 game.toggle_fill(0, 1);
583 assert!(game.col_satisfied(0));
584 assert_eq!(game.cells[game.idx(0, 2)], CellState::Crossed);
585 }
586
587 #[test]
588 fn auto_crossed_cells_do_not_count_against_satisfaction_or_win() {
589 let grid = vec![vec![true, false]];
590 let mut game = NonogramGame::from_grid(grid);
591 game.toggle_fill(0, 0);
592 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
593 assert_eq!(game.status, GameStatus::Won);
594 }
595
596 #[test]
597 fn undo_reverts_auto_crossing_along_with_the_completing_move() {
598 let grid = vec![vec![true, false], vec![false, true]];
599 let mut game = NonogramGame::from_grid(grid);
600 game.toggle_fill(0, 0);
601 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
602 game.undo();
603 assert_eq!(game.cells[game.idx(1, 0)], CellState::Empty);
604 }
605
606 #[test]
607 fn un_satisfying_a_line_later_leaves_its_auto_crosses_in_place() {
608 let grid = vec![vec![true, false], vec![false, true]];
609 let mut game = NonogramGame::from_grid(grid);
610 game.toggle_fill(0, 0);
611 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
612 game.toggle_fill(0, 0); assert!(!game.row_satisfied(0));
614 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
615 }
616
617 #[test]
618 fn empty_line_is_crossed_out_from_the_start() {
619 let grid = vec![vec![true, false], vec![false, false]];
620 let game = NonogramGame::from_grid(grid);
621 assert_eq!(game.row_clues[1], vec![0]);
622 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
623 assert_eq!(game.cells[game.idx(1, 1)], CellState::Crossed);
624 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
626 }
627
628 #[test]
629 fn reset_reapplies_the_empty_line_crossing() {
630 let grid = vec![vec![true, false], vec![false, false]];
631 let mut game = NonogramGame::from_grid(grid);
632 game.toggle_fill(0, 0);
633 game.reset();
634 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
635 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
636 }
637
638 #[test]
639 fn wrongly_crossed_required_cell_blocks_win() {
640 let grid = vec![vec![true, false]];
641 let mut game = NonogramGame::from_grid(grid);
642 game.toggle_cross(0, 0); assert_eq!(game.status, GameStatus::Playing);
644 game.toggle_fill(1, 0); assert_eq!(game.status, GameStatus::Playing);
646 }
647
648 #[test]
649 fn undo_redo_round_trip() {
650 let grid = vec![vec![true, true]];
651 let mut game = NonogramGame::from_grid(grid);
652 assert!(!game.can_undo());
653 game.toggle_fill(0, 0);
654 game.toggle_fill(1, 0);
655 assert_eq!(game.status, GameStatus::Won);
656 assert!(game.can_undo());
657 game.undo();
658 assert_eq!(game.status, GameStatus::Playing);
659 assert_eq!(game.cells[1], CellState::Empty);
660 assert!(game.can_redo());
661 game.redo();
662 assert_eq!(game.status, GameStatus::Won);
663 }
664
665 #[test]
666 fn reset_keeps_puzzle_clears_progress() {
667 let grid = vec![vec![true]];
668 let mut game = NonogramGame::from_grid(grid);
669 game.toggle_fill(0, 0);
670 assert_eq!(game.status, GameStatus::Won);
671 let row_clues_before = game.row_clues.clone();
672 game.reset();
673 assert_eq!(game.status, GameStatus::Playing);
674 assert!(game.cells.iter().all(|&c| c == CellState::Empty));
675 assert!(!game.can_undo());
676 assert_eq!(game.row_clues, row_clues_before);
677 }
678
679 #[test]
680 fn random_is_deterministic_per_seed() {
681 let a = NonogramGame::random(10, 10, 0.4, 42);
682 let b = NonogramGame::random(10, 10, 0.4, 42);
683 assert_eq!(a.row_clues, b.row_clues);
684 assert_eq!(a.col_clues, b.col_clues);
685 }
686
687 #[test]
688 fn col_satisfied_after_filling_whole_column() {
689 let rows: [u16; 10] = [15, 15, 63, 63, 255, 255, 1023, 1023, 1023, 1023];
692 let grid: Vec<Vec<bool>> = rows
693 .iter()
694 .map(|&row| (0..10).map(|x| (row >> x) & 1 != 0).collect())
695 .collect();
696 let mut game = NonogramGame::from_grid(grid);
697 assert_eq!(game.col_clues[0], vec![10]);
698 for y in 0..10 {
699 game.toggle_fill(0, y);
700 }
701 for y in 0..10 {
702 assert_eq!(game.cells[game.idx(0, y)], CellState::Filled);
703 }
704 assert!(
705 game.col_satisfied(0),
706 "column 0 should be satisfied after filling all 10 of its cells"
707 );
708 }
709
710 #[test]
711 fn alternate_valid_solution_also_wins() {
712 let grid = vec![vec![true, false], vec![false, true]];
716 let mut game = NonogramGame::from_grid(grid);
717 game.toggle_fill(1, 0);
718 game.toggle_fill(0, 1);
719 assert_eq!(game.status, GameStatus::Won);
720 }
721
722 #[test]
723 fn random_avoids_degenerate_puzzles() {
724 let game = NonogramGame::random(4, 4, 0.0, 7);
727 let filled = (0..4)
728 .flat_map(|y| (0..4).map(move |x| (x, y)))
729 .filter(|&(x, y)| game.is_solution_filled(x, y))
730 .count();
731 assert!(filled > 0 && filled < 16);
732 }
733
734 #[test]
735 fn solve_line_determines_overlap() {
736 let line = vec![None; 5];
737 let clue = vec![3];
738 let result = solve_line(&line, &clue);
739 assert_eq!(result, vec![None, None, Some(true), None, None]);
742 }
743
744 #[test]
745 fn solve_line_all_empty_on_zero_clue() {
746 let line = vec![None; 4];
747 let clue = vec![0];
748 let result = solve_line(&line, &clue);
749 assert_eq!(result, vec![Some(false); 4]);
750 }
751
752 #[test]
753 fn solve_line_respects_known_filled_cell() {
754 let line = vec![None, Some(true), None, None, None];
755 let clue = vec![3];
756 let result = solve_line(&line, &clue);
757 assert_eq!(
762 result,
763 vec![None, Some(true), Some(true), None, Some(false)]
764 );
765 }
766
767 #[test]
768 fn solve_line_respects_known_empty_cell() {
769 let line = vec![None, None, Some(false), None, None];
770 let clue = vec![3];
771 let result = solve_line(&line, &clue);
772 assert_eq!(result, line);
783 }
784
785 #[test]
786 fn solve_line_multi_run() {
787 let line = vec![None; 7];
788 let clue = vec![2, 1];
789 let result = solve_line(&line, &clue);
790 assert_eq!(result.len(), 7);
806 }
807
808 #[test]
809 fn is_logically_solvable_returns_true_for_simple_puzzle() {
810 let solution = vec![
811 vec![false, true, false],
812 vec![true, true, true],
813 vec![false, true, false],
814 ];
815 let game = NonogramGame::from_grid(solution);
816 assert!(is_logically_solvable(
817 game.width,
818 game.height,
819 &game.row_clues,
820 &game.col_clues,
821 ));
822 }
823
824 #[test]
825 fn is_logically_solvable_returns_false_for_ambiguous_puzzle() {
826 let solution = vec![vec![true, false], vec![false, true]];
827 let game = NonogramGame::from_grid(solution);
828 assert!(!is_logically_solvable(
829 game.width,
830 game.height,
831 &game.row_clues,
832 &game.col_clues,
833 ));
834 }
835
836 #[test]
837 fn random_logical_is_line_solvable() {
838 let game = NonogramGame::random_logical(8, 8, 0.5, 42);
839 assert!(is_logically_solvable(
840 game.width,
841 game.height,
842 &game.row_clues,
843 &game.col_clues,
844 ));
845 }
846
847 #[test]
848 fn random_logical_rejects_unsolvable_seed() {
849 for seed in 0..10 {
853 let game = NonogramGame::random_logical(6, 6, 0.4, seed);
854 assert!(is_logically_solvable(
855 game.width,
856 game.height,
857 &game.row_clues,
858 &game.col_clues,
859 ));
860 }
861 }
862}