Puzzles: rated training on Lichess Puzzles, with Themes, Hints and a History #440

Closed
opened 2026-10-16 08:13:49 +00:00 by root · 18 comments
root commented 2026-10-16 08:13:49 +00:00 (Migrated from gitlab.core-project.space)

Problem Statement

  • A Player can only play whole Games, Local or Online. There is nothing to train tactics on between Games, and no measure of how a Player's tactical level evolves.
  • The site has no notion of a Puzzle, a rating, or a training history.

Solution

  • A signed-in Player opens Puzzles and gets a Puzzle near their level: the opponent's move plays, then the Player finds the line. The server checks every Move, so the answer cannot be read from the browser.
  • Each Account has a Puzzle rating (Glicko-2) that only the first try at each Puzzle moves: a wrong Move, a Hint, showing the solution or giving up fails it on the spot. After a failure the Player may retry, unrated.
  • The Player can train on one Theme (fork, pin, mate in 2…) from a curated list.
  • A History page lists every Puzzle the Player has tried, and any of them can be replayed, unrated.
  • Puzzles are a curated sample of the Lichess puzzle database (CC0), loaded on every stack.

User Stories

Access

  1. As a Player with no Session, I want opening Puzzles to take me to sign-in and back, so that I land on Puzzles once signed in.
  2. As a Guest, I want to be asked to sign in or sign up to use Puzzles, so that my Puzzle rating and history belong to an Account.
  3. As a signed-in Player, I want a Puzzles entry in the navigation, so that I can find training from anywhere.

Solving

  1. As a Player, I want the board to show the Puzzle's position and then play the opponent's Move, so that I see what I am answering.
  2. As a Player, I want the board oriented with my Color at the bottom, so that I read the position from my side.
  3. As a Player, I want to be told whose Turn it is and what to look for ("find the best Move for White"), so that the task is clear.
  4. As a Player, I want only my own pieces movable, so that I cannot play the opponent's side.
  5. As a Player, I want a correct Move to be answered by the opponent's reply automatically, so that the line plays out like a Game.
  6. As a Player, I want a Puzzle that ends with my Move to be marked solved at once, so that I know I found it.
  7. As a Player, I want any Move that delivers Checkmate to count, even if it differs from the stored line, so that a valid mate is never marked wrong.
  8. As a Player, I want any other Move off the stored line to be marked wrong, with the right Move shown, so that I learn the answer.
  9. As a Player, I want Promotion to work in a Puzzle as in a Game, so that promotion Puzzles are solvable.
  10. As a Player, I want the last Move highlighted and a checked King shown, as in a Game, so that the board reads the same everywhere.

Puzzle rating

  1. As a Player, I want a Puzzle rating that starts at 1500, so that I have a level from my first Puzzle.
  2. As a Player, I want my Puzzle rating to rise when I solve a Puzzle and fall when I fail one, so that it tracks my level.
  3. As a Player, I want the change shown right after the result (+12 / −9), so that I feel each Puzzle's weight.
  4. As a new Player, I want my Puzzle rating to move quickly at first, so that it reaches my level in a few dozen Puzzles.
  5. As a new Player, I want an icon beside a provisional Puzzle rating, with a tooltip explaining it moves quickly at first while the site learns my level, so that early swings do not surprise me.
  6. As a Player whose Puzzle rating has settled, I want the provisional icon gone, so that the number reads as trustworthy.
  7. As a Player, I want the rating to move on my first wrong Move, not at the end, so that the result is honest and immediate.
  8. As a Player, I want my Puzzle rating the same across devices, so that training anywhere counts.

Retry, Hint, solution

  1. As a Player who failed a Puzzle, I want to retry it from the start, unrated, so that I can find the line myself.
  2. As a Player, I want a Hint that highlights the piece to move, so that I get unstuck without seeing the full answer.
  3. As a Player, I want to be warned before my first Hint on a rated Attempt that it counts as a failure, so that I choose knowingly.
  4. As a Player, I want "Show solution" to play the rest of the line on the board, so that I learn it.
  5. As a Player, I want "Show solution" and giving up on a rated Attempt to count as a failure, so that the rating stays meaningful.
  6. As a Player, I want "Next" after a solved or failed Puzzle, so that I can keep training.

Choosing the next Puzzle

  1. As a Player, I want the next Puzzle to be close to my Puzzle rating, so that it is neither trivial nor hopeless.
  2. As a Player, I want never to be served a Puzzle I already tried, so that every served Puzzle counts.
  3. As a Player, I want a Puzzle still to be served when none is left near my rating, so that I never hit a dead end.
  4. As a Player who reloads or leaves mid-Puzzle, I want the same Puzzle back, so that my progress is kept, and so that no one can skip a hard Puzzle for free.
  5. As a Player, I want to give up a pending Puzzle explicitly, so that I can move on at the cost of a failure.

