Simulation methodology

Методология оффлайн-симуляции слота: что и зачем считать перед каждым релизом math, какие артефакты собирать, как организовать CI-регрессии. Применима для real-time бэка как обязательный pre-deploy шаг (см. discrete-outcomes vs real-time).

Зачем симулировать, если математика real-time

  1. Подтвердить таргетную RTP — теоретическая RTP сходится только в пределе. Симуляция миллионов спинов с фиксированным seed-сетом даёт точечную оценку с известным CI; без неё мы не узнаем о regressions, пока их не поймает прод.
  2. Cert-evidence — лаборатории (GLI/eCOGRA/UKGC) требуют отчёт по симуляции: RTP, HF, volatility, max-win frequency, hit-rate по фичам. Без оффлайн-pipeline собрать это нечем.
  3. Регрессии — изменение reelstrip / paytable / multiplier-таблицы должно ронять CI, если ломает math-таргеты.
  4. Балансировка фич — нужно знать, какая доля RTP уходит в base / freegame / max-win, чтобы продуктово настраивать профиль.
  5. Frontend-debug — uncompressed JSON-«книги» из симулятора используются как фикстуры для прокликивания UI на любом сценарии (без ожидания, что нужный исход выпадет в живом тесте).

Архитектура симулятора

Repeat-цикл

Шаблон одного спина (адаптированный из gamestate.run_spin Stake SDK):

function runSpin(sim: number, ctx: GameContext): RoundResult {
  ctx.rng.seed(sim);            // детерминированный PRNG → точный replay
  let repeat = true;
  while (repeat) {
    ctx.resetBook();             // обнулить локальные счётчики раунда
    ctx.drawBoard();             // weighted-pick reelstrip + случайные стоп-позиции
    evaluateWins(ctx);           // win_data + emit reveal/winInfo events
    ctx.wallet.updateGameTypeWins(ctx.gametype);
    if (ctx.checkFsCondition()) {
      runFreespinFromBase(ctx);
    }
    ctx.evaluateFinalWin();
    repeat = !ctx.checkRepeat(); // не совпали criteria/win_criteria → крутим заново
  }
  ctx.imprintWins();             // принять раунд: добавить в library и force-records
  return ctx.book;
}

Ключевое: тот же sim_id → тот же seed → тот же исход. Это даёт точный replay для cert-аудита, регрессий и dispute resolution.

Criteria / quotas — сегментация выборки

Чистая случайная выборка из N симуляций даст единицы max-win-сценариев на миллион (плохо для статистики). Решение Stake — заранее распределить sim_id-ы по criteria через quota, потом repeat-цикл крутит спин, пока не выпадет нужный исход.

См. BetMode + Distribution. В контексте симулятора:

  • quota=0.001 для wincap → 0.1% всех симуляций mode-а это max-win.
  • quota=0.4 для 0 → 40% всех симуляций — нулевые спины.
  • quota=0.5 для basegame — основной массив платящих базовых спинов.
  • quota=0.1 для freegame — триггеры freegame.

Quota нормализуются (сумма не обязана быть 1). Минимум 1 симуляция на criteria.

Force-records — аналитический хук

В коде record({...}) помечает интересные исходы:

ctx.record({
  kind: countSpecial(ctx.board, "scatter"),
  symbol: "scatter",
  gametype: ctx.gametype,
});

Все record за раунд буферизируются и попадают в force_record_<mode>.json только если раунд принят (imprintWins). Файл — словарь:

[
  {
    "search": {"gametype": "basegame", "kind": 5, "symbol": "scatter"},
    "timesTriggered": 22134,
    "bookIds": [7, 12, ...]
  }
]

Используется для:

  • частоты редких событий («сколько раз 5 scatters»);
  • балансировки оптимизатором (помечаем max-win-сценарии для целевой ре-весовки);
  • prototyping UI: фронт-симулятор фильтрует «покажи мне раунд с 5 scatters» по bookIds.

Force-boards — форсирование сценариев

Для редких criteria (max-win, freegame entry) случайная генерация даёт слишком низкий success rate. Используются:

  • Reel weighting в conditions — на max-win режиме веса reelstrip-ов сдвигаются на «жирные» ({"BR0": 1, "WCAP": 5} → reelstrip WCAP выпадает в 5 раз чаще).
  • forceSpecialBoard(criteria, numForceSyms) — поиск по reelstrip позиций с нужным символом и принудительный выбор стоп-позиции, дающей нужное число специальных символов на board.
  • Multiplier values weighting — на max-win сегменте веса множителей wild-а смещены к крупным значениям ({50: 50, 20: 90, 10: 100, 5: 60, ...}).

Это только для симулятора. На проде — равномерные веса по reelstrip-у.

Артефакты симулятора

АртефактНазначение
library/books/books_<mode>.json[l.zst]Каждая запись — {id, payoutMultiplier, events:[...], criteria, baseGameWins, freeGameWins}. Уплывает только в фикстуры фронта и в cert-отчёт; на проде не нужен.
library/lookup_tables/lookUpTable_<mode>.csvid, weight=1, payout. Используется аналитикой и (опционально) оптимизатором весов.
library/lookup_tables/lookUpTableIdToCriteria_<mode>.csvКакому criteria принадлежит каждый id.
library/lookup_tables/lookUpTableSegmented_<mode>.csvРазделение payout по baseGameWins / freeGameWins. Для split-RTP анализа.
library/forces/force_record_<mode>.jsonКастомные ивенты по record(...).
library/configs/config.jsonSnapshot конфига (хеши reelstrips, paytable, betmodes). Для cert-evidence.
library/par-sheet.html или .xlsxСводный PAR sheet с RTP / HF / volatility / max-win / split base/free.

Размер выборки

Эмпирические ориентиры:

  • Smoke / debug — 100–1 000 симуляций, compression=false, 1 thread. Только проверить, что код не падает и события эмитятся правильно.
  • Pre-PR / unit-CI — 100K симуляций per mode, ~30 секунд на N CPU. Проверка, что RTP не уехал относительно baseline.
  • Pre-release / cert-batch — 10M+ симуляций per mode, distributed по worker-ам. Точечная оценка RTP с CI ±0.05pp, надёжные max-win frequency и tail-распределения.
  • Stake-style publish (если когда-то захотим) — типично 100K+ за один publish-batch, после lookup-table оптимизации.

CI-баланс: ставим в pre-PR быстрый прогон с заранее зафиксированным seed-сетом (детерминированный → один и тот же RTP при тех же reelstrip-ах), а тяжёлые батчи гоняем nightly или pre-release.

CI-регрессии

Минимальный набор check-ов перед merge:

  1. Snapshot RTP — фиксированный seed-сет, ожидаемый RTP с толерансом ±0.1pp.
  2. HF-snapshot — hit-rate per criteria (basegame / freegame / max-win / 0-win) — точное равенство на тех же seed-ах.
  3. Wallet invariant — для каждого раунда runningBetWin === basegameWins + freegameWins (см. Wallet manager pattern).
  4. Max-win cappayoutMultiplier <= maxWin для всех симуляций (никогда не превышаем cap из-за edge-case).
  5. Property-based (fast-check-style) — на random board сумма wins не превышает theoretical max; ни одна линия не платит дважды и т.д.

Любой merge, ломающий любой из этих check-ов, требует явного апдейта baseline и code-review с явным acknowledgement, что меняем math-таргет.

Связанные страницы