Why
Daily puzzle games live or die on one thing: whether today's answer was fair. The football ones mostly aren't. They either pick from a hand-curated list of 200 obvious names, or they scrape a database and serve you a 2011 J-League squad player. I wanted to build the whole loop myself: the data pipeline that decides who can be an answer, the difficulty model that decides who is today's answer, and the game around both. It's also the only project I've taken all the way to a public launch, domain, hosting, privacy policy and all.

What it does
- Blitz, the default daily mode. Three players, 60 seconds each, with hints dropping every ten seconds as the clock runs down. You get splits per player and a total time, so the leaderboard is a race rather than a score.
- Classic, one player per day, scored. You start with a single club revealed and spend reveals to expose the rest of the career, then three bonus hints (position, nationality, age). Scoring starts at 1000: reveals share a 700-point budget across the career length, a wrong guess costs 25 and burns a reveal, and time decays a point every 5 seconds up to a 150 cap. Solving always banks at least 25.
- The career route map. The career isn't a table, it's a transfer route: a solid line through the clubs you've unlocked, dashing off into the unknown for the ones you haven't.
- A difficulty model, not a difficulty label. Answer selection scores each player by
effectiveFame = sitelinks × familiarity × era, tiers them headline / stretch / deep, and caps how many hard picks a single day can contain. This came out of a day where the round went 0/9 and I had to build the calibration tooling to stop guessing about it. - Its own data pipeline. Roughly 3,300 answerable players and 7,700 searchable names, parsed from Wikipedia infoboxes and enriched from Wikidata, with validation gates and offline fixture tests.
- Daily ops tooling. Scripts that report live solve rates, preview the next seven days of scheduled rounds, flag data-quality problems, and gate a release on calibration thresholds.

Architecture
- App. Next.js 15 App Router with React 19 and TypeScript, deployed on Vercel. Server routes for puzzle, guess, reveal, search, leaderboard and session telemetry.
- Data. Postgres via Drizzle ORM: Neon in production, PGlite (embedded Postgres) locally and in tests, so the test suite runs against real Postgres with no external service.
- Ingest. A standalone TypeScript pipeline (discover, fetch, parse infobox, enrich from Wikidata, validate, emit) that regenerates the player dataset. Fixture-backed and runnable fully offline against a local cache.
- Play model. Plays are stored as append-only event logs (reveal / guess / give-up) and all state is derived from those events, so scoring and replay can't drift from what the player actually did.
- Hardening. Signed identity cookies, per-route rate limiting, bounded queries, a schema-version init guard so DDL doesn't run on every cold start, and batched session telemetry to keep the free-tier database from being held awake.
- Testing. 43 test files across unit tests, API integration tests against in-memory Postgres, and Playwright end-to-end specs.
- Design. A vintage matchday-programme print system: paper ground, deep bottle-green used as ink rather than background, turf green for actions, Anton / Archivo / IBM Plex Mono, and referee-card red and yellow kept semantic (red for a wrong guess, yellow for a hint).

What's next
Near term it's content and calibration: broadening the answer pool, improving player search ranking, and re-running ingest to fix careers that came out of Wikipedia truncated.
Stack
Next.js 15, React 19, TypeScript, Tailwind CSS v4, Drizzle ORM, Postgres (Neon), PGlite, Vercel, Vitest, Playwright, Wikipedia and Wikidata APIs.