Happy Games: 24 browser games with zero frameworks, one deploy

2026-11-05 · 40 min · Stack Frame

Don't look for a game — just start one. A browser arcade in plain HTML, CSS and JavaScript: 24 games, zero frameworks, zero build step, zero dependencies. AI ranging from pattern scoring to WebAssembly, multiplayer over D1 and polling, narration streamed from an LLM, all deployed to Cloudflare Pages in one command.

Play: happygames.pages.dev · Source: github.com/zhoujungis/happy_games

Introduction: why another "mini game collection"

The web does not lack game portals; it lacks a sample project that can be read end to end and reproduced in one click. Happy Games is positioned explicitly as:

  1. A teaching sample — every folder runs on its own, with no "install 300 MB of node_modules, then understand five layers of abstraction" tax;
  2. Real technical depth — under the zero-framework constraint it still does AI (from heuristics to a WASM engine), realtime multiplayer, streamed narration, accounts and end-to-end tests;
  3. Deployable — the whole site is static files plus Cloudflare Pages Functions and D1, with no containers and no servers; one wrangler command ships it.

Read this if you want a starting scaffold for a browser game, a real case of organising a large front-end without a framework, a minimal full stack on the Cloudflare stack (Pages / Functions / D1), or simply ten minutes of play.

1. Thirty-second overview

DimensionKey fact
Games24 across four sections: board games 6 / puzzles 6 / casual 6 / Chinese history 6
ConstraintsNative HTML / CSS / JS (ES Modules), no framework, no build, no dependencies; double-click an HTML file to run locally
AIPattern scoring (gomoku) → alpha-beta pruning (Chinese chess) → GNU Go compiled to WebAssembly (Go) → three difficulty tiers (jungle chess / reversi)
MultiplayerAeroplane chess for 2–4 players in real time, rooms with passwords, D1 state, polling sync, AI filling empty seats
NarrationLife simulation and the history brawl call OpenRouter; the server proxies SSE to the browser
BackendCloudflare Pages Functions (file path = route) plus D1 (SQLite in the cloud)
Testsnode --test: 25 files, 288 cases (pure functions and engines); Playwright smoke tests on desktop and mobile
Deploynpm run deploy → wrangler pages deploy .; layered caching configured in _headers
Design rulesSelf-contained folders, pure-function isolation, a data-driven home page, offline-capable

The four sections

SectionThemeSix games
Board gamesStrategy on a small boardGomoku, Chinese chess, Go (GNU Go WASM), jungle chess, reversi, aeroplane chess (multiplayer)
PuzzlesLogic and deductionRule blocks, light refraction, sudoku, maze escape, echo walker, sokoban
CasualPick up and playSpark survivors, life simulation (AI narration), flappy bird, hiking sim, jump jump, stack tower
Chinese historyHistorical figures and institutionsSeven days of decree, Chinese merchant, Tang magistrate, guess the emperor, history brawl (AI judge), imperial exam

2. All 24 games at a glance

Board games

GameOne-line ruleHard part
Gomoku15×15 against the computerAI scores eight pattern weights (five in a row 100000, open four 10000, four-three 1000 …); three difficulties map to different search depths
Chinese chessHuman vs machinePort of the open-source itlwei/Chess engine: move generation with piece rules, nine piece-square value tables, alpha-beta search; the server also offers a GLM-4.7-Flash move proxy as a fallback
GoHuman vs machineGNU Go compiled to WebAssembly (~8.7 MB), running in a Web Worker with Komi 7.5; the main thread talks to it over {cmd:'init'/'play'/'genmove'}, with colour normalisation and a crash move log
Jungle chessRiver and trap rulesElephant beats lion, rat beats elephant, rivers and traps; three AI tiers; animal emoji pieces
ReversiEight-direction flippingThree AI tiers plus undo
Aeroplane chessThe only multiplayer game2–4 players in real time, classic roll/jump/fly rules, AI filling empty seats so a game can start any time

Puzzles

GameOne-line ruleHighlight
Rule blocksPush words to rewrite rules ("wall is stop", "water is sink")Inspired by Baba Is You; live rule panel, multiple levels, undo and reset; the goal is to make any "you" meet any "win"
Light refractionRotate mirrors and splitters to light crystalsDark grid, 45° rotation, a physics puzzle built on laser splitting
SudokuClassic 9×9Three difficulties, timed challenge, pencil marks, conflict highlighting
Maze escapeTimed traversal of a random mazeHigher difficulty means larger, twistier mazes; a new map every round
Echo walkerEvery six steps, your old path becomes an echo chasing youShadow mechanic: collect seals while avoiding your past self; almost no tolerance for a bad route
Sokoban15 hand-made levelsGradual difficulty, warm retro floor palette

