Position Equity — оценка вероятности позиции

Панель Position Equity (компонент PositionEquity.svelte) показывает вероятность победы стороны, чей ходдо броска кубиков (pre-roll equity) — и переводит её в совет по кубу удвоения (doubling cube). Это ядро ценности аналитики: «в этой позиции при таком-то фильтре сторона X побеждает в ~Y% случаев».

Pre-roll equity

Это вероятность до того, как кубики брошены. Она усредняет по всем 216 возможным броскам, поэтому отвечает на вопрос «насколько хороша сама позиция», а не «что делать после конкретного броска».

Панель используется в двух местах:

  • Openings Explorer (minSample = 1) — корневая позиция дерева дебютов;
  • Game Viewer, вкладка Analytics (minSample = 10) — позиция на текущем ходу разбираемой партии.

Две методики, один блок

Данных по позиции может быть много (сыгранные партии в БД) или мало / совсем нет (off-book позиция). Поэтому equity считается двумя способами, но визуально это один и тот же блок — меняется только методика, а не структура. Обе методики приблизительные и обе несут доверительный интервал (CI).

flowchart TD
    A["Позиция (FEN) + фильтры"] --> B["GET /api/positions/equity"]
    B -->|"decided ≥ minSample"| C["Метод A: Recorded<br/>эмпирический win-rate по партиям"]
    B -->|"decided < minSample<br/>(или 404 / пусто)"| D["Метод B: Estimate<br/>клиентский Monte-Carlo в Web Worker"]
    C --> E["Единый блок:<br/>% + CI + шкала удвоения + вердикт"]
    D --> E
    D -.->|"батчи по 64,<br/>фоновое уточнение"| D

decided = wins + draws + losses — это размер выборки (партия с неопределённым исходом увеличивает games, но не decided).


Метод A — по сыгранным партиям (Recorded)

Backend возвращает агрегат { wins, draws, losses, win_rate, side_to_move }. Equity — это средний игровой счёт (победа = 1, ничья = ½, поражение = 0):

Доверительный интервал — нормальное приближение к среднему (ничьи учтены через дисперсию счёта, а не как бинарный исход), реализовано в equityInterval():

Вердикт по кубу показывается только при надёжной выборке: decided ≥ MIN_SAMPLE (30). Ниже — мягкий плейсхолдер «Not enough games».

Это тоже приблизительно

«По живым партиям» ≠ «точно». Это выборка: при 30 партиях CI широкий, при 10 000 — узкий. Поэтому CI и доверительная полоса на шкале присутствуют в обоих методах.


Метод B — клиентский Monte-Carlo (Estimate)

Когда партий мало, backend быстро отвечает «данных нет» (sparse / 404), и на клиенте запускается фоновая Monte-Carlo оценка. Архитектурный выбор:

Почему на клиенте

Что может быть посчитано на клиенте — пусть считается на клиенте. Backend отдаёт быстрый «отрицательный» ответ, после чего клиент в фоне постоянно улучшает точность: даёт быстрый предварительный результат и уточняет его со временем. На клиенте мы не лимитируем вычисления и не нагружаем backend.

Движок (WASM, скомпилированный из Scala 3) исполняется в Web Worker (вне главного потока), считает rollout-ы батчами по 64, отдаёт каждый батч на главный поток, где они пулятся в уточняющуюся оценку. Результат кэшируется по FEN.

flowchart LR
    UI["PositionEquity.svelte"] -->|"start(dfen)"| W["Web Worker<br/>mc-worker.js"]
    W -->|"import"| ENG["WASM движок<br/>estimateEquity()"]
    ENG -->|"батч × 64 rollouts"| W
    W -->|"postMessage(batch)"| POOL["pool.ts<br/>addBatch / poolEquity"]
    POOL -->|"уточнённая оценка + CI"| UI
    W -->|"done / error"| UI

Математика самого оценщика (Rao-Blackwellization, дисперсия, пулинг батчей) вынесена в отдельную заметку:

Подробно про алгоритм

🎓 Rao-Blackwellized Monte-Carlo — что такое rollout, почему дисперсия низкая, как пулятся батчи и как CI стягивается как .

Equity стороны, чей ход, берётся из оценки (whiteWin или blackWin), CI — 1.96 · SE вокруг неё. Вердикт по кубу показывается только когда CI стал достаточно узким (MC_VERDICT_MAX_CI_WIDTH = 0.1, т.е. ширина ≤ 10 п.п.) — чтобы не советовать удвоение по шумной оценке в 64 rollouts.


Куб удвоения (doubling cube)

Equity переводится в зону действия по кубу. Границы (explorer.ts):

ЗонаEquity стороны, чей ходВердикт
behind< 25% (TAKE_POINT)«You’re behind» — соперник в своём окне удвоения, защищайся
hold25–65%«Hold — no double yet» — копи перевес к окну
double65–75% (DOUBLE_WINDOW_LOHI)«Offer the double» — соперник должен принять (его шанс > 25%)
too_good> 75%«Too good» — удваивай, ожидай пас

На шкале (gauge) это четыре сегмента (красный / серый / синий / янтарный), стрелка-иголка на текущем equity и серая полоса 95% CI вокруг неё — в обоих методах одинаково.


Правила куба в Dice Chess

Зоны выше заимствованы из теории куба удвоения в нардах, но правила куба в Dice Chess имеют три особенности, которые определяют выбор порогов.

