Game review
Key moments
Each move is graded by how much it lowers the mover's winning chances, judged by Stockfish at depth 14: ?! inaccuracy, ? mistake, ?? blunder. The green arrow shows the better move. Accuracy is the average of Lichess's per-move accuracy formula.
How to play
New to chess, or want to get better? The chess course teaches the game in five levels, from how the pieces move to rook endings, with exercises on the board, in six languages (and any other through translation) and optional spoken instructions.
Click a piece to select it — its legal destinations are marked with a dot (or a ring, on a capture) — then click a highlighted square to move there. White moves first. Kings can castle with a rook (kingside O-O or queenside O-O-O) if neither has moved, the squares between are empty, and the king doesn't pass through or land on an attacked square. Pawns can capture en passant immediately after an enemy pawn's two-square advance, and promote — to your choice of queen, rook, bishop or knight — on reaching the far rank.
A king in check is ringed in red; you must get out of check immediately. Checkmate ends the game for the side with no way out; if the side to move has no legal move but isn't in check, it's stalemate — a draw. The game is also drawn automatically when the same position occurs for the third time (threefold repetition), after 50 moves by each side without a capture or a pawn move (the 50-move rule), and when neither side has enough material left to mate. Undo steps back one move at a time; Flip board turns the board around for the other player without changing whose turn it is.
Resign and draw offers. Resign asks for a second click. Against a friend, Offer draw asks the other player to accept or decline (a move instead of an answer declines). Stockfish takes a careful look at the position and accepts only when it isn't better.
Typing moves. Start typing anywhere on the page and the move box takes it: algebraic notation (e4, Nf3, exd5, O-O, e8=Q, upper or lower case) or from-and-to squares (e2e4). Press Enter to play. A pawn reaching the last rank becomes a queen unless you name another piece.
Clocks. Choose a time control under Game. Each clock starts with that side's opponent's first move, and every move adds the increment. A player whose time runs out loses, unless the other side has too little left to mate — then it's a draw.
Review your game. When a game ends, Stockfish goes through it on your device, which takes a few seconds. It draws a graph of White's winning chances and gives each side an accuracy score. Every move is graded: ?! inaccuracy, ? mistake, ?? blunder (and "book" for known opening theory). Key moments lists the mistakes. Show puts that position on the board with a green arrow for the better move. Retry asks you to find a better move yourself: Stockfish checks your answer, and it counts if it's about as strong as the engine's choice. You can also review on demand (Review this game) and turn the automatic review off.
Step through any game with the arrows under the board, the ← → Home End keys, a click on a move in the move list, or a click on the graph. The board is read-only while you look back; Back to the game returns. A new move also brings you back.
Analysis. Engine analysis shows Stockfish's three best lines for the position on the board, with a blue arrow for its first choice. With seven pieces or fewer, the endgame tablebase (Lichess's online service) adds the result with perfect play: win, draw or loss, the moves to mate, and the best moves. In the opening, the panel names the opening and lists the book moves. Against a friend you can click a book move to play it. Analysis and review are available after a game against Stockfish, or at any time when you play a friend.
The coach (against Stockfish, under Opponent) checks each of your moves before it is played. If a move loses too much, it stops you: "Careful — Qh5 is a blunder: it allows a loose piece". You can take the move back, see why (your move and the punishing reply as arrows), or play it anyway. If the move simply misses something much stronger, it says so. Hint shows the piece to move, then the move. Show threats shows what the opponent would do if it could move again. On "On", the coach warns before mistakes and gives unlimited hints. On "Light", it warns only before blunders and gives three hints a game. After three games where it hardly had to step in, it steps back to Light by itself, and a rough game brings it back.
Your training. Reviewed games against Stockfish go into your profile. Each mistake is labelled with the tactic it allowed or missed (the key moments show this, with a link to practise it), and becomes a puzzle that comes back in spaced repetition. My training shows your strong and weak themes and builds a plan for the week.
For children. Chess for kids teaches the pieces in eight little worlds. Children collect stars, capture sleepy pieces, win a pawn war and find checkmates, with a friendly knight who reads every instruction aloud, stars and trophies to win, and big squares for small fingers.
Puzzles. The puzzle trainer has 44,943 puzzles from the public-domain Lichess puzzle database. It gives you a personal puzzle rating (Glicko-2) and puzzles near it, and covers 73 themes; the course's tactics and endgame lessons link to their own theme. There's also a daily puzzle and timed runs. A solved puzzle opens here for analysis.
Save and share. Copy or download the game as PGN, the standard format every chess program
reads, or copy the current position as FEN. Copy link gives an address that replays the game
move by move. Load a game or position accepts a pasted PGN or FEN; against the computer you
take over the side to move. Links can also carry ?fen=, ?moves= or
?pgn=, plus &opponent=engine — the course uses this for its
“Play this position against Stockfish” buttons.
To play the computer, set Play against to Computer — Stockfish, pick your colour and a strength, and just play. Level 1 blunders freely and suits a first game; level 8 is the engine at full (lite) strength and will beat any human. You can switch sides or strength mid-game — the engine picks up from the current position. In this mode Undo takes back your last move and the computer's reply, so it's your turn again.
3D view. Press 3D (above the board, or under Look) to see the board in perspective, with the pieces standing up. With Pieces: Staunton the 3D board uses modelled, lit Staunton pieces (and the flat board uses a detailed SVG set). Drag anywhere on it to turn it around and tilt it — or use the Tilt and Rotate sliders, and Reset view to go back. Everything else works the same.
How the code is built
The whole game is this one HTML file — no build step, no libraries (the optional
Stockfish engine is a separate file it loads on demand; see below). All the
JavaScript lives in a single function (an IIFE) at the bottom, split into
labelled sections with // === banners, mirroring the structure of the
Python/Tkinter version this was ported from:
-
The rules live in one shared file,
src/rules.js, used by this page, the course and the tests. They are pure functions over an 8×8 array of single-letter piece codes (uppercase White, lowercase Black,nullempty), with no DOM and no shared state: move generation (pseudoMovesFor,generateLegalMoves,attackedBy), positions to and from FEN,play(position, move), algebraic notation, the draw rules, and PGN.tests/rules.test.jschecks move generation by perft: counting every move sequence to a fixed depth and comparing with published counts for standard test positions (197,281 sequences four plies from the start, for example). -
Notation (
toSAN) — turns a move into algebraic notation (Nf3,exd5,O-O,e8=Q+), disambiguating with a file or rank only when another legal move of the same piece type shares the destination square.parseMovereads typed moves leniently. -
The screen & the glue (
RENDER,ANIMATION,FLOW) — the only code allowed to touch the DOM.render()paintsstateonto the board;FLOWchangesstatein response to clicks.
A move is a plain array, not a class
Every move is [fr, fc, tr, tc, flag, extra] — from-square, to-square, and
an optional flag ("double", "ep", "promo",
"O-O", "O-O-O") with an extra payload (the promotion piece
letter, for "promo"). pseudoMovesFor generates every
geometrically legal move for one piece — including castling and en passant — without
checking whether it leaves its own king in check.
Legality is "does this expose my king?"
generateLegalMoves takes every pseudo-legal move, plays it on a
cloned board, finds that side's king, and asks attackedBy whether
the opponent attacks that square. Only moves that leave the king safe are legal. This
is the same trick chess engines have used for decades: generate loosely, filter by
simulation, rather than hand-coding pin and discovered-check logic.
attackedBy is the whole rulebook in one function
To ask "is this square attacked by color X", the function checks, from that square outward: the eight knight-jump offsets for an enemy knight, the eight adjacent squares for an enemy king, the two diagonal-forward squares for an enemy pawn, then walks the four diagonals for a bishop/queen and the four ranks/files for a rook/queen, stopping at the first piece it meets in each direction. Check detection, castling-through-check, and "is this move legal" all reduce to calls to this one function.
The computer opponent is Stockfish, talking UCI
The engine is Stockfish.js
— the open-source Stockfish 18
compiled to WebAssembly (the ~7 MB "lite, single-threaded" build, GPLv3), vendored in
chess/engine/. It runs in a Web Worker so it never freezes the page, and
is only downloaded the first time you pick the computer as opponent. The page talks to it in
plain-text UCI, the protocol every chess GUI uses: it sends
position startpos moves e2e4 e7e5 … (the whole game, so the engine also sees
repetitions; a game that began from a loaded position sends position fen … instead), then go depth N, and waits for a bestmove g1f3 line.
That string is matched against this page's own legal-move list, and played through the same
makeMove a click uses — so the engine can never make a move the rules here don't allow.
Strength levels set Stockfish's Skill Level option (0–20, which makes it
deliberately choose weaker moves) together with a search-depth cap. If you undo or start a new
game while it's thinking, the page sends stop and throws away the answer that
comes back.
Game review: winning chances, not centipawns
A second Stockfish worker (so the opponent is never disturbed) evaluates every position of the game at
depth 14. src/review.js converts each score into White's winning chances,
2 / (1 + e−0.00368·cp) − 1, the curve Lichess uses. It then grades each
move by how far it lowered the mover's chances: 0.1 is an inaccuracy, 0.2 a mistake, 0.3 a blunder.
Working in chances rather than centipawns means a slip from +8 to +6 in a won position barely counts,
while +0.3 to −1.0 does. Per-move accuracy follows Lichess's formula, and a side's accuracy is their
average. Moves that follow the opening book are labelled "book", unless the engine calls them a mistake:
the book contains traps too, fool's mate among them. tests/review.test.js covers the
grading, the book and a fast UCI reader checked against the move generator.
The engine worker is driven strictly by the UCI rules: a new search waits for the stopped search's
bestmove before sending options. Sending options mid-search can make the single-threaded
WebAssembly build abort, which is how this was found. A worker the browser kills is restarted.
The coach is the analysis engine, asked before each move
When you move against Stockfish with the coach on, the move is held while the second Stockfish worker evaluates
the positions before and after it at depth 11. The position before has usually been evaluated already, in the
background, while you were thinking. If the move costs 0.2 of winning chances (0.3 on Light), the move is not
played. src/motifs.js names what the engine's reply would do: fork, pin, skewer, loose piece,
discovered attack, back-rank mate and so on. Its rules were tuned against the theme labels of about 11,000 Lichess
puzzles. The coach's searches run one at a time through a small queue, because a new search would stop the one
running.
Opening names from a public-domain catalogue
The 3,864 named lines of lichess-org/chess-openings
(CC0) are replayed through the rules by tests/build-openings.js into
src/openings-data.js, which is loaded once the page is idle. Positions are matched by
placement, side to move and castling rights, so a transposition finds its name whatever the move order.
Draws are counted, not guessed
After every move the page stores a position key: the pieces, the side to move, the castling rights, and the en-passant square, but only when an en-passant capture is actually possible (that is the FIDE definition of "the same position"). Threefold repetition means the newest key occurs three times. The 50-move rule reads the FEN half-move counter, which resets on every capture and pawn move. Insufficient material means only kings remain, plus at most one knight or bishop, or bishops that all stand on squares of one colour. The checks run in that order after the test for mate, so a move that mates always wins.
Clocks run on timestamps
Each side's remaining time is stored as a number. The clock that is running is that number minus the
time since its Date.now() start, so a slow or throttled browser tab can't make it drift.
A 100 ms timer only repaints the display and notices when a flag falls.
Undo is snapshots, not reverse moves
Before every move, the entire board, castling rights, en-passant square and last move are cloned onto a history stack (same approach as the Go and Othello ports). Undo pops the stack and restores it wholesale — no move has to be "run backwards".
Board flip = re-reading one array
cells[r][c] always maps to the DOM cell for logical square (r, c),
regardless of orientation; only buildGrid()'s construction order changes
when flipped, walking visual rows/columns and computing which logical square lands
there. Every other function only ever thinks in logical coordinates.
The 3D view is the same board, transformed
Switching to 3D doesn't swap in a 3D engine. The very same board gets a CSS
perspective and a rotateX(tilt) rotateZ(rotation) transform, with
transform-style: preserve-3d carried down to every square. Each piece then applies
the inverse rotation — rotateZ(-rotation) rotateX(-tilt), read from two CSS
variables — around its base, so the flat Unicode glyph stands up and faces you like a real
chessman wherever the board is turned. Four faces folded down with
rotateX/Y(±90°) make the board's sides. Because it's still the same DOM, the browser
maps clicks through the transform onto the right square, so moves, highlights, animation, undo
and the engine all work unchanged. A drag longer than a few pixels turns the board, and the click
at the end of it is swallowed so it can't also move a piece.
Staunton pieces: SVG in 2D, real geometry in 3D
With Pieces: Staunton, the flat board shows Colin M.L. Burnett's classic SVG set (the one
used on Wikipedia and lichess, here under the GPL). In 3D the pieces become actual 3D models
drawn with three.js (WebGL,
vendored, only downloaded when first needed). Each piece is a LatheGeometry — a
profile of radius against height, spun around its axis like wood on a lathe — plus a few extra
parts: rook battlements, the queen's coronet of spheres, the king's cross, and a knight's head
that is a 2D silhouette extruded into a solid. A directional light casts soft shadows onto a
board whose squares are painted into a canvas texture from the same CSS colours as the flat
board. The hidden DOM board is still the one source of truth: a click on the 3D view is
raycast to a square (a piece you can see wins, unless a highlighted target square is under the
pointer) and handed to that square's normal click handler.
Look = CSS variables + a data attribute
Board colours are CSS custom properties. Choosing "Walnut" just sets
data-board="walnut" on <html>; a CSS rule redefines
--sq-light/--sq-dark; the browser repaints. Pieces are plain
Unicode glyphs (♔♞♟…) in one fixed ink colour — White's glyphs are hollow outlines and
Black's are solid silhouettes, so the two sides read apart without any per-piece color
logic at all.