Skip to content

Domain Conventions

The Architecture & Schema page shows the shape of the data. This page captures the conventions you cannot infer from a column’s type or name. Each item below has already caused a real bug — keep them in mind when querying or building on the data.

A roll is three dice, each a piece type (pawn, knight, bishop, rook, queen, king). It is stored as the sorted piece letters, cased by the side to move:

  • White to move → upper-case, e.g. BPQ (bishop, pawn, queen).
  • Black to move → lower-case, e.g. bpq.

The ingest API accepts dice as numeric piece codes (1=pawn, 2=knight, 3=bishop, 4=rook, 5=queen, 6=king); the server normalizes them to the cased-letter form above before storing, and a CHECK constraint (chk_dice_sorted_letters) guarantees only letters are ever stored.

Both time_initial_sec and time_increment_sec strictly contain values in seconds. So a 3+0 game will be stored as (180, 0) and a 1+1 game as (60, 1).

  • games.result is from White’s perspective: 1 White win, -1 Black win, 0 draw, NULL unknown/unfinished.
  • A continuation’s win rate is computed for the side to move at that position (turns.active_color combined with result). It is therefore White’s rate from the start, but it flips as you drill into deeper, black-to-move positions.

stake_currency is an in-game currency (GOLD), not an ISO 4217 code, and amounts are whole numbers. A stake of 0 means a tournament game — surface it as “no stake” rather than 0 GOLD.

The bundled Dice Chess engine validates legality but plays greedily and weakly — it is not a “best move” oracle. The analytical reference for “what is good” is the empirical win rate of strong human players, not the engine and not the bots (which are weak as well).

Positions are de-duplicated by normalized_fen (board + side to move + castling + en passant). Analytics group continuations by the resulting position (turns.position_after_id), so different micro-move orderings that reach the same position collapse into a single line.

A turn with empty/NULL played_moves has two very different meanings, told apart by whether the position changed:

  • Legitimate passposition_after_id <> position_id (the side to move flips). The player rolled only piece-types that cannot move, so the turn auto-passes. A real game event (~188k turns in the current dump). Kept by analytics; it is a valid continuation (its resulting position is the opponent to move).
  • No-op self-loopposition_after_id = position_id (identical position, side does not flip because the game ended). The player rolled but never moved — typically a draw agreed or game abandoned/timed-out before the first move (common in tournaments). Always the last turn of the game. This is not a legal move and is excluded from continuations analytics (AND t.position_after_id <> t.position_id).