Правила сайта (Doubling)

  • Игрок при своём ходе может предложить удвоить ставку (кнопка ×2).
  • Соперник либо принимает удвоение, либо сдаётся.
  • После того как игрок предложил удвоение, его ×2 неактивна, пока соперник не предложит удвоение в ответ.
  • Если банк партии вот-вот превысит баланс игрока, принять удвоение нельзя — придётся RESIGN.
  • Нельзя сесть за стол с банком (×2-режим) / ставкой (Standard) больше своего баланса.

1. Живой куб (recube). Удваивать дважды подряд нельзя — право переходит к принявшему. Это живой куб: у владельца есть ценность повторного удвоения, поэтому теоретический take point ниже (~20–22%), а точка паса лидера выше (~78–80%).

2. Гаммонов нет. Удваивается только ставка (×2), исход бинарный (победа / сдача) — нет гаммона (двойной стоимости победы) и бэкгаммона (тройной). Поэтому нет поправки take point на риск гаммона и нет зоны «too good = играть на гаммон, не удваивая». Наша модель (без гаммон-логики; too_good = «удваивай, жди сдачи») этому соответствует. ✓

3. Конечный банкролл. Money-game-теория (25/75) предполагает бесконечный банк. Здесь куб ограничен балансом: у предела игрок обязан сдаться даже с выгодным take, а ограниченный рост куба «подмертвляет» его → take point ползёт обратно к 25%. То есть для «глубоких» балансов ближе ~20–22% (живой куб), для «мелких» — ~25% (мёртвый).

Сравнение с рекомендациями профи

ПорогКлассика (нарды, money)У насКомментарий
Take point (соперник принимает)25% (мёртвый) / ~20–22% (живой)TAKE_POINT = 25%консервативно; лимит банкролла оправдывает 25%
Cash / «too good» (соперник пасует)75% (мёртвый) / ~78–80% (живой)DOUBLE_WINDOW_HI = 75%зеркало take point’а; консервативно
Когда удваивать~70% (rule of thumb)DOUBLE_WINDOW_LO = 65%было 60% (рановато), поднято ближе к ориентиру

Выбор: оставляем 25% / 75% как честный консервативный дефолт — живой куб тянул бы к ~20–22% / ~78–80%, но лимит банкролла тянет обратно к мёртвым 25/75. Нижнюю границу подняли 60% → 65% ближе к профессиональному ~70%, с небольшим запасом на волатильность (в «взрывных» позициях удваивают раньше).

Источники по теории куба: Backgammon Galaxy — Doubling Cube, Youngerman — Doubling Cube Strategy (bkgm.com), Backgammon101 — The Doubling Cube.


Единый блок: состояния

Блок всегда состоит из: шапка → метка метода → большая цифра → строка CI → шкала удвоения → вердикт → (для оценки) полоса прогресса. Различаются только источник и индикатор прогресса.

stateDiagram-v2
    [*] --> Loading
    Loading --> Error: запрос упал
    Loading --> Recorded: decided ≥ minSample
    Loading --> Estimating: decided < minSample
    Estimating --> Refining: пришёл первый батч
    Estimating --> Unavailable: worker завершился без батчей / сбой
    Refining --> Refining: новый батч (CI сужается)
    Refining --> Done: достигнут бюджет rollouts
    Recorded --> [*]
    Done --> [*]
СостояниеМеткаЗаголовокПрогресс
Recorded📊 RecordedSide to move 68.9%
Estimating🎲 Estimateспиннер Estimating…индетерминантная полоса
Refining🎲 Estimate≈ 49.8% + N rollouts · CIдетерминантная полоса rollouts / цель + refining; CI стягивается
Unavailable🎲 EstimateEstimate unavailable
Errorсообщение об ошибке

Кубик — это метод, а не случайность

Кубик 🎲намеренный значок методики Estimate (Monte-Carlo «бросает кубики»), у живых данных — значок базы 📊. Раньше кубик появлялся непоследовательно; теперь это часть единой визуальной логики.


Ключевые параметры и выборы

ПараметрЗначениеГдеСмысл
minSample1 (Explorer) / 10 (Game Viewer)проп компонентапорог переключения Recorded → Estimate
MIN_SAMPLE30explorer.tsнадёжность вердикта в Recorded
MC_TARGET_ROLLOUTS4000PositionEquity.svelteбюджет оценки = знаменатель полосы прогресса
batch64mc-worker.jsrollout-ов на один вызов движка (потом пулинг)
maxPlies60mc-worker.jsгоризонт одного rollout (в UI; движок по умолчанию 1000)
MC_VERDICT_MAX_CI_WIDTH0.1PositionEquity.svelteпорог ширины CI для показа вердикта на оценке
TAKE_POINT / DOUBLE_WINDOW_LO / _HI0.25 / 0.65 / 0.75explorer.tsграницы зон куба (см. «Правила куба в Dice Chess»)

Почему так

  • Единый блок — равноценная подача: пользователю важна оценка и её неопределённость, а не то, где и как она посчитана. Меняется методика, не структура.
  • Клиентский MC — не нагружает backend, уточняется без лимита, мгновенный предварительный ответ.
  • Гейтинг вердикта по CI — честность: совет по удвоению только когда оценка достаточно уверенная.
  • Кэш по FEN — повторный заход в позицию мгновенный.

Связанные заметки

Открытые улучшения

  • Path-версионирование статических WASM-ассетов (cache-busting при апгрейде движка).
  • Пулинг кэша по симметрично-эквивалентным позициям (через canonicalKey).
  • Бенч «WASM vs чистый JS» перед статьёй о компиляции Scala → WASM.