Skip to main content

egui_nonogram/
game.rs

1//! Renderer-agnostic nonogram game logic.
2
3/// The player-visible state of a single cell.
4#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
5pub enum CellState {
6    /// Not yet marked by the player.
7    #[default]
8    Empty,
9    /// Marked as part of the picture.
10    Filled,
11    /// Marked as definitely *not* part of the picture. Purely a player aid:
12    /// never required to match the solution, and freely reversible.
13    Crossed,
14}
15
16/// The current status of the game.
17#[derive(Clone, Copy, PartialEq, Eq, Debug)]
18pub enum GameStatus {
19    /// The puzzle is not yet solved.
20    Playing,
21    /// Every row and column satisfies its clue. Nonograms can have more than
22    /// one grid that does this, so this isn't necessarily *the* solution.
23    Won,
24}
25
26/// A single row or column clue: run-lengths of consecutive filled cells,
27/// e.g. `[3, 1]` for a line shaped `###.#`. An all-empty line's clue is
28/// `[0]`, never an empty vec, so it can be compared directly against a
29/// freshly-computed clue from the player's current grid.
30pub type Clue = Vec<u8>;
31
32/// The nonogram game state: board dimensions, the hidden solution, the
33/// player's grid, derived clues, and undo/redo history.
34///
35/// There is no losing state — every action is reversible via
36/// [`Self::undo`]/[`Self::redo`], and the game only ever transitions from
37/// [`GameStatus::Playing`] to [`GameStatus::Won`] (or back, if an undo
38/// reverts a winning move).
39pub struct NonogramGame {
40    /// Board width in cells.
41    pub width: usize,
42    /// Board height in cells.
43    pub height: usize,
44    /// Flat row-major grid of the hidden solution (`solution[y * width + x]`).
45    /// Private: consumers query it via [`Self::is_solution_filled`] rather
46    /// than reading it directly, keeping the door open to a more compact
47    /// representation later without breaking the public API.
48    solution: Vec<bool>,
49    /// Flat row-major grid of the player's current marks (`cells[y * width + x]`).
50    pub cells: Vec<CellState>,
51    /// Clue for each row, top to bottom.
52    pub row_clues: Vec<Clue>,
53    /// Clue for each column, left to right.
54    pub col_clues: Vec<Clue>,
55    /// Current game status.
56    pub status: GameStatus,
57    undo_stack: Vec<Vec<CellState>>,
58    redo_stack: Vec<Vec<CellState>>,
59}
60
61impl NonogramGame {
62    /// Build a puzzle from an explicit solution grid (row-major, `grid[y][x]`).
63    /// Every row must have the same width. Use this for curated/picture puzzles.
64    ///
65    /// # Panics
66    ///
67    /// Panics if `grid` is empty, any row is empty, or rows have differing
68    /// lengths.
69    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    /// Build a procedurally-generated puzzle: each cell is independently
83    /// filled with probability `density`, reproducible via `seed`.
84    /// Regenerates until the result isn't degenerate (not all-empty, not
85    /// all-filled), so the clues are always meaningful. `density` is
86    /// clamped to `0.05..=0.95` — the exact extremes would make a
87    /// non-degenerate result impossible (and the regeneration loop would
88    /// never terminate).
89    ///
90    /// # Panics
91    ///
92    /// Panics if `width` or `height` is zero.
93    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    /// Clear the player's grid and undo/redo history, keeping the same
132    /// puzzle (solution and clues unchanged). To get a *different* puzzle,
133    /// construct a new [`NonogramGame`].
134    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    /// Whether `(x, y)` is part of the hidden solution.
148    pub fn is_solution_filled(&self, x: usize, y: usize) -> bool {
149        self.solution[self.idx(x, y)]
150    }
151
152    /// Toggle a cell between `Filled` and `Empty` (clearing any `Crossed`
153    /// mark in the process). This is the left-click action.
154    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    /// Toggle a cell between `Crossed` and `Empty` (clearing any `Filled`
164    /// mark in the process). This is the right-click action.
165    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    /// Directly clear a cell back to `Empty`.
175    pub fn clear_cell(&mut self, x: usize, y: usize) {
176        self.set_cell(x, y, CellState::Empty);
177    }
178
179    /// Directly set a cell to `Filled`, no toggling. Idempotent — a no-op
180    /// (no undo entry pushed) if the cell is already `Filled`. Intended for
181    /// click-and-drag painting, where the same target state is applied to
182    /// every cell the pointer passes over.
183    pub fn fill(&mut self, x: usize, y: usize) {
184        self.set_cell(x, y, CellState::Filled);
185    }
186
187    /// Directly set a cell to `Crossed`, no toggling. Idempotent, for the
188    /// same click-and-drag painting use case as [`Self::fill`].
189    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    /// Cross out the remaining `Empty` cells of row `y` and column `x` if
206    /// that line's filled cells already match its clue: once satisfied, no
207    /// other cell in the line can ever be filled without breaking the
208    /// match, so the rest are surely empty. Only the line containing the
209    /// just-changed cell can have changed satisfaction.
210    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    /// Cross out every row/column whose clue is `[0]`: on an all-`Empty`
216    /// grid, those lines are already satisfied, but [`Self::set_cell`]'s
217    /// reactive auto-crossing never runs on them since nothing ever
218    /// mutates a cell in an already-solved empty line. Called once at
219    /// construction and on [`Self::reset`] to keep the board consistent
220    /// with the clue dimming, which uses the same satisfaction check.
221    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    /// Whether row `y`'s currently-filled cells already match its clue. Used
253    /// both as a hint for the player (dim/strikethrough the clue) and,
254    /// together with [`Self::col_satisfied`], as the win condition.
255    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    /// Whether column `x`'s currently-filled cells already match its clue.
263    /// See [`Self::row_satisfied`].
264    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    /// Run-length-encode a line of booleans into a clue. An all-`false`
306    /// line yields `[0]`, matching standard nonogram convention and keeping
307    /// this directly comparable to a stored clue with no special-casing.
308    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); // correctly-empty cell, crossed out
382        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); // un-fill the completing cell directly (not undo)
433        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        // Row 0 isn't satisfied yet, so its cells are untouched.
445        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); // required cell, wrongly crossed instead of filled
463        assert_eq!(game.status, GameStatus::Playing);
464        game.toggle_fill(1, 0); // fill the wrong (non-solution) cell too
465        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        // "Stairs" puzzle bit pattern from the doneward catalog: width 10,
510        // rows &[15, 15, 63, 63, 255, 255, 1023, 1023, 1023, 1023].
511        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        // Diagonal solution: row clues [1],[1], col clues [1],[1] — but the
533        // anti-diagonal satisfies the exact same clues, so it's an equally
534        // valid solution and must also count as a win.
535        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        // Density 0.0/1.0 would be degenerate if not corrected for; the
545        // generator must still terminate and produce a non-degenerate grid.
546        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}