Response Handling
This guide explains the structure of game results returned by placeBet and how to handle different game scenarios.
The IGameResult Structure
Every placeBet response contains an IGameResult object at response.result.result:
const response = await placeBet({ backendURL, token: sessionToken, stake });
if (response.success) {
const gameResult: IGameResult = response.result.result;
}IGameResult has three fields:
interface IGameResult {
// Your scenario data - whatever was stored during generation
scenario: Record<string, unknown>;
// Engine state tracking
engineData: {
// Fixed-odds (entries-DB) games only — omitted by rules-engine games
// such as blackjack and crash.
entryIndex?: number;
scenarioInfo?: IScenarioInfo;
spinInfo?: ISpinInfo[];
playerChoice?: TPlayerChoiceFeatureAward[];
currentFeature?: string;
nextFeature?: string;
progressionCounters?: Record<string, number>;
canCollect?: boolean;
inProgress: boolean;
};
// Cumulative win amount (as a multiplier of stake)
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:
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 will contain exactly that object at runtime:
const { visibleSymbols, reelIndexes, winLines } = gameResult.scenario as MyGameResult;TIP
Define a TypeScript interface for your game's scenario data and 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 and what to do next.
inProgress
The most important field. When true, the round is not complete - you must call placeBet again to continue.
if (gameResult.engineData.inProgress) {
// Round continues - call placeBet again to get the next result
const next = await placeBet({ backendURL, token: sessionToken });
} else {
// Round complete
// Games without wager features: round auto-ends, winnings credited automatically
// Games with wager features (wagerFeatures or wagerStakeFeatures): call collect() to cash out
}scenarioInfo
Tracks multi-result scenario progression:
interface IScenarioInfo {
scenarioIndex: number; // Which scenario was selected
currentScenarioIndex: number; // Current step (0-based)
inProgress: boolean; // More steps in this scenario?
}Multi-result scenarios are created when you pass an array to addResult() in hizi engine generator:
// 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 win amount (totalWin) is only awarded on the final step of a scenario. Earlier steps return totalWin: 0.
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:
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, and the feature run ends when used === count. count is the running total of spins awarded to the feature: it starts at the award size and grows when the feature retriggers or is re-entered (the engine increments the existing entry rather than replacing it). Don't add count + used — that double-counts.
nextFeature tells you which feature entries will be used for the next spin. When it's undefined and the scenario is complete, the round ends.
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, where the engine draws each feature spin at runtime and exposes progression on engineData (spinInfo, currentFeature, nextFeature).
For slots built with consolidated rounds (consolidateRounds), the whole round is one scenario array replayed step by step, so there are no runtime counters — the engine leaves engineData.spinInfo empty. The equivalent data is baked onto each scenario step instead: scenario.spinInfo, scenario.featureName (the current feature), and scenario.nextFeature. The count / used semantics are identical, so this is purely a question of where you read from.
Read it through a single accessor that prefers the scenario copy and falls back to engineData, so your code works regardless of how the game was built:
import type { IGameResult, ISpinInfo } from '@hizi.io/engine-sdk';
// Feature fields baked onto a slot scenario (subset of IResult from @hizi.io/slot-types).
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, // undefined ⇒ round ends after this spin
};
}
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. This is used with playerChoice type feature awards from hizi engine generator.
Each option has a count (number of spins) and feature (which feature entries to use):
if (gameResult.engineData.playerChoice) {
// Present options to the player
const options = gameResult.engineData.playerChoice;
for (let i = 0; i < options.length; i++) {
console.log(`Option ${i}: ${options[i].count} spins for feature: ${options[i].feature}`);
}
// 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,
});
}The engine merges the chosen award into the existing spinInfo (preserving any in-progress features), then starts playing the chosen feature's spins. After this, the round continues as normal - keep calling placeBet until inProgress is false.
The totalWin Field
totalWin is the cumulative win amount as a multiplier of stake. To get the currency value:
const winInCurrency = gameResult.totalWin * stakeAmount;During multi-step rounds, totalWin accumulates across all steps. Each subsequent placeBet response adds to the running total.
Handling Common Game Features
Since gameResult.scenario is game-specific (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:
// 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 },
);At runtime, each placeBet call returns one cascade step. Use scenarioInfo.inProgress to know if more cascades are coming.
Free Spins
Free spins use featureAwards in hizi engine generator. The engine handles them automatically:
// 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
placeBet response received
│
▼
engineData.canCollect && engineData.playerChoice?
│
├── yes → Gamble opportunity: present gamble options to player
│ Player collects → collect({ backendURL, token })
│ Player gambles → placeBet({ backendURL, token, playerChoiceIndex: N })
│ └── Loop back to start
│
└── no
│
▼
engineData.playerChoice?
│
├── yes → Present options to player
│ Player selects option index
│ Call placeBet({ backendURL, token, playerChoiceIndex: N })
│ └── Loop back to start
│
└── no
│
▼
engineData.inProgress?
│
├── true → Call placeBet({ backendURL, token })
│ └── Loop back to start
│
└── false → Round complete, winnings credited
Enable spin buttonNext 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.