I Built a Browser Game with Next.js 16, Cloudflare Workers, and a D1 Database
I've been messing around with Next.js 16 since the beta dropped, and I wanted to build something that wasn't another todo app or SaaS landing page. Something fun, self-contained, and weird enough that I'd actually want to test it myself.
The result is 195-0, a browser-based world conquest draft game. You pick five people for a cabinet, assign each one a role, and run a simulation to see how many of the world's 195 nations your team can conquer. You just open the page and play. No signup, no install.
The Stack
- Next.js 16 (App Router, static export)
- Cloudflare Workers for API routes
- Cloudflare D1 for the leaderboard database
- Tailwind CSS v4 with oklch design tokens
- Deterministic PRNG for the Daily Challenge
The site is a static export served from Cloudflare's edge. The Worker handles /api/* routes only, which keeps the Worker small and the static assets fast.
Two Game Modes, One Component
Classic Draft lets you respin the country and category before viewing candidates. Daily Challenge gives everyone the same five rounds, same thirty candidates, and no respins. It resets at 00:00 UTC.
Both modes share the same draft flow component. The only difference is whether the RNG seed is random (Classic) or deterministic (Daily). I used a mulberry32 PRNG seeded with an FNV-1a hash of the current UTC date string:
function dailySeedFromDate(date: string): number {
let h = 0x811c9dc5;
for (let i = 0; i < date.length; i++) {
h ^= date.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
}
function mulberry32(seed: number): () => number {
return function () {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
Every player on the same day gets the exact same rounds. The Daily Challenge has its own leaderboard, so you're competing directly against other people who faced the same puzzle.
The Scoring System
This was the most fun part to design. Every cabinet starts at 125 points. The game calculates a role fit percentage from 0 to 100 based on internal weight tables that map each of 16 profession categories to each of 5 cabinet roles. On top of that:
- Graded combo bonuses: +11 for a top-tier category match, +8 for a second-tier match
- Global alliance bonus: +4 for two countries, +8 for three or more
- No-authority penalty: if you have zero Politicians, Dictators, or Military Commanders, your entire score gets multiplied by 0.65
- Joke pick penalties: placing Poets or Porn Stars in the General seat costs 14 points
- Battlefield variance: a random 0-20 swing so the same cabinet can score differently across runs
The game clamps the final score to 0-195. That's where the name comes from. Reaching 195 needs near-perfect role fits, combo bonuses, a multi-country alliance, and a high variance roll.
I wrote up the full weight tables and scoring formula on the cabinet roles reference page if you want to see the exact numbers.
Leaderboard with D1
The leaderboard was the part that pushed me to learn Cloudflare D1. Three ranking views: Daily Challenge, Classic Today, and All Time. The schema is straightforward:
CREATE TABLE leaderboard_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
commander_name TEXT NOT NULL,
score INTEGER NOT NULL,
mode TEXT NOT NULL,
day_key TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX idx_score ON leaderboard_entries(mode, day_key, score DESC);
CREATE INDEX idx_commander ON leaderboard_entries(commander_name, mode, day_key, score DESC);
The Worker keeps only the best score per commander per period. Competition ranking means tied scores share the same rank number, with the earlier submission shown first.
For local development, I built a localStorage mock that auto-falls back when the API is unavailable. I could develop and test the full game flow without deploying the Worker.
SEO and Content Strategy
Each route has its own metadata. I separated the how-to-play guide and the cabinet roles reference into dedicated pages rather than cramming everything onto the homepage. The how-to-play page covers respin rules, invalid combinations, and the scoring formula. The cabinet roles page goes deeper into weight tables and draft order strategy.
robots.txt disallows /api/ to keep user data off search engines, and the Worker sends X-Robots-Tag: noindex, noarchive on API responses.
What I Learned
The biggest surprise was how well static export plus edge Worker works for a game like this. The game itself runs entirely client-side. The Worker is only needed for leaderboard reads and writes. That split keeps latency low and the Worker's daily request count manageable.
The deterministic PRNG was harder than I expected. Getting the same seed to produce the same rounds across different browsers and time zones required careful date handling. I settled on a UTC date string (YYYY-MM-DD) as the seed input, which aligns the Daily reset with 00:00 UTC.
Try It
If you want to see how it works in practice, play the game here. The Daily Challenge is probably the best place to start since everyone gets the same puzzle. There's also a leaderboard if you want to see how your score compares.
Feedback welcome, especially on the scoring balance. I'm still tuning the weight tables.
Top comments (0)