Casual

GameOne-line ruleHighlight
Spark survivorsVampire-Survivors-likeAuto-firing, experience drops, three-choice upgrades (fire rate, projectiles, armour …); the build decides how many waves you survive
Life simulationText life simAllocate talents (intellect, physique, fortune …); the AI streams six chapters from childhood to a final verdict, ending with a 0–100 life score
Flappy birdTap to fly through pipesBlue sky, green grass, classic feel
Hiking simThree real routes: Aotai traverse, Gongga loop, Wusun trailTerrain, supplies, altitude and weather — a hiking simulator in the browser
Jump jumpHold to charge, release to jumpPerfect centre landings give combo bonuses; the subtle physics is the point
Stack towerDrop swinging blocks preciselyBetter alignment means a taller tower; overhang trimming and rising speed

Chinese history

GameOne-line ruleHighlight
Seven days of decreeComplete the imperial decree in seven daysThree actions per day across seven resources — silver, grain, public support, order, gentry, reputation, intelligence; report after three actions, settle on day seven
Chinese merchantTrade across famous citiesCompete for wealth with three AI merchants; prices and rival strategies shift
Tang magistrateRun a county for twelve seasonsFarms, markets and random events; balance tax, support, granary and order
Guess the emperorIdentify one of 200+ emperorsUnlimited guesses; early guesses give conventional clues, from the fifth onward a 50% chance to reveal an attribute comparison
History brawlTwo players draft, an AI judgesRandom dynasties, ten posts to fill with historical figures; the AI judges by dynasty fit > role fit > actual ability > lineup synergy, in a storyteller's voice
Imperial examSeven stages, three multiple-choice questions eachClassics, history and ancient institutions; one wrong answer ends the run

3. The hard parts

3.1 Four levels of AI, from pattern scoring to WebAssembly

The AI here is never "pick a random move"; it spans four technical levels.

(1) Pattern scoring — gomoku

Eight directions scan the shapes and score them:

PatternScoreMeaning
Five100000Already won
Open four10000Four with both ends open — forced win
Four / open three1000Four blocked at one end / three open at both
Three / open two100Three blocked at one end / two open
Two10
Single1

Difficulties control search depth: easy looks one move ahead and can slip; hard looks several ahead and prefers blocking your open three over completing its own four. Everything is a pure function, so node --test runs it directly.

(2) Alpha-beta pruning — Chinese chess

Ported from itlwei/Chess (MIT), keeping the engine core: move generation per piece rule (horse legs, elephant eyes, palace diagonals), piece-square value tables, and alpha-beta search with position evaluation. Board logic (xiangqi/logic.js) is fully separated from the engine (xiangqi/engine.js). A server endpoint /api/xiangqi-ai offers a GLM-4.7-Flash move proxy as a fallback when the local engine cannot produce a move in extreme positions.

(3) GNU Go → WebAssembly — Go

The hardest path in the project: the open-source GNU Go engine compiled to WebAssembly (weiqi/gnugo.js, about 8.7 MB), running in a Web Worker with a message protocol:

main → worker:  { cmd: 'init', size, seed }    start a game (Komi 7.5)
main → worker:  { cmd: 'play', i, j }          player move
main → worker:  { cmd: 'genmove' }             engine move
worker → main:  { ok, cmd, board, last }       board snapshot (colour-normalised)

The worker also does three important things: colour normalisation (the player's colour maps to 1, the other to 2, independent of GNU Go's internal constants, so rendering is always consistent); a move log so a crashed game can be replayed; and stdout/stderr capture through Module.print and printErr for diagnosis.

A world-class board engine running in the browser without blocking the main thread is a complete demonstration that front ends can host heavy AI.

(4) Three difficulty tiers — jungle chess and reversi

Both split AI by search depth into three tiers; reversi adds undo. Same pure-function approach, fully unit-testable.

3.2 Aeroplane chess: realtime multiplayer on D1

The only multiplayer game among the 24. Three layers:

