Chess Bot
← Back to Projects

Chess Bot

A real-time chess assistant that reads the board straight from a browser tab and suggests best moves through a Stockfish-powered Electron app.

ElectronChrome ExtensionNode.jsWebSocketStockfishNext.js

Overview

Chess Bot is a real-time move assistant made up of three separate pieces working together: a browser extension content script that reads the live board state directly out of the DOM, an Electron app that hosts a Next.js UI and runs the chess engine, and a WebSocket bridge connecting the two. When a position changes on screen, the extension sends it to the desktop app, which analyzes it with Stockfish and sends the best move back to be highlighted right on the board.

Architecture

The three pieces run in completely separate processes and can't talk to each other directly, so the whole system is built around message passing. The browser extension's content script runs inside the chess site's page and opens a WebSocket connection to `ws://localhost:3001`, a server started inside the Electron app's main process. The Electron main process also spawns Stockfish as a child process and talks to it over its stdin/stdout using the UCI protocol, then forwards results back out to any connected extension over the same WebSocket.

stockfishProcess = spawn(enginePath);
stockfishProcess.stdin.write('uci\n');
stockfishProcess.stdin.write('setoption name Threads value 4\n');

Reading the Board

There's no chess API to call here the extension reads the board the same way a person would look at it, by inspecting piece elements in the DOM (including inside a shadow root, since some chess sites render the board with a web component). Each piece element carries a class like `wp` or `bk` encoding its color and type, and its on-screen position is converted into a file/rank square by dividing its coordinates by the board's square size.

const fileIndex = Math.floor(cx / squareSize);
const rankIndex = Math.floor(cy / squareSize);
const square = 'abcdefgh'[fileIndex] + '87654321'[rankIndex];

Once every piece is mapped to a square, the board is walked rank by rank to build a standard FEN string the same notation chess engines expect as input. A MutationObserver watches the board element for DOM changes, so a new FEN is generated and sent automatically the instant a move is made, without any polling.

Engine Analysis

On the Electron side, incoming FEN strings are fed to Stockfish with `position fen ... / go movetime ...`. Since analysis takes time and new positions can arrive mid-analysis, a simple queue holds the latest pending FEN and only sends it once the engine reports it's free, so the engine never gets two overlapping analysis requests. The engine process also self-heals: if Stockfish crashes or exits, a timer automatically restarts it a couple seconds later.

Bridging the UI

The Electron window itself loads a Next.js app as its UI, running as a frameless window with a custom draggable title bar (`WebkitAppRegion: 'drag'`, with buttons explicitly excluded so they stay clickable). Since Electron's renderer process shouldn't have raw access to Node APIs for security reasons, a preload script using `contextBridge` exposes only a narrow, explicit API (`close`, `minimize`, a scoped `ipcRenderer`) to the React UI instead of the full Electron/Node surface.

contextBridge.exposeInMainWorld('electron', {
  close: () => ipcRenderer.send('window-close'),
  minimize: () => ipcRenderer.send('window-minimize'),
});

Highlighting the Move

When the extension receives a best move back over the WebSocket, it doesn't rely on the site's own UI to show it it draws its own highlight. Absolutely-positioned bordered `div`s are injected over the source and destination squares, calculated from the board's bounding box the same way the piece positions were read, then automatically removed after a few seconds.

Challenges

  • Reading board state reliably from a shadow DOM and web-component-based board without a public API to hook into
  • Keeping the engine responsive under rapid position changes without queuing up a backlog of stale analysis requests
  • Bridging three separate processes (browser extension, Electron main, Stockfish subprocess) cleanly through WebSocket and IPC without tightly coupling them
Available For Work

Curious about what we can build together? Let's ship something extraordinary!

im@knifi.ngAll rights reserved, © 2026