← All games for the curious and the sceptical

Under the hood

Ten games, one leaderboard, zero dependencies. No framework, no build step, no bundler, no tracking pixel, no cookie banner. What you see in view-source is the entire machine. These are the engineering notes.

The philosophy

  • Vanilla everything. The whole arcade is hand-written HTML, CSS and JavaScript. If a game needs physics, we write the physics. If it needs charts, we draw the charts.
  • View-source is a feature. Every file is commented like someone will read it — because the target audience for this page will.
  • Deterministic where it matters. Markets and datasets run on seeded RNG, so they're reproducible, testable, and fair.
  • Graceful degradation. If the leaderboard backend is down, every game still works and personal bests still save locally.

Performance budget

A game page loads, first visit, roughly:

AssetSizeNotes
HTML page4–11 KBSemantic, no div soup
style.css (shared)22 KBOne stylesheet for the whole site
Game JS7–14 KBUnminified, commented
Shared systems (seed, audio, achievements, stats, share)22 KBOne download, cached after the first page
Fonts (3× woff2)52 KBSelf-hosted, cached after first page
Total~100 KBZero requests to third parties

No framework means no 200 KB runtime before the first pixel. The heaviest thing on this site is the fonts, and they're optional.

Budget ceilings (flagship)

  • Games: 2 MB ceiling per game page. The flagship Warehouse Siege ships ~150 KB total (JS + SFX + canvas art) — a game page must never approach this ceiling; it exists as a hard guardrail, not a target.
  • Guide / content pages: 3 MB ceiling. Guide artwork lazy-loads below the fold; the hero image is the only above-the-fold art.
  • First paint: fast on mid-range Android hardware over 4G — no render-blocking JS, static canvas pre-rendered, zero third-party requests, always.
  • Ceilings are not targets. Every kilobyte over ~100 KB needs a reason written down.

Architecture

/ ├── index.html homepage: 10 game cards + hall of fame + daily strip ├── lore.html the lorebook ├── dev.html you are here ├── trophies.html trophy room: achievements + cross-game stats ├── sw.js service worker: offline-first PWA ├── manifest.webmanifest install metadata ├── 404.html ├── assets/ │ ├── style.css one stylesheet, whole site │ ├── icon.svg PWA icon (vector, 540 bytes) │ ├── fonts/ self-hosted woff2 (Archivo Black, Space Mono) │ └── js/ │ ├── manifest.js ★ single source of truth: the 10 games │ ├── site.js builds nav + footers from the manifest, │ │ barcodes, SW registration, │ │ footer sound toggle, window.PA debug API │ ├── seed.js ?seed= / ?daily=1 parsing, mulberry32, │ │ challenge links, calendar daily seeds │ ├── audio.js WebAudio synth SFX — zero audio assets │ ├── achieve.js event-sourced achievement engine + registry │ ├── stats.js cross-game plays / streaks / per-game bests │ ├── share.js canvas score cards + challenge-link rows │ ├── highscores.js leaderboard module (Supabase REST) │ ├── hs-config.js project URL + anon key (public by design) │ └── game-*.js one file per game: sim core + DOM shell └── games/ └── *.html one page per game, same chrome

Adding game 11 is a one-line manifest entry plus two new files. The nav, footers, sitemap and debug API pick it up automatically — no other file needs editing.

The interesting bits, per game

Stack the Deal — slab slicing

The whole physics model is one pure function. Given the slab below and the dropped slab as centre-x + width, the overlap is the new slab — a clean miss is null:

// assets/js/game-stack-the-deal.js function slice(prev, cur) { var l = Math.max(prev.x - prev.w / 2, cur.x - cur.w / 2); var r = Math.min(prev.x + prev.w / 2, cur.x + cur.w / 2); if (r - l <= 4) return null; // clean miss if (Math.abs(cur.x - prev.x) <= 6) return { x: prev.x, w: cur.w, perfect: true }; return { x: (l + r) / 2, w: r - l, perfect: false }; }

Eneloop Exchange — a seeded market

The 90-candle day is geometric Brownian motion with regime shifts and scheduled event shocks, built up front from a mulberry32 seed derived from the calendar date. Same date, same chart, for every player on Earth:

// assets/js/game-eneloop-exchange.js function dailySeed(y, m, d) { return (y * 10000 + m * 100 + d) >>> 0; } var ret = (drift - vol * vol / 2) + vol * gaussian(rng) + shock; var close = Math.max(1.0, open * Math.exp(ret));

Because the series is deterministic per seed, the leaderboard compares trading skill, not luck — and the market is unit-testable in Node.

Sockpuppet Sniffer — synthetic data with ground truth

The ten suspects are generated, not written: sockpuppets get lockstep burst hours shared with their burst group, template-parrot comment similarity (0.87–0.99), and young hyperactive accounts. The briefing teaches exactly the heuristics the generator is tuned against, and a reference classifier ships in the sim core:

// assets/js/game-sockpuppet-sniffer.js function ruleClassify(s) { var f = featuresOf(s), votes = 0; if (f.burstHours >= 3) votes++; if (f.meanSim >= 0.8) votes++; if (f.postsPerDay >= 12 && f.ageDays <= 30) votes++; return votes >= 2 ? 'sock' : 'legit'; }

Players are scored with the same confusion matrix as a real classifier evaluation: precision, recall, F1.

Warehouse Siege — a real-time tower defense

Six tower archetypes (slow / damage / stun / splash / taunt / aura), three upgrade tiers, seven enemy types with armor and aura mechanics, an economy with wave bonuses and 5% interest, and 20 waves of deterministic composition — same ?seed=, same siege:

// assets/js/game-warehouse-siege.js function waveComp(seed, wave) { // deterministic per (seed, wave) var rng = mulberry32(seed ^ (wave * 7919)); ... pickWeighted budget spend, then a seeded shuffle ... } function targetFirst(tx, ty, range, enemies) { // furthest-along-path in range function stepEnemy(e, dt, wps, now, speedMul) { // stun / slow / taunt aware

The sim core is pure and fully unit-tested in Node: wave determinism, HP scaling, armor halves price-tower damage on clippers, Karen auras slow nearby towers, the honeypot diverts shoppers off-path. The DOM layer is just rendering and input — 1×/2×/3× speed, pause on tab-hide, placement ghosts with range previews.

Canvas, done properly

Every canvas game scales for device pixel ratio (crisp on retina, capped at 2× for perf), clamps the frame delta so tab-switching can't teleport physics, and pauses on visibilitychange:

var dpr = Math.min(2, window.devicePixelRatio || 1); canvas.width = W * dpr; canvas.height = H * dpr; ctx.scale(dpr, dpr); var dt = Math.min(0.05, (ts - lastTs) / 1000); // clamp

The leaderboard

Scores go to a free Supabase project through the public REST API. The anon key in hs-config.js is public by design — row-level security on the scores table permits only SELECT and INSERT. There is nothing to steal: no accounts, no emails, no sessions, just handles and numbers.

// assets/js/highscores.js — the whole auth model function enabled() { return !!(CFG && CFG.url && CFG.anon); } // if the backend is unreachable, mount() renders the offline panel // and the per-browser personal best carries on working. Scores // submitted while offline queue in localStorage and sync later. // ?daily=1 games post to a day-keyed daily board.

The debug API

Open devtools on any page. PA.help() prints the tour:

HandleWhat it does
PA.versionArcade build version
PA.manifestThe 10-game manifest (slug, title, short, num)
PA.games[slug].simPure sim core: seeded RNG, market builder, dataset generator, classifiers — everything unit-testable, no DOM required
PA.resetBests()Wipe local personal bests
Seed.fromURL() / Seed.todaySeed()Challenge-seed parsing; calendar daily seed
SFX.play(name) / SFX.toggle()Synthesized sound effects; SFX.sounds lists all 17
ACH.unlock(id) / ACH.defsAchievement engine; the full 29-trophy registry
ArcadeStats.summary()Cross-game plays, streaks, per-game bests
Share.card(opts)Render a 1200×630 score-card PNG

Every game's sim core is registered before any DOM code runs, which means the interesting logic can be required and tested in Node with a stubbed document. That's not an accident — it's how the games were verified.

Seeded challenges — same run, anywhere

Every seeded game reads its seed from the URL. ?seed=12345 plays an exact shareable run; ?daily=1 plays today's calendar seed (YYYYMMDD), identical for every player on Earth. The parsing, the mulberry32 PRNG and the link builder live in one module:

// assets/js/seed.js function todaySeed() { var d = new Date(); return ((d.getFullYear() * 10000) + ((d.getMonth() + 1) * 100) + d.getDate()) >>> 0; } function challengeLink(slug, seed) { return '/games/' + slug + '.html?seed=' + ((seed >>> 0).toString(10)); }

After a run, the game-over panel offers Copy challenge link — send it to a mate and you're both trading the same ENLP chart or sniffing the same suspects. Deterministic content turns a single-player page into a fair multiplayer contest with no server.

Synthesized sound — no audio files

Seventeen sound effects, zero audio assets. assets/js/audio.js builds every sound from raw WebAudio oscillators at play time — enveloped square/triangle/sine tones and tiny arpeggios:

// assets/js/audio.js perfect: function () { seq([[523, 0.1], [659, 0.1], [784, 0.16]], 'triangle', 0.5); }, miss: function () { tone(300, 0, 0.3, 'sawtooth', 0.4, 70); },

The AudioContext is created lazily on the first user gesture (autoplay policy), the mute flag persists in localStorage (pa_mute), and there's a sound toggle in the footer of every page. SFX.sounds lists the registry for the curious.

Achievements, stats and the trophy room

Achievements are event-sourced: game code calls ACH.unlock('stack-ten') at the exact moment it happens. The engine (assets/js/achieve.js) holds the registry (29 trophies across all ten games), persists unlocks as {id: "YYYY-MM-DD"} in localStorage, fires a toast, and plays the unlock arpeggio. Cross-game stats — plays, per-game bests, days played, consecutive-day streaks — live in assets/js/stats.js, recorded by a one-liner at every game-over. The Trophy Room (trophies.html) renders both with zero backend.

// one line at game-over, in every game if (window.ArcadeStats) ArcadeStats.record('stack-the-deal', score); if (window.ACH) ACH.unlock('stack-ten'); // when it happens

Shareable score cards

assets/js/share.js draws a 1200×630 receipt-style PNG on a canvas — score, rank, run stats, a seeded barcode — and triggers a download. No server, no html2canvas, no screenshot service: Share.card({title, score, lines, seed}) is about 60 lines.

Offline-first PWA

sw.js caches every page, script, stylesheet and font on install (cache-first for same-origin GETs, stale cache versioned by a VERSION constant), so the whole arcade works on a plane. manifest.webmanifest makes it installable; the icon is a 540-byte SVG. Total added weight for the entire "advanced features" layer — seeds, synth audio, achievements, stats, share cards, PWA: ~22 KB of JavaScript.

Steal this

The patterns are free to take: manifest-driven nav, seeded daily content, sim-core-before-DOM, graceful-degradation leaderboards, DPR-aware canvas. The jokes are ours. The engineering is yours if you want it.