
Turning a player's worst moves into drills the engine grades
11 / 11 regression positions taggedMy DataTalks.Club AI Engineering Buildcamp capstone. It syncs a player's whole history, finds the moves that cost the most with Stockfish, brings them back as graded drills on a spaced schedule, and answers questions through a PydanticAI agent with twelve tools, which can also rank each practice session and write its prompts. It has since grown into a full trainer: labelled game review, bots that blunder by rating, a sparring partner built from past opponents, and an opening repertoire on its own schedule.
Club players collect hundreds of online games and rarely turn them into a training plan. An engine can say a move lost 2.3 pawns, but not which habit caused it: a loose piece, a missed check, an uncastled king, a queen that wandered in the opening. And nothing brings the same position back a week later, so the lesson fades.
I built Chess Coach Agent as my capstone for the DataTalks.Club AI Engineering Buildcamp. It imports a player's Chess.com, Lichess or PGN games, finds the moves that cost the most, and turns them into board drills that Stockfish grades and a schedule brings back. A PydanticAI agent answers questions about the game on screen, and when an API key is set the same agent also picks each practice session and writes its questions. It is told to take every chess fact from Stockfish and the stored games. In the latest demo recording the sample account holds 2,777 Chess.com games and 7,880 positions ready to drill.
After the course I kept going, against what Chess.com Game Review, Lichess and Aimchess already offer. The review now labels every move, eight bots and a sparring partner play full games, an opening repertoire runs on its own schedule, a screenshot of a board can become a position, and a Today page says what to do next. The rule from the first version still holds: every screen starts from the player's own games, and Stockfish decides what is true.
- 2,777
- games synced for the sample account
- 7,880
- positions from those games ready to drill
- 11 / 11
- regression positions tagged with the expected theme
- 3.0 → 5.5
- judge score for coaching text, out of 6
Starting from how players get better
The research on chess improvement points at practice more than play. Campitelli and Gobet's review concludes that a large amount of deliberate practice is necessary for master level, though not sufficient on its own. Deliberate practice means working on your own weak spots with feedback, which a casual player rarely has a coach to give. Memory research adds the other half: Cepeda and colleagues pooled 317 experiments and found that spaced study beats cramming, and that the best gap grows with how long you want to remember.
I compared twelve products against that before writing code, from Chess.com Game Review and Lichess to LLM coaches like Chessy and Chessiro. Each one covers part of the loop and none covers all of it.
| What exists | What it gives you | What's missing |
|---|---|---|
| Post-game engine analysis | Every move scored, the best line | Why it went wrong, and a reason to look again next week |
| Puzzle and course trainers | Spaced, graded drills | Positions from someone else's games, not yours |
| LLM chat coaches | A friendly explanation of your game | No documented check that the chess claims are right |
| A human coach | Your weaknesses, explained, with homework | Cost and availability |
So the brief stayed narrow: take the player's own games, find the moments that cost them, explain them in a coach's words, and bring each one back on a schedule. Stockfish decides what is true. The language model explains and organises.
- CloudYour gamesChess.com, Lichess, PGNup to 5,000
- CodeStockfishwinning chancesloss ≥ 0.20 = mistake
- CodeDrill builderworst moments first
- CodeGraderStockfish checks your move
- OutputSchedule1, 3 or 7 daysup to 30
due drills come back into practice, and difficulty follows your accuracy
import, detection, grading and scheduling never wait for a language model; building a session does when a key is set
Deciding what counts as a mistake

