Appearance
Slot Game Responses
This guide explains how to read the scenario field that placeBet returns for slot games. @hizi.io/engine-sdk (0.2.5 and later) exports the shapes below. 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.
For single-result spins, the scenario is a single IResult object. For multi-result spins (cascades, Hold & Win), the scenario is an array of IResult objects, one per step.
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;
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;
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
| Mode | Description |
|---|---|
winline | Consecutive matching symbols along a configured payline |
ways | Matching symbols in adjacent columns (left-to-right or both ways) |
scatter | Matching symbols anywhere on the board |
cluster | Adjacent matching symbols forming a connected group |
unconditional | Awards that fire on every spin regardless of symbols |
collector | Collector 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
The engine sets multipayCount on scatter wins when the configured award uses multipay. The win's winMultiplier already includes the multipay scaling. multipayCount is just the multiplier, so you can show "2× scatter pay" alongside the win.
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:
| Field | Where | Meaning |
|---|---|---|
win.winMultiplier | per-win | Final win value, after the engine applies every multiplier on this win |
win.symbolMultiplierTotal | per-win | Combined multiplier from symbols-with-multipliers in this win (uses the slot's symbolMultiplierBehaviour: highest / multiply / add). Only present when > 1 |
win.spinMultiplierValue | per-win | A 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.spinMultiplierTotal | per-spin | Sum of every win's spinMultiplierValue on this spin. The engine applies this to result.winAmount. Only present when > 0 |
result.cascadeMultiplierValue | per-spin | Final cascade multiplier value after all cascade steps. Only present when the config sets cascadeMultiplier |
result.featureMultiplier | per-spin | Effective 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
When cascade is active with multi-result scenarios, each placeBet call returns one step. Use scenarioInfo.inProgress to check if more cascade steps follow.
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.
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 stucktypescript
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:
| Source | Config | Behaviour |
|---|---|---|
| Static | featureConfig.multiplier | Flat multiplier that applies to every spin of every run of the feature. |
| Run-level random | featureConfig.randomMultiplierOptions | The 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 random | featureConfig.perSpinRandomMultiplierOptions | The 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 featurebookSpinWin: 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.collectorTargetDatalists 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(oncollectormode wins) lists the targets that a collector win actually gathers and the value it awards for each.scenario.collectorLevelDatais the level-progression state after this spin, when the slot has acollectorLevelConfig.
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. gameResult.scenario is then the array-of-IResult shape described in Single vs Multi-Result Scenarios below. It has one array entry per step of the round (base spin plus every feature spin), each carrying 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
const steps = Array.isArray(gameResult.scenario) ? gameResult.scenario : [gameResult.scenario];
for (const step of steps 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 step of steps as IResult[]) {
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 Counters | Single-Gameround Jackpots | |
|---|---|---|
| Where it lives | engineData.progressionCounters / engineData.progressionEvents | scenario (baked onto each IResult step) |
| Persistence | Across rounds, per player (and optionally per stake) | One gameround only, never carries over |
| Cash award | Paid on top of totalWin, not part of it | Already included in winAmount / totalWin. It is a normal win |
Counts toward maxPayout / max-exposure | No | Yes |
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
Single-result (most spins): gameResult.scenario is one IResult object.
Multi-result (cascades, H&W): gameResult.scenario is an array of IResult objects. engineData.scenarioInfo.inProgress is true. Each placeBet call returns the next step in the sequence.
typescript
// Check if this is a multi-result scenario
if (gameResult.engineData.scenarioInfo.inProgress) {
// Current step index
const step = gameResult.engineData.scenarioInfo.currentScenarioIndex;
// More steps to come - call placeBet({ backendURL, token })
}Minimal Example
typescript
import { placeBet, IGameResult } from '@hizi.io/engine-sdk';
// Define your scenario type matching hizi engine slot's IResult
interface ISlotScenario {
board: string[][];
wins?: Array<{
mode: string;
positions: Array<{ column: number; row: number }>;
winMultiplier?: number;
featureWin?: { featureName: string; featureCount: number };
}>;
winAmount: number;
cascadeData?: {
count: number;
steps: Array<{ board: string[][]; wins: any[]; winAmount: number }>;
};
holdAndWinData?: {
multipliers: (number | null)[][];
livesRemaining: number;
totalMultiplier: number;
ended: boolean;
};
featureMultiplier?: number;
// ... other optional fields as needed
}
const response = await placeBet({ backendURL, token, stake });
if (response.success) {
const { scenario, engineData, totalWin } = response.result.result;
const slot = scenario as ISlotScenario;
// Render the board
renderBoard(slot.board);
// Highlight wins
for (const win of slot.wins ?? []) {
highlightPositions(win.positions);
}
// Handle cascades
if (slot.cascadeData) {
for (const step of slot.cascadeData.steps) {
await animateCascade(step);
}
}
// Continue if round is in progress
if (engineData.inProgress) {
await placeBet({ backendURL, token });
}
}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
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Fixed grid | columns, rows | Starburst (NetEnt) | Standard fixed grid. |
| Variable reel heights (Megaways) | megaways | Bonanza 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) | reelSetConfig | Mechanical-style NetEnt slots | Board 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
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Wilds | symbol.isWild | 20 Super Hot (EGT) | Substitutes for paying symbols. |
| Expanding wilds | symbol.isExpandingWild | Starburst (NetEnt) | Wild expands to fill its reel. |
| Wild scatters | symbol.isWildScatter | Book of Ra (Novomatic) | Wild that also counts toward scatter awards (per-award opt-in via canIncludeWildScatters). |
| Per-reel symbol weighting | symbol.reelWeights[] | virtually every modern slot | Different probability per reel. |
| Reel spacing | symbol.reelSpacing | high-volatility scatter symbols | Minimum stop distance between occurrences on a reel. |
| Symbol stacking | symbol.minStacksymbol.maxStacksymbol.averageStack | Buffalo (Aristocrat) | A symbol either appears alone or in a contiguous group on a reel. averageStack skews stack sizes toward a target. |
| Symbol multipliers | symbol.multipliersymbolMultiplierBehaviour | Sweet Bonanza (Pragmatic Play) | Multiplies the win when this symbol is part of it. Combination across multiple multiplier symbols: highest / multiply / add. |
| Multi-position symbols | symbol.winlineCount | "double-wide" symbols in various Pragmatic Play / BTG titles | A symbol counts as N adjacent positions for win length. |
| Duplicating symbols | symbol.duplicatingCount | various "cloning wild" mechanics | Symbol clones itself onto another position before evaluation. |
| Symbol substitutions | symbolSubstitutions[] | 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
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Paylines | award.mode: 'winline'winlines[] | Book of Ra (Novomatic) | Configured payline patterns. |
| Ways pays | award.mode: 'ways' | 243 Ways (Microgaming) | Adjacent-reel matching, no payline patterns. |
| Scatter pays | award.mode: 'scatter' | universal | N matching symbols anywhere on the board. |
| Multipay scatter | award.multipay | various | Scatter pays floor(count / N) times for big counts. |
| Cluster pays | award.mode: 'cluster'adjacency | Aloha! Cluster Pays (NetEnt), Reactoonz (Play'n GO) | Connected groups of matching symbols. adjacency is fourWays (default) or eightWays. |
| Unconditional awards | award.mode: 'unconditional' | bonus picks, "always pays" mechanics | Fires every spin regardless of board state. |
| Collector awards | award.mode: 'collector'collectsSymbols | Money Train (Relax Gaming) | A collector symbol gathers values from target symbols on the board into one win. |
| Override wins | award.overrideWins | jackpot lines, super-wild wins | Only the highest override win pays. It suppresses other wins on the spin. |
| After-cascade evaluation | award.afterCascadeaward.cascadePersist | various | The engine evaluates the award only on the final post-cascade board (or its symbols persist through cascades). |
| Range-based scatter triggers | award.maxCount | bonus tiers (for example, 3/4/5 scatters → different awards) | Award only triggers when the symbol count is within [count, maxCount]. |
Cascades (tumble mechanics)
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Cascading wins | cascade | Gonzo'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 multiplier | cascadeMultiplier | Sweet Bonanza (Pragmatic Play), Sugar Rush (Pragmatic Play) | Multiplier ramps up across cascade steps. |
| First-step substitutions only | cascadeSubstitution | various | Substitutions apply only on the first cascade step (default is every step). |
Bonus features
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Free spins | featureConfig.type: 'freespins' | universal | Standard 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 features | featureConfig.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.holdAndWin | Lightning 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.bookSpinSymbolAllowList | Book 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 entry | featureRandomEntryConfigfeatureRandomEntryBaseWeighting | random Hold & Win drops on a base spin | Weighted 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 multiplier | featureConfig.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.randomMultiplierOptions | Random Multiplier Free Spins variants | The 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 multiplier | featureConfig.perSpinRandomMultiplierOptions | "every free spin a fresh global multiplier" variants | The 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).
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Sticky wilds / sticky symbols | stickySymbols | Dead or Alive (NetEnt) | Listed symbols persist on the board across feature spins once they land. |
| Sticky wins | stickyWins | various | All symbols involved in any winning combination become sticky. |
| Sticky winlines | stickyWinlines | various | Symbols on a winning payline become sticky. |
| Sticky symbol limit | stickySymbolLimit | various | Caps how many symbols can be sticky simultaneously. |
| Sticky overwrite rules | stickySymbolOverwriteRules | upgrading 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).
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Multiplier collection | multiplierCollectionConfigs | various "multiplier symbol counter" free spins features | Counts 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 multiplier | collectedSpinMultiplier | Gates of Olympus (Pragmatic Play) | Spin multiplier values accumulate across feature spins (add = sum, multiply = product). application is always or onHit. |
Collector mechanics
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Target value pool | collectorTargetValues | Money Train (Relax Gaming) | The engine assigns weighted random values to each target symbol type when a collector evaluation runs. |
| Collector level progression | collectorLevelConfig | Big 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
| Mechanic | Config | Example | Notes |
|---|---|---|---|
| Feature awards from wins | awardResult.featureAward | universal scatter-bonus triggers | A win awards featureCount spins of a named feature. |
| Spin multiplier awards | awardResult.spinMultiplier | Gates of Olympus (Pragmatic Play), using board multiplier symbols | A 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) | potAwards | 3 Pots of Egypt (3 Oaks), combined trigger tables | The engine evaluates multiple scatter groups together against one outcome table. This allows correlated triggers and shared probability budgets. |
| Progression counters | progressionCountersawardResult.progressionAwards | various "meter/charge/level-up" mechanics | Wins increment named counters. A counter at 1.0 awards configured features. Counters can be stakeSpecific for separate counters per stake. |
| Single-gameround jackpots | singleGameRoundProgressionawardResult.progressionAwards | "Three Pot Progression"-style in-round jackpot pots | Same 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 resolution | progressionCounter.onComplete.type | various player-choice bonus picks | When a counter completes, the engine either rolls the awards randomly or surfaces them as a player choice via the SDK. |
| Max payout cap | maxPayoutconsolidateRounds | Pragmatic Play 5,000× / 25,000× max-win cap | When cumulative win reaches the cap the round ends immediately. |
| Consolidated rounds | consolidateRounds | bundled bonus/H&W games | Bundles all feature spins into the base scenario instead of separate feature DBs. maxPayout requires this. |
| Scenario variety cap | maxScenariosPerEntry | (generator-level tuning) | Caps stored scenarios per unique entry. A higher cap gives more variety and a larger DB. |
| Zero-win scenario cap | maxScenariosPerZeroWinEntry | (dead-spin tuning, default 2000) | Separate, larger cap for 0-win spins (~60% of volume), so dead-spin variety is not starved. |
| Loose scenario cap | looseScenarioCap | (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. |
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
- Response Handling: the
IGameResultstructure andengineDatafields. - Plinko Responses: how to read plinko game responses.
- Types & Interfaces: the full SDK type reference.