Skip to content

feat(game): add nonogram as third daily puzzle type#1

Merged
moyunzero merged 2 commits into
masterfrom
feat/nonogram
May 25, 2026
Merged

feat(game): add nonogram as third daily puzzle type#1
moyunzero merged 2 commits into
masterfrom
feat/nonogram

Conversation

@moyunzero
Copy link
Copy Markdown
Owner

@moyunzero moyunzero commented May 25, 2026

Summary

  • Add 数绘 (nonogram) as the third daily puzzle type alongside sudoku and binary
  • 8×8 engine with 30 curated patterns, mirror transforms, and deterministic daily generation
  • Game UI: tap cycle / long-press clear, completion against hidden solution (no mid-game conflict highlighting)
  • Result screen NonogramRevealCard on win
  • Fix: column clues render top-to-bottom in Picross block order (removed erroneous reverse)
  • Tests: patterns.test.ts (8×8, clue self-consistency, all mirror transforms), NonogramGrid.test.tsx, engine + selector coverage

Test plan

  • npm run typecheck
  • npm test (205 tests)
  • npm run lint
  • Dev panel: force 数绘 → play → complete → result reveal
  • Verify column clues read top-to-bottom (e.g. rocket col 2: 2 above 3)
  • Fresh install / invalid old 10×10 snapshot regenerates today’s puzzle

Notes

  • Old 10×10 nonogram snapshots fail validation and rebuild for the day (expected)
  • .planning/phases/nonogram/PLAN.md updated locally (gitignored)

Made with Cursor

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Nonogram (数绘) as a new daily puzzle game type alongside Sudoku and Binary puzzles.
    • Introduces interactive nonogram grid with clue system for solving picture puzzles.
    • Displays completed artwork reveal card upon puzzle completion.
    • Includes Nonogram game rules and instructions.
  • Tests

    • Added comprehensive test coverage for Nonogram game logic, puzzle generation, and UI components.

Review Change Stack

Introduce 8×8 picross with a 30-pattern library, daily selection, persistence,
in-game grid UI, and result reveal. Fix column clue display order and add pattern
regression tests for clue consistency across mirror transforms.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2448d6f6-92b6-45b1-ad2e-c33777a4dac9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a third puzzle game type, Nonogram (Picross), to the daily puzzle app. It includes type definitions, core grid algorithms, deterministic puzzle generation using 30 curated patterns, daily game integration, persistent storage support, interactive UI components, and comprehensive test coverage across all layers.

Changes

Nonogram Game Type

