Live app: solitaire.puter.com
Klondike solitaire, built with Vite and plain JavaScript (ES modules, no framework). The game menu in the header picks draw-one or draw-three Klondike, either with Vegas scoring (a $52 ante, $5 per card played home, one pass through the deck when drawing one and three when drawing three, and a running bank across games), or Spider with one, two, or four suits.
Each game lives behind the same rules adapter (src/game/modes.js): board,
drag, and the save/stats plumbing call it instead of a specific game's
rules. Klondike's rules are in src/game/state.js and rules.js, Spider's
in src/game/spider.js. Desktop windows get realistic card art via a vendored SVG deck;
small screens fall back to plain top-left rank-and-suit cards. Games and
stats sync to Puter cloud storage for a signed-in player; the header ends
with puter.js's account menu (<puter-user-menu>: sign in and out, and the
Apps launcher on puter.com) wherever the loaded SDK provides it.
npm ci
npm run build
This produces static assets in dist/. Serve that directory with any static
file server. The repo root is not directly servable — index.html loads
/src/main.js as an ES module, which starts with CSS imports a browser
cannot resolve without Vite's dev server or a build step.
npm run dev
Starts the Vite dev server with hot module reload.
npm test
Runs the Vitest suite (npm run test:watch for watch mode). Coverage is
src/game/** (pure game rules) plus the save/stats sync layer in
src/services/**; rendering and drag-and-drop are verified by playing the
game rather than with DOM tests.
src/vendor/cardmeister.js is a vendored public-domain (Unlicense) SVG
playing-card deck — see src/vendor/README.md for licensing and integration
notes.
The game is installable and playable offline, following the structure of
the sibling checkers project (site.webmanifest, a full icon set, social
preview and search metadata) plus one deliberate extension: an offline
service worker.
public/sw.js is registered from src/main.js, production builds only
(import.meta.env.PROD) — a service worker on the dev server would
intercept fetches for files Vite hasn't rebuilt yet, turning every
subsequent change into a stale-cache mystery. It uses runtime caching, not
a precache manifest: Vite content-hashes every asset filename on each
build, so a hand-written precache list would go stale the moment a new
build shipped.
- Only same-origin
GETrequests are ever touched. Cross-origin requests — notablyjs.puter.com— are never intercepted or cached, so a signed-in session is never served stale or offline from here. - Navigations (loading
/) try the network first and fall back to whatever shell was cached on a previous visit, so a cold offline launch still boots. - Everything else (hashed JS/CSS bundles, icons, the vendored deck) is served cache-first and populated lazily as it's requested.
- Busting the cache: bump the
CACHEconstant at the top ofpublic/sw.js(e.g.solitaire-v1→solitaire-v2). Onactivate, every cache whose name isn't the current one is deleted. For code deploys this is belt-and-braces, not load-bearing — a new build'sindex.htmlis always fetched fresh from the network on the next online visit and references new hashed filenames the cache doesn't have yet, so the worst case without a bump is cache bloat (orphaned old-hash entries), not staleness. It is load-bearing, though, whenever an unhashedpublic/asset changes content without changing its filename —site.webmanifestor an icon, say — sincecacheFirstwill keep serving a returning visitor's (or installed PWA user's) old copy of that exact filename indefinitely until the bump forces a clean slate.
playwright, sharp, and png-to-ico are deliberately not project
dependencies — they're only needed to regenerate images, so they're
installed with --no-save and never touch package.json or
package-lock.json.
The social preview and the store screenshots drive a real browser against
the running dev server, so the vendored deck (src/vendor/cardmeister.js)
renders the actual card art, not a mockup. The icon set needs neither a
browser nor the dev server — its source is a file.
# A later `npm i --no-save …` prunes the earlier --no-save installs, so ask
# for everything you'll need in one go.
npm i --no-save sharp png-to-ico playwright
npx playwright install chromium
node tools/render-icons.mjs # public/favicon.ico, favicon-*.png, icon-*.png, icon-maskable-512.png, apple-touch-icon.png
npm run dev # serves the dev server the two shots below are taken of
node tools/render-og.mjs # public/og-image.png (1200x630)
node tools/store-screenshots.mjs # store/screenshots/ — 4 scenes x 3 sizesChanging the icon touches all three: tools/og-image.html puts
icon-128.png in the social preview's brand badge, and the app's own header
shows it (index.html's .brand .mark), so it appears in every store
screenshot too.
Icons (icon.png + tools/render-icons.mjs): icon.png at the repo
root is the master — a 1178x1178 RGBA tile, an Ace of Spades over a red card
back on felt green, with a squircle silhouette and transparent corners. Keep
it at least 512px: every emitted file is a sharp lanczos3 downsample of it
and never an upscale.
favicon-16x16,favicon-32x32,icon-128,icon-192,icon-512are straight downsamples with the transparent corners intact, so the tile's own silhouette shows against whatever a tab strip or title bar paints behind it.favicon.icois assembled from 16/32/48 renders withpng-to-ico; the 48px render isn't kept as a standalone file.apple-touch-icon.png(180) is flattened onto felt — iOS paints black behind any alpha and then applies a squircle mask squarer than this master's corners, so untouched transparent corners would survive the mask as black wedges.icon-maskable-512.pngis the manifest'spurpose: "maskable"icon: the tile inset to the middle 80% over full-bleed felt, so a platform's circle or teardrop crop lands on felt instead of slicing the ace.
The two full-bleed backgrounds are a vertical gradient sampled from the
master's own top and bottom edges (FELT_TOP/FELT_BOTTOM in the script),
so the fill continues the tile's gradient rather than butting a flat colour
against it. Re-sample those two constants if the artwork is ever replaced.
Social preview (tools/og-image.html + tools/render-og.mjs): a real
1200x630 page in the app's own felt-green and system-font look, showing an
actual in-progress Klondike board — stock, waste, two started foundations,
a played-into tableau — rendered with the same <playing-card> element the
game itself uses, not an empty table. Facebook, LinkedIn, and Slack cache
share images per URL, so after changing the picture either re-scrape it in
their debuggers or bump the filename.
App store screenshots (tools/store-screenshots.mjs, see STORE_LISTING.md):
four scenes — a fresh deal, a mid-game board, the win cascade mid-flight, and
the same board narrow enough to trigger the plain phone card face — each
rendered at 1920x1080, 1024x768, and 1080x2280 into store/screenshots/.
Every board is staged by writing a real serialize.js save into
localStorage['solitaire.save.v1'] and letting the app restore it, exactly
as a resumed game would, rather than drawn by hand; see the tool's own header
comment for the full staging rationale, including why the win-cascade scene
is the one exception (its pre-win position is hand-arranged, not autoplayed)
and why that scene's exact composition varies between runs.
This repository, including all its contents, sub-projects, modules, and components, is licensed under MIT unless explicitly stated otherwise. Third-party libraries included in this repository may be subject to their own licenses.