A full history is too much to review by hand. The sample account has 2,777 games, and a coach that flagged a fixed number per game would invent mistakes in clean games and hide them in messy ones.
I let the engine decide. A background job pulls up to 5,000 games, skips any it has already analysed, and runs Stockfish on every move. A game can come back with no critical moments at all, or with a long list.
Centipawns alone mislead. Losing two pawns of advantage matters in a level game and hardly at all when you're a rook up. So scores become winning chances first, with the formula Lichess publishes, and a mistake is a drop in those chances.
winning_chances = 2 / (1 + exp(-0.00368208 × cp)) − 1 (cp clamped to ±1000) loss = winning_chances_before − winning_chances_after
- Inaccuracy0.10
- Mistake0.20
- Blunder0.30
A move Stockfish itself picked as best is never labelled, which keeps out the noise where two searches disagree slightly. Set the evaluation before and after a move to see where the lines fall.
Is it a mistake?
The same pawn and a half matters far less when you are already winning big. Losses of 0.10, 0.20 and 0.30 mark an inaccuracy, a mistake and a blunder; only mistakes and blunders become drills. This is the formula from the project, running in your browser.
Only after the engine has confirmed a loss do the theme detectors run. They explain a real loss in human terms and never create one.
| Theme | When it applies |
|---|---|
| Time pressure | A mistake played with under 10% of the base clock left, read from the PGN clock. Checked first |
| Loose piece | The move newly leaves a minor or major piece attacked and undefended |
| Opening drift | An engine-significant, repeated early queen move |
| King safety | Danger near the king rises while it is still uncastled |
| Endgame conversion | An engine-significant error in a simplified position |
| Missed tactic | The default, when the loss is real and nothing narrower fits |
Labelling every move

Mistakes alone make a review read like a list of faults. Players coming from Chess.com expect every move labelled and an accuracy for each side, and without them the review felt thinner than the free tools they already use.
I kept the same winning-chances scale, so a label and a drill never disagree about one move. Each move goes through the checks in a fixed order and takes the first label that fits.
| Label | Rule, in winning chances |
|---|---|
| Book | Loses under 0.10 and stays inside a named line in the Lichess openings list |
| Miss | Loses 0.10 or more right after the opponent lost 0.20, giving back their error |
| Blunder, mistake, inaccuracy | Loses 0.30, 0.20 or 0.10 |
| Brilliant | Loses under 0.04, gives up at least two pawns of material, and the mover was not already clearly winning |
| Great | The only good move: the second engine line is 0.20 worse and changes the likely result |
| Best, excellent, good | The engine's move or a loss under 0.01, under 0.04, and the rest |
Accuracy uses the Lichess formula: each move's win-percentage loss becomes a 0 to 100 score, and the game figure averages a mean weighted by how volatile the position was with a harmonic mean, so one blunder in a calm game costs more than it would in chaos.
The review page keeps its panels under four tabs, Moments, Moves, Coach and Summary, and adds an eval bar and graph, a Retry this move button on any labelled mistake, a heatmap of which side controls each square, and an annotated PGN download. The coach can now point as well as talk. Its answer carries up to four arrows and six highlighted squares, and the server drops any arrow that isn't a legal move in that position before the board draws it. The browser's own speech engine reads answers aloud, and each answer has helpful and not-helpful buttons.
Turning a mistake into a drill


