Game Flow
This guide shows how to implement the complete game cycle using the @hizi.io/engine-sdk helper functions.
Complete Example
import { login, loadConfig, placeBet, collect, reportAnimationEnd, TNetworkResponse, IConnectReply, ILoadConfigReply, IPlaceBetReply, ICollectReply, ILoadConfigConfig, IGameResult, IGameSettings } from '@hizi.io/engine-sdk';
class HiziGame {
private sessionToken: string | null = null;
private backendURL: string | null = null;
private gameConfig: ILoadConfigConfig | null = null;
async initialize(): Promise<void> {
// Step 0: Get launch parameters from URL
const loginURL = new URLSearchParams(window.location.search).get('login');
const loginToken = new URLSearchParams(window.location.search).get('token');
// Step 1: Login and get session token
await this.performLogin(loginURL, loginToken);
// Step 2: Load game configuration
await this.loadGameConfig();
console.log('Game initialized');
console.log('Available stakes:', this.gameConfig.stakes);
console.log('RTP:', this.gameConfig.rtp);
}
private async performLogin(loginURL: string, launchToken: string): Promise<void> {
const response = await login({ loginURL, launchToken });
if (!response.success) {
throw new Error(`Login failed: ${response.error.message}`);
}
this.sessionToken = response.result.token;
this.backendURL = response.result.backendURL;
// Apply operator platform settings — stake limits, enabled features, UI rules, etc.
// The frontend is responsible for honouring these; the engine does not re-enforce them.
this.applyGameSettings(response.result.gameSettings);
}
private applyGameSettings(settings?: IGameSettings): void {
if (!settings) return;
// Clamp stake selector to operator limits (these override the game's own stake list).
this.minStake = settings.minStake;
this.maxStake = settings.maxStake;
this.customStakes = settings.customStakes;
this.defaultStakeIndex = settings.defaultStakeIndex;
// Gate UI features.
this.ui.setAutoplayEnabled(settings.autoplayEnabled);
this.ui.setGambleEnabled(settings.gambleEnabled);
this.ui.setTurboEnabled(settings.turboEnabled);
this.ui.setPackageBuyEnabled(settings.packageBuyEnabled);
// Presentation / regulatory rules.
this.ui.setForceOrientation(settings.forceOrientation);
this.ui.setDisplayRTP(settings.displayRTP, settings.showExactRTP);
this.ui.setAbbreviateAmounts(settings.abbreviateAmounts);
// External URLs (home, history, top-up, loss-limit).
this.ui.setExternalURLs({
home: settings.homeURL,
history: settings.historyURL,
topup: settings.topupURL,
lossLimit: settings.lossLimitURL,
});
// If the operator requires animation-end reporting, flag it so the game loop calls it.
this.reportAnimationEnd = settings.reportAnimationEnd;
}
private async loadGameConfig(): Promise<void> {
const response = await loadConfig({
backendURL: this.backendURL,
token: this.sessionToken,
});
if (!response.success) {
throw new Error(`Config load failed: ${response.error.message}`);
}
this.gameConfig = response.result.config;
}
async spin(stakeIndex: number): Promise<IGameResult> {
const stakeAmount = this.gameConfig.stakes[stakeIndex];
const response = await placeBet({
backendURL: this.backendURL,
token: this.sessionToken,
stake: stakeAmount,
});
if (!response.success) {
throw new Error(`Bet failed: ${response.error.message}`);
}
const gameResult = response.result.result;
// Display the result using scenario data
this.displayResult(gameResult);
// Handle multi-step rounds
if (gameResult.engineData.inProgress) {
await this.continueRound(gameResult);
}
return gameResult;
}
private async continueRound(previousResult: IGameResult): Promise<void> {
// If the engine is waiting for a player choice, handle it first
if (previousResult.engineData.playerChoice) {
const chosenIndex = await this.presentPlayerChoice(previousResult.engineData.playerChoice);
const response = await placeBet({
backendURL: this.backendURL,
token: this.sessionToken,
playerChoiceIndex: chosenIndex,
});
if (!response.success) {
throw new Error(`Player choice failed: ${response.error.message}`);
}
const gameResult = response.result.result;
this.displayResult(gameResult);
if (gameResult.engineData.inProgress) {
await this.continueRound(gameResult);
}
return;
}
// Continue the round
const response = await placeBet({
backendURL: this.backendURL,
token: this.sessionToken,
});
if (!response.success) {
throw new Error(`Continue round failed: ${response.error.message}`);
}
const gameResult = response.result.result;
this.displayResult(gameResult);
// Keep continuing until the round is complete
if (gameResult.engineData.inProgress) {
await this.continueRound(gameResult);
}
}
private async presentPlayerChoice(options: { count: number; feature: string }[]): Promise<number> {
// Game-specific UI to let the player choose
// Return the index of the selected option
console.log('Player must choose:');
options.forEach((opt, i) => console.log(` ${i}: ${opt.count} spins [${opt.feature}]`));
return 0; // Replace with actual player selection
}
async collectWins(): Promise<void> {
// Only needed for games with wager features.
// Games without wager features auto-end the round and credit winnings.
if (!this.gameConfig.wagerFeatures?.length && !this.gameConfig.wagerStakeFeatures?.length) return;
const response = await collect({
backendURL: this.backendURL,
token: this.sessionToken,
});
if (!response.success) {
throw new Error(`Collect failed: ${response.error.message}`);
}
console.log('Wins collected');
}
private displayResult(gameResult: IGameResult): void {
// gameResult.scenario contains your scenario data
// (whatever you stored during generation with hizi engine generator)
console.log('Scenario data:', gameResult.scenario);
console.log('Total win so far:', gameResult.totalWin);
console.log('Round in progress:', gameResult.engineData.inProgress);
}
}Step-by-Step Breakdown
1. Login
The game is launched with URL parameters containing a login URL and a short-lived token. Exchange these for a long-lived session token:
const loginURL = new URLSearchParams(window.location.search).get('login');
const launchToken = new URLSearchParams(window.location.search).get('token');
const response = await login({ loginURL, launchToken });
if (response.success) {
const sessionToken = response.result.token;
const backendURL = response.result.backendURL;
const gameSettings = response.result.gameSettings; // apply before showing the game
}Apply gameSettings immediately after login
The login response carries the operator's platform settings in gameSettings. Your frontend must read these and honour them — the engine does not re-enforce them for you. At minimum, apply minStake / maxStake / customStakes / defaultStakeIndex to your stake selector, and respect autoplayEnabled, gambleEnabled, turboEnabled, packageBuyEnabled, forceOrientation, displayRTP, historyURL / homeURL / topupURL, and the regulatory flags for your jurisdiction.
See the login endpoint and the full IGameSettings type for all available fields.
2. Load Configuration
After login, load the game configuration to get available stakes and game settings:
const response = await loadConfig({ backendURL, token: sessionToken });
if (response.success) {
const config = response.result.config;
console.log('Stakes:', config.stakes); // e.g. [10, 20, 50, 100]
console.log('RTP:', config.rtp); // e.g. 95
console.log('buy-features:', config.buyFeatures); // Available buy-features
}The config object also includes:
currencyMultiplier- Multiplier applied to stakes for the player's currencyversion- Engine version stringbuyFeatures- Array of available buy-feature options (see buy-features)
3. Place a Bet
Place a bet with one of the configured stake amounts:
const stakeAmount = config.stakes[selectedStakeIndex];
const response = await placeBet({
backendURL,
token: sessionToken,
stake: stakeAmount,
});
if (response.success) {
const gameResult = response.result.result;
// Your scenario data (whatever you stored during generation)
const scenarioData = gameResult.scenario;
// Win amount (as a multiplier - multiply by stake for currency value)
const totalWin = gameResult.totalWin;
// Whether more placeBet calls are needed
const inProgress = gameResult.engineData.inProgress;
}4. Handle Multi-step Rounds
If engineData.inProgress is true, the round is not complete. This happens when:
- The scenario has multiple results (multi-result scenario from hizi engine generator)
- Free spins or bonus features are in progress
- A player choice is pending
Call placeBet again to get the next result:
if (gameResult.engineData.inProgress) {
const nextResponse = await placeBet({ backendURL, token: sessionToken });
const nextResult = nextResponse.result.result;
// Check engineData for what's happening
if (nextResult.engineData.scenarioInfo.inProgress) {
// Multi-result scenario - more steps in the same scenario
}
if (nextResult.engineData.nextFeature) {
// Free spins / bonus feature - check spinInfo for remaining spins
}
}TIP
Keep calling placeBet({ backendURL, token: sessionToken }) until engineData.inProgress is false. The engine handles all state tracking internally.
5. Collect Winnings
Games without wager features (config.wagerFeatures and config.wagerStakeFeatures are both absent) auto-end the round and credit winnings automatically when the game completes. You do not need to call collect().
Games with wager features (either type) keep the round open so the player can collect or continue wagering. Collect winnings when canCollect is set:
if (gameResult.engineData.canCollect) {
const response = await collect({ backendURL, token: sessionToken });
if (response.success) {
console.log('Collected:', response.result.amountCredited);
}
}WARNING
Do not call collect while a round is in progress. Wait until engineData.inProgress is false. Only call collect on games that have wager features configured.
WebSocket Support
For lower latency, enable WebSocket communication. All subsequent API calls will automatically route through the WebSocket connection. The webSocketURL comes from the login() response (result.webSocketURL):
import { enableWebSockets } from '@hizi.io/engine-sdk';
const wsHandler = await enableWebSockets(webSocketURL);
// All calls now use WebSocket automatically
const response = await placeBet({
backendURL,
token: sessionToken,
stake: stakeAmount,
});The transport takes effect the moment enableWebSockets() is called (SDK ≥ 0.2.3):
- API calls made while the socket is still connecting are held back and sent once it opens — they never race the handshake over HTTP.
- If the connection drops later, requests that were in flight fail with a
NETWORKERROR, and the next API call automatically reopens the socket. Concurrent calls share a single reconnection attempt; connection attempts time out afterdefaultNetworkTimeout. - API calls fall back to HTTP only before
enableWebSockets()is called, afterclose(), or when the initial connection fails (the returned promise rejects in that case).
Use the returned WebSocketHandler to manage the connection:
wsHandler.isConnected(); // true while the socket is open
wsHandler.close(); // close the socket; API calls revert to HTTPOlder SDK versions
Before 0.2.3 the WebSocket transport only activated once the socket finished opening, and any call made earlier fell back to HTTP silently. If you target an older SDK, await enableWebSockets() before placing the first bet — this matters especially for crash, where the round runs over the socket.
Next Steps
- Response Handling - Deep dive into
IGameResultand scenario data. - buy-features - How to implement buy-feature functionality.
- Error Handling - Handle network and game errors gracefully.