Layer / File(s) Summary
Type contracts and nonogram spec
lib/puzzles/types.ts, lib/puzzles/nonogram/spec.ts
GameType expanded to 'sudoku' | 'binary' | 'nonogram'. New types NonogramPuzzle, NonogramPlayState, NonogramCellState defined with puzzle metadata, play grid, and cell state unions. Fixed 8×8 spec with cell constants (NONOGRAM_EMPTY: -1, NONOGRAM_FILL: 1, NONOGRAM_CROSS: 0). Type guard isNonogramPuzzle added.
Grid utilities and core algorithms
lib/puzzles/nonogram/grid.ts, lib/puzzles/nonogram/transform.ts, lib/puzzles/nonogram/validate.ts, lib/puzzles/nonogram/clues.ts
Grid creation/cloning/parsing utilities; mirrorX/mirrorY/applyTransform for puzzle variation; isCompleteAndValid, cycleCellValue, clearCell for validation and interaction; computeLineClues and computeClues derive nonogram clue arrays from boolean grids.
Puzzle generation and hashing
lib/puzzles/nonogram/patterns.ts, lib/puzzles/nonogram/hash.ts, lib/puzzles/nonogram/generator.ts
NONOGRAM_PATTERNS array defines 30 curated 8×8 glyphs with IDs and titles. FNV-1a hash with nono- prefix ensures deterministic puzzle identification. generateNonogramPuzzle(seed) selects pattern, applies seeded mirror transforms, computes clues, returns assembled puzzle.
Daily game integration
lib/puzzles/dailySelector.ts, lib/daily/dailyHydrate.ts
Game-type pool includes nonogram; buildNonogramPuzzle helper implements retry/fallback pattern (up to 50 attempts to avoid hash collision, deterministic sub-seed fallback). Daily hydration initializes nonogram playState with empty grid; emptyPlayStateForGameType(gameType) abstracts grid creation per game type.
Storage layer validation and repair
lib/storage/snapshotValidate.ts, lib/storage/snapshotPrep.ts
isValidNonogramPuzzle and isNonogramGrid validate puzzle structure, cell values, and clue/solution dimensions. Snapshot shape, puzzle consistency, and play-state consistency checks extended for nonogram. Repair logic regenerates puzzles from snapshot seed, updates version, ensures playState integrity.
Board interaction and session
hooks/useNonogramBoard.ts, hooks/useGameBoardSession.ts, contexts/DailyGameContext.tsx
useNonogramBoard hook tracks selected cell, computes canComplete, derives localized status hint, provides memoized press/long-press handlers. useGameBoardSession wires nonogram state, includes nonogram in UI flags (showBoardChrome, canComplete, statusHint), returns extended object with isNonogram, nonogramPuzzle, nonogramBoard. DailyGameContext initializes nonogram playState from snapshot or creates empty grid.
Interactive grid and reveal
components/grid/NonogramGrid.tsx, components/game/NonogramGameSection.tsx, components/result/NonogramRevealCard.tsx
NonogramGrid renders 8×8 interactive grid with computed cell dimensions, column/row clues, Pressable cells with empty/fill/cross states, row/column selection highlights, and accessibility labels. NonogramGameSection wraps grid in styled HairlineCard with maxWidth layout. NonogramRevealCard displays animated grid where cells fill with accentSunset color per solution and renders animated title.
App screen integration
app/game.tsx, app/result.tsx, components/dev/DevToolsPanel.tsx, lib/copy/gameRules.ts, constants/dev.ts
GameScreen conditionally renders NonogramGameSection for nonogram sessions. ResultScreen renders NonogramRevealCard on success for nonogram puzzles. DevToolsPanel adds "数绘" button to trigger nonogram regeneration. Game rules object includes nonogram rule set; type labels and dev constants updated.
Comprehensive test coverage
__tests__/lib/puzzles/nonogram/clues.test.ts, __tests__/lib/puzzles/nonogram/generator.test.ts, __tests__/lib/puzzles/nonogram/patterns.test.ts, __tests__/lib/puzzles/nonogram/validate.test.ts, __tests__/components/grid/NonogramGrid.test.tsx, __tests__/lib/puzzles/dailySelector.test.ts
Test clue computation correctness (line/grid clues), pattern dataset validity (30 patterns, unique IDs/titles, transform consistency), generator determinism and payload shape, cell cycling and completion validation, grid component rendering, and daily selector nonogram path.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A rabbit's ode to Nonogram's launch:

Eighty patterns squared, and clues compile,
Grid cells dance through empty-fill-cross file,
Mirrors transform with seeded delight,
Puzzles hashed bright with nono-light! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding nonogram as a third daily puzzle type alongside sudoku and binary.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/nonogram

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
__tests__/lib/puzzles/dailySelector.test.ts (1)

60-68: ⚡ Quick win

Add puzzle-hash coherence assertion for nonogram, consistent with other game types.

This test should also verify that selector-level puzzleHash matches the puzzle payload hash.

