Captcha Royale

A battle royale where every round is a procedurally generated CAPTCHA and a wrong answer knocks you out

Screenshot of Captcha Royale
The landing page.

Up to 16 players enter a room, a CAPTCHA appears, and a wrong answer or a timeout knocks you out. Last one standing wins. I built it because CAPTCHAs are designed to be easy for humans and humans still fail them all the time. The puzzles don't have to stop a bot, so I could make them as hard as I liked.

One engine, generated from a seed

The engine is about 11,000 lines of Rust across 28 generator modules, compiled to a 234 KB WebAssembly module. Every random choice inside it (character selection, warp control points, noise positions, decoy placement) comes from a ChaCha8 stream seeded from the round. The plan was for the browser and the Worker to run the same module. The server sends a seed, the client renders the puzzle from it, and the server regenerates the same puzzle from the same seed to check the answer, so no image and no solution ever crosses the network.

The client half of that is built. The Worker doesn't check answers yet, so the game trusts the client and eliminations come from the timer alone.

The engine generates 31 CAPTCHA types. Tier 1 is distorted text, arithmetic with decoy digits, image grids, dot counting, clock reading, fractions, and graph reading. Tier 2 opens after round 3 with perceptual puzzles: rotated objects, Ishihara-style color grids, mirror matching, balance scales. Tier 3 after round 6 adds spatial reasoning, multi-step checks, boolean logic, and path tracing. Tier 4 after round 9 is the nasty stuff, metamorphic puzzles, adversarial typography, cascades, and jigsaws. Difficulty is a blend of player level and round number, and the distorted-text time limit runs from 10 seconds down to 5 as it climbs.

Modes

Battle Royale is 4 to 16 players, up to 20 rounds, elimination on a miss. Sprint is 2 to 8 players solving the same 10 CAPTCHAs for the fastest total time, with no elimination. Endless is solo, keeps going until you fail, and stores your high score in localStorage. Battle Royale came first and the other two are for practicing and for people who found one-mistake elimination too harsh.

Rooms, queues, ratings

The backend is Cloudflare Workers with a Durable Object per match room. The room derives the round seed, runs the timer, records answers, and broadcasts eliminations over WebSocket. A Durable Object is a single-threaded actor, so two answers that arrive in the same instant are processed in order and there's no race over who got eliminated. A separate Durable Object runs the matchmaking queue. Player data is in D1. Sign-in is Google OAuth. The React frontend is on GitHub Pages.

Players start at 1000 Elo in six brackets: Bronze below 800, Silver to 1000, Gold to 1200, Platinum to 1500, Diamond to 2000, Master above that. The matchmaker fills from your own bracket, widens to the neighboring brackets after 30 seconds in queue, and to two brackets either side after 60. The K-factor is 40 for your first 30 matches, 24 up to 100, and 16 after that. In a room of \(n\) players your adjustment is the average of pairwise updates against everyone else,

\[\Delta R_i = \frac{1}{n-1} \sum_{j \neq i} K\bigl(S_{ij} - E_{ij}\bigr), \qquad E_{ij} = \frac{1}{1 + 10^{(R_j - R_i)/400}},\]

with \(S_{ij} = 1\) if you outlasted player \(j\). A Bronze player who survives deep into a Gold lobby gains a lot, since \(E_{ij}\) is small against every higher-rated opponent.