Чтение ответа Nardex

Эндпоинт /api/v1/positions/analyze возвращает один PositionAnalysis на каждую отправленную позицию. Эта страница — пополевой справочник: что значит каждое свойство, как оно связано с остальными и как сложить пер-ходовой выход в пер-партийный PR. Если ещё не видел форму запроса — начни с 5-минутного quickstart.

Форма ответа

Каждый вызов возвращает один объект. Сигнатура TypeScript (зеркало Rust-исходника):

interface PositionAnalysis {
  alternatives: Alt[];          // ранжированные кандидаты, лучший первый
  chosen?: Alt;                 // твой ход; отсутствует, если ты его не передал
  equity_loss: number;          // chosen.equity vs alternatives[0].equity
  grade: 'ok' | 'doubtful' | 'error' | 'blunder';
  equity_kind: 'money' | 'match';
  forced: boolean;              // true, если реального выбора не было
  analysis_depth: 'ply_zero' | 'ply_two' | 'ply_three';
}

interface Alt {
  play?: MoveDetail[];         // для checker-play решений
  cube_action?: 'take' | 'drop' | 'double' | 'no_double';
  equity: number;
  probabilities: ProbVector;   // форма зависит от варианта игры
}

Массив alternatives

alternatives — это ранжирование движком легальных ходов-кандидатов (или cube-действий). Сортировка по убыванию equity — то есть alternatives[0] и есть лучший ход. Длина зависит от уровня глубины и настроек запроса; для типичных позиций — 5–20 элементов.

У каждого Alt есть либо массив play (решение checker-play), либо строка cube_action (решение по кубу) — никогда оба сразу. Что именно — определяет поле decision.type запроса.

chosen vs best

Если в запросе ты передал сыгранный ход, движок ищет его в alternatives и возвращает как chosen. Два производных числа:

  • equity_loss = alternatives[0].equity - chosen.equity. Всегда ≥ 0. Ноль значит, что ты нашёл лучший ход.
  • grade = бакет порогов equity_loss. Дефолтные пороги: ok < 0.02, doubtful 0.02–0.08, error 0.08–0.20, blunder ≥ 0.20. Конфигурируется пер-деплою.

Если ход не передан (decision.played пустой), chosen отсутствует, equity_loss равен 0, а grade'ok' по конвенции. Используй chosen === undefined, чтобы поймать этот случай.

Probabilities — длинные нарды vs короткие

В Alt.probabilities лежит разбиение сети на win/lose. Форма различается по варианту, потому что системы призов различаются:

// длинные нарды — вложенные ординальные вероятности
{
  win:    0.62,   // P(X выигрывает, включая марс и кокс)
  win_m:  0.18,   // P(победа >= марс)
  win_k:  0.02,   // P(победа >= кокс)
  lose:   0.38,
  lose_m: 0.05,
  lose_k: 0.0
}

// короткие нарды (backgammon) — money-game призовые бакеты
{
  win:     0.62,
  win_g:   0.14,  // P(выигрыш гаммоном)
  win_bg:  0.01,  // P(выигрыш бэкгаммоном)
  lose:    0.38,
  lose_g:  0.04,
  lose_bg: 0.0
}

Важно для длинных нард: поля win_m/win_k — вложенные ординальные, а не 6-классовый softmax. win_m = P(победа >= марс) включает кокс, win_k — самое строгое. Не пытайся складывать их как независимые бакеты.

Уровни analysis_depth

Доступны три настройки глубины:

  • ply_zero — прямая оценка нейросетью, доли миллисекунды на GPU. Подходит для массового скоринга или low-latency UI.
  • ply_two — 2-ply lookahead с NN на листьях. Дефолт для analyze-эндпоинта. Десятки мс.
  • ply_three — более глубокий поиск, сотни мс. Самый точный, но квоты и rate-limit бюджеты на этом уровне жёстче.

Значения equity с разных глубин не строго сравнимы — не агрегируй equity_loss из ply_zero и ply_two в один PR без нормализации.

forced и другие крайние случаи

forced: true означает, что у ходящей стороны не было реального выбора — обычно один легальный ход (или позиция куба, где решение недоступно). Когда считаешь PR по партии, отфильтруй forced-ходы и из числителя, и из знаменателя. Их включение искусственно занижает PR.

Другие крайние случаи:

  • Game-over позиции (ходы невозможны) возвращают alternatives: [] и forced: true.
  • Bear-off позиции в позднем race могут иметь alternatives длиной 1 — единственный легальный ход. Считается forced.
  • Cube-решения в Crawford-партии возвращают alternatives только с 'no_double'. Куб недоступен.

Агрегация пер-ходового в PR

Стандартный рецепт (конвенция gnubg) — 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;
}

Если хочешь PR-позиционный и PR-кубовый отдельно, группируй по 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) };
}

И на 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

Поле equity_kind на каждом PositionAnalysis говорит, в каких единицах даны значения equity — в money или match-winning-chance. Шкалы разные:

  • 'money' — equity в очках money-game: победа = +1, проигрыш = −1, выигрыш гаммоном = +2 и так далее. Диапазон обычно [−3, +3].
  • 'match' — equity как шанс выиграть матч с текущего счёта. Диапазон [0, 1]. Считается по таблице матч-equity (по умолчанию: post-Crawford safe).

Если смешать money и match equity в одном PR-агрегате, цифры будут бессмысленны. Фильтруй по equity_kind, когда оцениваешь целый матч.

Дальше

FAQ

Что именно означает equity_loss?
Это equity лучшего хода движка минус equity сделанного (chosen) хода, выраженное в единицах equity money-game. Всегда неотрицательное; 0 значит, что был сыгран лучший ход.
Почему grade отделён от equity_loss?
grade — это дискретизация equity_loss по фиксированным порогам (ok / doubtful / error / blunder). Пороги зашиты в движке, но конфигурируемы пер-деплою. Воспринимай grade как UI-подсказку, а equity_loss — как источник правды.
Что лежит в поле probabilities?
Для длинных нард: вложенные ординальные — { win, win_m, win_k, lose, lose_m, lose_k }. Для коротких нард (backgammon): { win, win_g, win_bg, lose, lose_g, lose_bg }. Названия разные, потому что системы призов разные. Точная форма — в выходах crates/engine.
Как агрегировать пер-ходовой equity_loss в PR?
PR (конвенция gnubg) = (сумма equity_loss по не-forced ходам / число не-forced ходов) × 500. Ходы с forced: true исключаются и из числителя, и из знаменателя.
Анализируются ли также cube-действия?
Да — когда decision.type === "cube_decision", chosen и alternatives — это объекты cube_action (а не play). Считай их отдельно, когда строишь PR-позиционный vs PR-кубовый.
Что говорит analysis_depth?
Уровень глубины поиска. ply_zero = прямая оценка нейросети (самый быстрый). ply_two = 2-ply lookahead с сетью на листьях (по умолчанию). ply_three = более глубокий поиск, медленнее. Значения equity с разных глубин не строго сравнимы.