🔬 Дизайн: анализ партий

Что это

Продуктовое применение обеих serving-моделей: динамическая шкала оценки по ходу партии, разложение каждого свинга на удачу и решение (фича, которой нет у шахматных баз — она возможна только в игре с костями) и оценка каждого хода человека против выбора модели. Спроектировано 10.08.2026; issues: analytics#276 (roll-aware эндпоинт), analytics#277 (грейдер ходов), analytics-ui#206 (сама фича, три фазы).

Связанные: ml-lab-journal · model-artifacts-storage-and-deploy · what-is-lightgbm · kcp-feature-tactics-vs-search-depth

Почему это вообще возможно: экономика перевернулась

Та же модель, что не годится для бота, идеально годится для анализа — из-за того, когда платится цена оценки:

Бот (live)Анализ партии
Оценок на действиесотни (1-ply) … сотни тысяч (2-ply)одна на ход партии
Бюджетсекунды на ход, всегдаминута на партию, один раз
Повторное использованиенеткэш навсегда (позиции дебюта шарятся между партиями)

Стоимость одной оценки ~40–60 мс, и она сидит в фичах (4 DFS по 216 исходов в KcpFeatures), а не в модели (микросекунды). Для анализа это приемлемо; для листа поиска — нет. Подробно: запись журнала от 10.08.

Какие модели и в каких ролях

Обе — LightGBM objective=regression на полном корпусе 19 991 736 строк (только king_captured), версионированы с манифестами в приватном ev (ev#24):

МодельВходыРаскладкаРоль в фичеСтатус
kcp_nodice_serving_20260809.onnx13ровно KcpFeatures.extractшкала (pre-roll точки); оценка позиций после хода в грейдере✅ в проде (EV_MODEL_PATH, analytics v0.6.0)
kcpdice_serving_20260809.onnx19KcpFeatures.extract ++ 6 счётчиков костей (--dice-last)компонента удачи: P(win | позиция, бросок)экспортирована, ждёт #276 (EV_ROLL_MODEL_PATH)

Ловушка двух 19-входовых раскладок

В ev существуют ДВЕ раскладки шириной 19: обучающая (кости в середине) и серверная (кости в конце). По ширине они неразличимы, а скоринг «не своей» раскладкой не падает — он возвращает правдоподобные неверные числа. Поэтому serving-модели обучены --dice-last (сервер дописывает 6 чисел к массиву движка, а не вклеивает в середину), загрузчики проверяют ширину и называют раскладку в ошибке, а происхождение каждого файла закреплено манифестом.

Механика и формулы

Все вероятности — с точки зрения ходящего (как в моделях); UI нормализует к белым для графика: P_white = side=='w' ? p : 1−p.

Шкала. Точка на каждой границе хода (pre-roll позиция — ровно те моменты, где сейчас показывается панель equity): P_pre(pos_t) от 13-входовой модели. Середины ходов не оцениваем — это не начала ходов, pre-roll число там врало бы (та же логика, по которой панель на них скрывается). Терминальная позиция — 0/1 из результата, не из модели.

Удача броска. На ходе t выпали кости dice_t:

luck(t) = P_roll(pos_t, dice_t) − P_pre(pos_t)

— насколько сам бросок улучшил или ухудшил положение ходящего, до всяких решений. Требует 19-входовую модель (#276).

Качество решения. Все легальные пути хода перечисляет TurnGenerator (движок), каждая результирующая позиция оценивается 13-входовой моделью; вероятность ходящего после пути = 1 − model(результат), потому что в результирующей позиции ход соперника; путь со взятием короля терминален — ровно 1.0 без модели:

decision(t) = P_after(сыгранный путь) − P_after(лучший путь)   // ≤ 0

Свинг между соседними точками шкалы ≈ luck(t) + decision(t) + невязка модели — разложение показывается пользователю как две полоски под графиком: «что дали кости» и «что сделал игрок».

Агрегаты на партию: суммарная удача каждой стороны («кому везло»), accuracy = средний winrate-loss, маркеры зевков при −decision(t) выше порога. Порог не выдумывается: калибруется по ground-truth набору наказанных зевков из mine_blunders.py (ev) по рейтинг-бендам — отдельная задача в ev.

Стоимости

ОперацияЦенаКогда платится
точка шкалы30–70 мс~30–40 на партию ≈ 2–4 с первый просмотр, дальше кэш по FEN
компонента удачи+40–60 мс на ходвместе со шкалой (фаза 2)
грейдинг одного ходадо ~6,6 с (сотни путей × KCP)только по явной кнопке «Analyze game», с прогрессом; дедуп транспозиций и параллелизм внутри запроса срезают типичный случай до ~1–3 с
грейдинг партии~1 мин на 4 ядраходин раз, кэш по (normalized_fen, dice_sorted) — дебютные ходы шарятся между всеми партиями

Фазы поставки (каждая самостоятельно полезна)

  1. Шкала — чистый UI, бэкенд уже готов (существующий /win-probability + существующий кэш fetchWinProbability). Фан-аут с ограниченной конкуренцией — как 56-вызовная матрица.
  2. Полоски удачи — после #276; деградирует до фазы 1, если roll-эндпоинт отвечает 503.
  3. Оценка ходов — после #277; кнопка «Analyze game», winrate-loss на ход, зевки, accuracy.

На деплое без моделей (503 всюду) фича просто отсутствует с одним тихим уведомлением — не ошибка.

Честные пределы (обязаны дойти до текстов UI)

  • Грейдер — 1-ply оценщик силы ~сильного клубного игрока (60.8% против aggressive), не оракул: расхождение с моделью — улика, не приговор. Тексты: «Model suggests», не «Correct was». Глубокую тактику на 2+ хода вперёд он не видит.
  • Шкала дрожит ±1–2 пп между спокойными ходами (MAE модели ~0.9 пп на пробах) — сглаживать или квантовать подачу, не изображать точность.
  • Разложение свинга содержит невязку (модель несовершенна в обеих точках) — полоски удачи/решения подавать как оценку, суммы за партию честнее одиночных ходов.

Открытые вопросы

  • Порог «зевка»: калибровка по mine_blunders.py по рейтинг-бендам (задача в ev).
  • Персист кэша грейдинга: v1 — in-memory LRU; таблица в БД (game_id, model_version → JSONB) только если перезапуски заметно бьют по повторным анализам.
  • Показывать ли «лучший ход» явно или только дельту (спойлер-эффект в тренажёрном сценарии).
  • Тренажёрный режим поверх грейдера («найди лучший ход при этих костях») — естественное продолжение, отдельный дизайн.