Themes

  1. As a Player, I want to choose a Theme from a curated list, so that I train one motif.
  2. As a Player, I want Theme training to move the same Puzzle rating, so that I have one level, not twenty.
  3. As a Player, I want to go back to mixed training, so that the Theme is a filter I can drop.
  4. As a Player, I want Theme names in my language (English or French), so that I understand them.
  5. As a Player, I want my chosen Theme remembered while I train, so that "Next" stays in it.
  6. As a Player, I want a pending Puzzle served first even after switching Theme, so that switching is not a way to skip.

History and Replay

  1. As a Player, I want a History page of the Puzzles I tried, newest first, so that I can review my training.
  2. As a Player, I want each row to show the date, a small board of the position, the Puzzle's rating, solved or failed, and my rating change, so that I see how each went.
  3. As a Player, I want to filter the History to failed Puzzles only, so that I revisit my mistakes.
  4. As a Player with a long History, I want it paginated, so that the page stays fast.
  5. As a Player, I want to replay any Puzzle from my History, so that I can practise it again.
  6. As a Player replaying a Puzzle, I want it clearly marked unrated, so that I know it does not count.
  7. As a Player, I want a Replay never to move my Puzzle rating nor add a History row, but to update the row's "last replayed" date, so that the History stays one row per Puzzle.
  8. As a Player, I want Hints and "Show solution" free during a Replay, so that practice has no penalty.

Accounts and data

  1. As a Player who deletes my Account, I want my Puzzle rating and History deleted with it, so that nothing is left behind.
  2. As a Player who signs out, I want Puzzles to send me to sign-in, so that no other Player sees my training.

Operator

  1. As the owner, I want the Puzzle set loaded on every stack by an idempotent command, so that each stack has the same Puzzles and a rerun changes nothing.
  2. As the owner, I want the sample regenerable from the Lichess dump by a script, so that the selection can be revised later.

Seams

  • S1 — the API through its RPC contract (API integration test on in-process Postgres, ADR 0038). Covers 1-2, 4-12, 14-17, 20-22, 25-35, 37-38, 39-47, 49: serving (window, widening, unseen only, Theme filter, pinning), server-checked Moves (reply, alternative Checkmate, wrong Move with the right one), failure by wrong Move, Hint, solution and giving up, Puzzle rating changes and the provisional flag, unrated Replays, History rows, filter and pagination, Account-only access, deletion. The loader runs against the test database first, as the demo seed does, which also tests its idempotency.
  • S2 — the Glicko-2 module (pure unit test, the one new seam). Covers 14-17, 19: the update against the worked example in Glickman's Glicko-2 paper, single-result updates against a Puzzle, and the provisional threshold.
  • S3 — the app's router, mounted by the router harness, with the API fakes (front view test). Covers 1, 3-13, 16, 18-19, 22-27, 33, 35-37, 39-44, 46, 48: the sign-in redirect, the first Move played, orientation, Hint highlight and its warning, retry, solution, "Next", the rating change and the provisional icon and tooltip, the Theme picker, the History page and a Replay started from it.
  • S4 — apps/e2e (e2e). Covers 1, 9, 11, 15, 39, 43 in a real browser: a new spec signs in, solves a Puzzle, fails one, sees both in the History, replays one unrated.

Implementation Decisions

Domain and docs

  • Glossary (GLOSSARY.md), new terms: Puzzle (a FEN, a solution line, a fixed rating and deviation, Themes), Theme (a tag on a Puzzle; the curated Themes are those a Player can pick), Puzzle rating (an Account's Glicko-2 rating, deviation and volatility; always "Puzzle rating", never bare "rating", to leave room for Game ratings), Attempt (an Account's first, rated try at a Puzzle: pending, solved or failed), Replay (any later try; unrated, no Attempt), Hint (highlights the piece to move; fails an Attempt). Flagged ambiguity: Attempt vs Replay.
  • New ADR: the Puzzle rating rules. Glicko-2; updated after every result (each Attempt its own rating period), not in batches; Puzzle ratings fixed at their imported values; only the Attempt counts; a Hint, the solution or giving up fails it; no deviation growth for inactivity in v1; provisional while deviation > 110. The system constant τ is chosen in the ADR.
  • Puzzles are Account-only. A Guest has no Puzzle rating, so no claim (ADR 0035) is involved.

