Чтение ответа 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' | 'match_emg' | 'cube_dist';
forced: boolean; // true, если реального выбора не было
analysis_depth: 'ply_zero' | 'ply_two' | 'ply_three';
met_based: boolean; // true = equity_loss из реальной MET-таблицы (только куб)
cube_equities?: CubePanel; // ответ по кубовому ОФФЕРУ; отсутствует, если ответа нет
}
type CubePanel =
| ({ state: 'published' } & CubeEquities) // три ветки
| { state: 'rules_gated' } // куб запрещён правилами: веток нет и не будет
| { state: 'refused' }; // ветки считались и не опубликованы: пересчёт не поможет
interface CubeEquities {
no_double: number; // EMG относительно лучшей строки
double_take: number;
double_pass: number;
best: 'no_double' | 'double_take' | 'double_pass';
}
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. Конфигурируется пер-деплою.met_based—true, если этотequity_lossпосчитан по калиброванной таблице матч-эквити (MET); применимо только к кубовым решениям (decision.type === "cube_decision") и только когда у активного профиля есть посчитанная таблица. В этом случае потеря — в EMG-единицах (money-эквивалент). Иначеfalse— задействован константный fallback; у ходовых (checker-play) решений MET-ветки нет вообще, поэтому у них всегдаfalse.cube_equities— ответ по кубовому офферу. Приstate: 'published'это три ветки (No double / Double, take / Double, pass) в EMG относительно лучшей строки: у строкиbestзначение ровно0, у остальных — знаковая разница с ней. Строка может быть положительной: Double/pass обычно выгоднее для удваивающего, чем Double/take, и не является рекомендацией лишь потому, что выбирает соперник — читайbest, а не максимум из трёх. Приstate: 'rules_gated'куб запрещён ПРАВИЛАМИ (игра Кроуфорда, куб у соперника, потолок 64, мёртвый куб): веток нет и не будет — там ноль стоял бы на «без удвоения» не по эквити, и обе кубовые строки оказались бы положительными рядом с ошибкой, которую та же строка записывает за показ куба. Приstate: 'refused'ветки считались по сохранённым входам этой строки и не опубликованы (нефинитный MWC): пересчёт прочитает те же входы и откажет снова. Поле отсутствует целиком (а не обнуляется), когда ответа нет вообще: checker-play, ответ take/drop, константный fallback.
Если ход не передан (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) };
} Nardex сам считает такое же разделение на своей стороне: публичный профиль (/users/:id) отдаёт pr (чекер-хедлайн, то же самое, что и лидерборд), отдельный cube_pr (эта вторая корзина выше) и overall_pr (объединённое среднее по обеим корзинам, не среднее двух PR) —
клиентский splitPR над ответами API совпадает с тем, что игрок видит в своём публичном
профиле.
И на 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, когда оцениваешь целый матч.
Дальше
- 5-минутный quickstart — минимально-жизнеспособный запрос.
- Nardex vs gnubg vs XG — отличия движков.
- Разработчикам — запросить доступ к бете.
- How to Analyze Backgammon Games — человеческая версия этих метрик.
- «Как анализировать партию в нардах» — то же для длинных нард (на русском).
Частые вопросы
- Что именно означает
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-кубовый. Непоказанный дабл тоже оценивается: он приходит какcube_decision, у которогоchosen.cube_action === "no_double". - Что означает
met_based? trueзначит, что это кубовое решение оценено по настоящей таблице матч-эквити (MET) активного профиля —equity_lossэто разница match-winning-chance, нормализованная в money-эквивалентные (EMG) единицы.falseзначит, что использован константный fallback (нет MET-таблицы для профиля, money-игра или матч до 1 очка). У ходовых (checker-play) решений MET-ветки нет вообще, и у них всегдаfalseв этом поколении API.- Что такое
cube_equities? - Ответ панели по кубовому <em>офферу</em>, размеченный полем
state.publishedнесёт три ветки —no_double,double_take,double_pass— в EMG <strong>относительно лучшей строки</strong>, плюсbestс именем этой строки. Уbestзначение ровно 0, у остальных — знаковая разница с ней, поэтому строка может быть положительной: Double/pass обычно выгоднее для удваивающего, чем Double/take, и не является рекомендацией лишь потому, что выбирает <em>соперник</em>. Читайbest, а не максимум из трёх.rules_gatedзначит, что куб запрещён <em>правилами</em> (игра Кроуфорда, куб у соперника, потолок 64, мёртвый куб): веток нет и никакой пересчёт их не даст. Запрещённый оффер всё равно оценивается:equity_lossсписывается с показавшего куб, и именно поэтому тройка рядом не публикуется — её ноль считался бы от другого сравнения, чем сам заряд.refusedзначит, что ветки считались по сохранённым входам этой строки и не опубликованы (нефинитный MWC): куб здесь ничем не запрещён, но пересчёт прочитает те же входы и откажет снова — то есть это и не «ещё не посчитано». Поле отсутствует целиком (а не обнуляется), когда ответа нет вообще: checker-play, ответы take/drop, константный fallback. - Что говорит
analysis_depth? - Уровень глубины поиска.
ply_zero= прямая оценка нейросети (самый быстрый).ply_two= 2-ply lookahead с сетью на листьях (по умолчанию).ply_three= более глубокий поиск, медленнее. Значения equity с разных глубин не строго сравнимы.