Skip to content

Slot Game Responses ​

This guide explains how to read the scenario field that placeBet returns for slot games. The shapes below target @hizi.io/engine-sdk 0.2.18. Earlier versions used a separate package, @hizi.io/slot-types. @hizi.io/engine-sdk is the same package as the API client. You do not need an extra install to type-check your scenario handling on the frontend.

Overview ​

The slot engine inside hizi-engine-creator generates slot game databases. The creator's playGame() produces the IResult objects that it stores as scenarios. At runtime, gameResult.scenario contains exactly that structure.

Each API result contains one IResult scenario object. The generator can store an array, but the engine returns its current step. Cascades within one spin can also appear as the nested cascadeData.steps array.

Implement the shared frontend requirements first. Use rules and presentation to build the paytable from the effective configuration.

The IResult Structure ​

typescript
interface IResult {
  featureName?: string;
  board: TBoard;
  wins?: IWin[];
  winAmount: number;
  symbolSubstitutions?: ISymbolSubstitutionData[];
  duplicatingSymbolData?: IDuplicatingSymbolData[];
  stuckSymbols?: TStuckSymbols;
  bookSpinChosenSymbol?: string;
  bookSpinWin?: IBookSpinWin;
  spinMultiplierTotal?: number;
  multiplierCollectionData?: IMultiplierCollectionData[];
  cascadeData?: ICascadeData;
  boardStates?: IBoardState[];
  holdAndWinData?: IHoldAndWinData;
  wheelWins?: IWheelWin[];
  randomFeatureEntry?: string;
  featureMultiplier?: number;
  eventMultiplier?: number;
  collectorTargetData?: ICollectorTargetData[];
  collectorLevelData?: ICollectorLevelData;
  collectedSpinMultiplierData?: ICollectedSpinMultiplierData;
  reelRows?: number[];
  cascadeMultiplierValue?: number;
  featureRoundId?: number;
  spinInfo?: ISpinInfo[]; // consolidated rounds only - see Feature progression below
  nextFeature?: string; // consolidated rounds only - see Feature progression below
  singleGameRoundProgressionData?: ISingleGameRoundProgressionData[]; // jackpot games only - see Single-Gameround Jackpots below
}

Most fields are optional and only present when the corresponding feature is active. A minimal base game result without features contains just board, wins, and winAmount.

Reading the Board ​

board is a 2D array indexed by column (reel), then row (top to bottom):

typescript
type TBoard = Array<Array<string>>;
typescript
const scenario = gameResult.scenario as IResult;

// A 5×3 slot: 5 columns, 3 rows each
// scenario.board[0] = ['cherry', 'lemon', 'bar']     ← reel 0
// scenario.board[1] = ['seven', 'cherry', 'cherry']   ← reel 1
// ...

for (let col = 0; col < scenario.board.length; col++) {
  for (let row = 0; row < scenario.board[col].length; row++) {
    const symbol = scenario.board[col][row];
    // Render symbol at grid position (col, row)
  }
}

Reading Wins ​

wins is an array of IWin objects. Each describes one winning combination:

typescript
interface IWin {
  mode: 'winline' | 'ways' | 'scatter' | 'cluster' | 'unconditional' | 'collector';
  positions: Array<{ column: number; row: number }>;
  winMultiplier?: number;
  featureWin?: { featureName: string; featureCount: number };
  overrideWins?: boolean;
  symbolMultiplierTotal?: number;
  spinMultiplierValue?: number;
  multipayCount?: number;
  multipayResults?: IMultipayResult[];
  winlineIndex?: number;
  progressionAwards?: Record<string, number>;
  collectedSymbols?: ICollectedSymbol[];
}

Highlighting winning positions ​

typescript
if (scenario.wins) {
  for (const win of scenario.wins) {
    for (const pos of win.positions) {
      // Highlight the symbol at (pos.column, pos.row)
    }
    console.log(`${win.mode} win: ${win.winMultiplier}x`);
  }
}

Win modes ​

ModeDescription
winlineConsecutive matching symbols along a configured payline
waysMatching symbols in adjacent columns (left-to-right or both ways)
scatterMatching symbols anywhere on the board
clusterAdjacent matching symbols forming a connected group
unconditionalAwards that fire on every spin regardless of symbols
collectorCollector symbols gathering target symbol values

Feature triggers ​

A win can trigger bonus features via featureWin:

typescript
for (const win of scenario.wins ?? []) {
  if (win.featureWin) {
    console.log(`Triggered ${win.featureWin.featureName} × ${win.featureWin.featureCount}`);
  }
}

featureWin only tells you what triggered. To track progression, read spinInfo, nextFeature, or currentFeature. These tell you which feature a spin belongs to, how many spins remain, and what plays next. Where they live depends on the round type. See Feature progression for the unified accessor that works for both. For consolidated slot rounds, the engine bakes them onto the scenario IResult (scenario.spinInfo, scenario.nextFeature, scenario.featureName). Otherwise, they come from engineData.

