How I built play-chess.in
I’ve always loved playing chess. It’s the game I keep coming back to: the puzzles, the openings, following the world’s top players, and the endless “what if I’d played that instead?” after a game.
My day job is data. I spend it moving data from source systems into data lakes and warehouses, and making sure it arrives complete, correct and on time. I wanted a personal project that was completely mine, built purely out of passion, and that would push me outside my usual stack. Building my own chess site was the obvious choice.
The result is play-chess.in: a free chess site with no account, no ads and no server. Everything runs in your browser. This post covers how it works, what changed along the way, and the open-source projects it stands on.

What’s on the site
- Play against eight bots, from Pip (~400 Elo) to Stockfish at full strength, with an optional chess clock, undo and rematches. Your game survives a page refresh.
- Game review after every game: accuracy for both sides, every move labelled from brilliant to blunder, an evaluation graph, and the stronger move drawn on the board.
- Puzzles: about 10,000 tactics puzzles, picked near your own puzzle rating.
- Openings: all 3,865 named openings, with a mode to practise lines from memory.
- Top players: FIDE’s top 100 in Standard, Rapid and Blitz.
- Tournaments: over-the-board events relayed live, with standings and engine evaluation.
How it started: an AI opponent
The first version was called Chess vs. AI, and the opponent was a large language model. On each turn the app sent the model the position, the move history and the list of legal moves, and asked for a move back as structured JSON.
It was a fun experiment, but it taught me something quickly: language models don’t really play chess. They suggest moves that sound plausible, and some of those moves are illegal. So every move had to be checked against the rules. An illegal move was sent back to the model with a note, up to three times, and if it still failed, the app played a random legal move instead. That kept the game working, but it wasn’t much of an opponent.
So I switched to a real chess engine: Stockfish, the strongest open-source engine in the world. For a while I kept the language model for what it’s good at, turning Stockfish’s analysis into written commentary on the game. In the end I removed the AI completely. The app now has no server code at all: no API keys, no rate limits, and nothing to pay for per move.
Stockfish in the browser
The most interesting technical part is that Stockfish runs inside your browser, compiled to WebAssembly.
- Web Worker: the engine runs in a Web Worker, so it never freezes the page while it thinks. The app talks to it in UCI (the Universal Chess Interface), the same text protocol desktop chess GUIs use: send a position, get back
bestmove e2e4. - Single-threaded build: I use the lite, single-threaded build, about 1.9 MB. The multi-threaded build needs
SharedArrayBuffer, which in turn needs special cross-origin isolation headers on every page; the single-threaded one avoids all that. It’s ready in about 0.2 seconds and searches around 21 moves deep in one second, which is plenty. - Trust nothing: every move, the engine’s included, is checked with chess.js before it’s played.
Making Stockfish play badly (on purpose)
Full-strength Stockfish would crush anyone, so each bot limits it:
| Bot | How it’s limited |
|---|---|
| Pip (~400), Rosa (~800), Max (~1100) | Skill Level 0 / 2 / 5, searching only 1 / 3 / 6 moves deep |
| Nova (1400), Leo (1700), Iris (2000), Viktor (2400) | UCI_LimitStrength with UCI_Elo set to the bot’s rating |
| Stockfish | Full strength |
The weakest three can’t use UCI_Elo, because it doesn’t go below 1320, so their ratings are estimates. Each bot thinks for about 0.8 seconds per move. In timed games it uses only a small share of its remaining time, so it never loses on the clock.
The game review
While you play, a second Stockfish, always at full strength, analyses every position to depth 14 in the background. When the game ends, the review opens instantly because the work is already done.
Each move is judged by how much of the mover’s winning chance it gave away. An engine evaluation like “+1.5” is converted to a 0–100% winning chance, and the drop from one move to the next decides the label: inaccuracy, mistake or blunder. Accuracy uses the same formula as Lichess:
const accuracy = 103.1668 * Math.exp(-0.04354 * winChanceDrop) - 3.1669;
A brilliant move (!!) is the engine’s own choice (or within 2% of it) that soundly sacrifices material, in a position that wasn’t already completely won. They’re rare, which is what makes finding one satisfying.
Puzzles and openings: the data engineer in me
This is where my day job crept in. The puzzles, openings and rankings are all static JSON files built by data pipelines:
- Puzzles come from the Lichess puzzle database, millions of puzzles in one large CSV. A build script filters them down to about 10,000 and buckets them by rating into files from 600 to 2600, so the browser only downloads the band it needs. Your puzzle rating is a real Elo rating: it starts at 1200, and each puzzle is picked within ±150 of it.
- Openings come from the Lichess opening list: 3,865 named openings with their ECO codes, families and move sequences.
- Top players come from FIDE’s official monthly rating lists. A scheduled GitHub Action runs on the 2nd of every month: it downloads about 35 MB of zipped lists, reads them as a stream, validates the result (it refuses to write a file that looks wrong), and commits the new file only if something changed. That commit redeploys the site automatically.
- Tournaments are read live from the Lichess Broadcasts API, straight from your browser, one request at a time and only while the page is open.
It’s a small version of what I do at work: take raw data from a source system, clean and validate it, and publish it somewhere it’s easy to use.
Building it step by step, with Claude Code
I built play-chess.in with Claude Code, Anthropic’s AI coding assistant, as my pair programmer. It’s a different way of working from my day job, and it let me build in a stack I don’t use at work.
What made it work was structure. The build was a series of 59 small steps, each with a clear “done when” check before moving on to the next: render a board, then legal moves, then the AI opponent, the clock, Stockfish, the review, puzzles, openings, top players and tournaments. I decided what to build and how it should behave, and every step was tested before the next one started. Working this way kept each change small enough to understand, check and fix.
Testing mattered to me as much as features:
- 264 unit tests (Vitest) cover the pure logic: the clock, move analysis, puzzle ratings, tournament standings and more.
- 87 end-to-end browser tests (Playwright) play through the real app. They replace Stockfish with a scripted fake engine and Lichess with recorded responses, so they’re fast and always give the same result.
- GitHub Actions runs linting, type checks, both test suites and a production build on every push.
The site is built with Next.js, React and TypeScript, styled with Tailwind CSS, and hosted on Vercel. Every response carries a Content Security Policy and the usual security headers.
Credits: the open-source projects behind it
None of this would exist without the open-source chess community, and every page on the site credits what it uses.
| Used for | Project | Licence |
|---|---|---|
| The bots and the analysis | Stockfish, via the stockfish.js WebAssembly build | GPL v3 |
| Chess rules | chess.js | BSD 2-Clause |
| The board | react-chessboard | MIT |
| Puzzles | Lichess puzzle database | CC0 |
| Openings | Lichess opening list | CC0 |
| Top players | FIDE rating lists | FIDE’s public monthly lists |
| Live tournaments | Lichess Broadcasts | Lichess’s free public API |
A special thank-you to Lichess, which releases its puzzles and openings into the public domain and offers a free API. It’s one of the best things about the chess world.
Try it
Play a game, try a few puzzles, or check the Elo of your favourite player at play-chess.in. If you find a bug or have an idea, I’d love to hear it.