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» — соперник в своём окне удвоения, защищайся |
hold | 25–65% | «Hold — no double yet» — копи перевес к окну |
double | 65–75% (DOUBLE_WINDOW_LO–HI) | «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 | 📊 Recorded | Side to move 68.9% | — |
| Estimating | 🎲 Estimate | спиннер Estimating… | индетерминантная полоса |
| Refining | 🎲 Estimate | ≈ 49.8% + N rollouts · CI | детерминантная полоса rollouts / цель + refining; CI стягивается |
| Unavailable | 🎲 Estimate | Estimate unavailable | — |
| Error | — | сообщение об ошибке | — |
Кубик — это метод, а не случайность
Кубик
🎲— намеренный значок методики Estimate (Monte-Carlo «бросает кубики»), у живых данных — значок базы📊. Раньше кубик появлялся непоследовательно; теперь это часть единой визуальной логики.
Ключевые параметры и выборы
| Параметр | Значение | Где | Смысл |
|---|---|---|---|
minSample | 1 (Explorer) / 10 (Game Viewer) | проп компонента | порог переключения Recorded → Estimate |
MIN_SAMPLE | 30 | explorer.ts | надёжность вердикта в Recorded |
MC_TARGET_ROLLOUTS | 4000 | PositionEquity.svelte | бюджет оценки = знаменатель полосы прогресса |
batch | 64 | mc-worker.js | rollout-ов на один вызов движка (потом пулинг) |
maxPlies | 60 | mc-worker.js | горизонт одного rollout (в UI; движок по умолчанию 1000) |
MC_VERDICT_MAX_CI_WIDTH | 0.1 | PositionEquity.svelte | порог ширины CI для показа вердикта на оценке |
TAKE_POINT / DOUBLE_WINDOW_LO / _HI | 0.25 / 0.65 / 0.75 | explorer.ts | границы зон куба (см. «Правила куба в Dice Chess») |
Почему так
- Единый блок — равноценная подача: пользователю важна оценка и её неопределённость, а не то, где и как она посчитана. Меняется методика, не структура.
- Клиентский MC — не нагружает backend, уточняется без лимита, мгновенный предварительный ответ.
- Гейтинг вердикта по CI — честность: совет по удвоению только когда оценка достаточно уверенная.
- Кэш по FEN — повторный заход в позицию мгновенный.
Связанные заметки
- 🎓 Rao-Blackwellized Monte-Carlo — математика оценщика.
- WebAssembly — почему движок исполняется как WASM.
- 🎓 Нормализованный FEN — ключ позиции (DFEN / нормализация).
Открытые улучшения
- Path-версионирование статических WASM-ассетов (cache-busting при апгрейде движка).
- Пулинг кэша по симметрично-эквивалентным позициям (через
canonicalKey).- Бенч «WASM vs чистый JS» перед статьёй о компиляции Scala → WASM.