Skip to content
How it works

How it works

Everything – the chess engine, the game server, the Wi-Fi access point and the web app – runs on one microcontroller with 512 kB of RAM. The firmware is written in Rust without the standard library (no_std), on top of Embassy and esp-hal.

    flowchart LR
  phone["Phone / laptop<br/>browser"] -- "Wi-Fi<br/>HTTP + JSON" --> web
  subgraph ESP32
    direction LR
    web["web server<br/>(picoserve)"] --> hub["game hub<br/>players, games, clocks"]
    hub -- "engine jobs" --> engine["chess engine<br/>search + NNUE"]
    hub --> display["display<br/>(ESP32-C6)"]
    wifi["Wi-Fi AP + home network<br/>DHCP, captive DNS"] --- web
  end
  

The pieces

PartWhat it does
chess-coreBoard representation (bitboards), move generation, SAN notation, draw rules
chess-engineSearch and evaluation – a Rust port of the Dog / tinychess engine
nnue-coreThe neural network evaluation, shared with the training code
chess-hubPlayers, challenges, games, clocks, the engine queue and the JSON API
chess-webHTTP routes; the whole web app is one gzip-compressed file of ~28 kB
chess-displayDraws only what changed on the 240×240 display
firmwareWi-Fi, networking, storage, tasks – glue for the ESP32
simThe same hub, engine and web app on a PC, for development and testing

All libraries are no_std, allocation-free and tested on a PC.

Running a chess engine next to a web server

The engine searches synchronously for seconds at a time, while the web server must answer within milliseconds. On the single-core C3 and C6 the engine therefore runs as a separate low-priority thread: whenever a network packet arrives or a timer fires, the scheduler pre-empts the engine immediately, and the engine gets the CPU back as soon as the network tasks are idle. On the dual-core S3 the engine has the second core to itself.

The engine checks every 1,024 nodes whether its job is still valid (a player moved, a game ended, a human needs the engine more than the demo game) and stops early if not.

The engine

  • Search: iterative deepening with aspiration windows, principal variation search, a transposition table, null-move pruning, late move reductions, futility pruning and razoring, check and recapture extensions, and a quiescence search with static exchange evaluation. Moves are ordered with the hash move, MVV-LVA, capture history and history / continuation heuristics.
  • Evaluation: an NNUE network 768 → 128×2 → 1 with 8-bit weights in the feature transformer and 8 output buckets (104 kB), trained for this project with bullet on 947 million positions. The accumulator is updated incrementally and lazily; on the ESP32-S3 the inner loops use the chip’s SIMD instructions.
  • Levels: lower levels limit the depth and add noise to the evaluation so the engine makes believable mistakes.

Making it fast enough took a series of measured optimisations – putting hot code into RAM, cheaper attack tables, lazy accumulator updates, branch-free legality checks – which took the ESP32-C6 from about 8,900 to 19,400 positions per second. Every optimisation keeps the search bit-for-bit identical: same moves, scores and node counts.

The web app

A single-page app in plain JavaScript, styled after lichess. It polls the board (the game every 0.7 s, the lobby every 1.5 s) instead of keeping WebSocket connections open, so the small TCP stack can serve more players. Joining the board’s network opens the app automatically via a captive portal (DHCP + a DNS server that answers every name).

Limits

ItemLimit
Players connected24
Games at once (including finished ones and the demo)8
Open challenges8
Game length400 plies, then a draw
Disconnected playerloses after 3 minutes