Nardex
  • Learn
    • Long Narde
    • Backgammon
    • Theory
    • Blog
    • FAQ
  • Leaderboard
  • Analytics
  • Developers
    • Analytics API
    • Engine training
  • Project
    • Android app
    • About
    • Software
    • Privacy Policy
Log in
en
Nardex
LearnLeaderboardAnalytics
Log in

Learn

Long NardeBackgammonTheoryBlogFAQ

Developers

Analytics APIEngine training

Project

Android appAboutSoftwarePrivacy Policy
en

In this section

In this section ▸
  • Nardex vs gnubg vs XG
  • API quickstart
  • Reading analysis output

In this section

  • Nardex vs gnubg vs XG
  • API quickstart
  • Reading analysis output

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' | 'match_emg' | 'cube_dist';
  forced: boolean;              // true if there was no real choice
  analysis_depth: 'ply_zero' | 'ply_two' | 'ply_three';
  met_based: boolean;           // true = equity_loss came from a real MET table (cube only)
  cube_equities?: CubePanel;    // the cube OFFER panel's answer; absent when there is none
}

type CubePanel =
  | ({ state: 'published' } & CubeEquities)  // the three branches
  | { state: 'rules_gated' }                 // the rules forbid this cube: no branches, ever
  | { state: 'refused' };                    // computed and unpublishable: a recompute refuses again

interface CubeEquities {
  no_double: number;           // EMG relative to the best line
  double_take: number;
  double_pass: number;
  best: 'no_double' | 'double_take' | 'double_pass';
  reply: 'double_take' | 'double_pass';  // the opponent's rational answer to the double
}

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 of equity_loss. Default thresholds are: ok < 0.02, doubtful 0.02–0.08, error 0.08–0.20, blunder ≥ 0.20. Configurable per deployment.
  • met_based — true when this equity_loss was computed off a calibrated match-equity table (MET); only applies to cube decisions (decision.type === "cube_decision"), and only when the active profile has a computed table. In that case the loss is in EMG (money-equivalent) units. Otherwise false — the constant-threshold fallback ran instead; checker-play decisions have no MET branch at all and are always false.
  • cube_equities — a cube offer panel's answer. With state: 'published' it is the three branches (No double / Double, take / Double, pass) in EMG relative to the best line: the best line is exactly 0 and the other two are signed differences from it. A line may be positive: Double/pass usually beats Double/take for the doubler, and the only reason it is not the recommendation is that the opponent picks between them — so read best, never the maximum of the three. With state: 'rules_gated' the RULES forbid this cube (a Crawford game, an opponent-owned cube, the 64 cap, a dead cube): there are no branches and there never will be — the zero would otherwise sit on No double for a reason unrelated to equity, so both double lines would read as gains beside the error the same row charges for showing that cube. With state: 'refused' the branches were computed from this row's own stored inputs and could not be published (a non-finite MWC): a recompute reads the same inputs and refuses again. The field is absent entirely, not zeroed, when there is no answer at all: checker plays, take/drop replies, the constant-threshold fallback.

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: [] and forced: true.
  • Bear-off positions late in the race may have alternatives length 1 — the only legal play. Treat as forced.
  • Cube decisions in the Crawford game return alternatives containing 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) };
}

Nardex reports this same split server-side: the public profile (/users/:id) returns pr (the checker-only headline number, same as the leaderboard), a separate cube_pr (this second bucket above), and overall_pr (a pooled mean over both buckets, not the average of the two PRs) — so a client-side splitPR over API responses lines up with what a player sees on their own public profile.

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.
  • How to analyze a long-narde game — same content for long narde.

Frequently asked questions

What does equity_loss mean 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 grade separate from equity_loss?
grade is a discretization of equity_loss with hard thresholds (ok / doubtful / error / blunder). The thresholds are fixed in the engine but configurable per-deployment. Treat grade as a UI hint, equity_loss as the source of truth.
What's in the probabilities field?
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/engine outputs for the exact shape.
How do I aggregate per-move equity_loss into PR?
PR (gnubg convention) = (sum of equity_loss over non-forced moves / count of non-forced moves) × 500. Filter forced: true moves out of both numerator and denominator.
Are cube actions analyzed too?
Yes — when the decision.type === "cube_decision", the chosen and alternatives are cube_action objects (not play). Score them separately when computing PR-positional vs PR-cube. A never-shown double is graded too: it comes back as a cube_decision whose chosen.cube_action === "no_double".
What does met_based mean?
true means this cube decision was graded against a real Match Equity Table (MET) for the active profile — equity_loss is a match-winning-chance difference normalised to money-equivalent (EMG) units. false means the constant-threshold fallback was used instead (no MET table for this profile, a money game, or a match-to-1). Checker-play decisions never have a MET branch and are always false this generation of the API.
What is cube_equities?
The cube <em>offer</em> panel's answer, tagged by state. published carries the three branches — no_double, double_take, double_pass — in EMG relative to the best line, plus best naming which line that is and reply naming the answer a rational opponent gives the double (double_take or double_pass — never no_double, which is not an answer). Read reply rather than comparing the two double lines: it is the branch the evaluation selected, and it is the only statement about the answer left when best is no_double. The best line is exactly 0 and the other two are signed differences from it, so a line can legitimately be positive: Double/pass usually beats Double/take for the doubler, and the only reason it isn't the recommendation is that the <em>opponent</em> chooses between them. Read best; never take the maximum of the three. rules_gated means the <em>rules</em> forbid this cube (a Crawford game, an opponent-owned cube, the 64 cap, a dead cube): there are no branches and no recompute will ever produce any. Such an offer is still graded — equity_loss charges whoever showed that cube — and that is exactly why no triple rides beside it: its zero would be measured from a different comparison than the charge. refused means the branches were computed from this row's own stored inputs and could not be published (a non-finite MWC): nothing forbids the cube here, but a recompute reads the same inputs and refuses again, so this is not a pending computation either. The field is absent entirely — never zeroed — when there is no answer at all: checker plays, take/drop replies, the constant-threshold fallback.
What does analysis_depth tell 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.

Learn

  • Long Narde
  • Backgammon
  • Theory
  • Blog
  • FAQ

Developers

  • Analytics API
  • Engine training

Project

  • Android app
  • About
  • Software
  • Privacy Policy

Support

  • support@nardex.ai
© 2026 Nardex support@nardex.ai Long Narde and Backgammon with AI