Puzzle source and loading

  • Source: the Lichess puzzle database (CC0). Its columns map onto a Puzzle: Lichess id, FEN, Moves (UCI), Rating, RatingDeviation, Themes; the rest is dropped.
  • Sample of about 50,000 Puzzles, drawn so that each curated Theme has a few hundred Puzzles in each 200-point band, committed to the repo gzipped. A one-off script, kept in the repo, makes it from the Lichess dump. Every sampled line is checked with the engine (Game.fromFen and each Move legal) and dropped if it fails.
  • Loader: an idempotent db:puzzles command in the API, upserting by Lichess id. Puzzles are product data, not demo data: it runs on every stack, outside the devtools gate (ADR 0039), next to the migrations in each stack's deploy.

Lichess line shape

  • A Lichess Puzzle's FEN is the position before the opponent's Move; the first Move of its line is the opponent's. Stored as is. The Player's Color is the side to move after that first Move.
  • A solution line alternates Player and opponent; it always ends on a Player Move.

Schema (Drizzle, committed SQL migration, ADR 0031)

  • Puzzle: Lichess id (key), FEN, solution line (UCI), rating, rating deviation, Themes (array, indexed for containment); index on rating.
  • Puzzle rating: one row per Account (deleted with it): rating, deviation, volatility, updated at. Created on the first Attempt at 1500 / 350 / 0.06; until then the defaults are read.
  • Attempt: Account, Puzzle, state (pending / solved / failed), Moves played so far, Puzzle rating before, rating change, served at, resolved at, last replayed at. Unique on (Account, Puzzle): it is the "seen" record and the History row. At most one pending Attempt per Account (partial unique index). Deleted with the Account.
  • Ratings kept as reals; shown rounded.

Selection

  • "Next" returns the Account's pending Attempt if there is one, whatever the Theme. Otherwise it serves a random Puzzle with no Attempt by this Account, rating within ±100 of the Puzzle rating, carrying the Theme if one is chosen; the window widens in steps until one is found. It creates the pending Attempt in the same transaction.
  • Randomness comes from a root-provided source, as the clock and minters do (ADR 0044), so tests and e2e pick deterministically.
  • With no unseen Puzzle left at all (or in the Theme), a contract error says so and the page says so.

Move checking

  • The server owns the solution. The client never receives the line beyond the Moves already played, until the Attempt or Replay is resolved; a failure returns the right Move, "Show solution" returns the rest.
  • A Move is correct if it equals the next Player Move of the line, or if it delivers Checkmate (engine status()), which also solves the Puzzle. A correct Move that does not end the line returns the opponent's reply.
  • Attempt: progress is in the Attempt row. A wrong Move, a Hint, "Show solution" or giving up resolves it failed and applies the Glicko-2 update at once, in one transaction. Solving resolves it solved and applies the update. Once resolved, the client goes on as a Replay.
  • Replay: stateless. The client sends the Puzzle id and the line played so far; the server checks the prefix and the new Move. A Replay start sets the Attempt's "last replayed at". Only a Puzzle with an Attempt by this Account can be replayed.

Glicko-2 module

  • A pure module in the API (not the engine: ratings are not chess rules): one function taking a Puzzle rating (rating, deviation, volatility), the Puzzle's rating and deviation, and the result (1 solved, 0 failed), returning the new Puzzle rating. The volatility step uses the iterative algorithm from Glickman's paper.
  • The provisional flag (deviation > 110) is computed in the API and sent; the front does not know the threshold.

API contract (puzzle router in @chess/contracts, oRPC)

  • next({ theme? }) → the served Puzzle: id, start FEN, the opponent's first Move, the Player's Color, whether it is an Attempt or a Replay, and the Account's Puzzle rating (with provisional flag). Themes of the Puzzle are not sent before it is resolved, since they give the motif away.
  • move({ puzzleId, move, line? }) → correct (with opponent reply, or solved), or wrong (with the right Move); a resolved Attempt also returns the rating change and new Puzzle rating. line only for a Replay.
  • hint({ puzzleId, line? }) → the square of the piece to move; on a pending Attempt it fails it first.
  • solution({ puzzleId, line? }) → the remaining line; fails a pending Attempt. Giving up is the same call.
  • replay({ puzzleId }) → the Puzzle from its start as a Replay.
  • history({ cursor?, failedOnly? }) → a page of Attempts: Puzzle id, start FEN, Puzzle rating, state, rating change, resolved at, last replayed at; cursor-paginated like the Game list.
  • rating() → the Account's Puzzle rating and provisional flag.
  • All need an Account Session (UNAUTHORIZED otherwise). New errors, each in REFUSAL_STATUS: Puzzle not found, no Puzzle left, not replayable (no Attempt yet), Move refused (illegal Move or stale line), unknown Theme.
  • Moves go over the wire as UCI, like Games (ADR 0021).