Override wins ​

When a win sets overrideWins, the engine pays only the highest-paying override win on the spin. The engine suppresses all other wins on that spin. Use this to detect wins that you should render exclusively (for example, a jackpot scatter that hides the line wins).

Multipay scatters ​

multipayCount records how many times an award pays. The parent's totals already contain those pays. Each pay can draw a different weighted result.

multipayResults attributes each triggering pay to its positions and drawn outcome:

typescript
interface IMultipayResult {
  positions?: Array<{ column: number; row: number }>;
  winMultiplier?: number;
  spinMultiplierValue?: number;
  featureWin?: { featureName: string; featureCount: number };
  progressionAwards?: Record<string, number>;
}

A pay's winMultiplier precedes the parent's symbolMultiplierTotal. Apply that multiplier only for the per-pay breakdown. Do not add the breakdown to the parent's win again. Missing pays can represent weighted outcomes that did not trigger.

Payline identity ​

For a winline win, use winlineIndex to select the exact effective line. Two wins can share starting positions but follow different lines.

Resolve config.winlines[scenario.featureName] ?? config.winlines.base before indexing. These are the creator browser export's maps. An imported contract can use a different schema. See rules and presentation.

Win Multipliers ​

Several multipliers can modify a win's payout (winMultiplier). The engine surfaces each multiplier on the win or the spin, so you can render the breakdown without recomputing it:

