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

  1. 🧩User opens “puzzles”
  2. 📥platform callsGET /v1/puzzles/next
  3. 🎬animate setupMove, then the user solves
  4. 📤per move:POST /v1/puzzles/{id}/attempt
  5. ✔️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

FieldTypeRequiredDefaultDescription
ratingnumber—1500player's puzzle rating — puzzles are drawn within ±300 of it
themestring—anyfilter by theme, e.g. "fork"; omit for any theme
userIdstring——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

FieldTypeRequiredDefaultDescription
userIdstringyes—player attempting the solve
plynumberyes—solution index being answered (1, 3, 5, …)
ucistringyes—the move played (UCI)
msTakennumber——time spent on the move (ms) — recorded, not rated
ratingobjectif rated—{ r, rd, vol } — the player's overall puzzle rating; omit to record unrated
themeRatingsobject——{ [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

RuleDetail
Puzzles are REST-onlyThey 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 ratesOne 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 ratingsSame 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 solvedone:true, solved:false — the (first-attempt) rating delta is negative. Show the solution; a retry is unrated.