Front

  • A new puzzles slice (ADR 0011), one page file per route (ADR 0043): Puzzles (/puzzles, with the Theme as a search parameter), History (/puzzles/history, with the failed-only filter and cursor), and a Replay route for one Puzzle.
  • Each Puzzle route needs an Account Session, with the same beforeLoad guard the account page uses (spec #409), remembering the destination.
  • The board is the existing board component: movableBy the Player's Color, orientation, last Move, checked King; the Hint highlight uses the existing marks. The click logic is the existing pure click resolver.
  • The opponent's first Move plays after a short beat on load, with the board's existing Move animation.
  • Server state through TanStack Query (ADR 0012); Moves sent as mutations, the Puzzle rating query refreshed after a result.
  • Provisional: an info icon next to the Puzzle rating with a tooltip ("Provisional rating — it moves quickly at first while we learn your level." / « Classement provisoire — il évolue vite au début, le temps d'évaluer votre niveau. »). Uses the design system's tooltip, or adds one there.
  • History rows show a small read-only board of the start position.
  • Copy in English and French (ADR 0025), including the names of the curated Themes: fork, pin, skewer, discovered attack, double check, mate in 1, mate in 2, mate in 3, back-rank mate, smothered mate, hanging piece, trapped piece, deflection, attraction, sacrifice, promotion, en passant, zugzwang, endgame, quiet move. The curated list is a constant shared by contract and front (the contract validates the Theme against it).

ADRs touched: 0001 (engine facade), 0011, 0012, 0019, 0021, 0025, 0026, 0027, 0030, 0031, 0038, 0039, 0043, 0044. New: the Puzzle rating ADR.

Testing Decisions

  • A good test drives the public seam (RPC calls, the rendered route, the browser) and asserts what a Player sees or the API answers, never tables or internal calls. The Glicko-2 module is the exception, tested directly because its arithmetic is the behaviour.
  • S1 prior art: rpc-games.integration.test.ts, rpc-game-reads.integration.test.ts (pagination, cursor errors), rpc-demo-seed.integration.test.ts (loading data then reading it), rpc-account-data.integration.test.ts (deletion), rpc-clock.integration.test.ts (an injected root source). Test Puzzles are small hand-written lines in a fixture, not the committed sample.
  • S2 prior art: the engine's pure unit tests. Cases: Glickman's worked example to the published precision; new vs settled Player solving the same Puzzle; a failure; the provisional boundary.
  • S3 prior art: router.test.tsx (mountRouter), the board harness, the route-guard tests from #409.
  • S4 prior art: play.spec.ts, my-games.spec.ts, accounts.spec.ts; the e2e stack loads Puzzles, and the injected random source makes the served one known.
  • Edge and unhappy paths: an illegal Move; a stale or forged Replay line; a Move after the Attempt resolved; a Hint after a failure (free); two tabs on the same pending Attempt; a Theme with no unseen Puzzle left; the window widening to the edge of the rating range; a Promotion as the solution; an alternative Checkmate; a Replay of a Puzzle never attempted; a signed-out call; the loader rerun.

Out of Scope

  • Guest or anonymous Puzzles.
  • Puzzle ratings that move with Attempts.
  • Deviation growth for inactivity.
  • A Puzzle rating graph, leaderboards, Puzzle ratings on profiles.
  • Timed modes (streaks, storms), daily Puzzle.
  • Puzzles generated from Games played on the site, or any engine analysis.
  • Themes beyond the curated list in the UI (all are stored).
  • Votes, comments, reporting a Puzzle.

Further Notes

  • Spans several merge requests: split with to-tickets, then implement-spec. A natural order: schema and loader → Glicko-2 module, ADR and glossary → rated training API → Puzzles page → Hints and solution → Themes → History and Replay → e2e.
  • Lichess puzzle database: https://database.lichess.org/#puzzles (CC0, no attribution required; a credit line on the Puzzles page is a courtesy worth adding).
  • Glicko-2: Mark Glickman, "Example of the Glicko-2 system", http://www.glicko.net/glicko/glicko2.pdf.
  • Not settled with the owner, decided here by default: Themes hidden until the Puzzle is resolved; a confirmation before the first Hint on an Attempt; the wording "find the best Move for White".
  • Size: the gzipped sample is a few MB in the repo; Git LFS is an option if the owner prefers.
## Problem Statement - A Player can only play whole Games, Local or Online. There is nothing to train tactics on between Games, and no measure of how a Player's tactical level evolves. - The site has no notion of a Puzzle, a rating, or a training history. ## Solution - A signed-in Player opens **Puzzles** and gets a Puzzle near their level: the opponent's move plays, then the Player finds the line. The server checks every Move, so the answer cannot be read from the browser. - Each Account has a **Puzzle rating** (Glicko-2) that only the first try at each Puzzle moves: a wrong Move, a Hint, showing the solution or giving up fails it on the spot. After a failure the Player may retry, unrated. - The Player can train on one **Theme** (fork, pin, mate in 2…) from a curated list. - A **History** page lists every Puzzle the Player has tried, and any of them can be replayed, unrated. - Puzzles are a curated sample of the Lichess puzzle database (CC0), loaded on every stack. ## User Stories Access 1. As a Player with no Session, I want opening Puzzles to take me to sign-in and back, so that I land on Puzzles once signed in. 2. As a Guest, I want to be asked to sign in or sign up to use Puzzles, so that my Puzzle rating and history belong to an Account. 3. As a signed-in Player, I want a Puzzles entry in the navigation, so that I can find training from anywhere. Solving 4. As a Player, I want the board to show the Puzzle's position and then play the opponent's Move, so that I see what I am answering. 5. As a Player, I want the board oriented with my Color at the bottom, so that I read the position from my side. 6. As a Player, I want to be told whose Turn it is and what to look for ("find the best Move for White"), so that the task is clear. 7. As a Player, I want only my own pieces movable, so that I cannot play the opponent's side. 8. As a Player, I want a correct Move to be answered by the opponent's reply automatically, so that the line plays out like a Game. 9. As a Player, I want a Puzzle that ends with my Move to be marked solved at once, so that I know I found it. 10. As a Player, I want any Move that delivers Checkmate to count, even if it differs from the stored line, so that a valid mate is never marked wrong. 11. As a Player, I want any other Move off the stored line to be marked wrong, with the right Move shown, so that I learn the answer. 12. As a Player, I want Promotion to work in a Puzzle as in a Game, so that promotion Puzzles are solvable. 13. As a Player, I want the last Move highlighted and a checked King shown, as in a Game, so that the board reads the same everywhere. Puzzle rating 14. As a Player, I want a Puzzle rating that starts at 1500, so that I have a level from my first Puzzle. 15. As a Player, I want my Puzzle rating to rise when I solve a Puzzle and fall when I fail one, so that it tracks my level. 16. As a Player, I want the change shown right after the result (+12 / −9), so that I feel each Puzzle's weight. 17. As a new Player, I want my Puzzle rating to move quickly at first, so that it reaches my level in a few dozen Puzzles. 18. As a new Player, I want an icon beside a provisional Puzzle rating, with a tooltip explaining it moves quickly at first while the site learns my level, so that early swings do not surprise me. 19. As a Player whose Puzzle rating has settled, I want the provisional icon gone, so that the number reads as trustworthy. 20. As a Player, I want the rating to move on my first wrong Move, not at the end, so that the result is honest and immediate. 21. As a Player, I want my Puzzle rating the same across devices, so that training anywhere counts. Retry, Hint, solution 22. As a Player who failed a Puzzle, I want to retry it from the start, unrated, so that I can find the line myself. 23. As a Player, I want a Hint that highlights the piece to move, so that I get unstuck without seeing the full answer. 24. As a Player, I want to be warned before my first Hint on a rated Attempt that it counts as a failure, so that I choose knowingly. 25. As a Player, I want "Show solution" to play the rest of the line on the board, so that I learn it. 26. As a Player, I want "Show solution" and giving up on a rated Attempt to count as a failure, so that the rating stays meaningful. 27. As a Player, I want "Next" after a solved or failed Puzzle, so that I can keep training. Choosing the next Puzzle 28. As a Player, I want the next Puzzle to be close to my Puzzle rating, so that it is neither trivial nor hopeless. 29. As a Player, I want never to be served a Puzzle I already tried, so that every served Puzzle counts. 30. As a Player, I want a Puzzle still to be served when none is left near my rating, so that I never hit a dead end. 31. As a Player who reloads or leaves mid-Puzzle, I want the same Puzzle back, so that my progress is kept, and so that no one can skip a hard Puzzle for free. 32. As a Player, I want to give up a pending Puzzle explicitly, so that I can move on at the cost of a failure. Themes 33. As a Player, I want to choose a Theme from a curated list, so that I train one motif. 34. As a Player, I want Theme training to move the same Puzzle rating, so that I have one level, not twenty. 35. As a Player, I want to go back to mixed training, so that the Theme is a filter I can drop. 36. As a Player, I want Theme names in my language (English or French), so that I understand them. 37. As a Player, I want my chosen Theme remembered while I train, so that "Next" stays in it. 38. As a Player, I want a pending Puzzle served first even after switching Theme, so that switching is not a way to skip. History and Replay 39. As a Player, I want a History page of the Puzzles I tried, newest first, so that I can review my training. 40. As a Player, I want each row to show the date, a small board of the position, the Puzzle's rating, solved or failed, and my rating change, so that I see how each went. 41. As a Player, I want to filter the History to failed Puzzles only, so that I revisit my mistakes. 42. As a Player with a long History, I want it paginated, so that the page stays fast. 43. As a Player, I want to replay any Puzzle from my History, so that I can practise it again. 44. As a Player replaying a Puzzle, I want it clearly marked unrated, so that I know it does not count. 45. As a Player, I want a Replay never to move my Puzzle rating nor add a History row, but to update the row's "last replayed" date, so that the History stays one row per Puzzle. 46. As a Player, I want Hints and "Show solution" free during a Replay, so that practice has no penalty. Accounts and data 47. As a Player who deletes my Account, I want my Puzzle rating and History deleted with it, so that nothing is left behind. 48. As a Player who signs out, I want Puzzles to send me to sign-in, so that no other Player sees my training. Operator 49. As the owner, I want the Puzzle set loaded on every stack by an idempotent command, so that each stack has the same Puzzles and a rerun changes nothing. 50. As the owner, I want the sample regenerable from the Lichess dump by a script, so that the selection can be revised later. ## Seams - **S1 — the API through its RPC contract** (API integration test on in-process Postgres, ADR 0038). Covers 1-2, 4-12, 14-17, 20-22, 25-35, 37-38, 39-47, 49: serving (window, widening, unseen only, Theme filter, pinning), server-checked Moves (reply, alternative Checkmate, wrong Move with the right one), failure by wrong Move, Hint, solution and giving up, Puzzle rating changes and the provisional flag, unrated Replays, History rows, filter and pagination, Account-only access, deletion. The loader runs against the test database first, as the demo seed does, which also tests its idempotency. - **S2 — the Glicko-2 module** (pure unit test, the one new seam). Covers 14-17, 19: the update against the worked example in Glickman's Glicko-2 paper, single-result updates against a Puzzle, and the provisional threshold. - **S3 — the app's router, mounted by the router harness, with the API fakes** (front view test). Covers 1, 3-13, 16, 18-19, 22-27, 33, 35-37, 39-44, 46, 48: the sign-in redirect, the first Move played, orientation, Hint highlight and its warning, retry, solution, "Next", the rating change and the provisional icon and tooltip, the Theme picker, the History page and a Replay started from it. - **S4 — `apps/e2e`** (e2e). Covers 1, 9, 11, 15, 39, 43 in a real browser: a new spec signs in, solves a Puzzle, fails one, sees both in the History, replays one unrated. ## Implementation Decisions Domain and docs - **Glossary** (`GLOSSARY.md`), new terms: **Puzzle** (a FEN, a solution line, a fixed rating and deviation, Themes), **Theme** (a tag on a Puzzle; the *curated* Themes are those a Player can pick), **Puzzle rating** (an Account's Glicko-2 rating, deviation and volatility; always "Puzzle rating", never bare "rating", to leave room for Game ratings), **Attempt** (an Account's first, rated try at a Puzzle: *pending*, *solved* or *failed*), **Replay** (any later try; unrated, no Attempt), **Hint** (highlights the piece to move; fails an Attempt). Flagged ambiguity: Attempt vs Replay. - **New ADR: the Puzzle rating rules.** Glicko-2; updated after every result (each Attempt its own rating period), not in batches; Puzzle ratings fixed at their imported values; only the Attempt counts; a Hint, the solution or giving up fails it; no deviation growth for inactivity in v1; provisional while deviation > 110. The system constant τ is chosen in the ADR. - Puzzles are Account-only. A Guest has no Puzzle rating, so no claim (ADR 0035) is involved. Puzzle source and loading - Source: the Lichess puzzle database (CC0). Its columns map onto a Puzzle: Lichess id, FEN, Moves (UCI), Rating, RatingDeviation, Themes; the rest is dropped. - **Sample** of about 50,000 Puzzles, drawn so that each curated Theme has a few hundred Puzzles in each 200-point band, committed to the repo gzipped. A one-off script, kept in the repo, makes it from the Lichess dump. Every sampled line is checked with the engine (`Game.fromFen` and each Move legal) and dropped if it fails. - **Loader:** an idempotent `db:puzzles` command in the API, upserting by Lichess id. Puzzles are product data, not demo data: it runs on every stack, outside the devtools gate (ADR 0039), next to the migrations in each stack's deploy. Lichess line shape - A Lichess Puzzle's FEN is the position *before* the opponent's Move; the first Move of its line is the opponent's. Stored as is. The Player's Color is the side to move after that first Move. - A solution line alternates Player and opponent; it always ends on a Player Move. Schema (Drizzle, committed SQL migration, ADR 0031) - **Puzzle**: Lichess id (key), FEN, solution line (UCI), rating, rating deviation, Themes (array, indexed for containment); index on rating. - **Puzzle rating**: one row per Account (deleted with it): rating, deviation, volatility, updated at. Created on the first Attempt at 1500 / 350 / 0.06; until then the defaults are read. - **Attempt**: Account, Puzzle, state (pending / solved / failed), Moves played so far, Puzzle rating before, rating change, served at, resolved at, last replayed at. Unique on (Account, Puzzle): it is the "seen" record and the History row. At most one pending Attempt per Account (partial unique index). Deleted with the Account. - Ratings kept as reals; shown rounded. Selection - "Next" returns the Account's pending Attempt if there is one, whatever the Theme. Otherwise it serves a random Puzzle with no Attempt by this Account, rating within ±100 of the Puzzle rating, carrying the Theme if one is chosen; the window widens in steps until one is found. It creates the pending Attempt in the same transaction. - Randomness comes from a root-provided source, as the clock and minters do (ADR 0044), so tests and e2e pick deterministically. - With no unseen Puzzle left at all (or in the Theme), a contract error says so and the page says so. Move checking - The server owns the solution. The client never receives the line beyond the Moves already played, until the Attempt or Replay is resolved; a failure returns the right Move, "Show solution" returns the rest. - A Move is correct if it equals the next Player Move of the line, or if it delivers Checkmate (engine `status()`), which also solves the Puzzle. A correct Move that does not end the line returns the opponent's reply. - **Attempt**: progress is in the Attempt row. A wrong Move, a Hint, "Show solution" or giving up resolves it failed and applies the Glicko-2 update at once, in one transaction. Solving resolves it solved and applies the update. Once resolved, the client goes on as a Replay. - **Replay**: stateless. The client sends the Puzzle id and the line played so far; the server checks the prefix and the new Move. A Replay start sets the Attempt's "last replayed at". Only a Puzzle with an Attempt by this Account can be replayed. Glicko-2 module - A pure module in the API (not the engine: ratings are not chess rules): one function taking a Puzzle rating (rating, deviation, volatility), the Puzzle's rating and deviation, and the result (1 solved, 0 failed), returning the new Puzzle rating. The volatility step uses the iterative algorithm from Glickman's paper. - The provisional flag (deviation > 110) is computed in the API and sent; the front does not know the threshold. API contract (`puzzle` router in `@chess/contracts`, oRPC) - `next({ theme? })` → the served Puzzle: id, start FEN, the opponent's first Move, the Player's Color, whether it is an Attempt or a Replay, and the Account's Puzzle rating (with provisional flag). Themes of the Puzzle are not sent before it is resolved, since they give the motif away. - `move({ puzzleId, move, line? })` → correct (with opponent reply, or solved), or wrong (with the right Move); a resolved Attempt also returns the rating change and new Puzzle rating. `line` only for a Replay. - `hint({ puzzleId, line? })` → the square of the piece to move; on a pending Attempt it fails it first. - `solution({ puzzleId, line? })` → the remaining line; fails a pending Attempt. Giving up is the same call. - `replay({ puzzleId })` → the Puzzle from its start as a Replay. - `history({ cursor?, failedOnly? })` → a page of Attempts: Puzzle id, start FEN, Puzzle rating, state, rating change, resolved at, last replayed at; cursor-paginated like the Game list. - `rating()` → the Account's Puzzle rating and provisional flag. - All need an Account Session (`UNAUTHORIZED` otherwise). New errors, each in `REFUSAL_STATUS`: Puzzle not found, no Puzzle left, not replayable (no Attempt yet), Move refused (illegal Move or stale line), unknown Theme. - Moves go over the wire as UCI, like Games (ADR 0021). Front - A new `puzzles` slice (ADR 0011), one page file per route (ADR 0043): Puzzles (`/puzzles`, with the Theme as a search parameter), History (`/puzzles/history`, with the failed-only filter and cursor), and a Replay route for one Puzzle. - Each Puzzle route needs an Account Session, with the same `beforeLoad` guard the account page uses (spec #409), remembering the destination. - The board is the existing board component: `movableBy` the Player's Color, orientation, last Move, checked King; the Hint highlight uses the existing marks. The click logic is the existing pure click resolver. - The opponent's first Move plays after a short beat on load, with the board's existing Move animation. - Server state through TanStack Query (ADR 0012); Moves sent as mutations, the Puzzle rating query refreshed after a result. - Provisional: an info icon next to the Puzzle rating with a tooltip ("Provisional rating — it moves quickly at first while we learn your level." / « Classement provisoire — il évolue vite au début, le temps d'évaluer votre niveau. »). Uses the design system's tooltip, or adds one there. - History rows show a small read-only board of the start position. - Copy in English and French (ADR 0025), including the names of the curated Themes: fork, pin, skewer, discovered attack, double check, mate in 1, mate in 2, mate in 3, back-rank mate, smothered mate, hanging piece, trapped piece, deflection, attraction, sacrifice, promotion, en passant, zugzwang, endgame, quiet move. The curated list is a constant shared by contract and front (the contract validates the Theme against it). ADRs touched: 0001 (engine facade), 0011, 0012, 0019, 0021, 0025, 0026, 0027, 0030, 0031, 0038, 0039, 0043, 0044. New: the Puzzle rating ADR. ## Testing Decisions - A good test drives the public seam (RPC calls, the rendered route, the browser) and asserts what a Player sees or the API answers, never tables or internal calls. The Glicko-2 module is the exception, tested directly because its arithmetic is the behaviour. - **S1** prior art: `rpc-games.integration.test.ts`, `rpc-game-reads.integration.test.ts` (pagination, cursor errors), `rpc-demo-seed.integration.test.ts` (loading data then reading it), `rpc-account-data.integration.test.ts` (deletion), `rpc-clock.integration.test.ts` (an injected root source). Test Puzzles are small hand-written lines in a fixture, not the committed sample. - **S2** prior art: the engine's pure unit tests. Cases: Glickman's worked example to the published precision; new vs settled Player solving the same Puzzle; a failure; the provisional boundary. - **S3** prior art: `router.test.tsx` (`mountRouter`), the board harness, the route-guard tests from #409. - **S4** prior art: `play.spec.ts`, `my-games.spec.ts`, `accounts.spec.ts`; the e2e stack loads Puzzles, and the injected random source makes the served one known. - Edge and unhappy paths: an illegal Move; a stale or forged Replay line; a Move after the Attempt resolved; a Hint after a failure (free); two tabs on the same pending Attempt; a Theme with no unseen Puzzle left; the window widening to the edge of the rating range; a Promotion as the solution; an alternative Checkmate; a Replay of a Puzzle never attempted; a signed-out call; the loader rerun. ## Out of Scope - Guest or anonymous Puzzles. - Puzzle ratings that move with Attempts. - Deviation growth for inactivity. - A Puzzle rating graph, leaderboards, Puzzle ratings on profiles. - Timed modes (streaks, storms), daily Puzzle. - Puzzles generated from Games played on the site, or any engine analysis. - Themes beyond the curated list in the UI (all are stored). - Votes, comments, reporting a Puzzle. ## Further Notes - Spans several merge requests: split with `to-tickets`, then `implement-spec`. A natural order: schema and loader → Glicko-2 module, ADR and glossary → rated training API → Puzzles page → Hints and solution → Themes → History and Replay → e2e. - Lichess puzzle database: <https://database.lichess.org/#puzzles> (CC0, no attribution required; a credit line on the Puzzles page is a courtesy worth adding). - Glicko-2: Mark Glickman, "Example of the Glicko-2 system", <http://www.glicko.net/glicko/glicko2.pdf>. - Not settled with the owner, decided here by default: Themes hidden until the Puzzle is resolved; a confirmation before the first Hint on an Attempt; the wording "find the best Move for White". - Size: the gzipped sample is a few MB in the repo; Git LFS is an option if the owner prefers.
root commented 2026-10-16 08:19:35 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #441

mentioned in issue #441
root commented 2026-10-16 08:19:40 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #442

mentioned in issue #442
root commented 2026-10-16 08:20:06 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #443

mentioned in issue #443
root commented 2026-10-16 08:20:32 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #444

mentioned in issue #444
root commented 2026-10-16 08:20:39 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #445

mentioned in issue #445
root commented 2026-10-16 08:20:44 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #446

mentioned in issue #446
root commented 2026-10-16 08:21:03 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #447

mentioned in issue #447
root commented 2026-10-16 08:21:26 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #441

marked this issue as related to #441
root commented 2026-10-16 08:21:27 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #442

marked this issue as related to #442
root commented 2026-10-16 08:21:28 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #443

marked this issue as related to #443
root commented 2026-10-16 08:21:29 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #444

marked this issue as related to #444
root commented 2026-10-16 08:21:30 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #445

marked this issue as related to #445
root commented 2026-10-16 08:21:31 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #446

marked this issue as related to #446
root commented 2026-10-16 08:21:32 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #447

marked this issue as related to #447
root commented 2026-10-18 11:22:54 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #448

mentioned in issue #448
root commented 2026-10-18 12:19:40 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #449

mentioned in issue #449
root commented 2026-10-18 13:02:27 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !554

mentioned in merge request !554
root commented 2026-10-19 09:51:27 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !555

mentioned in merge request !555
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-19 09:53:34 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
amaurycoudr/chess-project#440
No description provided.