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 in_gesture: bool,
63}
64
65impl NonogramGame {
66 pub fn from_grid(grid: Vec<Vec<bool>>) -> Self {
74 let height = grid.len();
75 assert!(height > 0, "solution grid must have at least one row");
76 let width = grid[0].len();
77 assert!(width > 0, "solution rows must have at least one column");
78 assert!(
79 grid.iter().all(|row| row.len() == width),
80 "solution rows must all have the same width"
81 );
82 let solution: Vec<bool> = grid.into_iter().flatten().collect();
83 Self::from_solution(width, height, solution)
84 }
85
86 pub fn random(width: usize, height: usize, density: f32, seed: u64) -> Self {
98 assert!(width > 0 && height > 0);
99 let density = density.clamp(0.05, 0.95);
100 let mut rng = fastrand::Rng::with_seed(seed);
101 loop {
102 let solution: Vec<bool> = (0..width * height).map(|_| rng.f32() < density).collect();
103 let filled = solution.iter().filter(|&&v| v).count();
104 if filled > 0 && filled < solution.len() {
105 return Self::from_solution(width, height, solution);
106 }
107 }
108 }
109
110 fn from_solution(width: usize, height: usize, solution: Vec<bool>) -> Self {
111 let row_clues = (0..height)
112 .map(|y| Self::line_clue(&solution[y * width..(y + 1) * width]))
113 .collect();
114 let col_clues = (0..width)
115 .map(|x| {
116 let col: Vec<bool> = (0..height).map(|y| solution[y * width + x]).collect();
117 Self::line_clue(&col)
118 })
119 .collect();
120 let mut game = Self {
121 width,
122 height,
123 solution,
124 cells: vec![CellState::Empty; width * height],
125 row_clues,
126 col_clues,
127 status: GameStatus::Playing,
128 undo_stack: Vec::new(),
129 redo_stack: Vec::new(),
130 in_gesture: false,
131 };
132 game.cross_out_empty_lines();
133 game
134 }
135
136 pub fn reset(&mut self) {
140 self.cells = vec![CellState::Empty; self.width * self.height];
141 self.status = GameStatus::Playing;
142 self.undo_stack.clear();
143 self.redo_stack.clear();
144 self.in_gesture = false;
145 self.cross_out_empty_lines();
146 }
147
148 #[inline]
149 pub fn idx(&self, x: usize, y: usize) -> usize {
150 y * self.width + x
151 }
152
153 pub fn is_solution_filled(&self, x: usize, y: usize) -> bool {
155 self.solution[self.idx(x, y)]
156 }
157
158 pub fn toggle_fill(&mut self, x: usize, y: usize) {
161 let new_state = if self.cells[self.idx(x, y)] == CellState::Filled {
162 CellState::Empty
163 } else {
164 CellState::Filled
165 };
166 self.set_cell(x, y, new_state);
167 }
168
169 pub fn toggle_cross(&mut self, x: usize, y: usize) {
172 let new_state = if self.cells[self.idx(x, y)] == CellState::Crossed {
173 CellState::Empty
174 } else {
175 CellState::Crossed
176 };
177 self.set_cell(x, y, new_state);
178 }
179
180 pub fn clear_cell(&mut self, x: usize, y: usize) {
182 self.set_cell(x, y, CellState::Empty);
183 }
184
185 pub fn fill(&mut self, x: usize, y: usize) {
190 self.set_cell(x, y, CellState::Filled);
191 }
192
193 pub fn cross(&mut self, x: usize, y: usize) {
196 self.set_cell(x, y, CellState::Crossed);
197 }
198
199 pub(crate) fn set_cell_state(&mut self, x: usize, y: usize, state: CellState) {
203 self.set_cell(x, y, state);
204 }
205
206 pub(crate) fn begin_gesture(&mut self) {
211 self.undo_stack.push(self.cells.clone());
212 self.redo_stack.clear();
213 self.in_gesture = true;
214 }
215
216 pub(crate) fn end_gesture(&mut self) {
218 self.in_gesture = false;
219 }
220
221 fn set_cell(&mut self, x: usize, y: usize, new_state: CellState) {
222 let idx = self.idx(x, y);
223 if self.cells[idx] == new_state {
224 return;
225 }
226 if !self.in_gesture {
227 self.undo_stack.push(self.cells.clone());
228 self.redo_stack.clear();
229 }
230 self.cells[idx] = new_state;
231 self.auto_cross_completed_lines(x, y);
232 self.check_win();
233 }
234
235 fn auto_cross_completed_lines(&mut self, x: usize, y: usize) {
241 self.cross_row_if_satisfied(y);
242 self.cross_col_if_satisfied(x);
243 }
244
245 fn cross_out_empty_lines(&mut self) {
252 for y in 0..self.height {
253 self.cross_row_if_satisfied(y);
254 }
255 for x in 0..self.width {
256 self.cross_col_if_satisfied(x);
257 }
258 }
259
260 fn cross_row_if_satisfied(&mut self, y: usize) {
261 if self.row_satisfied(y) {
262 for x in 0..self.width {
263 let idx = self.idx(x, y);
264 if self.cells[idx] == CellState::Empty {
265 self.cells[idx] = CellState::Crossed;
266 }
267 }
268 }
269 }
270
271 fn cross_col_if_satisfied(&mut self, x: usize) {
272 if self.col_satisfied(x) {
273 for y in 0..self.height {
274 let idx = self.idx(x, y);
275 if self.cells[idx] == CellState::Empty {
276 self.cells[idx] = CellState::Crossed;
277 }
278 }
279 }
280 }
281
282 pub fn row_satisfied(&self, y: usize) -> bool {
286 let line: Vec<bool> = (0..self.width)
287 .map(|x| self.cells[self.idx(x, y)] == CellState::Filled)
288 .collect();
289 Self::line_clue(&line) == self.row_clues[y]
290 }
291
292 pub fn col_satisfied(&self, x: usize) -> bool {
295 let line: Vec<bool> = (0..self.height)
296 .map(|y| self.cells[self.idx(x, y)] == CellState::Filled)
297 .collect();
298 Self::line_clue(&line) == self.col_clues[x]
299 }
300
301 pub fn can_undo(&self) -> bool {
302 !self.undo_stack.is_empty()
303 }
304
305 pub fn can_redo(&self) -> bool {
306 !self.redo_stack.is_empty()
307 }
308
309 pub fn undo(&mut self) {
310 if let Some(prev) = self.undo_stack.pop() {
311 self.redo_stack
312 .push(std::mem::replace(&mut self.cells, prev));
313 self.check_win();
314 }
315 }
316
317 pub fn redo(&mut self) {
318 if let Some(next) = self.redo_stack.pop() {
319 self.undo_stack
320 .push(std::mem::replace(&mut self.cells, next));
321 self.check_win();
322 }
323 }
324
325 fn check_win(&mut self) {
326 let won = (0..self.height).all(|y| self.row_satisfied(y))
327 && (0..self.width).all(|x| self.col_satisfied(x));
328 self.status = if won {
329 GameStatus::Won
330 } else {
331 GameStatus::Playing
332 };
333 }
334
335 fn line_clue(line: &[bool]) -> Clue {
339 let mut clue = Vec::new();
340 let mut run = 0u8;
341 for &filled in line {
342 if filled {
343 run += 1;
344 } else if run > 0 {
345 clue.push(run);
346 run = 0;
347 }
348 }
349 if run > 0 {
350 clue.push(run);
351 }
352 if clue.is_empty() {
353 clue.push(0);
354 }
355 clue
356 }
357
358 pub fn random_logical(width: usize, height: usize, density: f32, seed: u64) -> Self {
406 assert!(width > 0 && height > 0);
407 let density = density.clamp(0.05, 0.95);
408 let mut rng = fastrand::Rng::with_seed(seed);
409 let max_attempts = 200;
410
411 for _ in 0..max_attempts {
412 let solution: Vec<bool> = (0..width * height).map(|_| rng.f32() < density).collect();
413 let filled = solution.iter().filter(|&&v| v).count();
414 if filled == 0 || filled == solution.len() {
415 continue;
416 }
417
418 let game = Self::from_solution(width, height, solution);
419
420 if is_logically_solvable(width, height, &game.row_clues, &game.col_clues) {
421 return game;
422 }
423 }
424
425 panic!(
426 "could not generate a logically solvable puzzle after \
427 {max_attempts} attempts"
428 );
429 }
430}
431
432fn solve_line(line: &[Option<bool>], clue: &[u8]) -> Vec<Option<bool>> {
439 let n = line.len();
440
441 if clue.len() == 1 && clue[0] == 0 {
442 return vec![Some(false); n];
443 }
444
445 let mut filled_count = vec![0u32; n];
446 let mut total = 0u32;
447
448 #[allow(clippy::too_many_arguments)]
449 fn backtrack(
450 line: &[Option<bool>],
451 clue: &[u8],
452 line_pos: usize,
453 clue_idx: usize,
454 n: usize,
455 filled: &mut [bool],
456 filled_count: &mut [u32],
457 total: &mut u32,
458 ) {
459 if clue_idx == clue.len() {
460 if line[line_pos..].iter().all(|&c| c != Some(true)) {
461 *total += 1;
462 for (i, &f) in filled.iter().enumerate() {
463 if f {
464 filled_count[i] += 1;
465 }
466 }
467 }
468 return;
469 }
470
471 let run_len = clue[clue_idx] as usize;
472 let remaining_runs = &clue[clue_idx + 1..];
473 let remaining_run_sum: usize = remaining_runs.iter().map(|&r| r as usize).sum();
474 let remaining_gaps = remaining_runs.len().saturating_sub(1);
475 let remaining_min = remaining_run_sum + remaining_gaps;
476
477 let max_start = n.saturating_sub(remaining_min + run_len);
478
479 let gap = if remaining_runs.is_empty() { 0 } else { 1 };
481
482 let mut start = line_pos;
483 while start <= max_start {
484 if line[line_pos..start].contains(&Some(true)) {
485 break;
486 }
487 if line[start..start + run_len].contains(&Some(false)) {
488 start += 1;
489 continue;
490 }
491 for cell in filled[start..start + run_len].iter_mut() {
492 *cell = true;
493 }
494 backtrack(
495 line,
496 clue,
497 (start + run_len + gap).min(n),
498 clue_idx + 1,
499 n,
500 filled,
501 filled_count,
502 total,
503 );
504 for cell in filled[start..start + run_len].iter_mut() {
505 *cell = false;
506 }
507 start += 1;
508 }
509 }
510
511 let mut filled = vec![false; n];
512 backtrack(
513 line,
514 clue,
515 0,
516 0,
517 n,
518 &mut filled,
519 &mut filled_count,
520 &mut total,
521 );
522
523 if total == 0 {
524 return line.to_vec();
525 }
526
527 let mut result = vec![None; n];
528 for i in 0..n {
529 if line[i].is_some() {
530 result[i] = line[i];
531 } else if filled_count[i] == 0 {
532 result[i] = Some(false);
533 } else if filled_count[i] == total {
534 result[i] = Some(true);
535 }
536 }
537 result
538}
539
540pub fn is_logically_solvable(
544 width: usize,
545 height: usize,
546 row_clues: &[Clue],
547 col_clues: &[Clue],
548) -> bool {
549 let mut grid = vec![None; width * height];
550
551 loop {
552 let old = grid.clone();
553
554 for y in 0..height {
555 let line: Vec<Option<bool>> = (0..width).map(|x| grid[y * width + x]).collect();
556 let solved = solve_line(&line, &row_clues[y]);
557 for x in 0..width {
558 grid[y * width + x] = solved[x];
559 }
560 }
561
562 for x in 0..width {
563 let line: Vec<Option<bool>> = (0..height).map(|y| grid[y * width + x]).collect();
564 let solved = solve_line(&line, &col_clues[x]);
565 for y in 0..height {
566 grid[y * width + x] = solved[y];
567 }
568 }
569
570 if grid == old {
571 break;
572 }
573 }
574
575 grid.iter().all(|&c| c.is_some())
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn line_clue_examples() {
584 assert_eq!(NonogramGame::line_clue(&[false, false, false]), vec![0]);
585 assert_eq!(
586 NonogramGame::line_clue(&[false, true, true, false, true]),
587 vec![2, 1]
588 );
589 assert_eq!(NonogramGame::line_clue(&[true, true, true]), vec![3]);
590 assert_eq!(
591 NonogramGame::line_clue(&[true, false, true, false, true]),
592 vec![1, 1, 1]
593 );
594 }
595
596 #[test]
597 fn from_grid_computes_clues() {
598 let grid = vec![
599 vec![false, true, false],
600 vec![true, true, true],
601 vec![false, true, false],
602 ];
603 let game = NonogramGame::from_grid(grid);
604 assert_eq!(game.row_clues, vec![vec![1], vec![3], vec![1]]);
605 assert_eq!(game.col_clues, vec![vec![1], vec![3], vec![1]]);
606 }
607
608 #[test]
609 #[should_panic(expected = "same width")]
610 fn from_grid_rejects_ragged_rows() {
611 NonogramGame::from_grid(vec![vec![true, false], vec![true]]);
612 }
613
614 #[test]
615 fn row_and_col_satisfied() {
616 let grid = vec![vec![true, false], vec![false, true]];
617 let mut game = NonogramGame::from_grid(grid);
618 assert!(!game.row_satisfied(0));
619 game.toggle_fill(0, 0);
620 assert!(game.row_satisfied(0));
621 assert!(game.col_satisfied(0));
622 assert!(!game.row_satisfied(1));
623 }
624
625 #[test]
626 fn crossed_cells_do_not_count_against_satisfaction_or_win() {
627 let grid = vec![vec![true, false]];
628 let mut game = NonogramGame::from_grid(grid);
629 game.toggle_fill(0, 0);
630 game.toggle_cross(1, 0); assert_eq!(game.status, GameStatus::Won);
632 }
633
634 #[test]
635 fn completing_a_row_auto_crosses_its_remaining_cells() {
636 let grid = vec![vec![true, true, false], vec![false, false, true]];
637 let mut game = NonogramGame::from_grid(grid);
638 game.toggle_fill(0, 0);
639 assert_eq!(game.cells[game.idx(2, 0)], CellState::Empty);
640 game.toggle_fill(1, 0);
641 assert!(game.row_satisfied(0));
642 assert_eq!(game.cells[game.idx(2, 0)], CellState::Crossed);
643 }
644
645 #[test]
646 fn completing_a_column_auto_crosses_its_remaining_cells() {
647 let grid = vec![vec![true, false], 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(0, 2)], CellState::Empty);
651 game.toggle_fill(0, 1);
652 assert!(game.col_satisfied(0));
653 assert_eq!(game.cells[game.idx(0, 2)], CellState::Crossed);
654 }
655
656 #[test]
657 fn auto_crossed_cells_do_not_count_against_satisfaction_or_win() {
658 let grid = vec![vec![true, false]];
659 let mut game = NonogramGame::from_grid(grid);
660 game.toggle_fill(0, 0);
661 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
662 assert_eq!(game.status, GameStatus::Won);
663 }
664
665 #[test]
666 fn undo_reverts_auto_crossing_along_with_the_completing_move() {
667 let grid = vec![vec![true, false], vec![false, true]];
668 let mut game = NonogramGame::from_grid(grid);
669 game.toggle_fill(0, 0);
670 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
671 game.undo();
672 assert_eq!(game.cells[game.idx(1, 0)], CellState::Empty);
673 }
674
675 #[test]
676 fn un_satisfying_a_line_later_leaves_its_auto_crosses_in_place() {
677 let grid = vec![vec![true, false], vec![false, true]];
678 let mut game = NonogramGame::from_grid(grid);
679 game.toggle_fill(0, 0);
680 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
681 game.toggle_fill(0, 0); assert!(!game.row_satisfied(0));
683 assert_eq!(game.cells[game.idx(1, 0)], CellState::Crossed);
684 }
685
686 #[test]
687 fn empty_line_is_crossed_out_from_the_start() {
688 let grid = vec![vec![true, false], vec![false, false]];
689 let game = NonogramGame::from_grid(grid);
690 assert_eq!(game.row_clues[1], vec![0]);
691 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
692 assert_eq!(game.cells[game.idx(1, 1)], CellState::Crossed);
693 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
695 }
696
697 #[test]
698 fn reset_reapplies_the_empty_line_crossing() {
699 let grid = vec![vec![true, false], vec![false, false]];
700 let mut game = NonogramGame::from_grid(grid);
701 game.toggle_fill(0, 0);
702 game.reset();
703 assert_eq!(game.cells[game.idx(0, 1)], CellState::Crossed);
704 assert_eq!(game.cells[game.idx(0, 0)], CellState::Empty);
705 }
706
707 #[test]
708 fn wrongly_crossed_required_cell_blocks_win() {
709 let grid = vec![vec![true, false]];
710 let mut game = NonogramGame::from_grid(grid);
711 game.toggle_cross(0, 0); assert_eq!(game.status, GameStatus::Playing);
713 game.toggle_fill(1, 0); assert_eq!(game.status, GameStatus::Playing);
715 }
716
717 #[test]
718 fn undo_redo_round_trip() {
719 let grid = vec![vec![true, true]];
720 let mut game = NonogramGame::from_grid(grid);
721 assert!(!game.can_undo());
722 game.toggle_fill(0, 0);
723 game.toggle_fill(1, 0);
724 assert_eq!(game.status, GameStatus::Won);
725 assert!(game.can_undo());
726 game.undo();
727 assert_eq!(game.status, GameStatus::Playing);
728 assert_eq!(game.cells[1], CellState::Empty);
729 assert!(game.can_redo());
730 game.redo();
731 assert_eq!(game.status, GameStatus::Won);
732 }
733
734 #[test]
735 fn gesture_collapses_multiple_mutations_into_one_undo_step() {
736 let grid = vec![vec![true, true, true]];
737 let mut game = NonogramGame::from_grid(grid);
738 game.begin_gesture();
739 game.fill(0, 0);
740 game.fill(1, 0);
741 game.fill(2, 0);
742 game.end_gesture();
743 assert_eq!(game.status, GameStatus::Won);
744 game.undo();
745 assert_eq!(game.cells, vec![CellState::Empty; 3]);
746 assert!(!game.can_undo());
747 }
748
749 #[test]
750 fn gesture_retract_via_set_cell_state_does_not_add_undo_steps() {
751 let grid = vec![vec![true, true, true]];
752 let mut game = NonogramGame::from_grid(grid);
753 game.begin_gesture();
754 game.fill(0, 0);
755 game.fill(1, 0);
756 game.fill(2, 0);
757 game.set_cell_state(1, 0, CellState::Empty);
760 game.set_cell_state(2, 0, CellState::Empty);
761 game.end_gesture();
762 assert_eq!(game.cells[game.idx(0, 0)], CellState::Filled);
763 assert_eq!(game.cells[game.idx(1, 0)], CellState::Empty);
764 game.undo();
765 assert_eq!(game.cells, vec![CellState::Empty; 3]);
766 assert!(!game.can_undo());
767 }
768
769 #[test]
770 fn plain_mutations_outside_a_gesture_still_get_their_own_undo_step() {
771 let grid = vec![vec![true, true]];
772 let mut game = NonogramGame::from_grid(grid);
773 game.toggle_fill(0, 0);
774 game.toggle_fill(1, 0);
775 assert_eq!(game.status, GameStatus::Won);
776 game.undo();
777 assert_eq!(game.status, GameStatus::Playing);
778 assert_eq!(game.cells[1], CellState::Empty);
779 assert_eq!(game.cells[0], CellState::Filled);
780 }
781
782 #[test]
783 fn reset_keeps_puzzle_clears_progress() {
784 let grid = vec![vec![true]];
785 let mut game = NonogramGame::from_grid(grid);
786 game.toggle_fill(0, 0);
787 assert_eq!(game.status, GameStatus::Won);
788 let row_clues_before = game.row_clues.clone();
789 game.reset();
790 assert_eq!(game.status, GameStatus::Playing);
791 assert!(game.cells.iter().all(|&c| c == CellState::Empty));
792 assert!(!game.can_undo());
793 assert_eq!(game.row_clues, row_clues_before);
794 }
795
796 #[test]
797 fn random_is_deterministic_per_seed() {
798 let a = NonogramGame::random(10, 10, 0.4, 42);
799 let b = NonogramGame::random(10, 10, 0.4, 42);
800 assert_eq!(a.row_clues, b.row_clues);
801 assert_eq!(a.col_clues, b.col_clues);
802 }
803
804 #[test]
805 fn col_satisfied_after_filling_whole_column() {
806 let rows: [u16; 10] = [15, 15, 63, 63, 255, 255, 1023, 1023, 1023, 1023];
809 let grid: Vec<Vec<bool>> = rows
810 .iter()
811 .map(|&row| (0..10).map(|x| (row >> x) & 1 != 0).collect())
812 .collect();
813 let mut game = NonogramGame::from_grid(grid);
814 assert_eq!(game.col_clues[0], vec![10]);
815 for y in 0..10 {
816 game.toggle_fill(0, y);
817 }
818 for y in 0..10 {
819 assert_eq!(game.cells[game.idx(0, y)], CellState::Filled);
820 }
821 assert!(
822 game.col_satisfied(0),
823 "column 0 should be satisfied after filling all 10 of its cells"
824 );
825 }
826
827 #[test]
828 fn alternate_valid_solution_also_wins() {
829 let grid = vec![vec![true, false], vec![false, true]];
833 let mut game = NonogramGame::from_grid(grid);
834 game.toggle_fill(1, 0);
835 game.toggle_fill(0, 1);
836 assert_eq!(game.status, GameStatus::Won);
837 }
838
839 #[test]
840 fn random_avoids_degenerate_puzzles() {
841 let game = NonogramGame::random(4, 4, 0.0, 7);
844 let filled = (0..4)
845 .flat_map(|y| (0..4).map(move |x| (x, y)))
846 .filter(|&(x, y)| game.is_solution_filled(x, y))
847 .count();
848 assert!(filled > 0 && filled < 16);
849 }
850
851 #[test]
852 fn solve_line_determines_overlap() {
853 let line = vec![None; 5];
854 let clue = vec![3];
855 let result = solve_line(&line, &clue);
856 assert_eq!(result, vec![None, None, Some(true), None, None]);
859 }
860
861 #[test]
862 fn solve_line_all_empty_on_zero_clue() {
863 let line = vec![None; 4];
864 let clue = vec![0];
865 let result = solve_line(&line, &clue);
866 assert_eq!(result, vec![Some(false); 4]);
867 }
868
869 #[test]
870 fn solve_line_respects_known_filled_cell() {
871 let line = vec![None, Some(true), None, None, None];
872 let clue = vec![3];
873 let result = solve_line(&line, &clue);
874 assert_eq!(
879 result,
880 vec![None, Some(true), Some(true), None, Some(false)]
881 );
882 }
883
884 #[test]
885 fn solve_line_respects_known_empty_cell() {
886 let line = vec![None, None, Some(false), None, None];
887 let clue = vec![3];
888 let result = solve_line(&line, &clue);
889 assert_eq!(result, line);
900 }
901
902 #[test]
903 fn solve_line_multi_run() {
904 let line = vec![None; 7];
905 let clue = vec![2, 1];
906 let result = solve_line(&line, &clue);
907 assert_eq!(result.len(), 7);
923 }
924
925 #[test]
926 fn is_logically_solvable_returns_true_for_simple_puzzle() {
927 let solution = vec![
928 vec![false, true, false],
929 vec![true, true, true],
930 vec![false, true, false],
931 ];
932 let game = NonogramGame::from_grid(solution);
933 assert!(is_logically_solvable(
934 game.width,
935 game.height,
936 &game.row_clues,
937 &game.col_clues,
938 ));
939 }
940
941 #[test]
942 fn is_logically_solvable_returns_false_for_ambiguous_puzzle() {
943 let solution = vec![vec![true, false], vec![false, true]];
944 let game = NonogramGame::from_grid(solution);
945 assert!(!is_logically_solvable(
946 game.width,
947 game.height,
948 &game.row_clues,
949 &game.col_clues,
950 ));
951 }
952
953 #[test]
954 fn random_logical_is_line_solvable() {
955 let game = NonogramGame::random_logical(8, 8, 0.5, 42);
956 assert!(is_logically_solvable(
957 game.width,
958 game.height,
959 &game.row_clues,
960 &game.col_clues,
961 ));
962 }
963
964 #[test]
965 fn random_logical_rejects_unsolvable_seed() {
966 for seed in 0..10 {
970 let game = NonogramGame::random_logical(6, 6, 0.4, seed);
971 assert!(is_logically_solvable(
972 game.width,
973 game.height,
974 &game.row_clues,
975 &game.col_clues,
976 ));
977 }
978 }
979}