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 {
// 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?: TPlayerChoiceAward[];
currentFeature?: string;
nextFeature?: string;
progressionCounters?: Record<string, number>;
progressionEvents?: IProgressionEvent[];
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:
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 is the most important field. When true, the round is not complete. Call placeBet again to continue.
typescript
if (gameResult.engineData.inProgress) {
// Round continues - call placeBet again to get the next result
const next = await placeBet({ backendURL, token: sessionToken, config });
} 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
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 engine awards the win amount (totalWin) only 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:
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 feature entries the engine uses for the next spin. When nextFeature is 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. 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, // 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. 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, the round continues as normal. Keep calling placeBet until inProgress is false.
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 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
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 },
);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:
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
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.