Simulation methodology
Методология оффлайн-симуляции слота: что и зачем считать перед каждым релизом math, какие артефакты собирать, как организовать CI-регрессии. Применима для real-time бэка как обязательный pre-deploy шаг (см. discrete-outcomes vs real-time).
Зачем симулировать, если математика real-time
- Подтвердить таргетную RTP — теоретическая RTP сходится только в пределе. Симуляция миллионов спинов с фиксированным seed-сетом даёт точечную оценку с известным CI; без неё мы не узнаем о regressions, пока их не поймает прод.
- Cert-evidence — лаборатории (GLI/eCOGRA/UKGC) требуют отчёт по симуляции: RTP, HF, volatility, max-win frequency, hit-rate по фичам. Без оффлайн-pipeline собрать это нечем.
- Регрессии — изменение reelstrip / paytable / multiplier-таблицы должно ронять CI, если ломает math-таргеты.
- Балансировка фич — нужно знать, какая доля RTP уходит в base / freegame / max-win, чтобы продуктово настраивать профиль.
- 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}→ reelstripWCAPвыпадает в 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>.csv | id, 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.json | Snapshot конфига (хеши 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:
- Snapshot RTP — фиксированный seed-сет, ожидаемый RTP с толерансом ±0.1pp.
- HF-snapshot — hit-rate per criteria (basegame / freegame / max-win / 0-win) — точное равенство на тех же seed-ах.
- Wallet invariant — для каждого раунда
runningBetWin === basegameWins + freegameWins(см. Wallet manager pattern). - Max-win cap —
payoutMultiplier <= maxWinдля всех симуляций (никогда не превышаем cap из-за edge-case). - Property-based (
fast-check-style) — на random board сумма wins не превышает theoretical max; ни одна линия не платит дважды и т.д.
Любой merge, ломающий любой из этих check-ов, требует явного апдейта baseline и code-review с явным acknowledgement, что меняем math-таргет.