Puzzles
Serve a puzzle near the player's rating, validate the solve move by move, and rate the first attempt — overall and per theme. Puzzles are REST-only; they never touch the session socket.
The solve loop
- 🧩User opens “puzzles”
- 📥platform calls
GET /v1/puzzles/next - 🎬animate setupMove, then the user solves
- 📤per move:
POST /v1/puzzles/{id}/attempt - ✔️validates ply · returns reply + nextPly · grades at the end
Your backend proxies the two puzzle routes (they are server-to-server, like all REST): fetch the next puzzle, animate its setupMove, then validate each solution move with an attempt call. Mid-solve the engine returns the opponent's reply to animate plus nextPly; the final (or a wrong) move grades the attempt.
GET /v1/puzzles/next
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
rating | number | — | 1500 | player's puzzle rating — puzzles are drawn within ±300 of it |
theme | string | — | any | filter by theme, e.g. "fork"; omit for any theme |
userId | string | — | — | carried for the platform's own tracking; selection is by rating + theme |
http
GET /v1/puzzles/next?rating=1450&theme=fork
X-Service-Auth: <secret>
# 200 — one puzzle near the player's rating (±300); 404 (success:false) if none in range
{
"success": true, "response": "success", "responseCode": 200,
"data": {
"puzzleId": "pz_8842",
"fen": "r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR b KQkq - 3 4",
"setupMove": "d1f3", // opponent's setup move (UCI) — animate before the player solves
"rating": 1450,
"themes": ["fork", "hangingPiece"]
}
}POST /v1/puzzles/{id}/attempt
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
userId | string | yes | — | player attempting the solve |
ply | number | yes | — | solution index being answered (1, 3, 5, …) |
uci | string | yes | — | the move played (UCI) |
msTaken | number | — | — | time spent on the move (ms) — recorded, not rated |
rating | object | if rated | — | { r, rd, vol } — the player's overall puzzle rating; omit to record unrated |
themeRatings | object | — | — | { [theme]: { r, rd, vol } } per theme the platform tracks |
http
POST /v1/puzzles/{id}/attempt
X-Service-Auth: <secret>
{
"userId": "user_123",
"ply": 3, // solution index (1, 3, 5, …)
"uci": "d1h5",
"msTaken": 4200, // optional
"rating": { "r": 1400, "rd": 90, "vol": 0.06 }, // needed to rate
"themeRatings": { "fork": { "r": 1350, "rd": 100, "vol": 0.06 } } // optional, per theme
}
# 200 — correct, more moves remain: the engine returns the opponent reply + next ply
{ "success": true, "response": "success", "responseCode": 200,
"data": { "correct": true, "done": false, "solved": false, "reply": "e8e7", "nextPly": 5 } }
# 200 — solve finished (final correct move OR a wrong move → done:true).
# Only the FIRST attempt is rated; data.rating/themeRatings omitted otherwise.
{
"success": true, "response": "success", "responseCode": 200,
"data": {
"correct": true, "done": true, "solved": true,
"rating": { "before": { "r": 1400, "rd": 90, "vol": 0.06 },
"after": { "r": 1407.2, "rd": 86, "vol": 0.06 }, "delta": 7.2 },
"themeRatings": { "fork": { "before": {…}, "after": {…}, "delta": 6.1 } }
}
}Rules to know
| Rule | Detail |
|---|---|
| Puzzles are REST-only | They don't ride the session socket. The rating arrives as a request parameter — safe here ONLY because the route is platform-inbound (server-to-server). A client that could state its own puzzle rating would farm easy puzzles. |
| Only the first attempt rates | One Glicko-2 game vs the puzzle's rating, returning an overall delta plus a per-theme delta for each theme you track. Later attempts are recorded but unrated — no farming. |
| The platform owns puzzle ratings | Same model as game ratings: the engine computes, you store — overall and per-theme. Send them back on the next attempt. |
| A wrong move ends the solve | done:true, solved:false — the (first-attempt) rating delta is negative. Show the solution; a retry is unrated. |