create/join  →  state advance (roll/move/start)  →  polling sync
     │                      │                            │
     ▼                      ▼                            ▼
/api/feixingqi/create  /api/feixingqi/roll   client polls every 1.5s (in game)
/api/feixingqi/join    /api/feixingqi/move   / 2s (lobby); the server runs tick() to
                       /api/feixingqi/start  advance AI turns before responding
  • Rooms: POST /api/feixingqi/create creates a password-protected room and POST /api/feixingqi/join joins one; room state serialises to JSON in a single D1 row (code / password / state(JSON) / created_at / updated_at).
  • State advance: every move is validated server-side against cheating, and empty seats are driven by server-side AI, so a table can start with one human.
  • Polling: the client periodically fetches /api/feixingqi/state; before responding the server runs tick() to advance AI turns, so players always see the newest state with everyone else's moves applied.

D1 is both database and cache here — good enough, and it avoids the operational cost of WebSockets.

3.3 Streamed AI narration

Life simulation and the history brawl bring an LLM into the game, with the server proxying SSE.

Life simulation (/api/life/narrate)

  • After allocating birth attributes the client posts {attributes};
  • the server calls OpenRouter (inclusionai/ling-3.0-flash:free) and forwards the SSE stream verbatim as text/event-stream;
  • six chapters — childhood, youth, adulthood, middle age, later life, verdict — generate character by character, like someone telling a story across the table;
  • 429 rate limits trigger automatic backoff (three attempts, 2.5 s × attempt), and the prompt pins the output shape (130–180 characters per chapter, a final line "Life score: NN").

Prompt constraints:

You are the randomiser AI of a Chinese life simulator. Output exactly six headings, one per line:
[Childhood] [Youth] [Adulthood] [Middle age] [Later life] [Verdict]
The first five are 130-180 characters each, and must include concrete events, their impact,
and how they change the next stage. The final section must begin with "Life score: NN." (NN 0-100).

History brawl (/api/battle/analyze)

  • Two players alternately draft historical figures for ten posts (vanguard, logistics, …), then ask the AI judge;
  • the AI scores ten matchups in the order dynasty fit > role fit > ability > synergy, with each round precise to 0.1 and summing to exactly 10;
  • the voice is a storyteller's ("now then…", "behold…"), also streamed over SSE.

Both are lightweight "game + LLM" patterns: the server only authenticates, rate-limits and forwards the stream, never storing model state; the client consumes standard SSE and a failure does not break the game loop.

