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 игрока (стандартный спин, бонус-бай, фичи-старт). Параметры:

ПолеТипНазначение
namestring"base", "bonus", "superspin", …
costfloatМножитель ставки (100.0 для buy-bonus стоимостью 100×).
rtpfloatЦелевая RTP для этого режима.
max_winfloatCap (например, 5000.0).
is_featureboolЕсли true — фронт «залипает» на этом режиме без подтверждения.
is_buybonusboolМетка для UI: режим куплен напрямую (часто другая графика).
auto_close_disabledboolЕсли true — /endround не вызывается автоматически (актуально для бонус-режимов с возможностью вернуться даже при 0-payout).
distributionsDistribution[]Сегментация выборки.

Все режимы делят paytable, символы, payline-конфигурацию — отличаются параметрами выборки и стоимостью.

Distribution — сегмент выборки

Каждый Distribution — «корзина» для части симуляций mode-а. У него:

ПолеТипНазначение
criteriastringЯрлык: "basegame", "freegame", "wincap", "0", "superspin", …
quotafloatДоля общего числа симуляций mode-а (нормализуется).
win_criteriafloatoptional
conditionsdictСм. ниже. Параметры, которыми крутят математику только в этом сегменте.

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),
  ]),
]

Как читать:

  • В base 50% симуляций будут платными базовыми спинами без freegame, 40% — нулевыми, 10% — с триггером freegame, 0.1% — max-win (5000×).
  • В bonus (buy-bonus стоимостью 100×) почти все симуляции — freegame; max-win редкий.

Порядок criteria важен

При распределении sim_id-ов по корзинам перед запуском симулятора corteza проходит criteria по порядку. Это критично, потому что один реальный исход может попадать сразу в несколько (max-win почти всегда — freegame). Правило:

  1. Сначала самые узкие/специфичные (wincap).
  2. Потом более общие (freegame).
  3. Потом дополнения (0).
  4. Потом catch-all (basegame).

Если сделать наоборот — freegame заберёт все freegame-id-ы, и для wincap останется голодный пул, в котором почти не выпадает 5000×.

Warning

Это правило обязательно документировать в game spec и проверять в CI. Перепутанный порядок ломает балансировку молча: симулятор молотит freegame-ы вместо max-win-ов, RTP в bonus-mode уезжает.

Применение в нашем real-time бэке

На проде BetMode даёт:

  • Маршрутизацию /playmode определяет 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-флагов).

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