Self-contained chess puzzle trainer — pure HTML/JS, no server, just tactics 👉 try it now
I made this for myself and my son. We both enjoy chess puzzles, but online trainers come with rankings that go up and down. My boy gets upset when his rating drops after a failed puzzle.
Lichess puzzles are fantastic — if you're happy with them, just don't bother. But if you want to train your favorite collections (from PGN) without ratings or pressure, Czesia might help.
This whole thing came together in a few hours of pair-programming with AI coding agents — Claude and Cursor. Turns out they're pretty good at turning ideas into working code.
- Create a folder in
puzzles/with your collection name - Add an
info.jsonfile describing your collection - Add PGN files following the naming pattern
{number}_{motive}.pgn - Run
bun run buildto generate JSON files
puzzles/
└── my-collection/
├── info.json # Required: collection metadata
├── 01_pin.pgn
├── 02_fork.pgn
└── 03_mate_in_2.pgn
{
"id": "my-collection",
"name": "My Collection",
"source": "Original puzzles",
"puzzleIdPrefix": "MC",
"defaultType": "static",
"puzzlesPerFile": 250
}| Field | Required | Description |
|---|---|---|
id |
Yes | Folder name, used in URLs |
name |
Yes | Display name in the UI |
source |
Yes | Attribution shown with each puzzle |
puzzleIdPrefix |
Yes | Prefix for puzzle IDs (e.g., "MC" → "MC-01-145") |
defaultType |
Yes | "static" or "dynamic" (see below) |
puzzlesPerFile |
No | Max puzzles per JSON file; large PGNs get split |
- Static: Like puzzles from a chess book. You see the position, you find the best move. Board is shown from the side to move.
- Dynamic: Like Lichess training. Opponent plays first (often a blunder), then you punish it. Board is shown from your perspective.
Most book collections use "static". Use "dynamic" if your PGN includes the opponent's blunder as the first move.
Filename pattern: {number}_{motive}.pgn
Examples:
01_pin.pgn→ Motive: "Pin"02_back_rank_mate.pgn→ Motive: "Back Rank Mate"
Files not matching this pattern are skipped with a warning.
Required PGN header — the starting position:
[FEN "6k1/5pp1/7p/3p4/b7/6P1/5PKP/3R4 w - - 0 1"]
1. Rd1-d5 Ba4-c6 *
Optional — preserve original puzzle numbering with [OriginalID]:
[OriginalID "145"]
[FEN "6k1/5pp1/7p/3p4/b7/6P1/5PKP/3R4 w - - 0 1"]
1. Rd1-d5 Ba4-c6 *
If OriginalID is missing, puzzles are numbered 1, 2, 3... based on their order in the file.
bun install # First time only
bun run build # Generates data/ folder with JSON filesThe data/ folder is gitignored — it's generated fresh during CI deployment.
This project stands on the shoulders of two great libraries:
- Chessground — the beautiful, responsive chessboard (from Lichess)
- chess.js — the brains behind move validation
Huge thanks to their authors for making chess development so accessible.
A pre-commit hook auto-updates the version indicator in index.html. To restore it on a fresh clone:
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
DATE=$(date +%Y.%m.%d)
sed -i '' "s/<div class=\"version-indicator\">.*<\/div>/<div class=\"version-indicator\">$DATE<\/div>/" index.html
git add index.html
EOF
chmod +x .git/hooks/pre-commit