Reps: Wordle, but with JavaScript Developer Zander built Reps, a daily JavaScript puzzle game launched in June 2026 that presents one puzzle per day with three tests and a reference solution, now holding 118 puzzles stored as plain YAML files. Each submission runs in a fresh Web Worker per test with a 2 second kill switch, and a CI pipeline deploys a playable Cloudflare preview of every puzzle submitted via pull request. The project uses Vite, React, Monaco, ZUI and Cloudflare Workers with no backend. Reps: Wordle, but with JavaScript A write-up of the interesting parts of the daily JavaScript puzzle game I built in June. Puzzles as YAML data, a Web Worker sandbox, and a CI pipeline that deploys a playable preview of every submitted puzzle. I built Reps at the end of May and launched it a few days later https://zander.wtf/blog/2026-06-03-reps . In this age of AI I write less code than I used to, and it felt like I was losing a skill. Not the big stuff, that’s fine. The muscle memory. The bit where you know what reduce is going to do before you’ve finished typing it. Reviewing an agent’s diff keeps the judgement sharp. It does nothing for the hands. So I wanted a short daily thing. Like Wordle: one puzzle a day, five minutes, no backlog to feel guilty about. Write a small function in the browser, watch three tests run, share a row of coloured squares. Reps https://reps.zander.wtf is what came out of that. TL;DR - Every puzzle is a plain YAML file: prompt, three tests, a reference solution. There are 118 of them now. - Your code runs in a fresh Web Worker per test, with a 2 second kill switch. - The validator compiles every reference solution and runs it against its own tests. That’s the entire test suite. - Open a PR with a puzzle and CI deploys a playable Cloudflare preview and comments the link. - Promoting accepted puzzles into the live rotation is a workflow that commits back to main . - Vite, React, Monaco, ZUI https://zui.zander.wtf , Cloudflare Workers. No backend at all. The shape of it Every day you get one puzzle: a prompt with a worked example, and a Monaco editor with some starter code in it. Write the function, hit run, and three tests go green or red one at a time. Solve it and you get a spoiler-free share string, coloured squares and a time, never your code. The three tests are always in the same order: happy path, a second normal case, then an edge case. That’s a rule for contributors rather than a coincidence. The third one is where you find out your solution was hardcoded. The recommended solution sits behind a button you can press at any point, with no penalty. Reps is a warm-up, not an exam, and a puzzle you failed and can’t learn from is just a bad morning. So each one also carries an explanation written for someone who tried and couldn’t see the path. I press that button plenty. How puzzles are made I took this idea from Astro https://astro.build . Reps has its own content collection, made of YAML files, working much the way Astro’s content collections do. Content is files on disk, a schema says what shape those files have to be, and a check runs over them before any of it ships. No database, no CMS, no admin panel. Adding a puzzle is adding a file. So every puzzle is one YAML file. No registration, no imports, nothing to wire up: id: 11 title: 'Chunk an Array' difficulty: medium prompt: | Implement chunk arr, size so it splits arr into sub-arrays of length size the last chunk may be shorter . For example, chunk 1, 2, 3, 4, 5 , 2 returns 1, 2 , 3, 4 , 5 . functionName: chunk starterCode: | function chunk arr, size { // your code here } tests: - name: 'Even split' args: 1, 2, 3, 4, 5 , 2 expected: 1, 2 , 3, 4 , 5 - name: 'Exact fit' args: 1, 2, 3 , 3 expected: 1, 2, 3 - name: 'Empty input' args: , 3 expected: solution: | function chunk arr, size { const out = ; for let i = 0; i < arr.length; i += size out.push arr.slice i, i + size ; return out; } args is spread into the call, so args: 1,2,3 , 2 calls chunk 1,2,3 , 2 . A deep equality that handles arrays, plain objects and NaN compares the result against expected . It doesn’t handle Map or Set , so puzzles have to convert those before returning. The validator hard-fails if you try. A separate index.json holds the schedule: a launch date and an ordered list of puzzle ids that the app walks through a day at a time, wrapping back to the start when it reaches the end. Adding a puzzle file doesn’t put it in rotation, someone has to add it to that list, so the difficulty pacing stays curated. Running a stranger’s code in their own browser There’s no backend. Your solution never leaves your machine, which removes an entire category of problem and introduces one. An infinite loop would lock the tab. So each test gets its own Web Worker, and the runner holds a hard timeout over it: js const timer = setTimeout = { worker.terminate resolve { name: test.name, pass: false, error: Timed out after ${TIMEOUT MS} ms } }, TIMEOUT MS while true gives you a clean red “Timed out after 2000 ms” instead of a beachball. A fresh worker per test also means one bad test can’t take the other two down with it. Inside the worker, three details that each came from something going wrong: js const factory = new Function 'console', "use strict";\n${code}\n;return typeof ${functionName} === "function" ? ${functionName} : undefined; const fn = factory captureConsole const args = structuredClone test.args const received = fn ...args "use strict" stops accidental globals leaking between runs. structuredClone on the arguments means a solution that mutates its input can’t poison the comparison for the next test, and you can mutate freely without being punished for it. And console is passed in as a parameter, shadowing the global, so every console.log you left in your working gets captured and shown next to the test result rather than disappearing into a worker’s devtools context nobody opens. That last one was a whole commit on its own https://github.com/mrmartineau/reps.zander.wtf/commit/86a66a4 and it’s the single change that made debugging a failing puzzle feel normal. React puzzles A puzzle can set a kind and ask for a React component or a custom hook instead of a pure function. JSX is compiled in the browser with Sucrase https://github.com/alangpierce/sucrase , and component tests assert against the rendered DOM with a small declarative DSL: kind: react-component componentName: TaskList tests: - name: 'Renders one