Reviewing a mistake once doesn't make it stick. Chess.com's Retry and Lichess's “Learn from your mistakes” replay the position inside that one game, and then it's gone.
Stockfish grades every attempt, so the model never decides whether you were right.
I store each mistake as a training position, and without a model a session takes the most severe ones first. Every drill comes from the player's own games. The Today page opens on what is due, the latest game and the top leak, ranked by points lost per 10 games, and one button starts a session on that leak.
The drill adapts to the player: with three or more attempts on a theme, accuracy over the last ten sets the difficulty; before that, rating does. Beginners pick from three moves with a hint on offer, intermediate players from four, and advanced players type the move. The wrong choices are forcing moves, checks first, because those are the ones a player is tempted by.
Drill from move 13 of a stored game, White to move
You had a missed tactic pattern in your game. What would you play?
Difficultybeginner
Three choices, with a hint on offer.
The grade then sets when the position returns.
- Failed1 day
- Solved with a hint3 days
- Solved clean7 days
- Each further success×2, max 30
Add attempts to one position below and watch the gap stretch after each clean solve and drop back after a miss.
When a drill comes back
A miss brings the position back tomorrow. A hinted solve waits 3 days but still counts toward the streak. A clean solve waits 7, then 14, 28 and at most 30 as the streak grows. This is the rule in the project's training code.
A player with few stored mistakes on a theme would get a short session, so rated Lichess puzzles fill the gap. The library holds 1,250 of them, 250 per theme, mapped from Lichess tags such as hangingPiece to the app's themes. They are picked near the player's rating, 150 points lower for a beginner and 150 higher for an advanced player, and the search widens in steps until it finds ones the player hasn't seen. The player's own positions always come first; puzzles only top up the session.
Grading had one bug worth naming. Stockfish reports a forced mate as a score of 100,000, and the grader subtracted those raw, so missing a mate counted as a loss of about a thousand pawns. Both scores are now clamped to ±1000 centipawns before the subtraction, the same clamp the move labels use.
Building bots that lose like people
Playing full games against Stockfish teaches little below master level, because it never makes the mistakes a club opponent makes. Turning its depth down doesn't help either: a shallow engine plays strange moves, not human ones. Maia, the network trained to predict human moves, would be the right tool, but it needs lc0, which the container doesn't ship.
So I put a move model on top of Stockfish. It asks for the eight best moves, converts each to a win percentage, and samples from them. Lower ratings sample more loosely and sometimes blunder on purpose, more often in sharp positions with checks and captures on the board.
temperature = max(0.6, 14 × exp(−(rating − 600) / 600)) blunder_chance = min(0.30, 0.25 × exp(−(rating − 600) / 450)) × complexity (complexity 0.5 to 1.6) move ~ softmax(−loss / temperature + style bonus)
| Rating | Temperature | Blunder chance | Search depth |
|---|---|---|---|
| 600 | 14.0 | 25% | 6 |
| 1200 | 5.2 | 6.6% | 8 |
| 2000 | 1.4 | 1.1% | 11 |
Eight bots run from 600 to 2200 in three styles. Aggressive bots score checks, captures and moves toward the king higher. Tricky ones like quiet moves that leave a piece en prise as bait. Solid ones prefer castling, development and recaptures. Each plays book moves from the Lichess openings list for its first six to fourteen plies, more for stronger bots.
The ninth opponent is a sparring partner. It reads the first sixteen plies of every stored game, keeps the moves the player's real opponents chose, and replays them by frequency, with a note like “Your opponents played c6 here in 5 games.” Once it runs out of history it hands over to the same engine model at the player's own rating.
Over 150 samples per position, the 600 bot plays a losing move more than 15% of the time, the 2200 bot under 8%, and the gap is more than fourfold
No bot has been played against real players, so the ratings are labels, not measured Elo
The sparring partner replays c6 after e4 when five stored games did
Its book reads only sixteen plies
Scheduling an opening repertoire
Opening mistakes repeat because the same player meets the same openings. The game library already shows which lines a player reaches, so the repertoire builder starts from it rather than from a blank tree.
For the first twelve plies, it takes the move the player chose most often in games that scored at least 40%, and adds the opponent's three most common replies. If half the games with that move labelled it an inaccuracy or worse, the engine's move goes in instead, with a comment saying how often the player chose the old move and what the engine prefers, and the line stops. Positions are stored by FEN without move counters, so transpositions meet in one node.
- Wrong10 minutes
- First correct1 day
- Second correct3 days
- After that× ease, max 180
The drill shows the board after the shortest route to each due position and accepts any move in the repertoire from there. These intervals are longer than the mistake drills' 30-day cap, because an opening line is meant to be kept for years.
Grounding the coach in the engine

