Reading Nardex Analysis Output: Per-Move Equity, Grades, and Cube Actions
The /api/v1/positions/analyze endpoint returns one PositionAnalysis per position you submit. This page is the field-by-field
reference: what each property means, how it relates to the others, and how to roll the
per-move output into a per-game PR. If you haven't seen the request shape yet, start with
the 5-minute quickstart.
The response shape
Each call returns a single object. TypeScript signature (mirror of the Rust source):
interface PositionAnalysis {
alternatives: Alt[]; // ranked candidates, best first
chosen?: Alt; // your move; absent if you didn't supply one
equity_loss: number; // chosen.equity vs alternatives[0].equity
grade: 'ok' | 'doubtful' | 'error' | 'blunder';
equity_kind: 'money' | 'match';
forced: boolean; // true if there was no real choice
analysis_depth: 'ply_zero' | 'ply_two' | 'ply_three';
}
interface Alt {
play?: MoveDetail[]; // for checker-play decisions
cube_action?: 'take' | 'drop' | 'double' | 'no_double';
equity: number;
probabilities: ProbVector; // shape depends on game variant
} The alternatives array
alternatives is the engine's ranking of legal candidate moves (or cube
actions). Sorted descending by equity — i.e., alternatives[0] is the best move. Length depends on the depth tier and your request settings; expect 5–20 entries
for typical positions.
Each Alt has either a play array (checker-play decision) or a cube_action string (cube decision) — never both. The decision.type field of the request determines which.
chosen vs best
If your request included a played move, the engine looks for it among alternatives and returns it as chosen. Two derived numbers:
equity_loss = alternatives[0].equity - chosen.equity. Always ≥ 0. Zero means you found the best move.grade= the threshold-bucket ofequity_loss. Default thresholds are: ok < 0.02, doubtful 0.02–0.08, error 0.08–0.20, blunder ≥ 0.20. Configurable per deployment.
If you didn't supply a move (decision.played empty), chosen is
absent, equity_loss is 0, and grade is 'ok' by
convention. Use chosen === undefined to detect this case.
Probabilities — narde vs backgammon
Each Alt.probabilities contains the network's win/lose breakdown. The shape differs
by variant because the prize systems differ:
// narde — nested ordinal probabilities
{
win: 0.62, // P(X wins, includes mars and koks)
win_m: 0.18, // P(win >= mars)
win_k: 0.02, // P(win >= koks)
lose: 0.38,
lose_m: 0.05,
lose_k: 0.0
}
// backgammon — money-game prize buckets
{
win: 0.62,
win_g: 0.14, // P(gammon win)
win_bg: 0.01, // P(backgammon win)
lose: 0.38,
lose_g: 0.04,
lose_bg: 0.0
} Important for narde: the win_m/win_k fields are nested ordinals, not a 6-class softmax. win_m = P(win >= mars) includes koks; win_k is the strictest. Don't try to sum them as if they were independent buckets.
analysis_depth tiers
Three depth settings are exposed:
ply_zero— direct neural-network evaluation, sub-millisecond on GPU. Use for bulk scoring or low-latency UIs.ply_two— 2-ply lookahead with NN at leaves. Default for analyze endpoint. Tens of ms.ply_three— deeper search, hundreds of ms. Most accurate, but quota and rate-limit budgets are tighter at this tier.
Equity values from different depths are not strictly comparable — don't aggregate ply_zero and ply_two equity_losses into the same PR without normalization.
forced and other edge cases
forced: true means there was no real choice for the moving side — usually a
single legal move (or cube position with no decision available). When you compute PR over a
game, filter forced moves out of both numerator and denominator. Including
them artificially deflates PR.
Other edge cases:
- Game-over positions (no moves possible) return
alternatives: []andforced: true. - Bear-off positions late in the race may have
alternativeslength 1 — the only legal play. Treat as forced. - Cube decisions in the Crawford game return
alternativescontaining only'no_double'. The cube is unavailable.
Aggregating per-move into PR
The standard recipe (gnubg convention) — JavaScript:
function gamePR(positions) {
const meaningful = positions.filter(p => !p.forced);
if (meaningful.length === 0) return 0;
const totalLoss = meaningful.reduce((acc, p) => acc + p.equity_loss, 0);
return (totalLoss / meaningful.length) * 500;
} If you want positional PR and cube PR separately, group by chosen.cube_action !== undefined:
function splitPR(positions) {
const positional = positions.filter(p => !p.forced && !p.chosen?.cube_action);
const cubeActions = positions.filter(p => !p.forced && p.chosen?.cube_action);
const pr = arr => arr.length === 0 ? 0
: (arr.reduce((a, p) => a + p.equity_loss, 0) / arr.length) * 500;
return { positional: pr(positional), cube: pr(cubeActions) };
} And in Python:
def game_pr(positions):
meaningful = [p for p in positions if not p["forced"]]
if not meaningful:
return 0.0
total = sum(p["equity_loss"] for p in meaningful)
return (total / len(meaningful)) * 500 Money equity vs match equity
The equity_kind field on each PositionAnalysis tells you whether the equity
values are in money or match-winning-chance units. They are different scales:
'money'— equity in money-game points: a win = +1, a loss = -1, a gammon win = +2, etc. Range typically [-3, +3].'match'— equity as match-winning chance from current score. Range [0, 1]. Computed using a match-equity table (default: post-Crawford safe).
If you mix money and match equities into the same PR aggregate, your numbers will be
nonsense. Filter on equity_kind when scoring an entire match.
Next steps
- 5-minute quickstart — minimum-viable request.
- Nardex vs gnubg vs XG — engine differences.
- Developers — request beta access.
- How to Analyze Backgammon Games — the human-facing version of these metrics.
- «Как анализировать партию в нардах» — same content for long narde (Russian).
Frequently asked questions
- What does
equity_lossmean exactly? - It's the equity of the engine's best move minus the equity of the chosen move, expressed in money-game equity units. Always non-negative; 0 means you played the best move.
- Why is
gradeseparate fromequity_loss? gradeis a discretization ofequity_losswith hard thresholds (ok / doubtful / error / blunder). The thresholds are fixed in the engine but configurable per-deployment. Treatgradeas a UI hint,equity_lossas the source of truth.- What's in the
probabilitiesfield? - For narde: nested ordinal — { win, win_m, win_k, lose, lose_m, lose_k }. For backgammon: { win, win_g, win_bg, lose, lose_g, lose_bg }. The naming differs because the prize systems differ. See
crates/engineoutputs for the exact shape. - How do I aggregate per-move
equity_lossinto PR? - PR (gnubg convention) = (sum of equity_loss over non-forced moves / count of non-forced moves) × 500. Filter
forced: truemoves out of both numerator and denominator. - Are cube actions analyzed too?
- Yes — when the
decision.type === "cube_decision", thechosenandalternativesarecube_actionobjects (notplay). Score them separately when computing PR-positional vs PR-cube. - What does
analysis_depthtell me? - Search depth tier.
ply_zero= direct neural-network evaluation (fastest).ply_two= 2-ply lookahead with the network at leaves (default).ply_three= deeper search, slower. Equity values from different depths are not strictly comparable.