Appearance
Response Handling
This guide explains the structure of game results that placeBet returns. It also explains how to handle different game scenarios.
The IGameResult Structure
Every placeBet response contains an IGameResult object at response.result.result:
typescript
const response = await placeBet({ backendURL, token: sessionToken, stake, config });
if (response.success) {
const gameResult: IGameResult = response.result.result;
}IGameResult has three fields:
typescript
interface IGameResult {
scenario: Record<string, unknown>;
engineData: {
entryIndex?: number;
scenarioInfo?: IScenarioInfo;
spinInfo?: Array<ISpinInfo>;
playerChoice?: TPlayerChoiceAward[];
currentFeature?: string;
nextFeature?: string;
progressionCounters?: Record<string, number>;
progressionEvents?: IProgressionEvent[];
canCollect?: boolean;
inProgress: boolean;
bankedWin?: number;
baseStake?: number;
buyFeatureId?: string;
version?: string;
ladderState?: ILadderState;
};
totalWin: number;
}The scenario Field
The scenario field contains whatever scenario data you stored when calling addResult() in hizi engine generator. Its shape is entirely game-specific.
For example, if your generator stored:
typescript
await generator.addResult(
{
visibleSymbols: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
],
reelIndexes: [12, 45, 78],
winLines: [{ line: 0, symbols: [1, 1, 1], payout: 10 }],
},
{ feature: 'basegame', win },
);Then gameResult.scenario contains exactly that object at runtime:
typescript
const { visibleSymbols, reelIndexes, winLines } = gameResult.scenario as MyGameResult;TIP
Define a TypeScript interface for your game's scenario data. Cast gameResult.scenario to it. This gives you type safety for your game-specific fields.
The engineData Field
engineData tells you about the engine's internal state. It also tells you what to do next.
inProgress
This flag means the round has more work. Inspect canCollect, pending choices, and round status before another request.
Use the state decision order. Wait for a player choice or cashout decision when offered. A closed round or an operator pause takes precedence over automatic continuation.
scenarioInfo
scenarioInfo tracks the progression of a multi-result scenario:
typescript
interface IScenarioInfo {
scenarioIndex: number; // Which scenario was selected
currentScenarioIndex: number; // Current step (0-based)
inProgress: boolean; // More steps in this scenario?
}You create a multi-result scenario when you pass an array to addResult() in hizi engine generator:
typescript
// Generator side - creates a 3-step scenario
await generator.addResult(
[
{ grid: step1Grid }, // Step 0
{ grid: step2Grid }, // Step 1
{ grid: step3Grid }, // Step 2 (final - win awarded here)
],
{ feature: 'basegame', win: totalWin },
);At runtime, each placeBet call returns one step:
| placeBet call | scenarioInfo.currentScenarioIndex | scenarioInfo.inProgress | totalWin |
|---|---|---|---|
| 1st (initial) | 0 | true | 0 |
| 2nd | 1 | true | 0 |
| 3rd | 2 | false | Entry's win value |
INFO
The selected entry adds its win on its final scenario step. Earlier steps add no new entry win. The first consolidated round therefore reports totalWin: 0 until its final step. A later feature can retain an earlier accumulated total.
spinInfo and nextFeature
These fields track free spins and bonus features. spinInfo is an array with one entry per feature awarded so far this round:
typescript
interface ISpinInfo {
feature: string; // Feature name (e.g. 'freespin')
count: number; // Total spins AWARDED for this feature (not remaining)
used: number; // Spins already consumed (incl. the current spin)
}count is the total awarded, not the remaining count
Remaining spins = count - used. The feature run ends when used === count. count is the running total of spins awarded to the feature. It starts at the award size. It grows when the feature retriggers or the player re-enters the feature (the engine increments the existing entry instead of replacing it). Do not add count + used. That calculation double-counts spins.
nextFeature tells you which table the engine draws the next spin from. A table is a set of entries saved beside the basegame table. An absent nextFeature does not prove settlement. A gamble or choice can still be pending. Use the engine flags and round status.
Example free spin flow:
| Step | nextFeature | spinInfo | What happened |
|---|---|---|---|
| Base spin | 'freespin' | [{feature: 'freespin', count: 10, used: 0}] | Base spin triggered 10 free spins |
| Free spin 1 | 'freespin' | [{feature: 'freespin', count: 10, used: 1}] | First free spin played |
| Free spin 2 | 'freespin' | [{feature: 'freespin', count: 10, used: 2}] | Second free spin |
| ... | ... | ... | ... |
| Free spin 10 | undefined | [{feature: 'freespin', count: 10, used: 10}] | Last free spin, round ends |
Where this data lives: engineData vs scenario
The flow above is the non-consolidated case. Here, the engine draws each feature spin at runtime. It exposes progression on engineData (spinInfo, currentFeature, nextFeature).
For slots built with consolidated rounds (consolidateRounds), the whole round is one scenario array. The engine replays this array step by step. There are no runtime counters. The engine leaves engineData.spinInfo empty. Instead, the engine bakes the equivalent data onto each scenario step: scenario.spinInfo, scenario.featureName (the current feature), and scenario.nextFeature. The count and used semantics stay identical. The only difference is where you read the data from.
Single-Gameround Jackpots is a mechanic that only exists in this scenario-baked form. It forces consolidateRounds. It has no engineData equivalent as a fallback. Unlike progression counters, it has no runtime-persisted state at all.
Read it through a single accessor. The accessor prefers the scenario copy. It falls back to engineData if needed. This way, your code works regardless of how the game was built:
typescript
import type { IGameResult, ISpinInfo } from '@hizi.io/engine-sdk';
// Feature fields baked onto a slot scenario (subset of IResult from @hizi.io/engine-sdk).
interface SlotScenarioFeatureFields {
featureName?: string;
featureRoundId?: number;
spinInfo?: ISpinInfo[];
nextFeature?: string;
}
function featureProgress(result: IGameResult) {
const s = result.scenario as SlotScenarioFeatureFields;
const e = result.engineData;
return {
spinInfo: s.spinInfo ?? e.spinInfo, // ISpinInfo[] | undefined
currentFeature: s.featureName ?? e.currentFeature, // undefined ⇒ base game
nextFeature: s.nextFeature ?? e.nextFeature, // presentation only; inspect round state too
};
}
const fp = featureProgress(gameResult);
if (fp.currentFeature) {
const si = fp.spinInfo?.find(s => s.feature === fp.currentFeature);
if (si) console.log(`${fp.currentFeature}: spin ${si.used} of ${si.count} (${si.count - si.used} left)`);
}Retriggers vs re-entries
Because count is cumulative per feature, two separate runs of the same feature share one accumulating { count, used }. To scope a per-run "spin X of N" display, use the scenario's featureRoundId. A retrigger keeps the same featureRoundId and grows count. A re-entry increments featureRoundId.
playerChoice
When set, the player must make a selection before the game can continue. The playerChoice field appears for playerChoice type feature awards from hizi engine generator.
Each option is either a TPlayerChoiceFeatureAward ({ count, feature }) or a TPlayerChoiceCashAward ({ winMultiplier }). Use the isFeatureChoice / isCashChoice type guards exported from @hizi.io/engine-sdk to render each option correctly:
typescript
import { isFeatureChoice, isCashChoice } from '@hizi.io/engine-sdk';
if (gameResult.engineData.playerChoice) {
const options = gameResult.engineData.playerChoice;
for (let i = 0; i < options.length; i++) {
const option = options[i];
if (isFeatureChoice(option)) {
console.log(`Option ${i}: ${option.count} spins for feature: ${option.feature}`);
} else if (isCashChoice(option)) {
console.log(`Option ${i}: cash win ×${option.winMultiplier}`);
}
}
// After the player selects an option, send the choice index:
const chosenIndex = 0; // player's selection
const response = await placeBet({
backendURL,
token: sessionToken,
playerChoiceIndex: chosenIndex,
config,
});
}The engine merges the chosen award into the existing spinInfo (it preserves any in-progress features). Then the engine starts playing the chosen feature's spins. After this, inspect the updated state. Wait for any new choice or collect decision before continuing.
progressionCounters and progressionEvents
Games with progression counters expose two related fields. progressionCounters holds the counter values at the current step. progressionEvents lists what changed on this step: the increment for each counter, and any award that fired when the counter crossed its threshold:
typescript
for (const event of gameResult.engineData.progressionEvents ?? []) {
animateCounter(event.counter, event.value);
if (event.cashWin) showJackpotWin(event.counter, event.cashWin * stake); // already in totalWin
}Read progressionEvents to animate the counter and to deliver the award at the moment it happens. Read progressionCounters for the current fill level. Both fields update on each placeBet continuation. This means the engine signals an award on the exact step where it lands, even inside a multi-step round. See Progression Counters for the event shape and for full details on handling it.
The totalWin Field
totalWin is the cumulative win amount as a multiplier of stake. To get the currency value:
typescript
const baseStake = gameResult.engineData.baseStake;
if (baseStake === undefined) throw new Error('Resolve the original round stake');
const winInMinorUnits = gameResult.totalWin * baseStake;Treat totalWin as the current accumulated value. Replace your display with it. Do not sum successive totalWin values. Authoritative balance and settlement amounts determine wallet credit.
A bought feature retains the original selected stake in engineData.baseStake. The RGS round field gameRoundInfo.stake carries its full purchase debit. Never use that debit as the payout multiplier base.
bankedWin is a protected part of the outstanding win. It is not an additional prize or an immediate wallet credit.
Handling Common Game Features
gameResult.scenario is game-specific. It contains whatever you stored in hizi engine generator. How you handle features depends on your scenario data structure. Here are common patterns:
Cascades / Tumbles
If your generator stores cascade data, multi-result scenarios work well:
typescript
// In your generator
await generator.addResult(
[
{ grid: initialGrid, wins: initialWins }, // Initial spin
{ grid: afterCascadeGrid, wins: cascadeWins }, // After cascade
{ grid: afterSecondCascade, wins: secondCascadeWins }, // Second cascade
],
{ feature: 'basegame', win: totalWin },
);For that custom generator example, each placeBet returns one scenario step. Follow the round decision order before requesting another step.
Creator slots usually return all cascades of a spin inside scenario.cascadeData.steps. Animate that array within the current response. Do not send a request for each nested cascade. Read the slot cascade contract.
Free Spins
Free spins use featureAwards in hizi engine generator. The engine handles them automatically:
typescript
// Check if entering free spins (use the unified accessor - see "Where this data lives")
const fp = featureProgress(gameResult);
if (fp.nextFeature === 'freespin') {
const spinInfo = fp.spinInfo?.find(s => s.feature === 'freespin');
if (spinInfo) console.log(`Free spins: ${spinInfo.used} / ${spinInfo.count}`); // count is the total awarded
}Decision flow
- Check response success before accessing
result. - Decode its scenario with the current configuration.
- Render the returned boards and wins once for this result identity.
- Finish minimum timing and any required animation report.
- Respect operator pause, required dialogs, and session restrictions.
- Check round status, collect availability, choices, and continuation in that order.
- Recover any missing or contradictory state before another money request.
See the checked slot controller. Use the vertical's action rules for other games.
Next Steps
- buy-features - How to implement buy-feature support.
- Error Handling - Handle network and game errors.
- Types & Interfaces - Full type reference for
IGameResultand related types.