Play chess engines locally. Get every move graded. Drill your own mistakes with spaced repetition.
- What is pawnbook?
- The training loop
- Quickstart
- Opponents
- Architecture
- Configuration
- Development
- Release process
- Contributing
- Licensing
pawnbook is a self-hosted chess trainer built around four design pillars:
| Pillar | What it means |
|---|---|
| Mistakes are content | Every blunder you make becomes a drill card |
| Honest feedback | Elo is estimated from move quality, not win/loss; one-game noise is ≈ ±280 Elo |
| Human-shaped difficulty | The findability gate ensures only moves a player at your level could find become puzzles |
| Respect your time | FSRS caps the daily drill queue at 40 cards; graduated cards are retired permanently |
pawnbook is a single-user, single-machine tool. No accounts. No cloud sync. No leaderboards. Just you and the engines.
┌─────────────────────────────────────────────────────────────────┐
│ 1 PLAY Pick Maia (human-shaped) or Stockfish (tactical) │
│ at any strength from 1100 to full Stockfish 18 │
│ │
│ 2 ANALYSE Stockfish grades every move; Maia checks whether │
│ a human at your rating would have found better │
│ │
│ 3 QUIZ Blunders where a player at your level could have │
│ done better appear immediately, while fresh │
│ │
│ 4 DRILL FSRS resurfaces the same mistakes on the right │
│ schedule — days later, then weeks, then months │
└─────────────────────────────────────────────────────────────────┘
The central idea is findability: a position enters the drill queue only if the Maia model at your rating would choose the engine's best move with probability ≥ 4%. Engine-only brilliancies stay in the review but never clutter your queue.
# 1. Clone and fetch Maia weights (~200 MB)
git clone https://github.com/JohnnyFoulds/pawnbook.git
cd pawnbook
make setup
# 2. Build and start (compiles Stockfish 18 + lc0 from source — ~15 min)
docker compose up
# 3. Open the web UI
open http://localhost:3000Or pull a pre-built image:
docker pull johannesfoulds/pawnbook
docker run -p 3000:3000 -v $(pwd)/data:/app/data johannesfoulds/pawnbookPrerequisites: Node 22, Stockfish 18 binary, lc0 v0.32.1 binary, Maia weights.
npm install
cp .env.example .env # edit engine paths
npm start
open http://localhost:3000Terminal client (optional):
npm link # adds 'chess' to PATH
chess # connect to the local server
chess --host myserver:3000 # connect to a remote instance| Name | Rating | Style |
|---|---|---|
maia-1100 … maia-1900 |
1100–1900 | Human-like — makes the mistakes real people make |
maia-2200 |
2200 | Human-like (optional; present if weight file exists) |
sf-1400 … sf-2900 |
1400–2900 | Stockfish 18 with UCI_LimitStrength |
sf-max |
~3190 | Full-strength Stockfish 18 |
drawfish |
unrated | Plays for stalemate; casual games only |
Maia and Stockfish overlap in rating deliberately. Same number, very different feel: Maia loses like a human, Stockfish loses like a miscalibrated computer.
Ranked games update your strength estimate. Games against Drawfish are always unrated.
Browser / TUI
│
├── WebSocket /ws
└── REST /api/...
│
┌─────▼──────────────────┐
│ Node 22 server │
│ (Interface layer) │
└─────┬──────────────────┘
│ (one-way dependency)
┌─────▼──────────────────┐
│ Domain layer │
│ analysis · drilling │
│ repertoire · strength │
└─────┬──────────────────┘
│
┌─────▼──────────────────┐
│ Ports / Adapters │
│ SQLite • UCI pool │
└────────────────────────┘
Three strict layers; domain code never imports express, ws, better-sqlite3, or child_process. Every port has a real adapter (SQLite / UCI) and an in-memory fake used in tests. Composition happens in src/server.js.
Full reference: Architecture concepts
Create .env in the project root (copy .env.example to start):
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP + WebSocket listen port |
BIND_ADDR |
127.0.0.1 |
Listen address — use 0.0.0.0 only on a trusted network |
DATA_DIR |
./data |
Directory for chess.db and saved games |
STOCKFISH_PATH |
(none) | Absolute path to the Stockfish binary |
LC0_PATH |
(none) | Absolute path to the lc0 binary |
WEIGHTS_DIR |
./weights |
Directory containing maia-NNNN.pb.gz weight files |
LOG_LEVEL |
info |
pino log level (trace · debug · info · warn · error) |
ENGINE_MODE |
native |
container inside Docker, native otherwise |
Full reference: Configuration · Balance parameters
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage (≥ 90% branch coverage required)
npm run test:coverage
# Lint
npm run lint
# Lint + fix
npm run format
# Full pre-PR gate (lint + coverage + npm audit)
make verify
# Documentation site (local dev server)
npm run docs:dev
# Documentation site (production build)
npm run docs:buildTests are written before implementation (TDD). Deferred tests use test.fails(...) with a dynamic await import() — never a top-level import of a non-existent module.
pawnbook uses Conventional Commits and npm version for releases. A version bump automatically builds and publishes the Docker image.
# Patch release (bug fixes)
npm version patch
git push --follow-tags
# Minor release (new feature, backwards-compatible)
npm version minor
git push --follow-tags
# Major release (breaking change)
npm version major
git push --follow-tagsPushing a v*.*.* tag triggers the Docker release workflow, which:
- Builds a multi-arch image and pushes versioned tags to Docker Hub
- Runs a Trivy HIGH/CRITICAL container scan, uploading results to GitHub Security
- Creates a GitHub Release with auto-generated notes
- Fork the repo and create a branch:
feat/phase-N-<topic>targetingdevelopment - Write tests first —
make verifymust pass before opening a PR - Use Conventional Commits:
type(scope): subject - PRs target
development;development→masterat phase completion
See CONTRIBUTING.md for the full workflow and CODE_OF_CONDUCT.md.
pawnbook's source code (src/, tui/, public/, bin/, scripts/) is MIT.
The engines and Maia weights compiled into the Docker image are GPL-3.0 (Stockfish 18, lc0 v0.32.1, Drawfish, Maia weights). They are not vendored in this repo; the Dockerfile clones each from its upstream repository at build time. Engines communicate with the application over UCI stdio — an arm's-length arrangement that does not cause GPL-3 to reach pawnbook's own source.
See LICENSES.md for the full third-party inventory.