← DevLog

7 min read

Gunnie's Gambit: a chess engine in a browser tab, with nothing behind it

A free chess practice app where Stockfish runs in your browser and no server is involved. How it is built, and why an AI search tool first took it for a chess opening.

Gunnie's Gambit is a chess practice app at chess.gunnie.nl. You play the computer at a level you choose, ask for a hint when you are stuck, or set up any position and see what the engine thinks. It is free, it needs no account, and the source is on GitHub under the GPL.

The engine is Stockfish 19, compiled to WebAssembly. It runs in your browser. There is no server behind the site, only static files, so your games never leave your device.

The idea came from Untold: Chess Mates, the Netflix documentary about the 2022 controversy between Magnus Carlsen and Hans Niemann.

It is also a showcase. I built it with Claude Code, Anthropic's coding agent, in two days. That took it from the first commit to a live site with tests, CI and automatic deploys. That is how Gunnie works on client projects too. The code, the tests and the commit history are public, and they are meant to be read.

What it does

  • Play against the computer at seven levels, from Beginner to full strength. The evaluation and the best move are off by default. Switch them on when you want them, or ask for a single hint.

  • Analyse any position. Enter moves for both sides by tapping or by typing them, and see the best move, the evaluation and the top three lines, with arrows on the board.

  • Load a position from FEN, or continue a game from PGN.

  • It works on a phone, in a narrow desktop window and with the keyboard alone.

Two engines, not one

The page runs two copies of the engine, each in its own Web Worker. One plays the opponent. The other analyses the position for hints and the evaluation.

One engine would be simpler. But the opponent plays with a handicap, and the handicap is a setting inside the engine. If the same engine also gave your hints, your hints would be handicapped too. Two workers keep them apart.

The handicap needed one more trick. Stockfish limits its strength with UCI_Elo, and that setting starts at 1320. That is too strong for someone who has just learned the moves. So the two lowest levels use Skill Level 0 and 2 instead, with a search only one or three moves deep. At that depth the engine misses simple tactics, which is what a beginner needs to win now and then.

An answer for the wrong position

A UCI engine answers asynchronously. You give it a position, it streams lines of analysis back, and at the end it sends its best move. If you play a move while it is still thinking, the next lines it sends are about the old position. Show them, and the hint arrow points at a move that is no longer on the board.

A small class called SearchQueue deals with that. When the position changes during a search, it sends stop, waits for that search's bestmove (the protocol guarantees one) and only then starts the newest request. Requests that arrive while it waits replace each other, so only the latest one runs. Output from a stopped or replaced search is dropped.

It is 108 lines with seven unit tests of its own, and it is the part of the code I would show first.

No server, any host

Stockfish has stronger multi-threaded builds. Threads in a browser need SharedArrayBuffer, and that needs cross-origin isolation headers on every response. The single-threaded "lite" build needs none of that. It is 1.8 MB, the browser caches it, and the site runs from any static host.

It is deployed to Cloudflare Workers as static files from GitHub Actions. Every push runs lint, a format check, the tests and the build, and only then deploys. There is no UI framework. The page is 62 KB of JavaScript and 50 KB of CSS, 21 and 25 KB compressed.

The only third party is Cloudflare Web Analytics, which counts visits without cookies. Nothing about your games is sent anywhere.

Fair play

A chess engine in a browser tab can be misused, and the documentary that started this project is about cheating allegations. So this is a design rule and not a footnote. Gunnie's Gambit is for practice, for games against computer bots and for reviewing your own games. Using engine help in any online game against a human, rated or casual, or in an over-the-board tournament is against the rules and gets accounts banned on Chess.com and Lichess. The app has no features for reading moves from other sites, and it will not get them.

One word that moved the page

The first mobile Lighthouse run reported a layout shift of 0.169. Google counts anything under 0.1 as good.

Part of it was the font, the same problem as in the previous post: a fallback that was not scaled to match the web font. The bigger part was one word. The header shows the Gunnie logo followed by "Gambit", and a script filled that word in after the page had loaded. Without it, the header fitted on one line on a phone. With it, the header wrapped to two lines and pushed the whole page down 32 pixels.

The build now writes the word into the HTML, so the header has its final size at the first paint. The status line above the board got a reserved height for the same reason. Layout shift is now zero in every run. Mobile scores 96 to 99 on performance and 100 on accessibility, best practices and SEO. Desktop scores 100 on all four.

The general rule: anything a script writes above the fold moves the page on a slow load. If it is known at build time, put it in the HTML.

An AI search tool thought it was an opening

After launch I asked an AI search tool about Gunnie's Gambit. It answered that the name might be a nickname for a chess opening and suggested the King's Gambit and the Queen's Gambit. Fair enough. The site was a day old and in no index yet, and a gambit is an opening.

A name that means something else needs saying out loud. The structured data now has a disambiguatingDescription, "a web app for practising and analysing chess, not a chess opening". The FAQ on the page opens with the same question, and llms.txt says it in its first paragraph.

The second gap was Bing. Copilot runs on Bing's index, and ChatGPT search leans on it as well. Bing had never heard of the site. The build now serves an IndexNow key file, and the deploy pipeline notifies IndexNow after every release. Bing's URL inspection showed the page as discovered the same day.

While setting that up I found that gunnie.nl itself had never submitted its sitemap, not to Google Search Console and not to Bing Webmaster Tools. A site gets indexed without one, which is why nobody notices. Both are submitted now.

What is next

A review mode: after a game, the engine goes through every move and marks the inaccuracies, mistakes and blunders. After that, offline support and importing your finished games from Lichess and Chess.com. No dates. The commit history will show when.

Check yours

  • Search Bing for site:yourdomain.com. If it finds nothing, or less than you expect, add the site to Bing Webmaster Tools and submit the sitemap.

  • Open the Sitemaps page in Google Search Console and in Bing Webmaster Tools. If the list is empty, submit yours.

And if Search Console says "Couldn't fetch" straight after you submit a sitemap, wait before you debug. Both of mine showed it before Google had even tried. The Cloudflare logs had Googlebot fetching the file a minute later, with a 200.

Try it at chess.gunnie.nl. The code is at github.com/gunnie-agency/gunnies-gambit.