BetMode + Distribution
Паттерн сегментации выборки симуляций по criteria. Решает проблему «при N симуляций редкие события (max-win, freegame entry) встречаются недостаточно часто для устойчивой статистики». Заодно даёт удобную модель множественных режимов ставки (base / bonus / buy-bonus / superspin) на одной игре.
Идиома взята из Stake math-sdk (src/config/betmode.py, src/config/distributions.py); полезна и для precomputed, и для real-time подхода.
BetMode — режим ставки
Один режим ставки = одна «кнопка» в UI игрока (стандартный спин, бонус-бай, фичи-старт). Параметры:
| Поле | Тип | Назначение |
|---|---|---|
name | string | "base", "bonus", "superspin", … |
cost | float | Множитель ставки (100.0 для buy-bonus стоимостью 100×). |
rtp | float | Целевая RTP для этого режима. |
max_win | float | Cap (например, 5000.0). |
is_feature | bool | Если true — фронт «залипает» на этом режиме без подтверждения. |
is_buybonus | bool | Метка для UI: режим куплен напрямую (часто другая графика). |
auto_close_disabled | bool | Если true — /endround не вызывается автоматически (актуально для бонус-режимов с возможностью вернуться даже при 0-payout). |
distributions | Distribution[] | Сегментация выборки. |
Все режимы делят paytable, символы, payline-конфигурацию — отличаются параметрами выборки и стоимостью.
Distribution — сегмент выборки
Каждый Distribution — «корзина» для части симуляций mode-а. У него:
| Поле | Тип | Назначение |
|---|---|---|
criteria | string | Ярлык: "basegame", "freegame", "wincap", "0", "superspin", … |
quota | float | Доля общего числа симуляций mode-а (нормализуется). |
win_criteria | float | optional |
conditions | dict | См. ниже. Параметры, которыми крутят математику только в этом сегменте. |
Conditions
Базовые ключи (Stake-конвенция, расширяется под игру):
conditions = {
"reel_weights": {basegame: {"BR0": 1}, freegame: {"FR0": 1, "WCAP": 5}},
"mult_values": {basegame: {1: 1}, freegame: {2: 60, 3: 80, 5: 20, 10: 10, 20: 5, 50: 1}},
"scatter_triggers": {3: 50, 4: 20, 5: 5},
"force_wincap": False,
"force_freegame": True,
}reel_weights— какие reelstrips и с какими весами рандомить в этом сегменте. На max-win-сегменте поднимаем вес «жирной» полоскиWCAP.mult_values— взвешенный draw значений multiplier для wild-символов в freegame.scatter_triggers— взвешенный draw числа scatter-ов на board (для force-freegame сценариев).force_wincap/force_freegame— флаги, заставляющиеrunSpinкрутиться, пока не выпадет нужный исход.
Имена и набор conditions свободные — это просто bag для конкретной игры.
Пример: lines-game с base + bonus
freegame_condition = {
"reel_weights": {basegame: {"BR0": 1}, freegame: {"FR0": 1}},
"scatter_triggers": {3: 50, 4: 20, 5: 5},
"mult_values": {freegame: {2: 60, 3: 80, 5: 20, 10: 15, 20: 10, 50: 5}},
"force_freegame": True,
}
wincap_condition = {
"reel_weights": {basegame: {"BR0": 1}, freegame: {"FR0": 1, "WCAP": 5}},
"mult_values": {freegame: {2: 10, 3: 20, 5: 60, 10: 100, 20: 90, 50: 50}},
"scatter_triggers": {4: 1, 5: 2},
"force_wincap": True, "force_freegame": True,
}
bet_modes = [
BetMode(name="base", cost=1.0, rtp=0.97, max_win=5000, distributions=[
Distribution("wincap", quota=0.001, win_criteria=5000, conditions=wincap_condition),
Distribution("freegame", quota=0.1, conditions=freegame_condition),
Distribution("0", quota=0.4, win_criteria=0, conditions={...}),
Distribution("basegame", quota=0.5, conditions={...}),
]),
BetMode(name="bonus", cost=100.0, rtp=0.97, max_win=5000, is_buybonus=True, distributions=[
Distribution("wincap", quota=0.001, win_criteria=5000, conditions=wincap_condition),
Distribution("freegame", quota=0.999, conditions=freegame_condition),
]),
]Как читать:
- В
base50% симуляций будут платными базовыми спинами без freegame, 40% — нулевыми, 10% — с триггером freegame, 0.1% — max-win (5000×). - В
bonus(buy-bonus стоимостью 100×) почти все симуляции — freegame; max-win редкий.
Порядок criteria важен
При распределении sim_id-ов по корзинам перед запуском симулятора corteza проходит criteria по порядку. Это критично, потому что один реальный исход может попадать сразу в несколько (max-win почти всегда — freegame). Правило:
- Сначала самые узкие/специфичные (
wincap). - Потом более общие (
freegame). - Потом дополнения (
0). - Потом catch-all (
basegame).
Если сделать наоборот — freegame заберёт все freegame-id-ы, и для wincap останется голодный пул, в котором почти не выпадает 5000×.
Warning
Это правило обязательно документировать в game spec и проверять в CI. Перепутанный порядок ломает балансировку молча: симулятор молотит freegame-ы вместо max-win-ов, RTP в bonus-mode уезжает.
Применение в нашем real-time бэке
На проде BetMode даёт:
- Маршрутизацию
/play—modeопределяет cost, max_win, поведениеauto_close. - Конфигурацию math: какой reelstrip-сет дёргать (на проде обычно один — без force-сегментов).
- UI-флаги (
is_buybonus,is_feature).
Distribution на проде не используется напрямую (нет force-режимов). Это только симуляционный конструкт. Но конфиг един — те же bet_modes грузятся и в симулятор, и в runtime; runtime просто игнорирует distributions[*].conditions.force_*.
Идиомы для TS
-
BetMode и Distribution как отдельные типы:
type BetMode = { name: string; cost: number; rtp: number; maxWin: number; isFeature?: boolean; isBuybonus?: boolean; autoCloseDisabled?: boolean; distributions: Distribution[]; }; type Distribution = { criteria: string; quota: number; winCriteria?: number; conditions: DistributionConditions; }; type DistributionConditions = { reelWeights: Record<GameType, Record<ReelsetId, number>>; multValues?: Record<GameType, Record<number, number>>; scatterTriggers?: Record<number, number>; forceWincap?: boolean; forceFreegame?: boolean; [extra: string]: unknown; // game-specific }; -
simToCriteria(N, mode)— функция, раскидывающая[0..N)по criteria поquotaв порядке объявления. -
getDistributionConditions(ctx)— возвращает conditions для текущего criteria;runSpinдёргает оттудаreelWeights/multValues/scatterTriggers. -
На проде runtime использует те же типы, но вызывает только
getDistributionConditions(...)["reelWeights"](в одном «default» distribution без force-флагов).