app/puzzle.h

a tactics puzzle, normalized across Lichess’s two representations.

Puzzles reach the board by two roads (docs/lichess-docs/PLAN.md §3.2), and the two disagree by exactly one ply. This is the trap the whole feature turns on: get it wrong and every puzzle is silently off by a move.

• The API (/api/puzzle/next, /api/puzzle/batch/{angle}) sends NO FEN AT ALL. It describes the puzzle as the game’s pgn (SAN) plus initialPly: the solver’s position is that PGN replayed for initialPly + 1 plies, and the last of those plies IS the opponent’s move that created the tactic (i.e. lastMove). solution[0] is the SOLVER’s first move.

• The CC0 database CSV (database.lichess.org/#puzzles) gives FEN as the position one ply EARLIER — before the opponent’s move — and Moves[0] is the OPPONENT’s move; the solver plays Moves[1].

Both normalize to the same convention: fen is the position the solver faces, and solution begins with the solver’s move and alternates solver/opponent. Reaching it needs chess moves applied in BOTH cases — one ply for the CSV, initialPly + 1 for the API — which is why both roads pass through an engine.

The two engine steps happen at different times, and that is deliberate. The CSV is read where an engine is already at hand. The API’s puzzles arrive on a network callback on the UI thread, where there is no engine to use (the engine’s state is global and belongs to the session’s worker thread), so parseApiBatch stays a pure string parse and hands back UNRESOLVED puzzles; resolveFromPgn() finishes the job later, wherever an engine is legitimately owned. Puzzle::resolved() says which state a given puzzle is in.

namespace kirin
struct Puzzle
#include <puzzle.h>

Public Functions

inline bool resolved() const
inline bool replayable() const
inline bool valid() const
bool solverIsWhite() const
inline bool isSolverPly(size_t i) const

Public Members

std::string id
std::string fen
std::string lastMove
std::vector<std::string> solution
int rating = 0
std::vector<std::string> themes
std::string pgn
int initialPly = -1