3.4 Accounts and the admin console

  • Register / login / logout: POST /api/register|login|logout; passwords live in the D1 users table, sessions use a random token in the hg_token cookie with a 30-day expiry check;
  • GET /api/me lets the front end know the login state; home shows "username · logout", and the admin account unlocks a hidden console at /adminPages/;
  • middleware in functions/_middleware.js: unauthenticated HTML page requests 302 to /login.html, while static assets and /api/* pass; admin paths additionally require the admin username or return 403;
  • admin endpoints: GET /api/admin/users and GET /api/admin/game-stats.

3.5 Absurdly fast: caching and details

  • Zero dependencies, zero build: static assets open instantly, no bundle to wait for;
  • Layered caching in _headers:
    • .css/.js/.mjs → max-age=300, stale-while-revalidate=86400;
    • .wasm → max-age=604800, immutable — the GNU Go engine loads once and is cached for a week;
    • images → max-age=86400, stale-while-revalidate=604800;
  • Home page: three random recommendations per visit plus a "play something random" button;
  • Responsive: breakpoints at 850 px and 600 px, playable on phone and desktop, touch and keyboard;
  • Runtime: shared/game-runtime.js owns lifecycle, score and event reporting, and a failure there never blocks the game loop.

4. Stack

LayerTechnologyNotes
FrontendNative HTML / CSS / JS (ES Modules)No framework, no build, no dependencies; shared/theme.css defines the whole visual vocabulary (ink, vermilion, gold, jade)
Board renderingHand-written SVG / CanvasPer game; pure-function previews exported from shared/landing.js
AIGNU Go → WebAssembly (worker) / alpha-beta / pattern scoring / minimaxA world-class engine inside the browser
Cloud AIOpenRouter (streamed narration) / Moark (GLM-4.7-Flash chess proxy)SSE passthrough
BackendCloudflare Pages FunctionsFile path becomes the route, no configuration
DatabaseCloudflare D1Seven tables: users / sessions / battle_rooms / feixingqi_rooms / game_sessions / game_events / game_scores
TestsNode's built-in runner (25 files, 288 cases) plus Playwright smokePure functions plus browser E2E
DeployCloudflare Pageswrangler.toml binds Pages and D1
Caching_headers plus _routes.jsonFine-grained control, static assets skip Functions

5. Architecture

5.1 Front end: self-contained folders, data-driven home

index.html          home: section cards + daily picks + random game
/board /puzzle /fun /history   section pages with that section's cards
shared/landing.js   ★ single source of truth: sections + game config + SVG previews
shared/game-runtime.js  lifecycle, score and feedback event queue
shared/theme.css / cards.css / game.css

each game = one self-contained folder
<game>/index.html      page shell plus rules dialog
<game>/<game>.js       interaction and rendering (may include a worker)
<game>/<game>.css      its own styles
<game>/ai.js           pure AI, no DOM, unit-testable
<game>/engine.js       pure logic engine (moves, rules, state machine)
<game>/logic.js        board rules (captures, liberties)
<game>/levels.js       level data (e.g. 15 sokoban levels)

Three design rules.

  1. Shared theme. shared/theme.css defines the CSS variables (--ink, --accent-red, --accent-gold, --border, --surface, --shadow-soft …), shared/cards.css styles the cards, shared/game.css the in-game layout and state feedback. Every game reuses one visual language instead of inventing colours.
  2. Data-driven home page. shared/landing.js is the only data source: sections defines four sections × six games with id / name / description / href / accent / previewSvg, renderHome() draws the four entry cards and renderSection(id) draws that section's six cards. Recommendations, the random button and counts all derive from this — adding a game touches one place.
  3. Self-containment. One folder per game holds page, styles, logic and AI; adding a game touches no other game, and deleting one is deleting a folder.

Shared runtime (shared/game-runtime.js) creates a session per game, records key events, syncs score snapshots and submits results on finish or page leave: createGameRuntime({gameId, autoStart}) generates a sessionId, start() posts /api/game/sessions, setScore() deduplicates and pushes a score_snapshot, finish(outcome) posts /api/game/finish and flushes. Events batch with MAX_QUEUE=60 / FLUSH_SIZE=6 / FLUSH_DELAY=1600ms and an idempotent eventId; a network failure requeues instead of blocking the loop. A MutationObserver watches #score, [data-score], .score-value and #status to infer results, custom events (happy-game-score, happy-game-event) let a game report directly, and pagehide uses navigator.sendBeacon so scores are not lost.

The philosophy: the domain layer must not know about DOM, canvas, network or database. Pages consume domain snapshots, the runtime owns lifecycle and feedback, and ai.js / engine.js stay pure so Node can test them. docs/architecture.md fixes the boundary: page → shared UI/runtime → game domain → API client → Pages Functions → D1.

5.2 API reference

Everything under functions/api/; the file name is the route.

EndpointMethodPurpose
/api/registerPOSTCreate a user in D1
/api/loginPOSTSign in, issue the hg_token cookie (30 days)
/api/logoutPOSTSign out, destroy the session
/api/meGETCurrent session ({username} or 401)
/api/xiangqi-aiPOSTChess move proxy (FEN in, move out, via Moark)
/api/life/narratePOSTStreamed life story from attributes (SSE)
/api/battle/create join pick statePOST/GETHistory brawl rooms, drafting and state
/api/battle/analyze analysisPOST/GETAI judge, round by round, streamed
/api/feixingqi/create join start roll move statePOST/GETAeroplane chess rooms, rolling, moving, polling
/api/admin/usersGETAdmin: user list
/api/admin/game-statsGETAdmin: sessions and score statistics
/api/game/sessionsPOSTStart a game session
/api/game/eventsPOSTBatch key events (idempotent event_id, whitelisted types)
/api/game/finishPOSTSettle and write the final score

Shared helpers live in functions/api/feixingqi/_shared.js (json(), parseCookie(), whoami(), loadRoom(), saveRoom()) and functions/api/game/_shared.js. Business logic sits in functions/services/game-service.js, D1 access in functions/repositories/game-repository.js — Functions are transport adapters, services own the workflow, repositories own the SQL.

5.3 Database

schema.sql defines seven tables; incremental changes live in migrations/. New databases run the whole schema; existing ones apply migrations in order.

users            -- username PK, password, created_at
sessions         -- token PK, username, expires_at (idx_sessions_username)
battle_rooms     -- brawl rooms: code PK / password / state(JSON) / timestamps
feixingqi_rooms  -- aeroplane rooms: same shape, state holds seats/pieces/turn/dice/log
game_sessions    -- one row per run: id, game_id, username, anonymous_id, started_at,
                 --   ended_at, status, score, duration_ms, metadata(JSON)
game_events      -- whitelisted events: event_id UNIQUE, session_id, type, score, payload
game_scores      -- final scores: session_id PK, game_id, username, score, outcome, duration

Room state is one JSON row (simple enough), multiplayer uses client polling plus a server state machine, and D1 is both database and cache. Indexes cover the hot queries: game_sessions(game_id, started_at), game_events(session_id, created_at), game_scores(game_id, score DESC).

6. Quick start

Requires Node.js 18+ (both wrangler and node --test need it).

git clone https://github.com/zhoujungis/happy_games.git
cd happy_games
npm install          # only wrangler and playwright, no frontend dependencies
npm run dev          # static assets plus Pages Functions
# open http://localhost:8788

wrangler pages dev . reads the D1 binding from wrangler.toml. Local D1 must be created or synced first:

npx wrangler d1 create happy-games-db   # put the database_id into wrangler.toml
npx wrangler d1 execute happy-games-db --local --file=schema.sql
# or, for a remote database you already created in the dashboard:
npx wrangler d1 execute happy-games-db --remote --file=schema.sql

Tests

npm test          # Node's test runner over tests/*.test.js (25 files, 288 cases)
npm run check     # tools/check.mjs: JS syntax plus HTML charset/viewport checks
npm run test:e2e  # Playwright smoke: desktop and mobile home, canvas games, overflow

Testing in Node works because every AI and engine is a DOM-free pure function (gomoku/ai.js, xiangqi/engine.js, weiqi/logic.js, feixingqi/engine.js …): importing is enough to assert.

7. Deploying from zero

The site is published directly to Cloudflare Pages (pages_build_output_dir = ".") with no build step.

Step 1: create the D1 database. In the Cloudflare dashboard → Workers & Pages → D1 SQL Database → Create database, name it happy-games-db, then put the id into wrangler.toml:

name = "happy-games"
compatibility_date = "2025-01-01"
pages_build_output_dir = "."

[[d1_databases]]
binding = "DB"
database_name = "happy-games-db"
database_id = "your-database-id"

Then create the tables and verify:

npx wrangler d1 execute happy-games-db --remote --file=schema.sql
for f in migrations/*.sql; do npx wrangler d1 execute happy-games-db --remote --file="$f"; done
npx wrangler d1 execute happy-games-db --remote --command "SELECT name FROM sqlite_master WHERE type='table';"

Step 2: environment variables. In the Pages project → Settings → Variables and Secrets, add OPENROUTER_KEY if you want the life-simulation narration; without it that feature returns 503 and nothing else is affected. A separate key is needed if you use the Moark chess proxy (see the comment at the top of functions/api/xiangqi-ai.js).

Step 3: deploy.

npm run deploy
# equivalent to npx wrangler pages deploy .

The first run creates the happy-games Pages project; later runs publish incrementally, with Cloudflare handling the domain and HTTPS. Caching comes from _headers, and _routes.json keeps static assets out of Functions so only /api/* and HTML hit the worker:

{
  "version": 1,
  "include": ["/*"],
  "exclude": ["/*.css","/*.js","/*.mjs","/*.wasm","/*.png","/*.svg","/robots.txt","/rss.xml"]
}

Common traps

SymptomCauseFix
/api/me 401 while signed inThe hg_token cookie is not sentCheck credentials: 'same-origin' and that you are on the Pages domain, not file://
Life simulation 503OPENROUTER_KEY missingAdd the variable, then retry the deployment
D1 "no such table"schema.sql never ran remotelyRun wrangler d1 execute --remote --file=schema.sql again
Slow .wasm loadCache missConfirm _headers marks .wasm immutable; a second visit should come from cache

8. Adding a game: change two places, the whole site follows

Thanks to the data-driven design, adding a game takes two steps.

1. Create the folder (mirroring gomoku/):

<game>/
├── index.html     # shell: header + board/canvas + rules dialog, imports shared CSS
├── <game>.css     # game-specific styles
├── <game>.js      # interaction and rendering (may include a worker)
└── ai.js          # pure AI, testable with node --test

The HTML needs <meta charset>, <meta viewport>, a board container, score and status elements (#score, #status for the runtime's auto-snapshot), and a rules dialog; a module script imports <game>.js and calls createGameRuntime().

2. Register it in the matching section's games array in shared/landing.js:

{
  id: 'mygame',            // folder name and path segment
  name: 'My game',
  description: 'One-line summary',
  href: '/mygame/',
  accent: '#2980b9',
  previewSvg: `<svg viewBox="0 0 120 120">…</svg>`,
}

That is all: the home section cards, section pages, daily picks and the random button all include the new game without touching another file. That is what a single source of truth buys.

Constraints from docs/architecture.md: keep existing URLs and static deployment stable; migrate a representative game before batch migration; keep pure-logic tests green; add browser coverage for lifecycle, score, offline and responsive behaviour.

9. Project layout

├── index.html                  home: sections + daily picks + random game
├── login.html                  login and registration
├── adminPages/                 admin console (owner only)
├── board/ puzzle/ fun/ history/   section pages
├── shared/
│   ├── theme.css               global variables and base styles
│   ├── cards.css               card styles
│   ├── game.css                in-game shared styles
│   ├── game-runtime.js         ★ lifecycle, score and events
│   └── landing.js              ★ the single data source
├── functions/
│   ├── _middleware.js          login guard and admin auth
│   ├── services/               game-service.js
│   ├── repositories/           game-repository.js
│   └── api/
│       ├── register.js login.js logout.js me.js
│       ├── xiangqi-ai.js
│       ├── life/narrate.js
│       ├── battle/*.js         brawl rooms and AI judge
│       ├── feixingqi/*.js      multiplayer aeroplane chess
│       ├── game/*.js           sessions, events, settlement
│       └── admin/              admin endpoints and statistics
├── <game>/                     24 self-contained game folders
├── schema.sql                  seven tables
├── migrations/                 incremental D1 migrations
├── tests/                      25 files, 288 cases
├── e2e/                        Playwright smoke tests
├── tools/check.mjs             syntax and HTML meta checks
├── wrangler.toml               Pages and D1 configuration
├── _headers                    layered cache policy
├── _routes.json                static assets skip Functions
├── assets/                     README preview images exported from landing.js
└── docs/architecture.md        boundaries and migration rules

10. Why insist on zero frameworks

Not nostalgia — a trade-off:

  1. Readability first. Twenty-four games with completely different logic gain little from React or Vue abstractions, which add "understand the framework before the game". Native code lets a newcomer follow one game end to end in ten minutes.
  2. Portability. ai.js and engine.js depend on nothing; Node imports them directly, and moving to a mini-program, Electron or a server would only require rewriting the rendering layer.
  3. Performance and offline. No framework means no runtime cost, no hydration and no bundle. Apart from narration and multiplayer, every game works without network, and _headers caching makes repeat visits nearly offline.
  4. Teaching value. This is a front-end project that can be read completely: no black-box bundler, no two thousand lines of config — just HTML files that run once uploaded to Cloudflare Pages.

Zero frameworks does not mean zero conventions. The variable system in shared/theme.css, the single data source in landing.js, the lifecycle contract in game-runtime.js and the layering in functions/services form an invisible framework — agreed upon, not depended upon.

Closing

Happy Games tries to prove that even in the age of heavy front-end frameworks, native technology can still deliver a complete, maintainable, deployable full-stack product — as long as the boundaries are drawn (domain separate from rendering, data-driven home, layered backend), AI is written as pure functions, multiplayer as a state machine, and deployment handed to Cloudflare.

Each of the 24 games is its own small world: strategist on a board, escapee in a maze, merchant on a trade route, courtier under a seven-day decree, scholar in an imperial exam. Don't look for a game — start one.

Further reading: github.com/zhoujungis/happy_games · happygames.pages.dev · GNU Go · Cloudflare Pages · Cloudflare D1

Written against the master branch of happy_games as of September 2026; endpoints and tables follow schema.sql and wrangler.toml and may drift with later versions.

Along the same chain: NanoBrowser: hand-writing an in-process CDP and Halo Music: a multi-source music app.