Proposed diff
   it('returns real nonogram puzzle when type is nonogram', () => {
     const result = selectDailyGame({
       dateKey: '2026-05-16',
       forceGameType: 'nonogram',
     });
     expect(result.gameType).toBe('nonogram');
-    expect(isNonogramPuzzle(result.puzzle)).toBe(true);
+    expect(isNonogramPuzzle(result.puzzle)).toBe(true);
+    if (isNonogramPuzzle(result.puzzle)) {
+      expect(result.puzzle.puzzleHash).toBe(result.puzzleHash);
+    }
     expect(result.puzzleHash).toMatch(/^nono-/);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/lib/puzzles/dailySelector.test.ts` around lines 60 - 68, The test
for selectDailyGame should also assert that the selector-level puzzleHash equals
the hash of the returned puzzle payload: recompute the puzzle hash from
result.puzzle (using your project’s puzzle hashing utility, e.g.
computePuzzleHash or similar) and add an assertion like
expect(result.puzzleHash).toBe(computePuzzleHash(result.puzzle)); keep the
existing checks (selectDailyGame, isNonogramPuzzle) and add this single
coherence assertion so nonogram hashes are validated the same way as other game
types.
__tests__/lib/puzzles/nonogram/validate.test.ts (1)

30-33: ⚡ Quick win

Make the negative-case assertion independent of a specific cell value.

This case currently assumes the first cell is empty. Force a mismatch from the solution so the test remains stable if pattern/seed internals change.

Proposed diff
   it('is not complete when a fill cell is wrong', () => {
-    const play = createEmptyGrid();
-    play[0]![0] = NONOGRAM_FILL;
+    const play = puzzle.solution.map((row) =>
+      row.map((filled) => (filled ? NONOGRAM_FILL : -1)),
+    );
+    play[0]![0] = puzzle.solution[0]![0] ? -1 : NONOGRAM_FILL;
     expect(isCompleteAndValid(play, puzzle.solution)).toBe(false);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/lib/puzzles/nonogram/validate.test.ts` around lines 30 - 33, Test
currently assumes play[0][0] is empty; instead force a guaranteed mismatch with
the solution by assigning play[0]![0] to a value different from
puzzle.solution[0]![0] (for example, choose between NONOGRAM_FILL and
NONOGRAM_EMPTY based on equality) before asserting isCompleteAndValid(play,
puzzle.solution) is false; update the test that uses createEmptyGrid,
NONOGRAM_FILL, and isCompleteAndValid to compute a value !=
puzzle.solution[0]![0] so the negative-case is stable regardless of pattern/seed
internals.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/grid/NonogramGrid.tsx`:
- Around line 89-92: The computed cellSize can become zero or negative when
gridInner is non-positive; update the calculation in NonogramGrid.tsx so
cellSize is clamped to a minimum of 1 (e.g. compute rawSize =
Math.floor(Math.min(gridInner / cols, 36)) and then set cellSize = Math.max(1,
rawSize)) and keep clueColWidth and clueRowHeight computed from that clamped
cellSize (references: gridInner, cols, cellSize, clueColWidth, clueRowHeight,
maxWidth, clueBand).

In `@components/result/NonogramRevealCard.tsx`:
- Line 18: The computed cellSize in NonogramRevealCard (const cellSize =
Math.floor(size / Math.max(rows, cols))) can become 0 or negative for very small
size values; change the calculation to clamp the result to a minimum of 1 (e.g.,
wrap Math.floor(...) with Math.max(1, ...)) so cellSize is always positive and
cell rendering won't break.

In `@lib/puzzles/nonogram/generator.ts`:
- Line 23: Replace the non-null assertion on
NONOGRAM_PATTERNS[selectPatternIndex(seed)] with an explicit fail-fast guard:
first compute the index via selectPatternIndex(seed), then look up pattern =
NONOGRAM_PATTERNS[index]; if pattern is undefined, throw a clear Error that
includes context (e.g., "No nonogram pattern found", the index, and the seed) so
callers get a diagnostic instead of a silent runtime crash; otherwise continue
using pattern as before.

In `@lib/puzzles/nonogram/validate.ts`:
- Around line 9-25: The function isCompleteAndValid currently assumes playState
matches solution dimensions; add validation at the top to ensure playState is
defined, playState.length === solution.length (rows) and every playState[row] is
an array with length === cols (solution[0].length) before accessing cells; if
any check fails return false (or early-fail) so the non-null assertions around
playState[row]![col]! and comparisons with NONOGRAM_FILL are safe.

In `@lib/storage/snapshotValidate.ts`:
- Around line 3-8: The validation currently rejects the cross-mark cell by
treating any cell that is neither NONOGRAM_EMPTY nor NONOGRAM_FILL as invalid;
update the snapshot validation logic (the function that iterates the grid and
uses NONOGRAM_EMPTY and NONOGRAM_FILL) to treat the cross mark as an allowed
non-filled state: change checks that assert a cell must be exactly
NONOGRAM_EMPTY or NONOGRAM_FILL to instead only reject truly unknown/invalid
values and when counting filled cells only count cells equal to NONOGRAM_FILL
(so cross marks are allowed but not counted as filled). Apply the same change to
the other validation/repair check referenced at the end of the file (the
condition around line 136) so cross-marked cells are accepted across all
nonogram validation paths.

---

Nitpick comments:
In `@__tests__/lib/puzzles/dailySelector.test.ts`:
- Around line 60-68: The test for selectDailyGame should also assert that the
selector-level puzzleHash equals the hash of the returned puzzle payload:
recompute the puzzle hash from result.puzzle (using your project’s puzzle
hashing utility, e.g. computePuzzleHash or similar) and add an assertion like
expect(result.puzzleHash).toBe(computePuzzleHash(result.puzzle)); keep the
existing checks (selectDailyGame, isNonogramPuzzle) and add this single
coherence assertion so nonogram hashes are validated the same way as other game
types.

In `@__tests__/lib/puzzles/nonogram/validate.test.ts`:
- Around line 30-33: Test currently assumes play[0][0] is empty; instead force a
guaranteed mismatch with the solution by assigning play[0]![0] to a value
different from puzzle.solution[0]![0] (for example, choose between NONOGRAM_FILL
and NONOGRAM_EMPTY based on equality) before asserting isCompleteAndValid(play,
puzzle.solution) is false; update the test that uses createEmptyGrid,
NONOGRAM_FILL, and isCompleteAndValid to compute a value !=
puzzle.solution[0]![0] so the negative-case is stable regardless of pattern/seed
internals.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 94761d0a-091f-4b3e-896a-9c6d3f91e8d2

📥 Commits

Reviewing files that changed from the base of the PR and between bb0435f and c9a8fd9.

📒 Files selected for processing (30)
  • __tests__/components/grid/NonogramGrid.test.tsx
  • __tests__/lib/puzzles/dailySelector.test.ts
  • __tests__/lib/puzzles/nonogram/clues.test.ts
  • __tests__/lib/puzzles/nonogram/generator.test.ts
  • __tests__/lib/puzzles/nonogram/patterns.test.ts
  • __tests__/lib/puzzles/nonogram/validate.test.ts
  • app/game.tsx
  • app/result.tsx
  • components/dev/DevToolsPanel.tsx
  • components/game/NonogramGameSection.tsx
  • components/grid/NonogramGrid.tsx
  • components/result/NonogramRevealCard.tsx
  • constants/dev.ts
  • contexts/DailyGameContext.tsx
  • hooks/useGameBoardSession.ts
  • hooks/useNonogramBoard.ts
  • lib/copy/gameRules.ts
  • lib/daily/dailyHydrate.ts
  • lib/puzzles/dailySelector.ts
  • lib/puzzles/nonogram/clues.ts
  • lib/puzzles/nonogram/generator.ts
  • lib/puzzles/nonogram/grid.ts
  • lib/puzzles/nonogram/hash.ts
  • lib/puzzles/nonogram/patterns.ts
  • lib/puzzles/nonogram/spec.ts
  • lib/puzzles/nonogram/transform.ts
  • lib/puzzles/nonogram/validate.ts
  • lib/puzzles/types.ts
  • lib/storage/snapshotPrep.ts
  • lib/storage/snapshotValidate.ts

Comment on lines +89 to +92
const gridInner = maxWidth - clueBand * 14 - 8;
const cellSize = Math.floor(Math.min(gridInner / cols, 36));
const clueColWidth = clueBand * Math.max(12, cellSize * 0.55);
const clueRowHeight = clueBand * Math.max(12, cellSize * 0.55);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp computed cellSize to a positive minimum.

gridInner can become non-positive on narrow layouts, which can yield zero/negative cell dimensions and break rendering. Clamp cellSize to at least 1.

Proposed fix
-  const cellSize = Math.floor(Math.min(gridInner / cols, 36));
+  const cellSize = Math.max(1, Math.floor(Math.min(gridInner / cols, 36)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const gridInner = maxWidth - clueBand * 14 - 8;
const cellSize = Math.floor(Math.min(gridInner / cols, 36));
const clueColWidth = clueBand * Math.max(12, cellSize * 0.55);
const clueRowHeight = clueBand * Math.max(12, cellSize * 0.55);
const gridInner = maxWidth - clueBand * 14 - 8;
const cellSize = Math.max(1, Math.floor(Math.min(gridInner / cols, 36)));
const clueColWidth = clueBand * Math.max(12, cellSize * 0.55);
const clueRowHeight = clueBand * Math.max(12, cellSize * 0.55);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/grid/NonogramGrid.tsx` around lines 89 - 92, The computed cellSize
can become zero or negative when gridInner is non-positive; update the
calculation in NonogramGrid.tsx so cellSize is clamped to a minimum of 1 (e.g.
compute rawSize = Math.floor(Math.min(gridInner / cols, 36)) and then set
cellSize = Math.max(1, rawSize)) and keep clueColWidth and clueRowHeight
computed from that clamped cellSize (references: gridInner, cols, cellSize,
clueColWidth, clueRowHeight, maxWidth, clueBand).

size = 160,
}: NonogramRevealCardProps) {
const { rows, cols, solution, pictureTitle } = puzzle;
const cellSize = Math.floor(size / Math.max(rows, cols));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Ensure reveal cellSize stays positive.

A very small size prop can produce cellSize <= 0, which breaks cell rendering. Clamp to a minimum of 1.

Proposed fix
-  const cellSize = Math.floor(size / Math.max(rows, cols));
+  const cellSize = Math.max(1, Math.floor(size / Math.max(rows, cols)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cellSize = Math.floor(size / Math.max(rows, cols));
const cellSize = Math.max(1, Math.floor(size / Math.max(rows, cols)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/result/NonogramRevealCard.tsx` at line 18, The computed cellSize
in NonogramRevealCard (const cellSize = Math.floor(size / Math.max(rows, cols)))
can become 0 or negative for very small size values; change the calculation to
clamp the result to a minimum of 1 (e.g., wrap Math.floor(...) with Math.max(1,
...)) so cellSize is always positive and cell rendering won't break.

}

export function generateNonogramPuzzle(seed: number): NonogramPuzzle {
const pattern = NONOGRAM_PATTERNS[selectPatternIndex(seed)]!;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the non-null assertion with an explicit fail-fast guard.

Line 23 currently assumes a pattern always exists. If the catalog is ever empty/corrupted, this turns into a runtime crash path with weak diagnostics.

Suggested patch
 function selectPatternIndex(seed: number): number {
+  if (NONOGRAM_PATTERNS.length === 0) {
+    throw new Error('NONOGRAM_PATTERNS must include at least one pattern');
+  }
   const rng = mulberry32(deriveSubSeed(seed, 'nono-pattern'));
   return Math.floor(rng() * NONOGRAM_PATTERNS.length);
 }

 export function generateNonogramPuzzle(seed: number): NonogramPuzzle {
-  const pattern = NONOGRAM_PATTERNS[selectPatternIndex(seed)]!;
+  const pattern = NONOGRAM_PATTERNS[selectPatternIndex(seed)];
+  if (!pattern) {
+    throw new Error('Selected nonogram pattern is undefined');
+  }
   const transform = pickTransform(seed);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/puzzles/nonogram/generator.ts` at line 23, Replace the non-null assertion
on NONOGRAM_PATTERNS[selectPatternIndex(seed)] with an explicit fail-fast guard:
first compute the index via selectPatternIndex(seed), then look up pattern =
NONOGRAM_PATTERNS[index]; if pattern is undefined, throw a clear Error that
includes context (e.g., "No nonogram pattern found", the index, and the seed) so
callers get a diagnostic instead of a silent runtime crash; otherwise continue
using pattern as before.

Comment on lines +9 to +25
export function isCompleteAndValid(
playState: NonogramCell[][],
solution: boolean[][],
): boolean {
const rows = solution.length;
const cols = solution[0]?.length ?? 0;

for (let row = 0; row < rows; row += 1) {
for (let col = 0; col < cols; col += 1) {
const shouldFill = solution[row]![col]!;
const cell = playState[row]![col]!;
if (shouldFill && cell !== NONOGRAM_FILL) return false;
if (!shouldFill && cell === NONOGRAM_FILL) return false;
}
}
return true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add dimension validation before accessing cells.

The function assumes playState dimensions match solution dimensions but doesn't validate this. If dimensions mismatch, the non-null assertions on lines 18-19 could access undefined, leading to incorrect validation results.

🛡️ Proposed fix to add dimension check
 export function isCompleteAndValid(
   playState: NonogramCell[][],
   solution: boolean[][],
 ): boolean {
   const rows = solution.length;
   const cols = solution[0]?.length ?? 0;
+
+  if (playState.length !== rows) return false;
+  if (playState[0]?.length !== cols) return false;

   for (let row = 0; row < rows; row += 1) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/puzzles/nonogram/validate.ts` around lines 9 - 25, The function
isCompleteAndValid currently assumes playState matches solution dimensions; add
validation at the top to ensure playState is defined, playState.length ===
solution.length (rows) and every playState[row] is an array with length === cols
(solution[0].length) before accessing cells; if any check fails return false (or
early-fail) so the non-null assertions around playState[row]![col]! and
comparisons with NONOGRAM_FILL are safe.

Comment on lines +3 to +8
import {
NONOGRAM_COLS,
NONOGRAM_EMPTY,
NONOGRAM_FILL,
NONOGRAM_ROWS,
} from '../puzzles/nonogram/spec';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Allow cross-marked cells in nonogram play-state validation.

Line 40–43 currently rejects the cross state, so valid boards with crosses can be treated as inconsistent and get reset in repair paths.

💡 Proposed fix
 import {
   NONOGRAM_COLS,
+  NONOGRAM_CROSS,
   NONOGRAM_EMPTY,
   NONOGRAM_FILL,
   NONOGRAM_ROWS,
 } from '../puzzles/nonogram/spec';
@@
       (cell) =>
         cell === NONOGRAM_EMPTY ||
-        cell === 0 ||
+        cell === NONOGRAM_CROSS ||
         cell === NONOGRAM_FILL,
     ),
   );
 }

Also applies to: 35-45, 136-136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/storage/snapshotValidate.ts` around lines 3 - 8, The validation currently
rejects the cross-mark cell by treating any cell that is neither NONOGRAM_EMPTY
nor NONOGRAM_FILL as invalid; update the snapshot validation logic (the function
that iterates the grid and uses NONOGRAM_EMPTY and NONOGRAM_FILL) to treat the
cross mark as an allowed non-filled state: change checks that assert a cell must
be exactly NONOGRAM_EMPTY or NONOGRAM_FILL to instead only reject truly
unknown/invalid values and when counting filled cells only count cells equal to
NONOGRAM_FILL (so cross marks are allowed but not counted as filled). Apply the
same change to the other validation/repair check referenced at the end of the
file (the condition around line 136) so cross-marked cells are accepted across
all nonogram validation paths.

Regenerate package-lock.json with npm 10 so @emnapi/core and @emnapi/runtime are fully locked for wasm32-wasi transitive deps used by npm ci on Ubuntu.

Co-authored-by: Cursor <cursoragent@cursor.com>
@moyunzero moyunzero merged commit c11f9af into master May 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant