中文文档 | English
Play it now: https://justa-cai.github.io/chess-master/
The 7.3 MB engine is fetched lazily the first time the AI has to move, then cached by the browser — later visits and reloads load it from cache.
A pure frontend, zero-backend International Chess game: Stockfish 18 compiled to WebAssembly runs entirely in your browser (NNUE neural-network evaluation), behind six difficulty levels from beginner to grandmaster. No server, no account, no installation.
The app offers six difficulty levels, all of them served by the same Stockfish build. This is possible because the official Stockfish WASM build — unlike the Pikafish build the sibling 02.chess project has to work around — still ships the engine's own strength-limiting options:
option name Skill Level type spin default 20 min 0 max 20
option name UCI_LimitStrength type check default false
option name UCI_Elo type spin default 1320 min 1320 max 3190
option name MultiPV type spin default 1 min 1 max 256
So difficulty is a two-stage knob:
- Native weakening —
UCI_LimitStrength+UCI_Elocovers roughly 1320–3190 Elo. - Our own sampling — because
UCI_Elobottoms out at 1320 (too strong for a beginner), the lower levels additionally ask the engine for several candidate moves viaMultiPVand sample among them with a centipawn softmax temperature plus a blunder probability.
| Level | Name | Target Elo | Search | MultiPV | Temperature | Blunder rate |
|---|---|---|---|---|---|---|
| 1 | Beginner | 1320 | depth 2, ≤1.2 s |
6 | 800 | 35 % |
| 2 | Novice | 1320 | depth 4, ≤1.5 s |
5 | 500 | 12 % |
| 3 | Intermediate | 1500 | depth 6, ≤2.0 s |
4 | 200 | 2 % |
| 4 | Advanced | 1900 | depth 9, ≤3.0 s |
3 | 100 | 0 |
| 5 | Master | 2400 | depth 12, ≤4.0 s |
1 | 0 | 0 |
| 6 | Grandmaster | unlimited | ≤3.0 s | 1 | 0 | 0 |
The depth and movetime limits are both passed to go, and Stockfish stops at whichever
comes first — so a low level never searches too deep, and every level has a hard time ceiling.
UCI_LimitStrengthandSkill Levelare two independent mechanisms and are never mixed. That is not a guess: it was measured. On the same middlegame position,go depth 12withMultiPV 1:
| Configuration | bestmove |
nodes |
|---|---|---|
| default (Skill 20, no limit) | d2d3 |
47,397 |
LimitStrength + Elo 1320 |
f3g5 |
185,368 |
LimitStrength + Elo 1400 |
d2d3 |
91,067 |
LimitStrength + Elo 2400 |
d2d3 |
99,326 |
Note the first row: Skill Level already defaults to 20, yet the Elo 1320 row still plays
visibly worse (f3g5 is a planless move). In other words UCI_LimitStrength derives its own
strength during search and ignores Skill Level. Each level therefore uses exactly one of the
two mechanisms — the Elo one, throughout. The finding is recorded in the header comment of
js/difficulty.js so the experiment does not have to be repeated.
Centipawn loss caps keep low levels "weak but coherent". A candidate more than LOSS_CAP
centipawns behind the best move is discarded before sampling (400 / 300 / 200 / 120 cp for
levels 1–4; 600 cp in blunder mode). A level-1 opponent will hang a pawn, but it will not
give away a queen or miss a mate-in-one — that would read as a bug, not as an easy opponent.
The level numbers are initial values awaiting calibration: they must be confirmed by engine-vs-engine matches across adjacent levels, with colours swapped, and the results written back into the comment table in
js/difficulty.js.
- Single engine, no second download. Levels 1–6 are all Stockfish. The whole engine is
two files totalling 7.3 MB (
stockfish-18-lite-single.wasm7.30 MB +stockfish-18-lite-single.js21 KB), fetched once and cached. Compare the sibling02.chess, which needs a 49 MB neural-network weights file on top of its engine. - Runs on GitHub Pages with no special headers. The engine is the lite single-threaded
build —
Threadsreports amaxof1, soSharedArrayBufferis not used and COOP/COEP cross-origin isolation headers are not required. That is why this project has nocoi-serviceworker.js, unlike02.chess. - Answering your own moves, not just playing them. The sidebar table records the real
search depth, node count, NPS, elapsed time and evaluation of every move from the engine's
actual UCI output. When sampling picks a move other than the engine's first choice, the row
is explicitly marked "suboptimal". Fields that are genuinely unavailable show
-; nothing is invented. - Review analysis with an evaluation curve. The engine re-scores the whole game move by
move at full strength, labels each move (best / good / inaccuracy / mistake / blunder) and
draws a win-probability curve. Loss is computed as
V_i + V_{i+1}from the engine's own scores — plies the engine did not reach are shown as gaps, not as zeros. - PGN import / export with SAN notation, and ECO opening recognition for 3,810 named openings (longest-prefix matching over the UCI move sequence).
- Complete rule handling — castling, en passant, promotion (a four-way picker: queen,
rook, bishop, knight — never silently defaulted), threefold repetition, the fifty-move rule
and insufficient material. All of it lives in
js/rules.json top of chess.js; the UI never re-implements a rule. - Three modes — human vs AI (either colour), human vs human on one machine, and engine vs engine, so you can watch two levels play each other.
- Undo takes back two plies in human-vs-AI (the AI's move and yours) and rebuilds the sidebar log to match.
- No main-thread jank — the engine runs in a dedicated Web Worker.
The project ships a static development server, server.py, bound to port 6325
by default:
python3 server.py
# then open http://127.0.0.1:6325/Unlike 02.chess's server, this one does not inject COOP/COEP headers — the single-threaded
engine does not need them. It only does the two things that are genuinely necessary:
- serves
*.wasmwith the correctapplication/wasmMIME type, so the browser does not fall back to the slow path; - sends long-lived cache headers for
.wasm/.nnue/ font files andno-storefor everything else, so edits show up on refresh.
Any static server works, e.g. python3 -m http.server 8000.
node scripts/build_openings.mjs
# reads third-party/chess-openings/{a..e}.tsv, writes data/openings.json (3,810 openings)The site is 100 % static — no build step, no dependency install, no server. Push the repository and point GitHub Pages at the branch root:
git init
git add -A
git commit -m "Chess Master: Stockfish 18 WASM, six difficulty levels, pure frontend"
git branch -M main
git remote add origin git@github.com:<user>/chess-master.git
git push -u origin mainThen Settings → Pages → Source: Deploy from a branch → main / / (root).
Two details that matter:
.nojekyllis committed at the repo root. It stops GitHub Pages from running Jekyll over the site, which would otherwise slow the build down and silently skip files it treats as special.- No
coi-serviceworker.jsis needed. The engine is single-threaded, so theCross-Origin-Opener-Policy/Cross-Origin-Embedder-Policyheaders GitHub Pages cannot set are simply not required. (This is the one place where this project is meaningfully simpler than02.chess.) - All asset paths are resolved at runtime —
js/engines/bridge.jsderives the engine URL from its own script URL andjs/openings.jsderives the data URL fromimport.meta.url— so the site works correctly both at a domain root and under a project sub-path such as/chess-master/.
index.html page shell; loads difficulty.js + bridge.js as classic
scripts, then app.js as an ES module
js/
difficulty.js single source of truth for difficulty (classic script → global)
rules.js chess.js wrapper — the only place rules are implemented
openings.js ECO opening recognition (data/openings.json)
analysis.js review analysis + SVG evaluation curve
app.js the single orchestrator: game state, engine scheduling, PGN, review
ui/
board.js chessground integration + promotion picker
panel.js sidebar rendering
menu.js menu pages, dialogs, toasts
engines/
bridge.js engine facade: Worker lifecycle + UCI protocol + request pairing
stockfish/ Stockfish 18 lite single-threaded WASM (unmodified)
vendor/
chessjs/ chess.js 1.4.0 (unmodified)
chessground/ chessground 9.2.1 + brown/cburnett styles (unmodified)
data/openings.json compiled opening book
third-party/chess-openings/ CC0 source TSVs for the book
styles/ design tokens → base → components → main
scripts/build_openings.mjs TSV → JSON compiler
specs/ prd.md, ui.md, project_tree.md
server.py local static dev server (port 6325)
NOTICE.md third-party components and licenses
LICENSE GNU GPL v3.0
GNU General Public License v3.0 — see LICENSE.
The project is GPL-3.0 rather than something more permissive because its core engine, Stockfish, is GPL-3.0 and is compiled to WebAssembly and distributed with the site; the GPL's copyleft propagates to the whole distributed artifact.
Third-party components and their licenses are listed in NOTICE.md. In short:
| Component | License | Modified? |
|---|---|---|
Stockfish 18 lite single-threaded WASM (stockfish@18.0.8) |
GPL-3.0 | no — byte-identical to the npm package |
| chess.js 1.4.0 | BSD-2-Clause | no |
| chessground 9.2.1 | GPL-3.0-or-later | no |
| cburnett piece set / brown board (inside chessground's CSS) | CC BY-SA 3.0 | no |
| lichess-org/chess-openings | CC0-1.0 | compiled to JSON |
If you redistribute this project, keep these notices and the corresponding license texts.