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 {
376 assert!(width > 0 && height > 0);
377 let density = density.clamp(0.05, 0.95);
378 let mut rng = fastrand::Rng::with_seed(seed);
379 let max_attempts = 200;
380
381 for _ in 0..max_attempts {
382 let solution: Vec<bool> = (0..width * height).map(|_| rng.f32() < density).collect();
383 let filled = solution.iter().filter(|&&v| v).count();
384 if filled == 0 || filled == solution.len() {
385 continue;
386 }
387
388 let game = Self::from_solution(width, height, solution);
389
390 if is_logically_solvable(width, height, &game.row_clues, &game.col_clues) {
391 return game;
392 }
393 }
394
395 panic!(
396 "could not generate a logically solvable puzzle after \
397 {max_attempts} attempts"
398 );
399 }
400}
401
402fn solve_line(line: &[Option<bool>], clue: &[u8]) -> Vec<Option<bool>> {
409 let n = line.len();
410
411 if clue.len() == 1 && clue[0] == 0 {
412 return vec![Some(false); n];
413 }
414
415 let mut filled_count = vec![0u32; n];
416 let mut total = 0u32;
417
418 #[allow(clippy::too_many_arguments)]
419 fn backtrack(
420 line: &[Option<bool>],
421 clue: &[u8],
422 line_pos: usize,
423 clue_idx: usize,
424 n: usize,
425 filled: &mut [bool],
426 filled_count: &mut [u32],
427 total: &mut u32,
428 ) {
429 if clue_idx == clue.len() {
430 if line[line_pos..].iter().all(|&c| c != Some(true)) {
431 *total += 1;
432 for (i, &f) in filled.iter().enumerate() {
433 if f {
434 filled_count[i] += 1;
435 }
436 }
437 }
438 return;
439 }
440
441 let run_len = clue[clue_idx] as usize;
442 let remaining_runs = &clue[clue_idx + 1..];
443 let remaining_run_sum: usize = remaining_runs.iter().map(|&r| r as usize).sum();
444 let remaining_gaps = remaining_runs.len().saturating_sub(1);
445 let remaining_min = remaining_run_sum + remaining_gaps;
446
447 let max_start = n.saturating_sub(remaining_min + run_len);
448
449 let gap = if remaining_runs.is_empty() { 0 } else { 1 };
451
452 let mut start = line_pos;
453 while start <= max_start {
454 if line[line_pos..start].contains(&Some(true)) {
455 break;
456 }
457 if line[start..start + run_len].contains(&Some(false)) {
458 start += 1;
459 continue;
460 }
461 for cell in filled[start..start + run_len].iter_mut() {
462 *cell = true;
463 }
464 backtrack(
465 line,
466 clue,
467 (start + run_len + gap).min(n),
468 clue_idx + 1,
469 n,
470 filled,
471 filled_count,
472 total,
473 );
474 for cell in filled[start..start + run_len].iter_mut() {
475 *cell = false;
476 }
477 start += 1;
478 }
479 }
480
481 let mut filled = vec![false; n];
482 backtrack(
483 line,
484 clue,
485 0,
486 0,
487 n,
488 &mut filled,
489 &mut filled_count,
490 &mut total,
491 );
492
493 if total == 0 {
494 return line.to_vec();
495 }
496
497 let mut result = vec![None; n];
498 for i in 0..n {
499 if line[i].is_some() {
500 result[i] = line[i];
501 } else if filled_count[i] == 0 {
502 result[i] = Some(false);
503 } else if filled_count[i] == total {
504 result[i] = Some(true);
505 }
506 }
507 result
508}
509
510pub fn is_logically_solvable(
514 width: usize,
515 height: usize,
516 row_clues: &[Clue],
517 col_clues: &[Clue],
518) -> bool {
519 let mut grid = vec![None; width * height];
520
521 loop {
522 let old = grid.clone();
523
524 for y in 0..height {
525 let line: Vec<Option<bool>> = (0..width).map(|x| grid[y * width + x]).collect();
526 let solved = solve_line(&line, &row_clues[y]);
527 for x in 0..width {
528 grid[y * width + x] = solved[x];
529 }
530 }
531
532 for x in 0..width {
533 let line: Vec<Option<bool>> = (0..height).map(|y| grid[y * width + x]).collect();
534 let solved = solve_line(&line, &col_clues[x]);
535 for y in 0..height {
536 grid[y * width + x] = solved[y];
537 }
538 }
539
540 if grid == old {
541 break;
542 }
543 }
544
545 grid.iter().all(|&c| c.is_some())
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn line_clue_examples() {
554 assert_eq!(NonogramGame::line_clue(&[false, false, false]), vec![0]);
555 assert_eq!(
556 NonogramGame::line_clue(&[false, true, true, false, true]),
557 vec![2, 1]
558 );
559 assert_eq!(NonogramGame::line_clue(&[true, true, true]), vec![3]);
560 assert_eq!(
561 NonogramGame::line_clue(&[true, false, true, false, true]),
562 vec![1, 1, 1]
563 );
564 }
565
566 #[test]
567 fn from_grid_computes_clues() {
568 let grid = vec![
569 vec![false, true, false],
570 vec![true, true, true],
571 vec![false, true, false],
572 ];
573 let game = NonogramGame::from_grid(grid);
574 assert_eq!(game.row_clues, vec![vec![1], vec![3], vec![1]]);
575 assert_eq!(game.col_clues, vec![vec![1], vec![3], vec![1]]);
576 }
577
578 #[test]
579 #[should_panic(expected = "same width")]
580 fn from_grid_rejects_ragged_rows() {
581 NonogramGame::from_grid(vec![vec![true, false], vec![true]]);
582 }
583
584 #[test]
585 fn row_and_col_satisfied() {
586 let grid = vec![vec![true, false], vec![false, true]];
587 let mut game = NonogramGame::from_grid(grid);
588 assert!(!game.row_satisfied(0));
589 game.toggle_fill(0, 0);
590 assert!(game.row_satisfied(0));
591 assert!(game.col_satisfied(0));
592 assert!(!game.row_satisfied(1));
593 }
594
595 #[test]
596 fn crossed_cells_do_not_count_against_satisfaction_or_win() {
597 let grid = vec![vec![true, false]];
598 let mut game = NonogramGame::from_grid(grid);
599 game.toggle_fill(0, 0);
600 game.toggle_cross(1, 0); assert_eq!(game.status, GameStatus::Won);
602 }
603
604 #[test]
605 fn completing_a_row_auto_crosses_its_remaining_cells() {
606 let grid = vec![vec![true, true, false], vec![false, false, true]];
607 let mut game = NonogramGame::from_grid(grid);
608 game.toggle_fill(0, 0);
609 assert_eq!(game.cells[game.idx(2, 0)], CellState::Empty);
610 game.toggle_fill(1, 0);
611 assert!(game.row_satisfied(0));
612 assert_eq!(game.cells[game.idx(2, 0)], CellState::Crossed);
613 }
614
615 #[test]
616 fn completing_a_column_auto_crosses_its_remaining_cells() {
617 let grid = vec![vec![true, false], vec![true, false], vec![false, true]];
618 let mut game = NonogramGame::from_grid(grid);
619 game.toggle_fill(0, 0);
620 assert_eq!(game.cells[game.idx(0, 2)], CellState::Empty);
621 game.toggle_fill(0, 1);
622 assert!(game.col_satisfied(0));
623 assert_eq!(game.cells[game.idx(0, 2)], CellState::Crossed);
624 }
625
626 #[test]
627 fn auto_crossed_cells_do_not_count_against_satisfaction_or_win() {
628 let grid = vec![vec![true, false]];
629 let mut game = NonogramGame::from_grid(grid);
630 game.toggle_fill(0, 0);
631 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
632 assert_eq!(game.status, GameStatus::Won);
633 }
634
635 #[test]
636 fn undo_reverts_auto_crossing_along_with_the_completing_move() {
637 let grid = vec![vec![true, false], vec![false, true]];
638 let mut game = NonogramGame::from_grid(grid);
639 game.toggle_fill(0, 0);
640 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
641 game.undo();
642 assert_eq!(game.cells[game.idx(1, 0)], CellState::Empty);
643 }
644
645 #[test]
646 fn un_satisfying_a_line_later_leaves_its_auto_crosses_in_place() {
647 let grid = vec![vec![true, false], vec![false, true]];
648 let mut game = NonogramGame::from_grid(grid);
649 game.toggle_fill(0, 0);
650 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
651 game.toggle_fill(0, 0); assert!(!game.row_satisfied(0));
653 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
654 }
655
656 #[test]
657 fn empty_line_is_crossed_out_from_the_start() {
658 let grid = vec![vec![true, false], vec![false, false]];
659 let game = NonogramGame::from_grid(grid);
660 assert_eq!(game.row_clues[1], vec![0]);
661 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
662 assert_eq!(game.cells[game.idx(1, 1)], CellState::Crossed);
663 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
665 }
666
667 #[test]
668 fn reset_reapplies_the_empty_line_crossing() {
669 let grid = vec![vec![true, false], vec![false, false]];
670 let mut game = NonogramGame::from_grid(grid);
671 game.toggle_fill(0, 0);
672 game.reset();
673 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
674 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
675 }
676
677 #[test]
678 fn wrongly_crossed_required_cell_blocks_win() {
679 let grid = vec![vec![true, false]];
680 let mut game = NonogramGame::from_grid(grid);
681 game.toggle_cross(0, 0); assert_eq!(game.status, GameStatus::Playing);
683 game.toggle_fill(1, 0); assert_eq!(game.status, GameStatus::Playing);
685 }
686
687 #[test]
688 fn undo_redo_round_trip() {
689 let grid = vec![vec![true, true]];
690 let mut game = NonogramGame::from_grid(grid);
691 assert!(!game.can_undo());
692 game.toggle_fill(0, 0);
693 game.toggle_fill(1, 0);
694 assert_eq!(game.status, GameStatus::Won);
695 assert!(game.can_undo());
696 game.undo();
697 assert_eq!(game.status, GameStatus::Playing);
698 assert_eq!(game.cells[1], CellState::Empty);
699 assert!(game.can_redo());
700 game.redo();
701 assert_eq!(game.status, GameStatus::Won);
702 }
703
704 #[test]
705 fn reset_keeps_puzzle_clears_progress() {
706 let grid = vec![vec![true]];
707 let mut game = NonogramGame::from_grid(grid);
708 game.toggle_fill(0, 0);
709 assert_eq!(game.status, GameStatus::Won);
710 let row_clues_before = game.row_clues.clone();
711 game.reset();
712 assert_eq!(game.status, GameStatus::Playing);
713 assert!(game.cells.iter().all(|&c| c == CellState::Empty));
714 assert!(!game.can_undo());
715 assert_eq!(game.row_clues, row_clues_before);
716 }
717
718 #[test]
719 fn random_is_deterministic_per_seed() {
720 let a = NonogramGame::random(10, 10, 0.4, 42);
721 let b = NonogramGame::random(10, 10, 0.4, 42);
722 assert_eq!(a.row_clues, b.row_clues);
723 assert_eq!(a.col_clues, b.col_clues);
724 }
725
726 #[test]
727 fn col_satisfied_after_filling_whole_column() {
728 let rows: [u16; 10] = [15, 15, 63, 63, 255, 255, 1023, 1023, 1023, 1023];
731 let grid: Vec<Vec<bool>> = rows
732 .iter()
733 .map(|&row| (0..10).map(|x| (row >> x) & 1 != 0).collect())
734 .collect();
735 let mut game = NonogramGame::from_grid(grid);
736 assert_eq!(game.col_clues[0], vec![10]);
737 for y in 0..10 {
738 game.toggle_fill(0, y);
739 }
740 for y in 0..10 {
741 assert_eq!(game.cells[game.idx(0, y)], CellState::Filled);
742 }
743 assert!(
744 game.col_satisfied(0),
745 "column 0 should be satisfied after filling all 10 of its cells"
746 );
747 }
748
749 #[test]
750 fn alternate_valid_solution_also_wins() {
751 let grid = vec![vec![true, false], vec![false, true]];
755 let mut game = NonogramGame::from_grid(grid);
756 game.toggle_fill(1, 0);
757 game.toggle_fill(0, 1);
758 assert_eq!(game.status, GameStatus::Won);
759 }
760
761 #[test]
762 fn random_avoids_degenerate_puzzles() {
763 let game = NonogramGame::random(4, 4, 0.0, 7);
766 let filled = (0..4)
767 .flat_map(|y| (0..4).map(move |x| (x, y)))
768 .filter(|&(x, y)| game.is_solution_filled(x, y))
769 .count();
770 assert!(filled > 0 && filled < 16);
771 }
772
773 #[test]
774 fn solve_line_determines_overlap() {
775 let line = vec![None; 5];
776 let clue = vec![3];
777 let result = solve_line(&line, &clue);
778 assert_eq!(result, vec![None, None, Some(true), None, None]);
781 }
782
783 #[test]
784 fn solve_line_all_empty_on_zero_clue() {
785 let line = vec![None; 4];
786 let clue = vec![0];
787 let result = solve_line(&line, &clue);
788 assert_eq!(result, vec![Some(false); 4]);
789 }
790
791 #[test]
792 fn solve_line_respects_known_filled_cell() {
793 let line = vec![None, Some(true), None, None, None];
794 let clue = vec![3];
795 let result = solve_line(&line, &clue);
796 assert_eq!(
801 result,
802 vec![None, Some(true), Some(true), None, Some(false)]
803 );
804 }
805
806 #[test]
807 fn solve_line_respects_known_empty_cell() {
808 let line = vec![None, None, Some(false), None, None];
809 let clue = vec![3];
810 let result = solve_line(&line, &clue);
811 assert_eq!(result, line);
822 }
823
824 #[test]
825 fn solve_line_multi_run() {
826 let line = vec![None; 7];
827 let clue = vec![2, 1];
828 let result = solve_line(&line, &clue);
829 assert_eq!(result.len(), 7);
845 }
846
847 #[test]
848 fn is_logically_solvable_returns_true_for_simple_puzzle() {
849 let solution = vec![
850 vec![false, true, false],
851 vec![true, true, true],
852 vec![false, true, false],
853 ];
854 let game = NonogramGame::from_grid(solution);
855 assert!(is_logically_solvable(
856 game.width,
857 game.height,
858 &game.row_clues,
859 &game.col_clues,
860 ));
861 }
862
863 #[test]
864 fn is_logically_solvable_returns_false_for_ambiguous_puzzle() {
865 let solution = vec![vec![true, false], vec![false, true]];
866 let game = NonogramGame::from_grid(solution);
867 assert!(!is_logically_solvable(
868 game.width,
869 game.height,
870 &game.row_clues,
871 &game.col_clues,
872 ));
873 }
874
875 #[test]
876 fn random_logical_is_line_solvable() {
877 let game = NonogramGame::random_logical(8, 8, 0.5, 42);
878 assert!(is_logically_solvable(
879 game.width,
880 game.height,
881 &game.row_clues,
882 &game.col_clues,
883 ));
884 }
885
886 #[test]
887 fn random_logical_rejects_unsolvable_seed() {
888 for seed in 0..10 {
892 let game = NonogramGame::random_logical(6, 6, 0.4, seed);
893 assert!(is_logically_solvable(
894 game.width,
895 game.height,
896 &game.row_clues,
897 &game.col_clues,
898 ));
899 }
900 }
901}