FieldWhereMeaning
win.winMultiplierper-winFinal win value, after the engine applies every multiplier on this win
win.symbolMultiplierTotalper-winCombined multiplier from symbols-with-multipliers in this win (uses the slot's symbolMultiplierBehaviour: highest / multiply / add). Only present when > 1
win.spinMultiplierValueper-winA spin-multiplier award that this win contributes (for example, multiplier symbols on the board). The engine aggregates this across all wins into result.spinMultiplierTotal
result.spinMultiplierTotalper-spinSum of every win's spinMultiplierValue on this spin. The engine applies this to result.winAmount. Only present when > 0
result.cascadeMultiplierValueper-spinFinal cascade multiplier value after all cascade steps. Only present when the config sets cascadeMultiplier
result.featureMultiplierper-spinEffective feature-level Win Multiplier that the engine applies to this spin's wins. The field does not show the source: the value can come from featureConfig.multiplier (static), randomMultiplierOptions (rolled once per run), or perSpinRandomMultiplierOptions (rolled fresh every spin). Only present when ≠ 1×. See Win Multiplier

winAmount is the final value that the spin paid out. The multipliers above are a breakdown for display purposes. If you multiply them yourself, you will not always reproduce winAmount exactly, because rounding happens at intermediate steps.

Symbol Substitutions and Duplications ​

When the slot config has symbolSubstitutions (for example, wilds that turn into specific paying symbols) or symbols with duplicatingCount (a symbol that copies itself onto another position), the engine reports what happened. You can then animate the transformation:

typescript
interface ISymbolSubstitutionData {
  position: { column: number; row: number };
  originalSymbol: string;
  newSymbol: string;
}

interface IDuplicatingSymbolData {
  sourcePosition: { column: number; row: number };
  targetPosition: { column: number; row: number };
  symbol: string;
}
typescript
for (const sub of scenario.symbolSubstitutions ?? []) {
  // Animate sub.originalSymbol turning into sub.newSymbol at sub.position
}
for (const dup of scenario.duplicatingSymbolData ?? []) {
  // Animate dup.symbol jumping from dup.sourcePosition to dup.targetPosition
}

scenario.board is always the final post-transformation board. Use boardStates (see Board States) when you also need the pre-transformation board. A base spin surfaces 'After Expanding Wilds', 'After Substitution', and 'After Duplication' there as separate entries, one per transform that actually changed the board.

Cascades / Tumbles ​

When the config enables cascades, cascadeData tracks the full tumble sequence within a single spin:

typescript
interface ICascadeData {
  count: number; // Number of cascade iterations (0 = no cascades)
  steps: ICascadeStep[]; // Each step of the cascade
}

interface ICascadeStep {
  inputBoard?: TBoard; // Board before transformations
  board: TBoard; // Board at this step
  wins: IWin[]; // Wins found at this step
  winAmount: number; // Win from this step
  cascadeMultiplier?: number; // Active multiplier on this step (only when cascadeMultiplier is configured)
}

When the slot config enables cascadeMultiplier, the multiplier ramps up across cascade steps. Each step's cascadeMultiplier is the value that the engine applies to that step's wins. The engine also surfaces the final value on the spin's top-level cascadeMultiplierValue.

typescript
if (scenario.cascadeData) {
  for (let i = 0; i < scenario.cascadeData.steps.length; i++) {
    const step = scenario.cascadeData.steps[i];
    // step.board = board state at this cascade step
    // step.wins  = wins found at this step
    // step.winAmount = payout from this step
    // Animate: remove winning symbols, drop new ones, show next board
  }
}

Step 0 is the initial evaluation. Steps 1 and later are cascade iterations. In each one, the engine removes winning symbols. Then it drops in new symbols.

TIP

Animate every nested cascadeData.steps entry within the same response. A generator-level multi-result scenario uses separate API continuations. These are different levels. Do not send an extra request for each nested cascade.

Hold & Win ​

holdAndWinData tracks the state of a Hold & Win (respin) feature:

typescript
interface IHoldAndWinData {
  multipliers: (number | null)[][]; // Payout multiplier at each position
  symbols?: (string | null)[][]; // Symbol at each stuck position
  livesRemaining: number; // Remaining respins
  totalMultiplier: number; // Sum of all placed multipliers
  ended: boolean; // Feature complete?
}
typescript
if (scenario.holdAndWinData) {
  const hw = scenario.holdAndWinData;
  // Render the grid: non-null entries in hw.multipliers are stuck symbols
  for (let col = 0; col < hw.multipliers.length; col++) {
    for (let row = 0; row < hw.multipliers[col].length; row++) {
      if (hw.multipliers[col][row] !== null) {
        // Show stuck symbol with its multiplier value
      }
    }
  }
  console.log(`Lives: ${hw.livesRemaining}, Total: ${hw.totalMultiplier}x`);
}

The engine bundles Hold & Win results as a multi-result scenario. The trigger spin and all H&W respins come as sequential steps.

Collector, booster, and locked-cell state ​

Use the returned snapshots and events. Do not reconstruct collector targets by comparing board images.

IHoldAndWinData fieldRender
collectorCollectedCumulative collector contribution, already in totalMultiplier.
collectorSweepsCollector landings on this respin only. Missing means no sweep.
boosterContributedCumulative booster contribution, already in the total.
lockedCells[col][row]Cells that still cannot receive coins. Missing means no lock grid.
pendingCellModifiersPersistent modifiers on empty cells. They are not paid yet.
cellBoosterStampsStamps created on this respin. Animate each stamp once.
cellBoosterContributedCumulative applied cell boosts, already in the total.
typescript
interface CollectorSweep {
  col: number;
  row: number;
  collected: number;
  multiplier?: number;
  from: Array<{ col: number; row: number }>;
}
interface PendingCellModifier {
  col: number;
  row: number;
  add: number;
  mult: number;
}
interface CellBoosterStamp {
  col: number;
  row: number;
  mode: 'add' | 'multiply';
  value: number;
  resolved: boolean;
}

A sweep's from cells identify the gathered coins. Read their values from the same snapshot's multipliers grid. Boosters resolve before collectors. collected already includes the sweep's optional multiplier.

Collectors that land together use the same earlier snapshot. They do not gather each other on that respin. A collector from an earlier respin can be a target. A landing on an empty board still produces a zero-value sweep.

A pending modifier transforms a future coin value p into (p + add) * mult. A stamp with resolved: true acts immediately. Otherwise, keep the marker until the returned state consumes it.

Render the trigger board before respin events. Some creator payloads include a holdAndWinEntryData snapshot outside the current IResult declaration. Validate that extension against the published game. Do not require it on every game or expect collector sweeps on that entry snapshot.

Keep the same grid, locked cells, banked total, and remaining lives when resuming. The feature's ended flag ends that feature. The engine's round state determines whether another feature or collection follows.

Sticky Symbols ​

During features with sticky symbols, stuckSymbols tracks which positions the engine locks:

typescript
type TStuckSymbols = Array<Array<string | null>>;
// Same shape as board - null means the position is not stuck
typescript
if (scenario.stuckSymbols) {
  for (let col = 0; col < scenario.stuckSymbols.length; col++) {
    for (let row = 0; row < scenario.stuckSymbols[col].length; row++) {
      if (scenario.stuckSymbols[col][row] !== null) {
        // This position has a locked symbol that persists across spins
      }
    }
  }
}

Multiplier Collection ​

multiplierCollectionData tracks accumulated multiplier progress within a feature:

typescript
interface IMultiplierCollectionData {
  symbol: string; // Symbol being collected
  count: number; // Total collected across all spins
  multiplier: number; // Current active multiplier
  target: 'winline' | 'ways' | 'scatter' | 'cluster' | 'bookwinline' | 'totalwin';
}
typescript
if (scenario.multiplierCollectionData) {
  for (const mc of scenario.multiplierCollectionData) {
    console.log(`${mc.symbol}: collected ${mc.count}, multiplier ${mc.multiplier}x on ${mc.target} wins`);
  }
}

Collected Spin Multiplier ​

For games like Gates of Olympus where multiplier symbols accumulate within a cascade or feature:

typescript
interface ICollectedSpinMultiplierData {
  collectedMultiplier: number; // Accumulated multiplier across spins
  appliedMultiplier: number; // Multiplier applied to this spin's wins (0 = not applied)
}

Win Multiplier ​

Every feature spin can carry a Win Multiplier from one of three sources. The result exposes all three via the same featureMultiplier field, so consumers do not need to branch on the source:

SourceConfigBehaviour
StaticfeatureConfig.multiplierFlat multiplier that applies to every spin of every run of the feature.
Run-level randomfeatureConfig.randomMultiplierOptionsThe engine rolls one value from the weighted list at the start of each feature run. The same value applies to every spin of that run.
Per-spin randomfeatureConfig.perSpinRandomMultiplierOptionsThe engine rolls a fresh value from the weighted list on every individual feature spin. The multiplier varies spin-to-spin within one run. Cascades within a single spin keep that spin's value, because cascades happen within one spin.

Precedence when more than one is configured: per-spin > run-level > static.

typescript
if (scenario.featureMultiplier !== undefined) {
  // e.g. show "5x" alongside the feature wins
  console.log(`Feature multiplier: ${scenario.featureMultiplier}x`);
}

The engine omits the field when the effective multiplier is 1× (no feature multiplier configured, or base spins).

Per-spin random and the DB shape. Per-spin random forces the feature into the consolidated or grouped DB path, the same way randomMultiplierOptions does. Outcomes cannot be pre-baked when the engine rolls the multiplier at runtime. So all spins of the run land in a single grouped scenario entry. Configurations that need each free spin to come from an independent scenario table should use multiple feature configs with different static multipliers. They should also route entries through a player-choice or weighted award.

Hold & Win. The engine ignores per-spin random on Hold & Win features. H&W settles cumulatively at the end of the respin sequence, so a per-spin pick has nowhere meaningful to land. H&W falls back to the run-level pick (or static). featureMultiplier reflects that single applied value.

Random-entry preview spins. When featureRandomEntryConfig replaces a base spin with a feature-style preview, the preview spin picks its own multiplier. It uses per-spin if the substituted feature configures it, otherwise run-level. It picks independently of any surrounding feature run.

Wheel Features ​

wheelWins contains results from wheel bonus spins:

typescript
interface IWheelWin {
  selectedSegment: number; // Index of the landing segment
  winAmount?: number; // Cash prize (if any)
  featureAward?: IFeatureWin; // Feature triggered (if any)
}

Board States ​

boardStates provides named snapshots that show intermediate board transformations. A base spin emits one entry per transform that actually changed the board, in pipeline order: 'Input', then any of 'After Expanding Wilds', 'After Substitution', or 'After Duplication' that applied. A spin where none of those changed the board has no boardStates at all. Feature spins add their own steps on top of that (for example, 'Generated', 'After Sticky', 'Book Expanded'):

typescript
interface IBoardState {
  name: string; // e.g. 'Input', 'After Expanding Wilds', 'After Substitution', 'After Duplication', 'Generated', 'After Sticky', 'Book Expanded'
  board: TBoard;
}

Use these to animate board transformations step by step. For example, show the raw generated board before it expands wilds or applies sticky symbols.

Cascade steps currently expose only their own before and after board, not this same per-transform breakdown. This gap is tracked separately.

Book Spins ​

During book-spin features:

  • bookSpinChosenSymbol: the expanding symbol that the engine selects for the feature
  • bookSpinWin: additional win data from the book expansion:
typescript
interface IBookSpinWin {
  symbolWinAmount: number; // Win from the chosen expanding symbol
  totalWin: number; // Total book spin win multiplier
}

Collector Features ​

When collector mechanics are active, the spin reports per-symbol target values as well as the player's level progression:

typescript
interface ICollectorTargetData {
  symbol: string; // Target symbol on the board
  position: { column: number; row: number };
  value: number; // Weighted random value assigned to this target instance
}

interface ICollectedSymbol {
  symbol: string; // Target symbol gathered by the collector
  position: { column: number; row: number };
  value: number; // Payout value assigned to this target
}

interface ICollectorLevelData {
  currentLevel: number; // 0-based index into the levels array
  collectedCount: number; // Collected in the current level (resets on level up)
  totalCollected: number; // Total across all levels in this feature
}
  • scenario.collectorTargetData lists the values that the engine rolls for each target symbol present on the board this spin (regardless of whether the collector gathered them).
  • win.collectedSymbols (on collector mode wins) lists the targets that a collector win actually gathers and the value it awards for each.
  • scenario.collectorLevelData is the level-progression state after this spin, when the slot has a collectorLevelConfig.
typescript
if (scenario.collectorLevelData) {
  const cl = scenario.collectorLevelData;
  console.log(`Level ${cl.currentLevel}, collected ${cl.collectedCount} this level, ${cl.totalCollected} total`);
}

Megaways (Variable Reel Heights) ​

When the slot config sets megaways, each reel generates a random number of rows per spin. The engine reports the chosen heights on reelRows, so you can size the grid before you render symbols:

typescript
if (scenario.reelRows) {
  // scenario.reelRows[col] = number of rows on reel col for this spin
  // scenario.board[col].length matches scenario.reelRows[col]
}

reelRows is only present when the active config (base or feature) has megaways enabled.

Random Feature Entry ​

If the slot config has featureRandomEntryConfig, the engine can replace a base spin with a feature-style spin. It applies the feature's symbols, awards, and multiplier. The spin's randomFeatureEntry carries the name of the feature whose config the engine used:

typescript
if (scenario.randomFeatureEntry) {
  console.log(`This spin used the "${scenario.randomFeatureEntry}" feature config`);
}

A random-entry spin can also queue additional spins of the same feature (when the feature's count > 1). Those queued spins are normal feature spins. They have featureName set instead.

Feature Run Identification ​

featureRoundId is a 1-based counter that distinguishes separate runs of the same feature within one round. If a feature triggers, completes, then re-triggers from a base spin or wheel award later in the round, the second run gets featureRoundId: 2:

typescript
// Group feature spins by run
const runs = new Map<string, IResult[]>();
for (const r of allResults) {
  if (r.featureName && r.featureRoundId !== undefined) {
    const key = `${r.featureName}#${r.featureRoundId}`;
    if (!runs.has(key)) runs.set(key, []);
    runs.get(key)!.push(r);
  }
}

Hold & Win in particular uses this to count respins within a single trigger. Every step of the run carries the same featureRoundId. So filtering on featureName and featureRoundId gives you the spins of one respin sequence.

Single-Gameround Jackpots ​

singleGameRoundProgressionData carries a jackpot mechanic that fills and pays entirely within one gameround. The engine creates it fresh at round start and discards it at round end. It never carries over between rounds. This is a different mechanic from Progression Counters, which persist for a player across rounds. See How this differs from Progression Counters below if you have already integrated that one.

typescript
interface ISingleGameRoundProgressionData {
  counter: string; // Jackpot name (matches an entry in the game's singleGameRoundProgression config)
  value: number; // Meter value after this step, always in [0, 1)
  completed?: {
    cashWin?: number; // Cash paid, in stake-multiplier units - already included in this step's winAmount
    featureAwards?: { feature: string; count: number }[]; // Feature spins granted on completion
  };
}

The mechanic needs the whole round to replay as one unit. So a game that configures any single-gameround jackpot always sets consolidateRounds. The round is then one stored multi-step scenario, described in Single vs Multi-Result Scenarios below. The engine returns one step per placeBet continuation, never an array. Each step carries its own singleGameRoundProgressionData. There is no engineData equivalent to read instead. Unlike spinInfo or progressionCounters, this mechanic has no runtime-persisted state. So the data lives only on the scenario steps you already have.

Reading the meter ​

Every jackpot that the game configures gets one entry in singleGameRoundProgressionData on every step, whether or not it changed that step. So you always have a complete, current fill level for every jackpot, without tracking state yourself between placeBet calls:

typescript
// One step per reply. Walk continuations while engineData.scenarioInfo reports inProgress.
const step = gameResult.scenario as IResult;

{
  for (const jackpot of step.singleGameRoundProgressionData ?? []) {
    renderMeter(jackpot.counter, jackpot.value); // fill level, 0 to just under 1
  }
}

Handling a trigger ​

completed is present only on the step where a jackpot's meter actually crosses 1.0. A meter can cross more than once in a single step, for example when several jackpot symbols land at once. In that case, cashWin is the total across every crossing on that step. featureAwards lists the combined spin counts.

typescript
{
  for (const jackpot of step.singleGameRoundProgressionData ?? []) {
    if (!jackpot.completed) continue;
    if (jackpot.completed.cashWin) {
      // Already included in step.winAmount / the round's totalWin - use this
      // only to show which jackpot paid and how much, not to add to a total.
      showJackpotWin(jackpot.counter, jackpot.completed.cashWin * stake);
    }
    for (const award of jackpot.completed.featureAwards ?? []) {
      showFeatureAward(jackpot.counter, award.feature, award.count);
    }
  }
}

The meter can also drop back to 0 with no completed, on a step where none of your own logic changed it. This happens when the jackpot's resetOnFeatureEnd scoping fires (the meter is scoped to one feature run rather than the whole round). Treat it the same as any other value change. Just re-render the meter at its new value.

How this differs from Progression Counters ​

Both mechanics look similar in shape: a fractional meter that fires an onComplete award. However, you read them from different places, and they mean different things:

Progression CountersSingle-Gameround Jackpots
Where it livesengineData.progressionCounters / engineData.progressionEventsscenario (baked onto each IResult step)
PersistenceAcross rounds, per player (and optionally per stake)One gameround only, never carries over
Cash awardPaid on top of totalWin, not part of itAlready included in winAmount / totalWin. It is a normal win
Counts toward maxPayout / max-exposureNoYes

If a game uses both, read each from its own location. A jackpot name is never also a progression counter name on the same game.

Single vs Multi-Result Scenarios ​

The API returns one scenario object per response. A multi-result entry advances through engineData.scenarioInfo.currentScenarioIndex. Its inProgress flag describes remaining entry steps.

A consolidated round can include base play and several features. Read its scenario.spinInfo, scenario.featureName, and scenario.nextFeature. Use engineData as the fallback for games with runtime feature progression.

A complete entry does not prove the whole round has settled. Inspect collect availability, player choices, and the round status. Follow the slot controller.

Rendering a result ​

The renderer receives an already decoded IGameResult. Route card and ladder gamble results to their specific views first. The fragment below handles a result with a slot board. view is your rendering adapter, not an SDK export.

typescript
import type { IGameResult, IResult } from '@hizi.io/engine-sdk';

async function present(result: IGameResult) {
  const slot = result.scenario as unknown as IResult;
  if (slot.cascadeData?.steps.length) {
    for (const step of slot.cascadeData.steps) {
      await view.presentCascade(step);
    }
  } else {
    await view.presentBoards(slot.boardStates, slot.board);
    await view.presentWins(slot.wins ?? [], slot.winAmount);
  }
  await view.presentFeatureState(slot);
  // The session controller decides the next legal action after timing and reports.
}

Validate the scenario at your application boundary. Unknown symbols, missing boards, or unsupported required mechanics must block new play. Display a recoverable configuration error with the operator's approved behavior.

Resolve rendering only after all required wins and transitions finish. Treat replay as presentation of saved state. Do not replay a debit, a wallet credit, or an operator round-start notification.

Supported Features ​

This is a complete reference of every mechanic the slot engine can model. The "Example" column points to a recognisable commercial slot that uses the same mechanic. This is useful shorthand when you discuss configurations with designers. The configuration field shows the relevant key on ISlotConfig, ISymbol, IAwardTrigger, or IFeatureConfig.

Grid & reels ​

MechanicConfigExampleNotes
Fixed gridcolumns, rowsStarburst (NetEnt)Standard fixed grid.
Variable reel heights (Megaways)megawaysBonanza Megaways (Big Time Gaming)Each reel rolls a random number of rows per spin within [minRows, maxRows]. Per-reel ranges use reelRowRange.
Fixed reel strips (reelsets)reelSetConfigMechanical-style NetEnt slotsBoard generation picks a window of rows consecutive symbols from each strip. You can weight multiple reelsets against each other. When enabled, this bypasses per-symbol reel weights and stacking.

Symbols ​

MechanicConfigExampleNotes
Wildssymbol.isWild20 Super Hot (EGT)Substitutes for paying symbols.
Expanding wildssymbol.isExpandingWildStarburst (NetEnt)Wild expands to fill its reel.
Wild scatterssymbol.isWildScatterBook of Ra (Novomatic)Wild that also counts toward scatter awards (per-award opt-in via canIncludeWildScatters).
Per-reel symbol weightingsymbol.reelWeights[]virtually every modern slotDifferent probability per reel.
Reel spacingsymbol.reelSpacinghigh-volatility scatter symbolsMinimum stop distance between occurrences on a reel.
Symbol stackingsymbol.minStack
symbol.maxStack
symbol.averageStack
Buffalo (Aristocrat)A symbol either appears alone or in a contiguous group on a reel. averageStack skews stack sizes toward a target.
Symbol multiplierssymbol.multiplier
symbolMultiplierBehaviour
Sweet Bonanza (Pragmatic Play)Multiplies the win when this symbol is part of it. Combination across multiple multiplier symbols: highest / multiply / add.
Multi-position symbolssymbol.winlineCount"double-wide" symbols in various Pragmatic Play / BTG titlesA symbol counts as N adjacent positions for win length.
Duplicating symbolssymbol.duplicatingCountvarious "cloning wild" mechanicsSymbol clones itself onto another position before evaluation.
Symbol substitutionssymbolSubstitutions[]Mystery Symbols (Fishin' Frenzy series, Reel Time Gaming)One symbol transforms into another via a weighted pick. uniformSubstitution makes all source occurrences resolve to the same target. noSubstitutionWeighting lets the source survive untransformed.

Win evaluation modes ​

MechanicConfigExampleNotes
Paylinesaward.mode: 'winline'
winlines[]
Book of Ra (Novomatic)Configured payline patterns.
Ways paysaward.mode: 'ways'243 Ways (Microgaming)Adjacent-reel matching, no payline patterns.
Scatter paysaward.mode: 'scatter'universalN matching symbols anywhere on the board.
Multipay scatteraward.multipayvariousScatter pays floor(count / N) times for big counts.
Cluster paysaward.mode: 'cluster'
adjacency
Aloha! Cluster Pays (NetEnt), Reactoonz (Play'n GO)Connected groups of matching symbols. adjacency is fourWays (default) or eightWays.
Unconditional awardsaward.mode: 'unconditional'bonus picks, "always pays" mechanicsFires every spin regardless of board state.
Collector awardsaward.mode: 'collector'
collectsSymbols
Money Train (Relax Gaming)A collector symbol gathers values from target symbols on the board into one win.
Override winsaward.overrideWinsjackpot lines, super-wild winsOnly the highest override win pays. It suppresses other wins on the spin.
After-cascade evaluationaward.afterCascade
award.cascadePersist
variousThe engine evaluates the award only on the final post-cascade board (or its symbols persist through cascades).
Range-based scatter triggersaward.maxCountbonus tiers (for example, 3/4/5 scatters → different awards)Award only triggers when the symbol count is within [count, maxCount].

Cascades (tumble mechanics) ​

MechanicConfigExampleNotes
Cascading winscascadeGonzo's Quest (NetEnt)The engine removes winning symbols. Remaining symbols drop down. New symbols fill from the top. This repeats until there is no win.
Progressive cascade multipliercascadeMultiplierSweet Bonanza (Pragmatic Play), Sugar Rush (Pragmatic Play)Multiplier ramps up across cascade steps.
First-step substitutions onlycascadeSubstitutionvariousSubstitutions apply only on the first cascade step (default is every step).

Bonus features ​

MechanicConfigExampleNotes
Free spinsfeatureConfig.type: 'freespins'universalStandard bonus spins.
Bonus spins (lower priority)featureConfig.type: 'bonusspins'Book of Holla: Bonus Spins (Hoelle Games)Only run when no free spins feature has remaining spins.
Wheel featuresfeatureConfig.type: 'wheel'Mega Moolah (Microgaming), Wheel of Fortune (IGT)Single weighted segment pick. The segment can pay cash, trigger another feature, or both.
Hold & Win (respin)featureConfig.holdAndWinLightning Link (Aristocrat)Coin symbols stick. The respin counter resets on every hit (lives). The feature ends when the grid is full or lives reach 0. fullScreenBonus, symbolWeights, blankWeight, and payouts shape the maths.
Book Spins (expanding feature symbol)featureConfig.bookSpinSymbolAllowListBook of Ra (Novomatic), Book of Dead (Play'n GO)The engine rolls one symbol from the allow list at feature start. On each spin, it expands to fill all positions and pays as a winline regardless of position.
Random feature entryfeatureRandomEntryConfig
featureRandomEntryBaseWeighting
random Hold & Win drops on a base spinWeighted chance to replace a base spin with a feature-style spin (using the feature's full config). Can also queue additional feature spins.
Static feature multiplierfeatureConfig.multiplier"wins multiplied by 3 during free spins"Flat multiplier on every win during the feature. The result surfaces this via featureMultiplier.
Random feature multiplier (run-level)featureConfig.randomMultiplierOptionsRandom Multiplier Free Spins variantsThe engine rolls one multiplier from a weighted list at the start of each feature run. This multiplier applies to the entire run. The result surfaces it via featureMultiplier. This forces the feature into a grouped DB entry.
Per-spin random feature multiplierfeatureConfig.perSpinRandomMultiplierOptions"every free spin a fresh global multiplier" variantsThe engine rolls a fresh multiplier from the weighted list on every individual feature spin. So the multiplier varies spin-to-spin within one run. This overrides both multiplier and randomMultiplierOptions. It applies to regular feature spins only. Hold & Win and wheel features fall back to the run-level pick. The result surfaces it via featureMultiplier. This forces the feature into a grouped DB entry.

Sticky mechanics (feature-scoped) ​

All fields below live on IFeatureConfig (featureConfig.X).

MechanicConfigExampleNotes
Sticky wilds / sticky symbolsstickySymbolsDead or Alive (NetEnt)Listed symbols persist on the board across feature spins once they land.
Sticky winsstickyWinsvariousAll symbols involved in any winning combination become sticky.
Sticky winlinesstickyWinlinesvariousSymbols on a winning payline become sticky.
Sticky symbol limitstickySymbolLimitvariousCaps how many symbols can be sticky simultaneously.
Sticky overwrite rulesstickySymbolOverwriteRulesupgrading stuck symbols (low → high)A new sticky can replace an existing stuck symbol per defined replacement rule.

Multiplier mechanics (per-feature) ​

All fields below live on IFeatureConfig (featureConfig.X).

MechanicConfigExampleNotes
Multiplier collectionmultiplierCollectionConfigsvarious "multiplier symbol counter" free spins featuresCounts collected symbols across all feature spins. Thresholds map count to active multiplier. Targets a specific win type (winline/ways/scatter/cluster/bookwinline/totalwin).
Collected spin multipliercollectedSpinMultiplierGates of Olympus (Pragmatic Play)Spin multiplier values accumulate across feature spins (add = sum, multiply = product). application is always or onHit.

Collector mechanics ​

MechanicConfigExampleNotes
Target value poolcollectorTargetValuesMoney Train (Relax Gaming)The engine assigns weighted random values to each target symbol type when a collector evaluation runs.
Collector level progressioncollectorLevelConfigBig Bass Bonanza (Pragmatic Play)Collector symbols progress through ranked levels. Each level has its own multiplier. Each level can grant additionalSpins and can transition to a different feature via nextFeature. Base game and features share this mechanic.

Triggers, awards & round control ​

MechanicConfigExampleNotes
Feature awards from winsawardResult.featureAwarduniversal scatter-bonus triggersA win awards featureCount spins of a named feature.
Spin multiplier awardsawardResult.spinMultiplierGates of Olympus (Pragmatic Play), using board multiplier symbolsA win contributes a spin-multiplier value. The engine sums it into result.spinMultiplierTotal and applies it to the spin's win amount.
Pot awards (combination-aware)potAwards3 Pots of Egypt (3 Oaks), combined trigger tablesThe engine evaluates multiple scatter groups together against one outcome table. This allows correlated triggers and shared probability budgets.
Progression countersprogressionCounters
awardResult.progressionAwards
various "meter/charge/level-up" mechanicsWins increment named counters. A counter at 1.0 awards configured features. Counters can be stakeSpecific for separate counters per stake.
Single-gameround jackpotssingleGameRoundProgression
awardResult.progressionAwards
"Three Pot Progression"-style in-round jackpot potsSame increment mechanism as progression counters, but scoped to one gameround only. There is no cross-round persistence. A cash award is part of winAmount/maxPayout, like any other win. Forces consolidateRounds. See Single-Gameround Jackpots.
Random-choice / player-choice award resolutionprogressionCounter.onComplete.typevarious player-choice bonus picksWhen a counter completes, the engine either rolls the awards randomly or surfaces them as a player choice via the SDK.
Max payout capmaxPayout
consolidateRounds
Pragmatic Play 5,000× / 25,000× max-win capWhen cumulative win reaches the cap the round ends immediately.
Consolidated roundsconsolidateRoundsbundled bonus/H&W gamesBundles all feature spins into the base scenario instead of separate tables. maxPayout requires this.
Scenario variety capmaxScenariosPerEntry(generator-level tuning)Caps stored scenarios per unique entry. A higher cap gives more variety and a larger DB.
Zero-win scenario capmaxScenariosPerZeroWinEntry(dead-spin tuning, default 2000)Separate, larger cap for 0-win spins (~60% of volume), so dead-spin variety is not starved.
Loose scenario caplooseScenarioCap(default on in the creator)Pools the scenario cap across outcomes that share win, feature spins, and progression, but differ only in cosmetic tags. This keeps scenarios.jsonl small on the large sims needed to reach jackpot-odds wins, without changing entries or weights.

Player controls and disclosures ​

Use stakes and money for stake buttons and purchases. Use settings for mute, turbo, stop, autoplay, and orientation.

Suppress celebrations for returns no greater than total stakes when required. Keep the returned amount visible. Include the complete purchase cost when comparing a bought round with its return.

Use gamble and banking for card and ladder gambles. Keep required acknowledgements and operator pause active across free features and recovery.

Show rules for every configured item in the feature inventory below. Source the actual symbols, awards, limits, and probabilities from the published game. Do not infer them from a visual theme or reference title.

Provably Fair Verification ​

When a slot round runs with config.rng === 'pf', every spin's outcome is fully determined by the RNG draws committed to in the round's pf block. Use pfVerify to have the engine replay the round from its revealed seeds and compare the result to what you recorded live.

Request ​

The slot replay needs the opening stake and any featureToBuy that it used. To replay each continuation step in the live round (wager-feature decisions, random-choice or player-choice awards), pass the matching playerChoiceIndex in actions. Free spin and feature cascades that resolve internally need no player choice and no entry in actions. The engine cascades them itself.

typescript
import { pfVerify } from '@hizi.io/engine-sdk';

const reply = await pfVerify({
  backendURL,
  token,
  serverSeed: pf.revealedServerSeed,
  clientSeed: pf.clientSeed,
  stake: 100,
  featureToBuy: 'freespin',     // only if the round opened on a buy feature
  actions: [
    { playerChoiceIndex: 1 },   // first wager-feature / playerChoice resolution
    { playerChoiceIndex: 0 },   // second
  ],
});

Reading the response ​

rngData mirrors the live round's pf.rngData row-for-row: same pf:start-end ids, same nonces[], same values[]. steps[] is the per-step IGameResult chain that placeBet returned in order. The terminal step is steps[steps.length - 1]. Compare against your recorded round:

typescript
if (!reply.success) {
  // handle reply.error
  return;
}
const { rngData, steps } = reply.result;

// 1. RNG audit
const audited = rngData.length === pf.rngData.length &&
  rngData.every((row, i) => deepEqual(row, pf.rngData[i]));

// 2. Scenario / totalWin (terminal step)
const terminal = steps[steps.length - 1];
const resultOk = deepEqual(terminal.scenario, lastLiveResult.scenario) &&
  terminal.totalWin === lastLiveResult.totalWin;

If both line up, the engine could not have rigged the outcome. Every draw and every cascade trace back to (serverSeed, clientSeed, your actions).

Next Steps ​