A language model will describe a chess position with confidence and get it wrong. It can't calculate, and a plausible variation reads the same as a real one. Chess is a bad place to trust it on facts.
I gave the agent twelve tools and told it to treat engine evaluations, legal moves and stored game facts as its only evidence. It answers in a panel beside the board on the review, practice, analysis and play pages. The Moments and Summary tabs never call the model.
Chess is a bad place to trust a language model on facts.
Four tools work on the selected game, the lessons and the board in front of the player. The other eight reach the database of stored games or Stockfish, and three of those exist for building practice sessions.
| Tool | Source | What it returns |
|---|---|---|
search_chess_principles | Lesson files, BM25 | Top 3 lesson snippets with scores |
inspect_critical_moments | Selected game | Up to 4 moments: theme, played and engine moves, eval swing |
inspect_position | Stockfish, python-chess | Best move, evaluation, checks and captures for a FEN |
build_training_drill | Selected game | A drill for the game's main weakness |
inspect_game | Database | Any stored game with its moments by severity |
compare_moves | Stockfish | Legality and evaluation for up to 6 candidates |
generate_position_quiz | Database | A quiz from the player's own moments |
evaluate_candidate_move | Database, Stockfish | Grades a move in a saved position and records it |
build_training_session | Database | The next adaptive quiz plus the player profile |
inspect_player_moments | Database | Up to 20 stored moments across the player's games |
rank_practice_moments | Database, model | Moment ids ordered by the player's recurring themes |
write_quiz_copy | Database, model | A prompt and hint for each position in a session |
The server adds verified context before the model runs, the answer must validate as a Pydantic model, and the browser keeps the conversation so the server holds no state. Step through one real answer from the evaluation set.
One question from the evaluation set, traced
“Why did I lose my queen on move 13 after Qe2? What should I have played instead?”
engine's movemove playedthreat
01 / 07Browser
POST /api/coach/ask
The Coach panel sends the question with the selected game, the position on the player's board, and the message history from earlier turns. The server keeps no conversation of its own.
question: "Why did I lose my queen on move 13 after Qe2? What should I have played instead?"
When the model is missing, fails or passes 60 seconds, the server sends an offline answer built from engine data, and the panel labels it as offline. It uses the moment on the player's board if there is one, and otherwise the game's top moment.
Letting the agent build practice sessions
Practice has gone back and forth. In June a twelve-tool agent sat behind the Start practice button, and I took it out along with the public chat routes, so that sessions came straight from Stockfish data. The plain version worked, but every drill asked the same question, “What would you play in this position?”, and the order ignored which habits a player keeps repeating.
So the agent is back as an option. With a key set, starting a session runs the same twelve-tool agent: it reads the stored moments, ranks them against the player's recurring themes, builds the session and writes a prompt and hint for each position. The merge kept the puzzle top-up, and the model can only reorder moments that already exist. Ids it invents are dropped, and hints are dropped for everyone above beginner.
Without a key
- Moments ordered by severity
- Template prompts for each theme
- No wait on a model
With a key
- The agent ranks moments by recurring themes
- Prompts and beginner hints written per position
- Session start waits on the model
Retry on one move and puzzle-only sessions skip the agent; if the agent fails, severity order and templates take over, though ranking and copy still try the model when a key is set
The trade is latency for prompts that name what happened in the position, and I haven't measured how long that wait is. It sits in the one place I had removed it from. Sessions have no overall deadline the way coach answers do, only a timeout on each model request. Stockfish still grades every answer, so a weak prompt can make a drill read badly but can't mark a wrong move as right. The prompt text itself is checked by nothing except its instruction not to invent moves.
Reading a board from a screenshot
Players often meet a position outside their own games: a puzzle in a book, a stream, a friend's phone. Typing it in square by square is slow enough that most never do.
The Set up position page takes an image. A vision model returns the board as eight rows of eight piece letters with the orientation, side to move and its confidence, and python-chess turns that into a position. Then the rules check it: one king each, no more than eight pawns or sixteen pieces a side, no pawns on the back ranks, and the side not to move not in check. If a check fails, the problems go back to the model for one more try, and the player can fix any square by hand before analysing.
- Gemini Flash6 / 6
- Claude Haiku3 / 6
Six boards is a small set, and it isn't in the repository yet, so this result can't be rerun the way the others can. The position checks matter more than the model choice: they catch the errors that would otherwise reach the engine as an impossible position.
Choosing retrieval by measurement
The coach needs the right lesson to cite, and the knowledge base is small: four hand-written lessons on tactical vision, loose pieces, opening principles and king safety, cut into chunks of about 500 tokens. On a set that small, the obvious choice of vector search may not win.
I wrote 20 queries with known answers and benchmarked four strategies. At start-up, the server reads the results file and picks the winner.
- CodeTitle searchbaseline
- CodeBM25pure Pythonthe default
- CodeVectorbge-small, 384-d
- CodeHybridreciprocal rank fusion
hybrid replaces BM25 only if it matches it on hit rate and MRR while staying under twice its latency
| Strategy | Hit rate at 3 | MRR | Right source | Median latency |
|---|---|---|---|---|
| Title only | 0.35 | 0.35 | 0.65 | 0.084 ms |
| BM25 | 0.95 | 0.858 | 0.95 | 0.124 ms |
| Vector | 0.95 | 0.725 | 0.95 | 0.338 ms |
| Hybrid | 1.00 | 0.850 | 1.00 | 0.671 ms |
Judging the coach before trusting it
Good-sounding coaching text can still be wrong about the position, and nobody reads hundreds of answers by hand. The checks had to run on their own and be cheap enough to repeat.
I built four layers. Each answers one question, and each uses data I wrote rather than data a model generated.
| Layer | Question it answers | Result |
|---|---|---|
| Theme detection | Did Stockfish and the rules find the right theme? | 11 / 11 positions |
| Retrieval | Which search strategy should production use? | BM25, on 20 queries |
| Analysis text | Is the explanation grounded and coach-like? | 5.5 / 6 after tuning |
| Live coach | Does the agent pick tools and the right move? | 6.2 / 8 on 15 questions |
For the analysis text, an LLM judge scored theme, grounding and coaching quality against my reviewer notes, 0 to 2 each. A short format averaged 3.0 out of 6 and passed half the cases. A grounded format, with the played move, the engine move, the principle and a drill, scored 5.0. The judge then pointed out that the opening-drift answer never linked repeated queen moves to lost development and delayed castling. I rewrote it around that, the case went from 4 to 6, and the average reached 5.5. I also reviewed the two original cases by hand: both acceptable, with one improvement noted.
Good-sounding coaching text can still be wrong about the position.
The live coach got its own set: 15 questions phrased the way a player asks them, each with acceptable moves, forbidden moves and key points, every one checked with Stockfish at depth 20. I ran two system prompts on three models. Pick a measure, then a row.
15 questions · 3 models · 2 system prompts · one run each
Longer is better. The darkest bar is the best on this measure. Pick a row to see how the judge scored it.
Judge criteria, 0 to 2 each
Claude Haiku 5.5 · baseline
- Grounded in evidence1.53
- Correct on key points1.40
- Useful drill1.80
- No invented lines1.47
A pass is 6 of 8. The judge was told the coach's engine runs at depth 9, so evaluations within about 0.7 pawns of the depth-20 reference count as matching. No answer from any model recommended a forbidden move.
Haiku and Gemini Flash tie on the judge, but Haiku answers in about a third of the time, so Haiku is the default in the repository. The judge is also Haiku, so it could favour itself; Gemini matching it on the same scale suggests the gap to MiniMax is real. Neither prompt won clearly. With one run of 15 questions, half a point is noise, so the baseline prompt stays. This run used the nine tools the coach had at the time. I haven't rerun it on the twelve-tool agent.
On the knight-fork question every model missed the depth-20 best, Qh4, because the coach's engine prefers Qf6
The coach repeats its depth-9 engine. Engine depth caps how good the advice can be
Haiku fell back to the offline answer on 1 to 2 of 15 questions, then passed 12 of 12 on a rerun
It looks like an intermittent provider error. The results file has no cause, but the code now returns the exception as fallback_reason and logs it
The fallback named the game's first moment, not the one asked about
The judge scored it low, correctly. The fallback now looks for the moment on the player's board first; that change hasn't been through this evaluation
The whole comparison, 90 answers and 90 judge calls, took 8.5 minutes and at most $0.38. The tests swap in PydanticAI's test model and a fake judge, so CI runs the same pipeline with no key.
The practice agent is the thinnest part of this. Ten scenarios list the tools it should call, but the published 10 out of 10 is an offline run that feeds the expected tools back in and uses a judge that passes everything, so it proves only that the pipeline runs. A --live flag scores real runs, and no live result is in the repository. The check I can stand behind is a manual one: three scenarios reviewed by hand, all acceptable, with no invented positions.
Watching every answer in production
Once real players use it, the questions change: how often does the model fail over to the offline answer, which tools does it call, and do players find the explanations useful?
I log in two layers. A JSONL event log works with no account. With a Logfire token set, PydanticAI is instrumented, so every coach question becomes a coach_ask span with each model request and each tool call, arguments and results included.
| Event | Logged when |
|---|---|
games_sync_completed | A full-history sync job ends |
analysis_completed | A game finishes analysis, with moment and theme counts |
training_session_created | A practice session starts |
quiz_attempted | A practice move is graded, with hints used |
moment_feedback | A player rates a critical moment helpful or not |
coach_answered | The coach answers, with tools, tokens, cost and any fallback_reason |
coach_feedback | A player rates a coach answer helpful or not |
practice_agent_completed | A session is built, with tools, duration and whether it fell back |
Collect
Each critical moment and coach answer has helpful and not-helpful buttons. A moment rating stores the moment, theme, FEN and comment.
Export
make feedback-exportturns moment ratings and not-helpful coach answers into rows marked as candidates.Review
A person checks each row and adds the PGN. Noisy ratings never become ground truth on their own.
Re-run
Accepted rows join the dataset, and the theme and judge evaluations run on them.
Shipping one container
A capstone is graded on whether someone else can run it. Stockfish, a database, a Python API and a React app are four things to install, and each is a chance for a reviewer to give up.
I put them in one image: the built React app, Stockfish, Alembic migrations, lesson ingestion and FastAPI. Render runs that image with managed Postgres and pgvector; locally, SQLite needs no credentials.
Sync once
A durable job per player fetches the whole history and reports progress the page polls.
Store
Postgres in production, SQLite on a laptop. Games, moments and attempts stay relational; only the lesson chunks get vectors.
Test
The 154 pytest tests need no network or key. CI runs it, both offline evaluations, Postgres migrations and the frontend build.
Deploy
One Render blueprint, with
/api/healthas the health check and the API key asked for at setup.
The game library was the slowest screen. It sent every game's stored analysis in one response, 78 MB for the sample account. Now the server keeps a light index of each player's games in memory and loads the heavy analysis only for the page on screen, 50 games at a time, and the first page is 26 KB.
Against the course rubric, the project scores 35 of 35 on its own scorer script. A strict reviewer might take two points off, because the docker-compose frontend proxies the API to the wrong host.
Naming what is still weak
The numbers above come from small sets I wrote myself. They show the loop works, not that the coach is right about chess in general.
Known limits
- Four lessons in the knowledge base
- 11 ground-truth positions, two of them reviewed by hand
- Stockfish at depth 9 by default caps the advice
- Review-page explanations are templates over six themes
- Bot ratings are labels; no bot has played a rated human
- The six vision test boards aren't checked in
- The practice agent has no live evaluation or session deadline
- Coach and practice routes have no sign-in or rate limit, so they can spend the API key
Build next
- Calibrate the bots against real games, or ship Maia when lc0 fits the container
- Move sync to a worker with a lease, so a restart can't strand a job
- Pool Stockfish across games, so a full history syncs faster
- Run the agent scenarios live and measure how long a session takes to start
- Rerun the coach evaluation on the twelve-tool agent
- Show tool use on the quality page; tokens and cost are already there
Notes
- The engine numbers in the traced answer are from the evaluation run on 29 October 2026. Move choices in the drill figure are one shuffle of the distractors the training code picks, and its prompt and hint are the templates used without a model.
- Screens are frames from the demo recording in the repository, made on 29 October 2026. The live demo's database starts empty, so the sample account's data isn't there until someone syncs it.
- The live demo reports
minimax/minimax-m3as its model, while the repository's blueprint sets Claude Haiku 5.5. The evaluation numbers above are for the models named in them. - On Render's free plan the event log lives in
/tmp, so the quality page resets on redeploy. - The repository holds the sample games, the four lessons and every evaluation dataset, so each number here can be rerun.
Sources
- Campitelli and Gobet, Deliberate practice: necessary but not sufficient ↗, Current Directions in Psychological Science, 2011.
- Cepeda, Pashler, Vul, Wixted and Rohrer, Distributed practice in verbal recall tasks ↗, Psychological Bulletin, 2006.
- Lichess accuracy and Win% ↗, and the 0.10 / 0.20 / 0.30 thresholds in the Lichess source ↗.
- PydanticAI ↗, the agent framework, and Logfire ↗ for tracing.