Sudoku: Constraint Solving and Offline Play

A bilingual Sudoku PWA with local puzzle generation, persistent progress, and offline play.

Sudoku showing an English desktop game with the board and number controls arranged side by side
The board opens ready to play, with number entry and the most frequent actions close at hand.

Open directly into a game

I designed and built Sudoku around a simple requirement: opening the website should open a playable board. The game generates its own puzzles, remembers progress, and works across phones, tablets, and desktop browsers. English and Chinese presentation, installation, and offline play are part of that same experience.

The interface gives the board most of the space. Frequent controls remain nearby, while saved puzzles, statistics, and backup tools open in compact panels. Dieter Rams’s design principles informed this emphasis on useful controls and restrained presentation.

I used native JavaScript and CSS, with Vite for building and Vercel for static hosting. The engineering work connects puzzle correctness with predictable editing, persistent progress, and interruptions that an ordinary game session can encounter.

Play Sudoku


Separate an entry from a commitment

Newly entered numbers appear in blue. Submission checks filled, editable cells: correct values become bold black numbers and lock in place, while incorrect values receive an underline and error feedback. Empty cells are ignored, and mistakes have no life limit. Completing a board requires an explicit submission.

Notes occupy a small three by three arrangement inside each cell. Selecting a number highlights matching entries and candidate notes. Confirming a value removes that candidate from notes in its row, column, and box. Speculative entries leave those notes intact, preserving the player’s reasoning until a value has been checked.

Ben Shneiderman’s research on direct manipulation provided a reference for visible objects, incremental actions, and immediate feedback. I applied those ideas to selection, entry, and checking, with feedback kept beside the board and focus returning to the affected cell after a single-cell undo.


Make undo preserve confirmed progress

Confirmation changes the meaning of history. Suppose a player enters one correct number and one incorrect number, then submits. Undo should remove the latest editable entry in one action while preserving the confirmed number. An extra step that only clears error styling would make the control harder to predict.

I built history normalization around the current confirmed state. Earlier snapshots retain locked numbers, remove candidates excluded by those numbers, and skip differences that affect only feedback. Meaningful edits clear the redo branch; actions that leave the board unchanged preserve it.

Answer assistance follows the same rules. Revealing one cell inserts an editable value that can be undone and still requires submission. Viewing the whole answer opens a read-only board; returning restores the original entries, notes, selection, and note mode.

Using either assistance feature marks an unfinished run as assisted. That flag survives undo, and assisted runs are excluded from completion statistics and time records. The timer pauses during panels, background activity, manual pause, generation, and full-answer inspection.


Generate puzzles with exactly one solution

The engine stores the board as 81 values. Nine-bit masks track occupied digits in each row, column, and box. Bit operations combine those masks to calculate a cell’s legal candidates.

The solver uses backtracking with minimum remaining values, selecting the empty cell with the fewest candidates. Randomized candidate order produces varied completed grids. Generation then removes clues in a shuffled sequence, retaining a removal only when the puzzle still has one solution. Counting stops when a second solution is found, avoiding further work on an already ambiguous board.

Final validation checks the complete answer, agreement between clues and answer, and uniqueness of the remaining puzzle. Four difficulty tiers target 45, 37, 30, and 25 clues, with uniqueness taking priority when additional clues must remain. The tiers are defined by clue count.

New-game generation normally runs in a Web Worker. The application validates returned data before accepting it, and worker errors, timeouts, or invalid output fall back to local generation. Generation and validation remain available offline.


Adapt the controls to the available space

Desktop layouts place the controls beside the board. On phones in portrait orientation, controls move underneath it, with digits 1 through 9 aligned in a single row with the board’s columns. Landscape and short-screen layouts account for the available height, while safe-area insets leave room for cutouts and home indicators.

The board exposes grid semantics and cell descriptions covering position, value, and state. Keyboard navigation, visible focus, common undo and redo shortcuts, reduced-motion preferences, and forced-color presentation support different ways of playing. Underlines accompany error colors, and note mode has explicit state cues.

English and Chinese dictionaries ship with the application. Switching languages preserves the board and edit history, updates interface and accessibility labels, and works offline. Language preference is stored separately from game progress.

Sudoku's Chinese mobile interface with the complete board and a single row of number keys aligned with its columns
The Chinese phone layout keeps the board and number entry in one compact workspace.

Protect progress before writing

Progress persists in localStorage using a versioned format. Restoration validates values, notes, confirmation flags, edit histories, and the relationship between clues and solution. Invalid existing data remains available for recovery export, and storage failures produce a visible message.

Backups include current progress, notes, saved puzzles, and records. Import validates that content before replacement. Ordinary play requires no account or application database; moving progress between browsers or devices uses JSON export and import.

Multiple windows require an additional check. Storage events arrive asynchronously, so a window can attempt a save before learning that another window changed the board. Before writing, the application rereads stored progress. Compatible timer and bookmark changes can merge, while a detected board conflict requires the stale window to load the newer state.

The application saves the previous puzzle before waiting for new generation. Late worker results and file reads also respect a pending conflict prompt, preventing an earlier operation from dismissing the decision to restore newer progress.

Sudoku's Saved puzzles panel showing a saved board thumbnail, progress, and backup import and export controls
The saved puzzles panel shows board previews, progress, and backup controls.

Cache a complete application version

A custom Vite plugin hashes the production build and generates a service-worker cache manifest. Installation fetches the complete resource set, including the puzzle worker. If a fetch fails, the incomplete cache is discarded.

After successful online caching, the application can reopen, resume a board, and generate puzzles without a connection. Updates wait for existing game windows to close before activating, allowing an open session to keep its application version. The new worker then removes older application caches.

Fingerprinting also separates asset lifetimes. Built assets use immutable caching, while the application shell and service worker revalidate. This keeps delivery aligned with the versioned offline resources.


Verify behavior beyond a solved board

I validated generated puzzles with an independent Algorithm X solver, checking Sudoku constraints, clue consistency, and solution uniqueness.

Unit and browser tests also cover edit sequences, confirmed values, assistance, storage validation, language changes, timing, worker failures, offline use, and races between windows. These transitions matter because a valid puzzle can still be undermined by an incorrect restore or an undo that changes committed progress.

Performance work targeted repeated processing during interaction. Internally created, deeply frozen history snapshots and validated saved objects can be reused. External mutable data is checked again, and rendering updates changed text, attributes, classes, and notes in the existing DOM. Meaningful edits retain immediate persistence.

A local Chromium benchmark used four-times CPU throttling, a 390 × 844 viewport, 100 saved expert puzzles, and 200 history states. Each action was measured 30 times with immediate persistence. Median synchronous entry processing fell from 188.3 ms to 130.8 ms; redo fell from 183.2 ms to 98.5 ms. These measurements describe interaction processing under that workload.

An 81-cell game gave me a concentrated way to connect constraint solving with interface and state design. The result supports a complete session: start a puzzle, try an idea, confirm progress, revise an entry, and return later to continue.

Play Sudoku