A chalkboard-styled Sudoku game that installs as a Progressive Web App and runs 100% offline after the first load. Ships with 200 preloaded puzzles (50 each of Easy / Medium / Hard / Diabolical), with an optional in-app way to pull more from the public sudoku-exchange-puzzle-bank when you do have a connection.
Disclaimer: This project is 100% "vibe coded": every line was written by an AI coding assistant from conversational prompts, with no professional code review behind it. It works, and it's been tested for basic correctness, but don't expect the kind of scrutiny a hand-reviewed production codebase would get. Bug reports and PRs welcome.
- No monetization - no upfront cost, no subscription, no in-app purchases
- Offline - an internet connection is only needed to install the app and to download more puzzles; everything else works with it off
- No unnecessary permissions - the only thing it ever touches is local storage, and only to save your progress and hold puzzles you download
- No ads
- No notifications
- No popups
- No distraction
- No FOMO, no dark patterns - no streak-shaming, no fake urgency, no manufactured reasons to come back
- No telemetry - nothing is tracked, collected, or sent anywhere
- Fully offline PWA: a service worker precaches the entire app shell (HTML/CSS/JS/icons/puzzle data) on first visit. No CDN, no web fonts, no external runtime dependencies. Installable to the home screen / as a desktop app.
- 200 preloaded puzzles, 50 per difficulty, each solved and verified
ahead of time (see
gen_puzzles.py). - Download more puzzles from GitHub's puzzle bank, straight from the Library screen, while online; puzzles are de-duplicated against what you already have and solved locally in the browser before being saved.
- Notes/pencil-marks, undo, hints, mistake tracking (3-strike), a timer, autosave/resume per puzzle, and simple stats (solved count, streak, best time); all stored locally in IndexedDB. Nothing leaves the device except the optional GitHub puzzle download.
- Ships as a small static site behind nginx in Docker.
docker compose up -d --buildThen open http://localhost:8282. To use a different port, edit the
ports mapping in docker-compose.yml.
Without compose:
docker build -t nosku .
docker run -d -p 8282:8282 --name nosku noskuIt's a static site, and any static file server works:
cd public
python3 -m http.server 8282The service worker requires either
http://localhostorhttps://to register (browsers block SW registration on plainhttp://<lan-ip>), so for real offline testing on another device, put it behind HTTPS or uselocalhostport-forwarding.
Open the site in Chrome/Edge/Safari and use the "Download app" button in Settings, or the browser's own "Install app" / "Add to Home Screen" option. Once installed, the whole game (board, all 200 puzzles, stats) works with airplane mode on, as a real app in your app drawer/home screen, not just a bookmark.
This requires HTTPS. Browsers only offer a true install (and only run
the service worker that makes offline play possible) over a secure context:
https:// or http://localhost. Plain http:// on a LAN IP or hostname
(e.g. http://192.168.1.50:8282 or http://mele:8282) will never show a
real install prompt; "Add to Home Screen" in that case just creates a
plain browser-shortcut icon that still needs the site online to open, with
no offline support. If the "Download app" button says it can't offer a
one-tap install, this is almost always why.
A few practical ways to get HTTPS:
- GitHub Pages (easiest, and free): this repo includes a
.github/workflows/pages.ymlworkflow that publishespublic/straight to GitHub Pages on every push tomain. In the repo's Settings → Pages, set Source to "GitHub Actions" (not "Deploy from a branch", since that option can't point at thepublic/subfolder). Push once, and the app is live athttps://<username>.github.io/<repo>/with a real, trusted certificate, no server of your own required at all. This is a separate deployment from the Docker one; run either, or both. - Tailscale Serve: if you're already using Tailscale,
tailscale serve --bg 8282gives you a realhttps://<device>.<tailnet>.ts.netURL with a trusted certificate, reachable from your phone with zero extra config. - A reverse proxy with a domain: point a domain (even a free DDNS one) at your server and put Caddy or nginx-proxy-manager in front of this container; both provision Let's Encrypt certificates automatically.
- Quick local testing only: on the phone you're testing with, you can
add
http://<your-ip>:8282to Chrome'schrome://flags/#unsafely-treat-insecure-origin-as-secureflag. This tricks that one browser into treating the origin as secure, which is fine for trying things out, but it's a per-device hack, not a real fix, and won't help anyone else who opens the link.
From the Library screen, pick a difficulty tab and tap
"Download 25 more from GitHub". This fetches a random slice of that
difficulty's file directly from
raw.githubusercontent.com/grantm/sudoku-exchange-puzzle-bank (using
HTTP range requests so it never pulls the whole 10–35MB file), solves each
candidate puzzle locally, skips anything you already have, and saves the
rest to IndexedDB. This is the one feature that needs a live connection;
everything else keeps working offline whether or not you ever use it.
public/data/puzzles.json was generated from the same puzzle bank with
gen_puzzles.py: it samples 50 random puzzles per
difficulty from easy.txt / medium.txt / hard.txt / diabolical.txt,
solves each with a backtracking solver, and writes out
{ puzzle, solution, rating, difficulty, id } records. Re-run it against
a fresh clone of the puzzle bank to reshuffle the bundled set.
sudoku-pwa/
├── Dockerfile
├── docker-compose.yml
├── nginx.conf
├── gen_puzzles.py # (re)generates public/data/puzzles.json
└── public/ # everything served by nginx
├── index.html
├── manifest.json
├── service-worker.js
├── css/style.css
├── js/
│ ├── sudoku-engine.js # validation + backtracking solver
│ ├── db.js # IndexedDB wrapper
│ └── app.js # UI + game logic
├── data/puzzles.json # 200 bundled puzzles
└── icons/ # generated PWA icon set
Working on this project? See DEVNOTES.md first, it covers
several non-obvious decisions (especially around the board/number-pad
sizing) that are easy to accidentally undo.
The 200 bundled puzzles, and the optional in-app "download more" puzzles, come from the sudoku-exchange-puzzle-bank dataset by grantm, generated for the Sudoku Exchange website. The puzzles themselves were generated with QQWing Sudoku by Stephen Ostermiller, and graded for difficulty with Sukaku Explainer. Huge thanks to all three projects; this app wouldn't have a puzzle library without them.
This project is licensed under the MIT License.
The bundled puzzle data derives from the sudoku-exchange-puzzle-bank dataset (see Credits above), which is dedicated to the public domain by its author.