# hizi frontend guide: full text > SDK integration, all creator modes, operator contracts, and checked examples from this build. For selective reading, use https://docs.hizi.io/engine/sdk/llms.txt. # Build a frontend with the hizi engine SDK Use this guide to build a game frontend for the hizi Remote Gaming Server (RGS). It covers all 14 modes in hizi-engine-creator. Slots receive the complete implementation path. The engine selects outcomes and stores round state. The RGS handles authentication, wallet transactions, and operator services. Your frontend renders the returned result, controls player actions, and implements the operator and jurisdiction requirements. ```text Game frontend → SDK or game transport → hizi RGS → hizi engine ↔ Operator Interface ↔ operator lobby ``` ## Start here | Step | Implement | Deliverable | | --- | --- | --- | | 1 | [Getting started](https://docs.hizi.io/engine/sdk/getting-started.html) | Connect once. Load the current config and round. | | 2 | [Launch and loading](https://docs.hizi.io/engine/sdk/launch-and-loading.html) | Loading screens, messages, assets, audio, and ready handshake. | | 3 | [Stakes and money](https://docs.hizi.io/engine/sdk/stakes-and-money.html) | Valid stakes, default selection, full purchase cost, and balance display. | | 4 | [Slot game flow](https://docs.hizi.io/engine/sdk/game-flow.html) | Request lock, state decisions, timing, choices, and recovery. | | 5 | [Rules and presentation](https://docs.hizi.io/engine/sdk/rules-and-presentation.html) | Paytable, rules, return to player (RTP), and every configured mechanic. | | 6 | [Operator Interface](https://docs.hizi.io/engine/sdk/operator-interface.html) | Loading, round events, pause, audio, errors, and lobby actions. | | 7 | [Settings reference](https://docs.hizi.io/engine/sdk/settings-reference.html) and [jurisdictions](https://docs.hizi.io/engine/sdk/jurisdictions.html) | Validated controls for the player, product, and jurisdiction. | | 8 | [Recovery and session](https://docs.hizi.io/engine/sdk/recovery-and-session.html) | Reconnect, resume, reality checks, session totals, and safe exit. | | 9 | [RGS features](https://docs.hizi.io/engine/sdk/rgs-features.html) | Freeplays, discounts, promotions, history, and operator extensions. | | 10 | [Release checklist](https://docs.hizi.io/engine/sdk/release-checklist.html) | Evidence from normal, interrupted, and restricted play. | This guide documents SDK **0.2.18**. Some contracts require operator or certification input. ::: warning Release requirements A jurisdiction preset alone does not establish compliance. Apply the effective player limits and the rules for the specific product. For example, UKGC limits differ by player age and casino vertical. Resolve every applicable gap before enabling real-money play. ::: ## For AI agents Read the ordered guide above before writing money requests. Then read the game's vertical guide. - [SDK index for agents](https://docs.hizi.io/engine/sdk/llms.txt): ordered guide, game pages, and links to related documentation. - [Complete frontend guide as text](https://docs.hizi.io/engine/sdk/llms-full.txt): SDK pages, all game guides, operator contracts, and example source. - [Engine index for agents](https://docs.hizi.io/engine/llms.txt): links to individual pages with descriptions. - [Whole documentation site as text](https://docs.hizi.io/llms-full.txt): includes backend and platform references. The build generates these files from the same pages. It includes the checked TypeScript examples. Do not invent omitted settings, rule text, credentials, scenario fields, or operator contracts. ## Choose a vertical Requests, legal actions, and settlement differ between verticals. A scenario renderer alone is insufficient. | Creator mode | Guide | Main frontend difference | | --- | --- | --- | | Slot | [Slots](https://docs.hizi.io/engine/games/slot.html) | Boards, cascades, feature spins, choices, gamble, and banking. | | Plinko | [Plinko](https://docs.hizi.io/engine/games/plinko.html) | Row and risk table, returned bucket, immediate settlement. | | Hi/Lo | [Hi/Lo](https://docs.hizi.io/engine/games/hilo.html) | Card choices, skip, and explicit cashout. | | Over/Under | [Over/Under](https://docs.hizi.io/engine/games/overunder.html) | Threshold and direction table, one returned roll. | | Frogger | [Frogger](https://docs.hizi.io/engine/games/frogger.html) | Explicit progress or cashout, absolute multipliers. | | Mines | [Mines](https://docs.hizi.io/engine/games/mines.html) | Player tile picks, hidden board, and cashout. | | Keno | [Keno](https://docs.hizi.io/engine/games/keno.html) | Number selection, draw, and hit paytable. | | Match | [Match](https://docs.hizi.io/engine/games/match.html) | EXACT or AT_LEAST matches and numbered ball placement. | | Bingo | [Bingo](https://docs.hizi.io/engine/games/bingo.html) | Multiple tickets, card cells, draw sequence, and patterns. | | Progressive | [Progressive](https://docs.hizi.io/engine/games/progressive.html) | Survival steps and cashout. This mode is not a shared jackpot. | | Crash | [Crash](https://docs.hizi.io/engine/games/crash.html) | Live server events and cashout on one WebSocket connection. | | Roulette | [Roulette](https://docs.hizi.io/engine/games/roulette.html) | Multiple bets, total debit, and individual settlements. | | Blackjack | [Blackjack](https://docs.hizi.io/engine/games/blackjack.html) | Hands, legal actions, extra debits, insurance, and settlement. | | Blank | [Blank and custom](https://docs.hizi.io/engine/games/custom.html) | Preserve the imported game type or define a custom contract. | ## SDK surface | Area | Exports and reference | | --- | --- | | Session | `login`, `refresh`, `loadConfig`, `updateBalance`, `reportAnimationEnd`. See [endpoints](https://docs.hizi.io/engine/sdk/endpoints.html). | | Money actions | `placeBet`, `collect`. See [game flow](https://docs.hizi.io/engine/sdk/game-flow.html) and [gamble](https://docs.hizi.io/engine/sdk/gamble-features.html). | | Response handling | `TNetworkResponse`, `API_RETURNCODES`, `recoverableErrorCodes`, `defaultNetworkTimeout`. See [errors](https://docs.hizi.io/engine/sdk/error-handling.html). | | Freeplays | `getFreePlaysRemaining`, `getFreePlayStake`, `IFreePlayInfo`. See [freeplays](https://docs.hizi.io/engine/sdk/free-plays.html). | | Discounts | `IDiscountInfo`, `IDiscountEntry`, `IBalanceReply.discountInfo`. See [RGS features](https://docs.hizi.io/engine/sdk/rgs-features.html). | | Game helpers | `rouletteAdditionalData`, `blackjackInitialData`, `blackjackActionData`, `minesPickData`, `kenoPickData`, `bingoTicketData`. | | Crash | `CRASH_EVENTS`, `crashPlaceBetData`, `crashCollectData`. See the required [transport](https://docs.hizi.io/engine/sdk/crash-transport.html). | | WebSocket requests | `enableWebSockets`, `WebSocketHandler`. The handler does not expose Crash broadcasts. | | Choices | `isCashChoice`, `isFeatureChoice`, `TPlayerChoiceAward`. Preserve the server's original choice indexes. | | Results and configuration | `IGameResult`, `ILoadConfigConfig`, `IGameSettings`, and game-specific types. See [types](https://docs.hizi.io/engine/sdk/types.html). | | Compression | `decodeScenario`, `compressScenario`, `expandScenario`, `compressStep`, `expandStep`, `hasScheme`, `buildSymbolToInt`, `SCHEME_VERSION`. | | Provably fair results | `pfVerify`, `IPfVerifyOptions`, `IPfVerifyReply`. See [verification](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify). | The [endpoint reference](https://docs.hizi.io/engine/sdk/endpoints.html) describes operation names. Most operations use `POST backendURL` with an `op` body field. Do not append `/placeBet` to the supplied URL. ## Responsibilities Build the rendering, input, audio, timing, loading, rules, history presentation, and operator integration in your frontend. Keep a single owner for session state and money requests. Use approved localized content for mandatory messages and rules. Confirm the game version, currency, jurisdiction, player limits, and operator protocol before enabling play. The engine supplies outcomes and saved state. Treat them as authoritative. Never calculate payable winnings from animation, replay an uncertain bet, or automatically choose a gamble option. --- # Getting started Install the audited SDK version. Open a session and load its state before adding player controls. ## What you need - A published game configuration and its approved rules and assets. - A frontend URL configured for that game in Hizi. - An authorized test launch from the Hizi Backoffice or operator integration. - The player currency, language, mode, jurisdiction, and effective limits. - The selected Operator Interface protocol and its test environment. See the [Studio Quickstart](https://docs.hizi.io/studio-quickstart/) for game creation and frontend registration. Never embed a backoffice credential in the frontend. ## Install ```bash npm install --save-exact @hizi.io/engine-sdk@0.2.18 ``` The package contains TypeScript declarations. Use a browser build tool such as the tool already selected for your game. Keep the resolved SDK version in your lockfile. Install [Operator Interface](https://docs.hizi.io/operator-interface/guide/getting-started.html) with the supplied registry access. Its package is `@hoelle/operator-interface`. ## Connect once Use SDK `login()`. It returns the session token, the URLs, `gameSettings`, `operatorProtocol` and `tokenData`. `tokenData` holds the player, game, language, currency, currency multiplier, mode and any open round. The [launch guide](https://docs.hizi.io/engine/sdk/launch-and-loading.html) explains the single-use token and the complete loading sequence. Use the [SDK error helper](https://docs.hizi.io/engine/sdk/error-handling.html#controller-and-loader-errors) to retain operator error data. This function loads data only. Call it from your loader and handle errors in that loader. ```typescript import { loadConfig, login } from '@hizi.io/engine-sdk'; import { launchParams } from './launch'; import { SdkRequestError } from './sdk-error'; export async function loadSession(search: string) { const connected = await login(launchParams(search)); if (!connected.success) { throw new SdkRequestError(connected.error); } const connection = connected.result; const session = { backendURL: connection.backendURL, token: connection.token }; const loaded = await loadConfig(session); if (!loaded.success) { throw new SdkRequestError(loaded.error); } return { connection, session, loaded: loaded.result }; } ``` Do not call `login` again after this exchange. SDK `loadConfig` decompresses `gameResult` and `previousResults` with the returned config. `login()` copies eleven top-level keys and discards the rest of the raw connect reply. Most of it returns on the next call, because no other SDK operation narrows its reply. Two fields do not: `gameVariables.rcElapsed` and the singular `freePlayInfo`. Do not write your own connect call to recover them. See [what `login()` does not return](https://docs.hizi.io/engine/sdk/launch-and-loading.html#what-login-does-not-return). ## Before the first bet 1. Check `config.gameType` against the frontend's supported game type. Use the deployment contract for older configs without this field. 2. Apply the [settings](https://docs.hizi.io/engine/sdk/settings-reference.html) and effective [stake limits](https://docs.hizi.io/engine/sdk/stakes-and-money.html). 3. Set up [Operator Interface](https://docs.hizi.io/engine/sdk/operator-interface.html) and register incoming events. 4. Load the assets and [rules](https://docs.hizi.io/engine/sdk/rules-and-presentation.html) for every possible feature. 5. Restore `gameResult`, `gameRoundInfo`, and `amountToCollect`. See [recovery](https://docs.hizi.io/engine/sdk/recovery-and-session.html). 6. Finish required [loading messages](https://docs.hizi.io/engine/sdk/launch-and-loading.html). 7. Enable only the actions allowed by the restored state. A positive win does not mean that `collect` is available. An open round does not mean that a continuation can run automatically. For slots, use the checked [game flow example](https://docs.hizi.io/engine/sdk/game-flow.html). For other games, implement the decisions in the [vertical guide](https://docs.hizi.io/engine/games/). ## First verification Launch in demo mode through the real wrapper. Confirm the currency, stakes, rules, mute state, and loading handshake. Place one player-requested bet. Disconnect during the next response, then reload the page. The frontend must restore that round without placing another opening bet. Continue with the [release checklist](https://docs.hizi.io/engine/sdk/release-checklist.html). --- # Launch and loading screens The remote gaming server (RGS) supplies the launch and session data. The frontend owns the loading screens and input controls. ## Launch contract Read `login` and `token` from the query string with `URLSearchParams`. Preserve other launch parameters for the operator adapter. Do not decode a parameter twice. | Value | Source | Use | | --- | --- | --- | | Login URL and launch token | Query parameters `login`, `token` | Exchange once. A launch token can be single-use. | | Session token | Connect reply `token` | Send with each engine request. Keep it in memory. | | API address | SDK `backendURL`; raw reply `backendUrl` | Send SDK operations to this address. Do not construct endpoint paths. | | Refresh and logout addresses | SDK `refreshURL`, `logoutURL` | Retain the complete URLs, including their query strings. | | Socket address | SDK `webSocketURL` | Optional for slots. See [transport](https://docs.hizi.io/engine/sdk/game-flow.html#websocket-support). | | Settings | `gameSettings` | Apply before enabling any control. Reapply after refresh. | | Game and player | `tokenData.gameId`, `tokenData.playerId`, `tokenData.operatorId` | Use for Operator Interface setup. | | Language, currency, mode | `tokenData.language`, `tokenData.currency`, `tokenData.mode` | Select translations and display units. The mode is `REAL` or `DEMO`. Do not infer real mode from the balance. | | Currency factor | `tokenData.currencyMultiplier` | Returned stakes are already in account units. Do not multiply them again. See [stakes and money](https://docs.hizi.io/engine/sdk/stakes-and-money.html). | | Open round | `tokenData.gameRound` | A hash here means an unfinished round. Restore it before play. | | Operator protocol | `operatorProtocol` | Use the negotiated protocol. Do not select one from the hostname. | | Reality-check offset | Raw `gameVariables.rcElapsed` | Time already elapsed towards the next reminder. See [jurisdictions](https://docs.hizi.io/engine/sdk/jurisdictions.html#reality-checks) for the unit. | | Operator data | Top-level `passThroughData` on the connect reply | Route through the integration for that operator. | Use the platform launch flow from [Studio Quickstart](https://docs.hizi.io/studio-quickstart/set-your-frontend-url.html). A page opened without launch parameters needs a launch instruction. It must not place a real bet. ## Connect with `login()` Use SDK `login()`. It is the supported connect call. Do not write your own replacement for it. `login()` and `refresh()` return `tokenData`, and `tokenData` carries the session identity: `playerId`, `operatorId`, `gameId`, `language`, `currency`, `currencyMultiplier`, `mode`, and any open `gameRound`. The typed name of the game mode is `mode`, not `gameMode`. `ITokenData` is an open record (`[key: string]: unknown`), so the SDK also passes through any extra field your operator puts in `tokenData`. Use the [SDK error helper](https://docs.hizi.io/engine/sdk/error-handling.html#controller-and-loader-errors) to retain operator error data. ```typescript // Read the launch parameters the operator puts on your frontend URL, in the shape // SDK login() takes: { loginURL, launchToken }. login() builds the request itself. // // There is deliberately no replacement for login() here. login() is the supported // connect call. Re-implementing it to recover a dropped field risks exchanging a // single-use launch token twice, and drifts from the SDK on every release. export function launchParams(search: string): { loginURL: string; launchToken: string } { const params = new URLSearchParams(search); const loginURL = params.get('login'); const launchToken = params.get('token'); if (!loginURL || !launchToken) throw new Error('Launch URL requires login and token'); return { loginURL, launchToken }; } ``` ```typescript import { loadConfig, login } from '@hizi.io/engine-sdk'; import { launchParams } from './launch'; import { SdkRequestError } from './sdk-error'; const connected = await login(launchParams(location.search)); if (!connected.success) throw new SdkRequestError(connected.error); const connection = connected.result; const session = { backendURL: connection.backendURL, token: connection.token }; const loaded = await loadConfig(session); if (!loaded.success) throw new SdkRequestError(loaded.error); // Read the player, game, language and currency from connection.tokenData. // Restore loaded.result before enabling the first paid spin. ``` A launch token can be single-use. Exchange it once. Do not call `login()` twice, and do not add a second connect path beside it. ### What `login()` does not return `parseConnectReply()` copies eleven top-level keys: `token`, `backendURL`, `refreshURL`, `logoutURL`, `webSocketURL`, `balance`, `gameSettings`, `tokenData`, `freePlaysAvailable`, `featuresAvailable`, and `operatorProtocol`. It discards every other field of the raw connect reply. Most of what it drops you get again on the next call. Only `login()` and `refresh()` use this narrowing step. Every other SDK operation returns the raw reply unchanged, so `loadConfig` gives you `gameRoundInfo`, `amountToCollect` and the resumed round state straight after connecting. An error reply keeps `passThroughData`. Two fields do not come back: | Field | Consequence | | --- | --- | | `gameVariables.rcElapsed` | The elapsed time towards the next reality check. Without it a relaunch restarts the reminder interval at zero. | | `freePlayInfo` | The singular package-progress block (`used`, `granted`, `won`, `isLast`, `serial`). The plural `freePlaysAvailable` survives; this one does not. | **Do not fork `login()` to recover them.** A second exchange of a single-use launch token is a real failure, and a hand-written copy of `parseConnectReply()` drifts from the SDK on every release. The cost of that workaround is higher than the two fields are worth. Handle them as integration requirements instead: - Where the operator contract gives the wrapper the reality check, the wrapper owns the elapsed time. Send `realityCheck(...)` through the [Operator Interface](https://docs.hizi.io/engine/sdk/operator-interface.html) and do not track the offset yourself. - Where the game owns the reminder, ask hizi.io support to carry `rcElapsed` in `tokenData`, which `login()` does return. Record the gap in your integration notes until it is carried. See [jurisdictions](https://docs.hizi.io/engine/sdk/jurisdictions.html#reality-checks). - For freeplay progress, drive the ticket picker from `freePlaysAvailable`. The RGS puts `freePlayInfo` on every gameround reply once freeplays are active, and `placeBet` is not narrowed, so the block reaches you there. `IPlaceBetReply` does not declare it, so read it as an extra field and validate it yourself. Catch fetch failures and invalid JSON at the application boundary. Keep play disabled. Follow [error handling](https://docs.hizi.io/engine/sdk/error-handling.html). Do not log the launch URL, session token, or complete connection object. Restrict allowed launch hosts in your deployment configuration. Remove consumed credentials from visible history only after the wrapper has read its launch parameters. ## Loading sequence 1. Display a loading screen immediately. Disable all inputs that can place a bet. 2. Read saved preferences. Apply the operator's [storage restrictions](https://docs.hizi.io/engine/sdk/settings-reference.html#preferences-and-device-behavior). 3. Exchange the launch token. Preserve metadata that your operator needs. 4. Register [Operator Interface listeners](https://docs.hizi.io/operator-interface/api/events.html). Await `OperatorInterface.setup`. 5. Call `OperatorInterface.loadProgress(0)`. 6. Fetch `loadConfig`. Validate its game type, settings, currency, and available stakes. Custom keys from the game's `loadConfig` data (for example a paytable) appear at the top level of `config`. See [Load Config](https://docs.hizi.io/engine/generator/load-config.html). 7. Load the required fonts, translations, symbol textures, rules, and sounds. Report progress from `0` to `1`. 8. Call `gameDataLoaded()` after configuration and required game data are ready. 9. Restore any interrupted round. Display its last board or pending player choice. 10. Keep `loadMsg` visible for at least `minLoadTime` milliseconds after the message appears. 11. Show the paytable when `displayPaytableOnEnterGame` is true. Require acknowledgement before play. 12. Call `loadProgress(1)` and `loadComplete()` when loading ends. 13. Send `initialGameState(muted)`. Send the current stake and balance. 14. Call `gameReady()` when the player can interact with the restored game. `gameReady()` does not mean a new paid spin is available. A restored game can require collection or a bonus choice. Use `gameUIUpdated(buyMenuOpen, splashVisible, waitingForConfirmation)` when those states change. Keep a splash acknowledgement separate from a spin request. ## `loadingScreens` and messages The engine SDK defines no `loadingScreens` API or response field. Implement the loading screens in your frontend. The standard settings are `loadMsg` and `minLoadTime`. Treat `loadMsg` as text. Use `textContent` or your framework's escaped text binding. Do not insert it as HTML. Use an approved translation when the operator supplies a message identifier instead of prose. Keep essential instructions readable on narrow screens. Do not cover a required message with progress animations. If an asset fails, show a clear error with recovery or exit controls. Do not report successful loading. The RGS does not supply a complete set of translated jurisdiction messages. Include approved messages in your game assets. See [jurisdiction messages](https://docs.hizi.io/engine/sdk/jurisdictions.html#messages-and-session-displays). ## Audio, background tabs, and mobile Start the browser audio context from a player gesture. A successful unmute request can still require that gesture. Keep the audio icon consistent with the effective state. Track three independent conditions: the player's mute preference, an operator mute request, and suspension while hidden or paused. Play audio only when all conditions permit it. An operator resume must not override the player's mute preference. Handle mute, unmute, and toggle requests through one audio controller. Report actual changes with `muted()` or `unmuted()`. Stop sounds, music, and queued win sounds when muted. On `visibilitychange`, suspend presentation and new input. A hidden tab does not stop server time or a Crash round. Reconcile state before continuing after disconnection. Apply orientation and fullscreen restrictions before presenting controls. Support resize, safe areas, keyboard focus, and touch. Never stretch a fixed board into inaccessible buttons. See [settings](https://docs.hizi.io/engine/sdk/settings-reference.html#preferences-and-device-behavior). --- # Stakes and money A stake is the amount associated with a bet. A win multiplier is a ratio. Keep these values separate. ## Units | Value | Unit and interpretation | | --- | --- | | `stake`, balances, `amountToCollect`, `amountCredited` | Integer units of 1/100 of the account currency. For EUR, `100` means EUR 1.00. | | Reply `config.stakes`, `config.minStake`, `config.maxStake` | Amounts after the engine applies the currency multiplier. Do not multiply them again. | | Reply `gameSettings.minStake`, `maxStake`, `maxPackageStake`, `maxExposure` | Amounts after the RGS applies the currency multiplier. | | `gameSettings.customStakes` | Explicit amounts in 1/100 of the game currency. The RGS applies no multiplier to them. They replace the default ladder. | | `config.currencyMultiplier` | Conversion of authoring amounts into the account currency. This is not a display divisor. | | `IGameResult.totalWin`, `engineData.bankedWin` | Multipliers of the round's original base stake. | | Slot `scenario.winAmount`, win breakdowns | Multipliers for presentation. They are not separate wallet credits. | | `buyFeatures[].price` | Multiplier of the selected base stake. | | `rtp`, `featureRTP` | Percentages. `95.5` means 95.5%, not 0.955%. | | `maxWin`, `maxPayout` | Multipliers. The first describes one spin. The second describes the whole round. | | `maxWinOdds`, `maxPayoutOdds` | The `N` in “1 in N”. See the [known limitation](https://docs.hizi.io/engine/sdk/rules-and-presentation.html#rtp-and-maximum-wins). | For custom and social currencies, use the operator's display label and precision contract. Keep wire amounts unchanged. See [Currencies](https://docs.hizi.io/hizi-app/currencies.html). ## Select legal stakes The engine applies this precedence: 1. A nonempty `gameSettings.customStakes` list replaces the game ladder. 2. Without that list, operator min/max settings replace the corresponding game defaults. 3. The engine applies the currency multiplier to game defaults before comparison. 4. A game ladder with more than one entry also restricts accepted values. `loadConfig` supplies effective min/max values on current engines. Its ladder can exclude stakes that exceed maximum exposure. ::: warning Custom stakes bypass engine bounds Current `validateStake` skips min/max checks when `customStakes` is nonempty. Server acceptance does not prove a custom stake meets jurisdiction limits. Apply approved limits in the frontend. Resolve a conflicting configuration before release. ::: Build the frontend list from the custom ladder, or otherwise `config.stakes`. Remove duplicates, nonpositive values, and invalid amounts. Apply the approved bounds for this player, currency, and vertical. Use operator overrides when computing effective bounds. Do not intersect an operator override with an obsolete authoring default. Apply independent legal and player limits as additional bounds. ```typescript import { permittedStakes } from './slot-session'; // approvedMin/approvedMax include effective configuration and player limits. const stakes = permittedStakes(config, gameSettings, { min: approvedMin, max: approvedMax, }); if (stakes.length === 0) { // Disable paid play. Show a configuration error. Do not invent a stake. } ``` `stakes` is optional in the SDK. Some games use a numeric stake input. Validate that input against explicit effective bounds. Agree its increments and rounding with the operator. An absent ladder is not permission to accept every amount. ## Default and restored stakes Apply `defaultStakeIndex` to the final displayed ladder. Clamp an out-of-range index to an available entry. Do not use an index from the unfiltered ladder. When `forceDefaultStake` is true, ignore the saved preference at a new session. Otherwise restore a saved value only if it remains legal. An open round has its own stake. Restore that value before a continuation. Do not replace it with today's default or a newly filtered value. Lock the stake selector during the round. Resolve changed limits through recovery or operator support. Apply the same validation to pointer controls, keyboard controls, autoplay, and Operator Interface events. A wrapper request cannot bypass the stake validator. ## Feature purchases Show the base stake and the complete cost before confirmation: ```text base stake = 100 feature.price = 80 debit = 100 × 80 = 8000 ``` Send `stake: 100` and the returned feature `id`. Do not send `stake: 8000`. The engine calculates the debit. For slots, require `packageBuyEnabled` and a returned buy option. Check the complete cost against balance and approved purchase limits. Recheck after confirmation if settings or the stake changed. Current engine handlers compare `maxPackageStake` against the requested base stake. No handler compares it against the complete purchase cost. Apply the complete-cost check in your frontend. Include this difference in operator acceptance tests. Other verticals use `featureToBuy` to select an ordinary table, often at price `1`. These selections are not necessarily slot bonus purchases. Agree their permission mapping for that vertical. See [game guides](https://docs.hizi.io/engine/games/). ## Base stake on a bought round For a buy, the RGS can report the full debit in both `gameRoundInfo.stake` and `gameRoundInfo.baseStake`. Prefer `result.engineData.baseStake` for win conversion, including after reload. ```typescript import { roundBaseStake } from './slot-session'; const base = roundBaseStake(snapshot); const displayedWin = Math.floor(snapshot.result!.totalWin * base); ``` Example: a base stake of `100`, purchase price of `80`, and `totalWin: 12` yields `1200`. It does not yield `96000`. The helper rejects a bought round without its recorded base stake. Resolve an older round from authoritative history. Do not divide by a current feature price that might have changed. ## Balance, cumulative wins, and limits Replace the displayed balance with each authoritative balance reply. Do not add `totalWin` to it. A reply may already include the credit. Never sum successive `totalWin` values. They describe the accumulated result, not a sequence of new payments. Consolidated scenarios can report zero until the final step. The engine floors a converted win to integer units. Use server amounts for settlement and account displays. Use scenario amounts only for animation breakdowns. `maxExposure` limits a round's payout. The engine can cap `totalWin` and end the round early. Honor returned state even when feature counters still show unused spins. Do not add the discarded spins or an uncapped scenario sum back into the win. `calculatedMaxExposure` reports the reachable amount when the engine can calculate it. `maxPayoutCap` is not currently supplied by `loadConfig`. ## Net position A session net position measures play, not deposits or withdrawals. Do not calculate it from a balance difference. The session carries no net-position counters. `tokenData` is `V2_GameToken`, and it holds session identity only: `operatorId`, `playerId`, `gameId`, `mode`, `currency`, `currentToken`, `gameRound`, `walletMode`, `language`, `currencyMultiplier`, `device`, `commonDraw`. The `totalBetAmount` and `totalWinAmount` fields in the Operator API are per round, not per session. Retain a session ledger of confirmed round debits and credits. Key entries by round and transaction identity. Exclude wallet deposits and duplicate replay animations. Preserve the agreed session boundary through refresh and reload. Show unknown totals as unavailable while reconciling. Do not silently reset a required net position to zero. Agree how freeplays, discounts, pending winnings, and resumed rounds enter the session ledger. --- # Game flow The slot frontend is a state machine. Its current state determines which action can run. One lock serializes requests and presentation. ## State and action order ```text connect → loadConfig → restore → loading/rules acknowledgements │ ├─ ready → player starts one paid round ├─ choice → player selects an original option index ├─ collect-or-continue → player collects or risks the win └─ continuation → next required free slot step │ request → render → timing → report → inspect new state │ recovery on error ``` Operator pause, reality checks, expired sessions, and required dialogs block these transitions. Removing one block must not remove the others. Inspect `canCollect` and pending choices before continuing. `inProgress: true` alone is not an instruction to send another request immediately. ## Build order 1. Implement [launch and loading](https://docs.hizi.io/engine/sdk/launch-and-loading.html). 2. Normalize [settings](https://docs.hizi.io/engine/sdk/settings-reference.html). Calculate [legal stakes](https://docs.hizi.io/engine/sdk/stakes-and-money.html). 3. Restore the response from `loadConfig`. 4. Implement [slot rendering and rules](https://docs.hizi.io/engine/sdk/rules-and-presentation.html). 5. Connect [operator events](https://docs.hizi.io/engine/sdk/operator-interface.html) to the same action gates as local controls. 6. Add [freeplays](https://docs.hizi.io/engine/sdk/free-plays.html), [purchases](https://docs.hizi.io/engine/sdk/buy-features.html), [gamble](https://docs.hizi.io/engine/sdk/gamble-features.html), and [RGS features](https://docs.hizi.io/engine/sdk/rgs-features.html). 7. Exercise the [release checks](https://docs.hizi.io/engine/sdk/release-checklist.html). ## Slot controller The following controller is a compilable reference for requests, choices, recovery, banking, and timing. It targets slots. Other verticals have different action contracts. Its `View.render` callback is your renderer. Resolve that promise only after required presentation finishes. Implement operator notifications and dialog controls as described on their pages. The caller supplies `Policy` after resolving the player's configuration. Do not construct that policy from permissive defaults. Refresh it when session settings change. [Download the controller](https://docs.hizi.io/engine/sdk/examples/slot-session.ts). Save the [error helper](https://docs.hizi.io/engine/sdk/examples/sdk-error.ts) beside it. [Download the launch-parameter helper](https://docs.hizi.io/engine/sdk/examples/launch.ts). ```typescript import { SdkRequestError } from './sdk-error'; import { collect, decodeScenario, loadConfig, placeBet, reportAnimationEnd, isFeatureChoice, type IGameResult, type IGameRoundInfo, type IGameSettings, type ILoadConfigConfig, type ILoadConfigReply, type IPlaceBetOptions, type IPlaceBetReply, type ICollectReply, type ISessionOptions, type IBalanceReply, type IFreePlayInfo, type TPlayerChoiceAward, type ITokenData, } from '@hizi.io/engine-sdk'; export type Phase = 'ready' | 'choice' | 'collect-or-continue' | 'continuation' | 'recovery' | 'busy' | 'blocked'; export interface Snapshot { result?: IGameResult; round?: IGameRoundInfo; balance?: IBalanceReply; tokenData?: ITokenData; freePlays?: IFreePlayInfo[]; amountToCollect?: number; } export interface Policy { // Validated for this player, currency, vertical, and jurisdiction. stakes: number[]; minSpinTimeMs: number; buyEnabled: boolean; gambleEnabled: boolean; partialCollectEnabled: boolean; maxPurchaseAmount?: number; } export interface View { // Resolve only after all boards, wins, and feature transitions finish. render(snapshot: Snapshot, resumed: boolean): Promise; } // These dependencies also let tests model delayed or lost replies. const defaultApi = { collect, loadConfig, placeBet, reportAnimationEnd }; const defaultClock = { now: () => performance.now(), sleep: (ms: number) => new Promise(resolve => setTimeout(resolve, ms)), }; export function slotPhase(snapshot: Snapshot): Exclude { const { result, round, amountToCollect } = snapshot; if (round?.status && !['open', 'closed'].includes(round.status)) return 'recovery'; if (round?.status === 'closed') { return result?.engineData.inProgress || result?.engineData.canCollect || result?.engineData.playerChoice?.length ? 'recovery' : 'ready'; } if (!result) return round?.status === 'open' || (amountToCollect ?? 0) > 0 ? 'recovery' : 'ready'; const e = result.engineData; if (e.canCollect) return 'collect-or-continue'; if (e.playerChoice?.length) return 'choice'; if (e.inProgress) return 'continuation'; if (round?.status === 'open' || (amountToCollect ?? 0) > 0) return 'recovery'; return 'ready'; } export function roundBaseStake(snapshot: Snapshot): number { // The RGS can record the full purchase price in BOTH round stake fields. const stored = snapshot.result?.engineData.baseStake; if (typeof stored === 'number' && stored > 0) return stored; if (snapshot.result?.engineData.buyFeatureId) { throw new Error('Bought round has no recorded base stake. Resolve its history.'); } const stake = snapshot.round?.baseStake; if (typeof stake === 'number' && stake > 0) return stake; throw new Error('Round has no base stake. Recover its state.'); } export function permittedStakes( config: ILoadConfigConfig, settings: Partial, limits: { min: number; max: number }, ): number[] { // The caller supplies finite, approved bounds. No invented fallback ladder. if (!Number.isFinite(limits.min) || !Number.isFinite(limits.max) || limits.min <= 0 || limits.max < limits.min) throw new Error('Invalid stake limits'); const source = settings.customStakes?.length ? settings.customStakes : config.stakes; if (!source?.length) throw new Error('This game needs a validated stake input or an explicit ladder'); return [...new Set(source)].filter(n => Number.isSafeInteger(n) && n >= limits.min && n <= limits.max).sort((a, b) => a - b); } export class SlotSession { snapshot: Snapshot = {}; private busy = false; private uncertain = true; private blocks = new Set(); constructor( private session: ISessionOptions, public config: ILoadConfigConfig, public settings: Partial, public policy: Policy, private view: View, private api = defaultApi, private clock = defaultClock, ) { if (!Number.isFinite(policy.minSpinTimeMs) || policy.minSpinTimeMs < 0) { throw new Error('Invalid minimum spin time'); } } get phase(): Phase { if (this.busy) return 'busy'; if (this.uncertain) return 'recovery'; if (this.blocks.size) return 'blocked'; return slotPhase(this.snapshot); } block(reason: string): void { this.blocks.add(reason); } unblock(reason: string): void { this.blocks.delete(reason); } // Replace credentials after a successful refresh. Recover before any new bet. replaceSession(session: ISessionOptions): void { if (this.busy) throw new Error('Wait for the current request'); this.session = session; this.uncertain = true; } private assertPhase(...allowed: Phase[]): void { if (!allowed.includes(this.phase)) throw new Error(`Action unavailable in ${this.phase}`); } private isWager(option: TPlayerChoiceAward): boolean { return isFeatureChoice(option) && [ ...this.config.wagerFeatures ?? [], ...this.config.wagerStakeFeatures ?? [], ].includes(option.feature); } private accept(reply: IPlaceBetReply | ICollectReply, collecting = false): void { // A collect reply can carry a compressed scenario. collect() has no config option. if (collecting && reply.result) decodeScenario(reply.result, this.config); this.snapshot = { result: reply.result, round: reply.gameRoundInfo, balance: reply.balance ?? this.snapshot.balance, tokenData: reply.tokenData, freePlays: this.snapshot.freePlays, // A collect reply describes the requested amount, not guaranteed remaining funds. amountToCollect: collecting ? (reply.gameRoundInfo?.status === 'closed' ? 0 : undefined) : reply.amountToCollect, }; // IPlaceBetReply does not declare updated freeplays. Reload at the campaign boundary. } async restore(loaded?: ILoadConfigReply): Promise { if (this.busy) throw new Error('Wait for the current request'); this.busy = true; this.uncertain = true; try { if (!loaded) { const reply = await this.api.loadConfig(this.session); if (!reply.success) throw new SdkRequestError(reply.error); loaded = reply.result; } this.config = loaded.config; this.snapshot = { result: loaded.gameResult, round: loaded.gameRoundInfo, balance: loaded.balance, tokenData: loaded.tokenData, freePlays: loaded.freePlaysAvailable, amountToCollect: loaded.amountToCollect, }; await this.view.render(this.snapshot, true); this.uncertain = false; } finally { this.busy = false; } } private async bet(options: Omit): Promise { // Set the lock before the first await. A double click cannot start two requests. this.busy = true; const started = this.clock.now(); try { const reply = await this.api.placeBet({ ...options, ...this.session, config: this.config }); if (!reply.success) throw new SdkRequestError(reply.error); this.accept(reply.result); await this.view.render(this.snapshot, false); // Check elapsed time again after each wait. A timer is not a clock. while (this.clock.now() - started < this.policy.minSpinTimeMs) { await this.clock.sleep(this.policy.minSpinTimeMs - (this.clock.now() - started)); } if (this.settings.reportAnimationEnd === true) { const report = await this.api.reportAnimationEnd(this.session); if (!report.success) throw new SdkRequestError(report.error); } } catch (error) { // Never repeat a money request just because its response was lost. this.uncertain = true; throw error; } finally { this.busy = false; } } async start(stake: number, featureId?: string): Promise { this.assertPhase('ready'); if (!this.policy.stakes.includes(stake)) throw new Error('Stake is unavailable'); const feature = featureId === undefined ? undefined : this.config.buyFeatures?.find(f => f.id === featureId); if (featureId !== undefined && (!feature || !this.policy.buyEnabled)) throw new Error('Purchase is unavailable'); // The engine debits Math.ceil(targetPrice * stake), so ceil here too. const cost = Math.ceil(stake * (feature?.price ?? 1)); if (!Number.isSafeInteger(cost) || cost <= 0) throw new Error('Invalid debit'); if (feature && this.policy.maxPurchaseAmount !== undefined && cost > this.policy.maxPurchaseAmount) { throw new Error('Purchase exceeds its limit'); } if (!this.snapshot.balance) throw new Error('Refresh the balance before a paid bet'); if (cost > this.snapshot.balance.totalBalance) throw new Error('Insufficient balance'); await this.bet({ stake, ...(featureId === undefined ? {} : { featureToBuy: featureId }) }); } async ticket(ticket: IFreePlayInfo): Promise { this.assertPhase('ready'); const current = this.snapshot.freePlays?.find(t => t.currency === ticket.currency && t.stake === ticket.stake && t.feature === ticket.feature && t.count > 0); if (!current || !this.policy.stakes.includes(current.stake)) throw new Error('Ticket is unavailable'); if (this.snapshot.balance && current.currency !== this.snapshot.balance.currency) throw new Error('Ticket currency differs'); await this.bet({ stake: current.stake, useTicket: true, ...(current.feature ? { useTicketFeatureType: current.feature } : {}) }); // Ticket counts are not guaranteed on placeBet replies. Refresh before another ticket. this.snapshot.freePlays = undefined; } async choose(index: number): Promise { this.assertPhase('choice', 'collect-or-continue'); const choices = this.snapshot.result?.engineData.playerChoice ?? []; const option = choices[index]; if (!Number.isInteger(index) || !option) throw new Error('Choice is unavailable'); if (this.isWager(option) && !this.policy.gambleEnabled) throw new Error('Gamble is disabled'); await this.bet({ playerChoiceIndex: index }); } async continue(): Promise { this.assertPhase('continuation', 'collect-or-continue'); const e = this.snapshot.result!.engineData; if (e.playerChoice?.length) throw new Error('A player choice is required'); if (!e.inProgress) throw new Error('No continuation exists'); // In slots, a collectable continuation risks the existing win. if (e.canCollect && !this.policy.gambleEnabled) throw new Error('Gamble is disabled'); await this.bet({}); } async cashOut(amount?: number): Promise { this.assertPhase('collect-or-continue'); if (amount !== undefined) { const outstanding = this.snapshot.amountToCollect; if (!this.policy.partialCollectEnabled || this.snapshot.result?.engineData.inProgress !== true || !Number.isSafeInteger(amount) || amount <= 0 || outstanding === undefined || amount >= outstanding) throw new Error('Invalid bank amount'); const base = roundBaseStake(this.snapshot); const result = this.snapshot.result!; const banked = result.engineData.bankedWin ?? 0; const requested = Math.floor(amount / base * 100) / 100; const nextBanked = Math.floor((banked + requested) * 100) / 100; // These requests take a different engine path. Use explicit full collect instead. if (nextBanked <= banked || nextBanked >= result.totalWin) throw new Error('Bank amount must protect only part of the at-risk win'); } this.busy = true; try { const reply = await this.api.collect({ ...this.session, ...(amount === undefined ? {} : { amount }) }); if (!reply.success) throw new SdkRequestError(reply.error); this.accept(reply.result, true); // Do not add bankedWin to balance or subtract it from the outstanding amount. await this.view.render(this.snapshot, false); // Older replies omit result/round state. Reload instead of assuming a close. if (!reply.result.result || reply.result.gameRoundInfo?.status !== 'closed') this.uncertain = true; } catch (error) { this.uncertain = true; throw error; } finally { this.busy = false; } } } ``` ### Connect it to your frontend `loadSession()` is the loader from [getting started](https://docs.hizi.io/engine/sdk/getting-started.html#connect-once). ```typescript const { connection, session, loaded: reply } = await loadSession(location.search); const game = new SlotSession( session, reply.config, connection.gameSettings ?? {}, approvedPolicy, { render: (snapshot, resumed) => renderer.present(snapshot, { resumed }) }, ); await game.restore(reply); // Complete Operator Interface setup, loading, and required acknowledgements. // Then route every control through game.start, game.choose, or game.cashOut. ``` `approvedPolicy` contains validated stakes, timing, permissions, and purchase limits. `renderer` is your game-specific view. They are application components, not SDK exports. Use `game.phase` to enable buttons. Catch every rejected action at the UI boundary. Show a suitable error without resending its request. A plain `continuation` can advance after its presentation finishes. Require player input for `choice` and `collect-or-continue`. An empty option array is not a choice prompt. After a partial collect, call `restore()` to reload the outstanding amount before further decisions. Banking does not change the wallet balance. A freeplay start calls `ticket`. Reload ticket availability before the next ticket. The response type does not guarantee updated ticket counts on every bet. The example intentionally leaves a failed request in `recovery`. Call `restore()` after resolving connection or operator errors. A new session token requires `replaceSession()` first. ### Boundaries of the example The controller does not select a legal jurisdiction, provide translated rules, draw reels, or implement a casino wrapper. The linked guides specify those responsibilities. Test the combined frontend, not only this class. The controller applies minimum timing to each bet step. It does not implement an approved dynamic timing scheme. It also does not infer a legacy bought round's missing base stake. ## WebSocket support HTTP works for ordinary slot requests. To use the SDK request transport: ```typescript import { enableWebSockets } from '@hizi.io/engine-sdk'; const socket = await enableWebSockets(connection.webSocketURL!); // The SDK now sends loadConfig/placeBet/collect through this transport. // On teardown or session replacement: socket.close(); ``` Validate the optional URL before this call. Handle setup rejection. The helper uses one module-global transport; do not share it between independent player sessions. A dropped connection causes in-flight requests to fail. The next SDK call attempts to reconnect. Only `close()` explicitly returns subsequent requests to HTTP. Reconnection does not authorize a repeat bet. The returned handler exposes only `close()` and `isConnected()`. It does not expose the native socket or a broadcast callback. SDK 0.2.18 ignores messages without a matching request ID. Crash therefore needs the [documented broadcast transport](https://docs.hizi.io/engine/sdk/crash-transport.html). ## Other game types Use each vertical's [game guide](https://docs.hizi.io/engine/games/). Blackjack requires legal actions and can add stakes. Mines requires picks. Crash continues on server time. Do not apply the slot's automatic continuation behavior to those games. --- # Rules and slot presentation The frontend explains the game and displays server outcomes. It must not calculate a replacement outcome or infer unpublished odds. ## Rules supplied to the player Make the rules accessible before play and during an idle or decision state. Show these items for the actual deployed configuration: - Game objective, controls, and the meaning of each symbol. - Stake unit, available stakes, and total cost of an action. - Pay direction, paylines, ways, clusters, scatter pays, and wild behavior. - Conditions for each bonus, retrigger, random entry, and player choice. - Freeplay terms, fixed ticket stake, and distinctions from awarded feature spins. - Purchase price, feature RTP, and maximum possible win where applicable. - Gamble rules, odds, collection, protected money, and end conditions. - Return-to-player (RTP), maximum win, payout caps, and required win odds. - What happens after disconnection, reload, inactivity, or an unfinished round. - Operator history, exit, and responsible-gambling controls. Use the certified rules and deployed configuration as the source. A slot-shaped graphic does not define a legal product classification. UK rules must explain the game and likelihood of winning before commitment. [UKGC RTS 3](https://www.gamblingcommission.gov.uk/standards/remote-gambling-and-software-technical-standards/rts-3-rules-game-descriptions-and-the-likelihood-of-winning). ## Slot configuration from the creator The creator's browser exporter supplies this presentation structure through `loadConfig`: | Field | Shape and use | | --- | --- | | `grid` | `{ base: { rows, columns }, [feature]: { rows, columns } }`. Select dimensions for the current feature. | | `symbols` | `{ base: [...], [feature]: [...] }`. Entries contain names and appearance information such as reels, spacing, and stacks. | | `winlines` | `{ base: number[][], [feature]: number[][] }`. Each line contains a row index for each reel. | | `awards` | `{ base: [...], [feature]: [...] }`. Each award contains a `trigger` and `results`. | | `features` | Names of configured slot features. These are not all purchasable. | | `featureConfigs` | Feature-specific settings, including wheel and Hold and Win configuration. Common fields listed above are separated out. | | `symbolMap`, `scenarioSchema` | Scenario compression metadata. Pass the complete config to `placeBet`. | | `buyFeatures` | Runtime purchase options. Use the returned `id`, price, initial spins, and RTP. | For each map, read the current feature key when present. Otherwise use `base`. Resolve `win.winlineIndex` against that effective feature's winlines. An award trigger contains symbol names, count, mode, and optional maximum count, adjacency, wild-scatter, or override behavior. Results can be one result or weighted alternatives. Do not present a variable award as a guaranteed fixed payment. The CLI slot export can contain only `symbolMap` in its presentation configuration. Imported games can have custom data. Validate required presentation fields at load. Supply a versioned rules and asset manifest when the archive lacks them. Symbol names are not texture URLs. Maintain an explicit name-to-asset map. Include assets for every feature and transformed symbol. Do not replace an unknown winning symbol with another symbol. ## RTP and maximum wins Show `config.rtp` as a percentage. Use `showExactRTP` for supplied precision. Use `buyFeatures[].featureRTP` for each purchase. Do not substitute the base RTP for every purchase. Display `maxWin` and `maxPayout` in the units required by the rules. Convert with the base stake for currency displays. Apply `calculatedMaxExposure` or the configured exposure cap where appropriate. Some configurations omit these figures. Absence does not mean zero. Blackjack, Crash, and Bingo do not have the same entry-derived metrics as slots. Current `maxPayoutOdds` duplicates `maxWinOdds`. It does not independently describe an accumulated maximum payout. `maxWinOdds` can also be absent when the top prize exists only in a buy pool. Obtain approved odds before enabling a market that requires a missing or unreliable figure. Do not fabricate it. ## Render a slot result Use the SDK `IResult` type for the slot scenario. Validate unknown data before rendering. Read the [full field guide](https://docs.hizi.io/engine/games/slot.html). | Mechanic | Result fields | Rendering requirement | | --- | --- | --- | | Fixed or variable reels | `board`, `reelRows` | Read `board[column][row]`. Allow different row counts per reel. | | Lines, ways, scatter, cluster, unconditional wins | `wins`, `positions`, `mode`, `winlineIndex` | Highlight the returned positions and effective payline. | | Overrides and multipay | `overrideWins`, `multipayCount`, `multipayResults` | Show the breakdown without adding it again to the reported win. | | Expanding wilds, substitutions, duplication | `boardStates`, `symbolSubstitutions`, `duplicatingSymbolData` | Animate recorded transformations. Finish on the returned board. | | Cascades or tumbles | `cascadeData.steps`, `inputBoard`, `cascadeMultiplier` | Animate every supplied step inside this response. Do not request a bet for each array entry. | | Sticky and changing sticky symbols | `stuckSymbols`, `boardStates` | Replace state from the latest result. Do not retain symbols after the feature resets. | | Book expansion | `bookSpinChosenSymbol`, `bookSpinWin` | Show the chosen symbol and the recorded expansion. | | Static, random, and event multipliers | `featureMultiplier`, `eventMultiplier`, `spinMultiplierTotal` | Display applied values. They already affect the outcome. | | Multiplier collection | `multiplierCollectionData`, `collectedSpinMultiplierData` | Distinguish accumulated and applied values. | | Collectors and levels | `collectorTargetData`, `wins[].collectedSymbols`, `collectorLevelData` | Display assigned values and returned level progress. | | Hold and Win | `holdAndWinData` | Restore coin values, symbols, lives, locks, pending modifiers, boosters, and collector sweeps. | | Wheel | `wheelWins[].selectedSegment`, `featureAward` | Land on the returned segment. Process awarded feature state. | | Random feature entry | `randomFeatureEntry` | Show the transition described by the result. | | Freespins and respins | `spinInfo`, `featureName`, `nextFeature`, `featureRoundId` | Support both scenario and engine carriers. Remaining spins equal `count - used`. | | Player choices | `engineData.playerChoice` | Render cash and feature options. Preserve original indexes. | | Persistent progression | `engineData.progressionCounters`, `progressionEvents` | Replace values. Animate deltas once. Respect `stakeSpecific`. | | Within-round jackpots | `singleGameRoundProgressionData` | Use the scenario state. Reset at the next round, not the next response. | | Card and ladder gambles | `scenario.suit`, `scenario.rank`, `engineData.ladderState` | Switch to the gamble renderer. A card result has no slot board. | ### Avoid double counting The server's `totalWin` determines the outcome. Slot `winAmount` and component wins explain its presentation. They do not authorize additional credits. Consolidated rounds deliver one stored scenario step per `placeBet`. Intermediate steps can return `totalWin: 0`. Show their local animation amounts without treating zero as a loss or final settlement. A cascade's nested steps differ from separate scenario continuations. Render the nested array locally. Then inspect `engineData` for the next permitted request. ### Feature state Prefer scenario `spinInfo`, `featureName`, and `nextFeature` when present. Otherwise use the engine equivalents. The [unified accessor](https://docs.hizi.io/engine/sdk/response-handling.html#where-this-data-lives-enginedata-vs-scenario) covers both forms. `count` is the total awarded. `used` is the amount consumed. Retriggers increase `count`. Re-entry into the same feature can change `featureRoundId`. Do not reset every counter because one feature starts again. A full meter does not promise a pending win unless the result specifies one. Do not add progression awards to `totalWin` a second time. ## Complete the presentation Await board animation, local cascade steps, mandatory messages, feature transitions, and the required result display. Keep controls locked during these steps. A low return still needs a readable result. Suppress celebratory effects when required; do not suppress the amount or imply that the stake returned in full. Use `performanceMode` to simplify effects on weak devices. Preserve board order, selected outcomes, amounts, and timing. A reduced-animation mode must remain a complete explanation of the result. --- # Operator Interface Use the existing [Operator Interface documentation](https://docs.hizi.io/operator-interface/) to connect your frontend to the operator. - [Installation](https://docs.hizi.io/operator-interface/guide/getting-started.html) - [Setup and usage](https://docs.hizi.io/operator-interface/guide/usage.html) - [Incoming events](https://docs.hizi.io/operator-interface/api/events.html) - [Methods](https://docs.hizi.io/operator-interface/api/methods.html), including loading, round notifications, errors, and reality checks - [Operator-specific guides](https://docs.hizi.io/operator-interface/operators/) --- # Jurisdictions and responsible gambling A jurisdiction defines the rules that apply to a product and player. The frontend must apply the resolved session settings. A jurisdiction preset is a platform configuration. It is not a game certificate. Supported deployment markets can also lack a preset. Confirm the game's approval, operator integration, and applicable product classification before release. ## Platform preset inventory This table records all 13 named Hizi presets on 15 September 2026. It describes code defaults, not a complete statement of local law. Amounts below are authoring values in 1/100 of the base currency. The RGS converts them before returning session settings. Do not copy these amounts into a frontend currency table. | Preset code | Settings pinned by the preset | | --- | --- | | `jurisdiction-agcc` | `maxStake=5000`; `minSpinTime=3000`; reality checks on; stop, turbo, autoplay, and multiple instances off; autoplay loss limit required. | | `jurisdiction-at` | `minSpinTime=2000`; autoplay off; `maxPackageStake=5000`. | | `jurisdiction-br` | Gamble off; show `maxWin` odds; hide demo balance; translate errors; do not store autoplay; show total autoplay stake; inactivity timeout 1800 seconds; show paytable on entry. | | `jurisdiction-bg` | `minSpinTime=3000`; stop off. | | `jurisdiction-de` | `maxStake=100`; `minSpinTime=5000`; show RTP, jackpot odds, and both win odds; XRTP and coins off; stop, turbo, autoplay, package buy, and multiple instances off; dynamic timing on. | | `jurisdiction-gr` | `maxStake=2000`; `maxExposure=14000000`; `minSpinTime=2000`; stop off; `maxPackageStake=2000`. | | `jurisdiction-lt` | `minSpinTime=1000`; stop, gamble, and autoplay off; package buy on. | | `jurisdiction-mga` | Reality checks and win/loss display on; autoplay loss limit required. | | `jurisdiction-nl` | Turbo, autoplay, and package buy off; `defaultStakeIndex=0`; force default stake. | | `jurisdiction-caon` | `minSpinTime=2500`; stop, turbo, and autoplay off; show both win odds and net position; inactivity timeout 1200 seconds. | | `jurisdiction-pt` | `minSpinTime=3000`; package buy off. | | `jurisdiction-se` | `minSpinTime=3000`; stop, turbo, and autoplay off. | | `jurisdiction-ukgc` | `maxStake=500`; `minSpinTime=2500`; reality checks on; stop, turbo, autoplay, package buy, and multiple instances off; show net position and session timer; require autoplay loss limit; suppress celebrations for returns no greater than stake. | `jurisdiction-not-set` has no pinned settings. It does not select a permissive jurisdiction. Unlisted fields still require resolved values from the operator profile. South Africa is an example of a deployment market without a dedicated Hizi preset. Use the approved operator configuration and certificate for such markets. Do not substitute `jurisdiction-not-set` as evidence of compliance. ## UKGC requirements and preset gaps UKGC means the United Kingdom Gambling Commission. Its online-slot cap is £5 for players aged 25 or older. The cap is £2 for ages 18–24. It applies to total stakes within a game cycle. The preset's `500` cannot identify a player's age. Require the correct player-specific cap from the operator. Do not derive age from locale or a client preference. [UKGC stake guidance](https://www.gamblingcommission.gov.uk/licensees-and-businesses/guide/online-slots-stake-limit-guidance). For slots, enforce at least 2.5 seconds between cycle starts. Require a new press after release. Prevent held-button repeat and simultaneous games. Disable turbo and early reveal controls. Do not celebrate a return that is no greater than total stakes. For casino games outside slots and peer-to-peer poker, the minimum cycle interval is 5 seconds. The shared 2500 ms preset does not implement that distinction. [UKGC RTS 14](https://www.gamblingcommission.gov.uk/standards/remote-gambling-and-software-technical-standards/rts-14-responsible-product-design). Casino games require an individual commitment to each cycle. Disable autoplay through every input route. [UKGC RTS 8](https://www.gamblingcommission.gov.uk/standards/remote-gambling-and-software-technical-standards/rts-8-autoplay-functionality). Display elapsed session time for casino play outside peer-to-peer poker. Keep reality checks available through the agreed operator integration. [UKGC RTS 13](https://www.gamblingcommission.gov.uk/standards/remote-gambling-and-software-technical-standards/rts-13-time-requirements-and-reality-checks). Display the session's net position in account currency. Preserve its accounting across session refresh. [UKGC RTS 2](https://www.gamblingcommission.gov.uk/standards/remote-gambling-and-software-technical-standards/rts-2-displaying-transactions). Do not implement a universal rule that every bonus choice is a gamble. An awarded free feature and an elective wager need different controls. Classify the supplied options before applying restrictions. ## Timing, turbo, and stop Start a monotonic timer when accepting a new game cycle. Include network time and animation time in that interval. Enable the next cycle only after all these conditions hold: - The minimum interval has elapsed. - The required result presentation has finished. - The server state permits a new cycle. - Required animation reports have succeeded. - No operator pause, reminder, error, or session block remains. Recheck the clock when a timer wakes. Background tabs throttle timers. A timer callback alone does not prove that input can resume. Use the stricter reviewed interval where product rules exceed the returned preset. The slot example applies the interval to each presented bet step. This conservative policy can make a free feature slower. `dynamicMinSpinTime` does not specify a complete algorithm. A fixed minimum is the safe default until an approved dynamic policy exists. Do not use a fast earlier spin to justify an unapproved short later spin. Stop and turbo affect presentation only. They cannot change a result, cancel an accepted debit, or start a new cycle early. Stop-autoplay must always prevent another automatic paid start, even when early reel stopping is disabled. ## Germany: additional integration requirements The German regulator describes manual starts, a five-second interval, and disclosure of return and maximum-win probability. It also describes a five-minute break after acknowledgement at 60 minutes of continuous play. [GGL player-protection requirements](https://www.gluecksspiel-behoerde.de/de/fuer-spielende/die-spielerschutzmassnahmen-des-gluestv21-im-ueberblick/konkrete-spielerschutzmassnahmen-fuer-verschiedene-gluecksspielarten). The Hizi preset does not define the complete break state or its persistence. Agree which component owns the reminder and break. Preserve that state through reload. Keep new play disabled until the authoritative break ends. Do not infer permission for a product from the preset's name. Confirm approved text, game classification, stake exceptions, and the applicable certificate with the operator. ## Messages and session displays The SDK provides no universal `ukgcMessages` bundle. Supply approved localized text for these states: | State | Required frontend behavior | | --- | --- | | Loading notice | Display `loadMsg` for the configured minimum. | | Rules on entry | Display current rules before accepting play. | | Reality check | Display elapsed time. Include confirmed win/loss when required. Offer Continue, Exit, and History as applicable. | | Required break | State that play is paused. Show the remaining break time when available. Prevent new bets. | | Financial or responsible-gambling limit | Show the supplied restriction and permitted actions. Do not offer a retry that bypasses it. | | Session expired or inactive | Stop play. Offer the approved exit or reconnect flow. | | Interrupted round | Explain that the frontend restores the existing round. | | Insufficient balance | Show the error. Offer the operator's cashier action when available. | | Result no greater than stake | Display the returned amount without a win celebration. | Use the player's language. Keep required text readable in both orientations. Preserve keyboard focus in dialogs. A close icon or Escape key must not bypass required acknowledgement. ## Reality checks `rcInterval` is in milliseconds. The Operator API spec states this, and the SDK's `IGameSettings.rcInterval` comment incorrectly says seconds. No spec states the unit of `gameVariables.rcElapsed`. It is compared against `rcInterval`, so treat it as milliseconds, and confirm the unit with hizi.io support before a certified release. ```text elapsedTowardsReminder = initialRcElapsed + elapsedSinceConnection reminderDue = elapsedTowardsReminder >= rcInterval ``` After acknowledgement, reset the reminder interval according to the operator contract. Do not reset the overall session timer. An expired reminder blocks the next paid action. A long feature must not make the reminder disappear. Coordinate client timers with `pauseRequested`, `continueRequested`, and server error `REALITYCHECK`. Use `OperatorInterface.realityCheck` only for the agreed owner. It does not create a generic in-game dialog. Some operators require `OperatorInterface.sendRealityCheckResponse(backendURL, token, continued)`. That method belongs to `@hoelle/operator-interface`, not to the engine SDK. Check that response before continuing. Do not resend the rejected bet. See [Operator Interface](https://docs.hizi.io/operator-interface/api/methods.html#sendrealitycheckresponse). ## Autoplay where permitted Require a finite round count and an explicit start. Show total cost when requested. Require the configured loss limit. Provide a persistent stop control. Stop before the next paid bet after an error, limit, insufficient balance, stake change, bonus entry, player choice, or operator stop. Recheck permissions and limits for every iteration. Do not restart automatically after recovery or a reminder. Preserve original choice indexes when hiding disallowed options. Never automatically choose the first available gamble or bonus award. ## Release evidence Test the resolved settings for each supported market, currency, player tier, and vertical. Presets are useful fixtures, but they are only one input. Record the approved messages, timing, restrictions, recovery behavior, and certificate version. Use the [release checklist](https://docs.hizi.io/engine/sdk/release-checklist.html). --- # Recovery and session management A game round persists on the server. Reloading the browser does not cancel its bets or erase its choices. ## Restore before accepting a bet Call `loadConfig` after connection. The SDK decompresses `gameResult` and `previousResults` with the returned configuration. Retain these values together: | Value | Purpose | | --- | --- | | `gameResult` | Last authoritative scenario and engine state. | | `gameRoundInfo` | Round hash, status, stake, mode, and currency. | | `amountToCollect` | Outstanding amount, when supplied. | | `previousResults` | Earlier recorded results for reconstruction. Entries can lack usable engine results. | | `balance` | Current wallet display. | | `freePlaysAvailable` | Current ticket availability. | | `config.progressionCounterValues` | Persisted counters. Use the current result for counters inside an open round. | Do not infer the current game from the last frame cached by the frontend. Do not replay old requests to reconstruct it. ## Recovery decision table Apply the table in this order after restoring the visible state. The [slot controller](https://docs.hizi.io/engine/sdk/game-flow.html#slot-controller) implements these guards. | Returned state | Frontend action | | --- | --- | | Unknown or transitional round status, such as `closing` or `stranded`, or any value your integration does not recognise | Keep bets disabled. Reconcile or show a support action. | | Closed round with contradictory `inProgress` or `canCollect` | Treat the reply as inconsistent. Do not start another round. | | Open round without a usable result | Keep bets disabled. Read history or request integration support. | | `canCollect: true` | Present collection. Present only permitted current choices or continuations. | | Nonempty `playerChoice` | Present the original indexed options. Wait for player input. | | `inProgress: true`, without a decision | For slots, resume the next free continuation without a stake. | | Closed or completed state, without an outstanding amount | Finish any required presentation. Enable a new round after timing and operator gates. | | Outstanding money without an offered action | Reconcile. Do not invent a collect request from the amount alone. | For Blackjack, use `scenario.availableActions`. For Mines, wait for a tile choice. For Crash, rejoin its live round. Those verticals must not use a slot continuation loop. ## Reconstruct slot presentation Restore the board, feature name, run identifier, remaining spins, sticky symbols, Hold and Win state, counters, and gamble state. Read [slot responses](https://docs.hizi.io/engine/games/slot.html). Use `engineData.baseStake` for a bought round. Restore `engineData.buyFeatureId` for purchase attribution. Use `engineData.bankedWin` and `ladderState` when present. Use earlier results only for presentation that requires history. Filter out missing results. Do not add their `totalWin` values together. Suppress duplicate feature-award notifications, session ledger entries, and wallet credits. A result has already happened when the server returns it. Replaying its animation must not place its bet again. ## Lost replies and duplicate requests Allow one money request at a time per session. Set the lock before any `await`. Keep the lock through result presentation and required reporting. A timeout can happen after the server commits a bet. A transport request identifier does not make a bet idempotent. The SDK exposes no general idempotency key for `placeBet` or `collect`. After an uncertain result: 1. Stop autoplay and new input. 2. Keep the last confirmed state visible with a recovery message. 3. Restore the connection if needed. 4. Call `loadConfig` to discover the server state. 5. Apply the recovery table. Resolve closed outcomes through history when necessary. 6. Require a new player action before starting another paid round. Do not automatically resend the original stake, buy, gamble choice, tile pick, or collect amount. See [errors](https://docs.hizi.io/engine/sdk/error-handling.html). ## Refresh Use the returned `refreshURL` as supplied. SDK `refresh(refreshURL)` returns a new connection object, including a fresh `tokenData`. It narrows the reply exactly as `login()` does. See [what `login()` does not return](https://docs.hizi.io/engine/sdk/launch-and-loading.html#what-login-does-not-return). Wait for any in-flight money request before changing credentials. Replace the token and all returned URLs together. Reapply returned settings. Rebuild permitted stakes and action gates. Close the old socket before opening the new one. Call `loadConfig` again. Preserve the session timer, reality-check offset, and net-position accounting. Do not convert an expired request into a new paid bet. Honor `refreshDisabled` and operator navigation requirements. A reload request is not permission to create a new launch token or bypass an expired session. ## Exit and inactivity Use Operator Interface `quit()`, `history()`, or `cashier()` when the wrapper owns navigation. Otherwise use the supplied URL and `redirectTarget`. Honor `preventRedirect`. The SDK has no logout helper. If your integration requires logout, follow the supplied logout URL contract. Do not append a guessed route or revoke a session from an unload handler. `sessionTimeoutInSeconds` measures inactivity in seconds. Use real player activity, not reel ticks or network polling, to reset the activity timer. Stop new play when the interval expires. Follow the operator's session-expiry flow. Closing a page can leave a round open for resumption or server auto-close. Describe that behavior in the rules. Do not issue a speculative bet or cashout in `beforeunload`. ## Reporting animation completion SDK `reportAnimationEnd` and Operator Interface `spinAnimationStopped` are separate signals. One goes to the RGS. The other goes to the wrapper. When `gameSettings.reportAnimationEnd` is true, send the SDK report after the required presentation finishes. Check its response before enabling another action. Do not report when the response first arrives. A restored completed result does not automatically require a duplicate report. Determine outstanding reporting from the operator's resume contract. Keep a reporting failure separate from a failed wager. Never repeat the wager to repair its report. --- # Frontend release checks Use this checklist for a complete frontend. A successful SDK call or documentation build does not certify a game. ## Integration inputs Record the deployed engine version, archive version, SDK version, operator protocol, currencies, jurisdictions, and product classifications. Retain approved rules and translations. The frontend must have a defined behavior for each setting in the [reference](https://docs.hizi.io/engine/sdk/settings-reference.html). ## Shared acceptance matrix | Area | Required cases and expected behavior | | --- | --- | | Launch | Valid launch, missing parameters, expired token, refresh, and wrapper setup. Exactly one launch exchange. | | Loading | Slow assets, failed assets, localization, `loadMsg`, minimum duration, entry paytable, and restored round. No early paid input. | | Stakes | Minimum, maximum, below/above bounds, custom list, empty list, range input, invalid saved stake, and forced default. Validate all input routes. | | Currency | At least two currency multipliers, custom/social display, complete buy cost, and bought-round recovery. No double conversion. | | Requests | Double click, held key, touch repeat, wrapper spin, and autoplay. At most one money request at a time. | | Timing | Fast network, slow network, hidden tab, pause during animation, turbo, stop, and report failure. No early cycle start. | | Money | Loss, equal-stake return, win, cap, partial bank, full collect, and lost credit response. Balance matches the server. | | Recovery | Reload during base spin, cascade, feature, choice, gamble, and collect. Continue the existing round without another debit. | | Errors | Insufficient balance, processing, rate limit, session expiry, reality check, network loss, and unknown code. No automatic money retry. | | Operator Interface | Every required incoming event, notification ordering, pause acknowledgement, navigation, promotion, and error dialog. | | Audio and device | Local and operator mute/unmute, audio unlock, hidden tab, orientation, fullscreen restriction, resize, and teardown. | | Rules | Every enabled mechanic, paytable variant, feature RTP, maximum win, required odds, and missing information. | | Responsible gambling | Reminder offset, elapsed time, net position, financial limit, inactivity, required break, and reload persistence. | | Freeplays | Multiple ticket types, fixed stake, wrong currency, final ticket, feature ticket, expired availability, and reload. | | Promotions | Discount expiry, refreshed eligibility, package mapping, and correct accounting. | | Accessibility | Readable required text, keyboard focus, touch targets, scrollable rules, and non-dismissible mandatory acknowledgement. | ## Slot outcome fixtures Use approved test outcomes in a test environment. Do not wait for rare outcomes to occur naturally. Never enable outcome forcing in production. Exercise every mechanic present in the archive: - Base loss, small return, stake-equivalent return, large win, and exposure cap. - Every pay mode, override win, multipay breakdown, and feature payline override. - Expanding wilds, substitutions, duplication, sticky changes, and book expansion. - Cascades with no continuation, multiple nested steps, and changing multipliers. - Freespin trigger, retrigger, re-entry, and different feature dimensions. - Hold and Win locks, boosters, cell modifiers, collector sweeps, full grid, and exhausted lives. - Wheel result, random feature entry, collector levels, and accumulated multipliers. - Persistent counters, cash awards, feature awards, and mixed player choices. - Within-round jackpots and their reset at the next round. - Card colour/suit gambles, ladder climb/bust/top, and bank-then-lose. - Consolidated and non-consolidated scenarios, with reload at each decision boundary. Verify displayed breakdowns against final `totalWin` and wallet settlement. Presentation values must not create duplicate credits. ## Every creator mode The creator exposes 14 modes. Each needs the shared acceptance matrix plus these cases: | Mode and guide | Inputs, results, and extra checks | | --- | --- | | [Slot](https://docs.hizi.io/engine/games/slot.html) | Stakes, bonuses, scenarios, and all enabled mechanics above. | | [Progressive](https://docs.hizi.io/engine/games/progressive.html) | No initial buy; survive/bust, player cashout decision, absolute multipliers, final auto-close. | | [Plinko](https://docs.hizi.io/engine/games/plinko.html) | Returned row/risk table ID, bucket index, multiplier, and one-round settlement. | | [Hi/Lo](https://docs.hizi.io/engine/games/hilo.html) | Original higher/lower/skip choice indexes, card odds, skip behavior, collect, and bust. | | [Over/Under](https://docs.hizi.io/engine/games/overunder.html) | Direction/threshold table, boundary numbers, roll, and settlement. | | [Frogger](https://docs.hizi.io/engine/games/frogger.html) | Risk table, progression, collect-or-continue, and final step. | | [Mines](https://docs.hizi.io/engine/games/mines.html) | Every call's pick, duplicate/invalid pick, mine-count table, board reveal, and collection. | | [Keno](https://docs.hizi.io/engine/games/keno.html) | Manual/automatic numbers, distinct valid picks, pick-count/risk table, matched numbers, and paytable. | | [Match](https://docs.hizi.io/engine/games/match.html) | Ball count, exact/at-least target, arrangement, one-based matched slots, and per-bet odds. | | [Bingo](https://docs.hizi.io/engine/games/bingo.html) | Ticket count, both variants, free cells, daubs, contained patterns, and full debit. | | [Crash](https://docs.hizi.io/engine/games/crash.html) | Shared request/event socket, multiple bets, auto/manual cashout, reconnect, and final balance. | | [Roulette](https://docs.hizi.io/engine/games/roulette.html) | Every offered bet shape, total stake, zero handling, wheel rules, and each settlement. | | [Blackjack](https://docs.hizi.io/engine/games/blackjack.html) | Available actions, side bets, split/double/insurance costs, hidden dealer card, and final settlement. | | [Blank/imported](https://docs.hizi.io/engine/games/custom.html) | Versioned custom schema, unknown-type rejection, and its declared input/recovery contract. | ## Jurisdiction evidence Test every named preset that the release supports. Include player-specific overrides and markets without presets. Read the [jurisdiction inventory](https://docs.hizi.io/engine/sdk/jurisdictions.html#platform-preset-inventory). For UKGC slots, test the £2 and £5 player tiers. For other casino classifications, test the applicable five-second cycle interval. Test the absence of autoplay, rapid reveal, simultaneous play, and inappropriate celebrations. For Germany, test the agreed reminder and break across refresh. For Brazil, test paytable on entry, translated errors, and demo-balance behavior. For Ontario, test the required odds, net position, and inactivity policy. Obtain the operator's acceptance of the complete integration and its jurisdiction configuration. Record the game certificate and approved messages. Do not mark a missing required odds value or session service as “passed”. ## Documentation example checks This repository runs `npm run check:sdk-examples`. It compiles the public examples against SDK 0.2.18 and runs deterministic request tests. Those checks cover guards, metadata, timing, and transport envelopes. They do not exercise a live wallet, certify rules, or replace operator-wrapper tests. --- # Frontend settings reference `login` and `refresh` return `gameSettings`. The remote gaming server (RGS) resolves profiles and overrides before sending these settings. The TypeScript interface declares many required properties. Actual replies can omit properties or contain null values. Validate the reply. Resolve missing values through the agreed integration profile. Do not treat a missing permission as permission to enable a feature. Apply settings to every input path, including keyboard shortcuts and Operator Interface events. Reapply settings after refresh. A local preference cannot enable an operator-disabled function. ## Bets and gameplay | Setting | Frontend requirement | | --- | --- | | `minStake`, `maxStake` | Validate the selected base stake. Use effective reply amounts. See [stake precedence](https://docs.hizi.io/engine/sdk/stakes-and-money.html#select-legal-stakes). | | `customStakes` | Replace the default ladder when nonempty. Apply independent legal and player limits. | | `defaultStakeIndex` | Select a valid index in the final displayed ladder. | | `forceDefaultStake` | Ignore the saved stake at a new session. Preserve the stake of an open round. | | `maxExposure` | Display the configured cap where required. Honor capped results and early round closure. | | `packageBuyEnabled` | Gate slot bonus purchases. Other verticals need an agreed mapping for table selection. | | `maxPackageStake` | Check purchase limits. Current handlers compare the base stake; the frontend must also check the complete cost. | | `gambleEnabled` | Gate elective slot gambles. Keep collection available when the engine offers it. Do not hide mandatory bonus choices. | | `partialCollectEnabled` | Gate banking part of a win. Explain that banking can leave the money inside the round. | | `autoplayEnabled` | Gate the autoplay menu and every automatic paid-start path. | | `autoplayLossLimitRequired` | Require a valid loss limit before autoplay starts. Stop when the limit is reached. | | `autoplayShowTotalStake` | Show stake × selected round count in the autoplay confirmation. | | `turboEnabled` | Gate faster presentation. Disabled turbo also applies to wrapper requests and stored preferences. | | `stopEnabled` | Gate early stopping of presentation. Never change the outcome or bypass timing. | | `minSpinTime` | Enforce milliseconds between accepted cycle starts. See [timing](https://docs.hizi.io/engine/sdk/jurisdictions.html#timing-turbo-and-stop). | | `dynamicMinSpinTime` | Select the approved timing policy. Do not infer an averaging algorithm from this boolean. | | `multipleInstancesAllowed` | Prevent your frontend from offering simultaneous games when false. Coordinate session enforcement with the operator. | | `reportAnimationEnd` | Report completed presentation to the RGS. Await success before another action. | ## Loading, rules, and amounts | Setting | Frontend requirement | | --- | --- | | `loadMsg` | Display the supplied loading message as text. | | `minLoadTime` | Minimum visible loading interval in milliseconds. Start it when the required message appears. | | `displayPaytableOnEnterGame` | Show the rules or paytable before play. | | `displayRTP` | Show the configured return-to-player (RTP) percentage in the rules. | | `showExactRTP` | Preserve supplied precision. Do not invent missing decimal places. | | `displayXRTP` | Show experienced RTP only for games with an approved definition and source. It is not theoretical `config.rtp`. | | `displayWinOdds` | Display requested `maxWin` and/or `maxPayout` odds. Verify availability and correctness before release. | | `displayJackpotOdds` | Display approved jackpot odds. Do not substitute a progression meter for probability. | | `skipWinsEqualToOrLessThanStake` | Suppress win celebrations for a return no greater than the total amount staked. Still display the result. | | `displayCoins` | Legacy coin display. It must not replace a required currency amount. | | `displayCurrency` | Control the configured currency display. A legal requirement takes precedence over a cosmetic setting. | | `currencyToDisplay` | Use the agreed display label. Do not change amounts or currency sent to the server. | | `abbreviateAmounts` | Abbreviate only where the integration permits it, normally social mode. Keep exact amounts accessible. | | `isSocial` | Apply social presentation. Do not infer it from a zero balance or demo tickets. | | `hideDemoBalance` | Hide the demo balance when requested. Keep stake and result controls understandable. | | `hideCompanyLogo` | Hide the provider logo where configured. Preserve required attribution and regulatory information. | | `translateErrors` | Map recognized error codes to approved local text. Preserve operator-supplied messages and actions. | ## Sessions and responsible gambling | Setting | Frontend requirement | | --- | --- | | `jurisdiction` | Select the tested jurisdiction configuration. This is not a country code or proof of certification. | | `rcEnabled` | Enable the agreed reality-check flow. Do not start a second reminder system when the operator owns it. | | `rcInterval` | Reality-check interval in **milliseconds**. The SDK's seconds comment is incorrect. | | `rcDisplayWinLoss` | Include confirmed session win/loss values in the reminder. | | `displayClock` | Display the current clock. This differs from elapsed session time. | | `displaySessionTimer` | Display elapsed session time. The SDK type also permits a string. The platform sends only a boolean. Treat a string as an operator-specific extension. | | `displayNetPosition` | Display session winnings minus stakes. Exclude deposits and withdrawals. | | `sessionTimeoutInSeconds` | Inactivity limit in **seconds**. A zero value disables this timeout where the profile permits it. | | `operatorHandlesErrors` | Forward errors through Operator Interface. Keep play blocked until the integration resolves them. | A true `rcEnabled` without a usable interval requires integration configuration. Do not silently omit the reminder. `login()` does not return `gameVariables.rcElapsed`, so agree the owner of the elapsed time with the operator. See [what `login()` does not return](https://docs.hizi.io/engine/sdk/launch-and-loading.html#what-login-does-not-return). ## Navigation | Setting | Frontend requirement | | --- | --- | | `homeEnabled` | Show the home control only when allowed. | | `homeURL` | Fallback destination for the lobby. Prefer the wrapper action where required. | | `historyURL` | Destination for account or game history. | | `topupURL` | Destination for deposits. Do not change balances locally after navigation. | | `lossLimitURL` | Destination for responsible-gambling limits. | | `redirectTarget` | Use `self`, `parent`, or `top` as configured. | | `preventRedirect` | Prevent automatic navigation. Use the operator's control or an explicit player action as agreed. | | `refreshDisabled` | Gate the game's reload controls and operator-requested refresh flow. | | `launcherType` | Distinguish `reelLink` and `online` launch behavior. Read the [ReelLink adapter](https://docs.hizi.io/operator-interface/operators/reellink.html). | ## Preferences and device behavior | Setting | Frontend requirement | | --- | --- | | `forceOrientation` | SDK values: `0` none, `1` landscape, `2` portrait. Hizi configuration also uses corresponding strings. Normalize the received value. | | `disableFullScreenMobile` | Prevent automatic mobile fullscreen. | | `doNotStoreSettings` | Do not persist listed preferences. The platform currently defines `autoplay`. | | `turboDefaultEnabled` | Additional platform field. Start turbo only when `turboEnabled` also permits it. | | `performanceMode` | Additional platform field: `enabled`, `landscape`, or `portrait`. Reduce costly animation in the selected mode. Preserve outcome information. | | `progressionAnimations` | Additional platform field. Gate progression animations while preserving counter values and awards. | The last three fields exist in Hizi configuration but lack explicit SDK properties. Access them through the settings index signature. Check types at runtime. There is no standard `loadingScreens` or mute setting in this interface. Build loading screens locally. Use Operator Interface audio events and player preferences for mute state. ## Apply defaults deliberately Use strict boolean checks after normalization. The string `"false"` is truthy in JavaScript. Do not use `Boolean(value)` on unvalidated settings. Resolve an absent restrictive setting from a reviewed integration profile. Keep paid play disabled if required limits or legal behavior remain unknown. Include the resulting settings object in acceptance fixtures, with credentials removed. The [release checks](https://docs.hizi.io/engine/sdk/release-checklist.html) define the cases to exercise. --- # 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; engineData: { entryIndex?: number; scenarioInfo?: IScenarioInfo; spinInfo?: Array; playerChoice?: TPlayerChoiceAward[]; currentFeature?: string; nextFeature?: string; progressionCounters?: Record; 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](https://docs.hizi.io/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](https://docs.hizi.io/engine/sdk/game-flow.html#state-and-action-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](https://docs.hizi.io/engine/core-concepts.html#multi-result-scenarios): ```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) } ``` ::: warning `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](https://docs.hizi.io/engine/games/slot.html#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)`); } ``` ::: tip 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](https://docs.hizi.io/engine/core-concepts.html#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](https://docs.hizi.io/engine/sdk/progression-counters.html) 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](https://docs.hizi.io/engine/sdk/progression-counters.html#progression-events) 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](https://docs.hizi.io/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](https://docs.hizi.io/engine/games/slot.html#cascades-tumbles). ### Free Spins Free spins use [`featureAwards`](https://docs.hizi.io/engine/core-concepts.html#feature-awards) 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 1. Check response success before accessing `result`. 2. Decode its scenario with the current configuration. 3. Render the returned boards and wins once for this result identity. 4. Finish minimum timing and any required animation report. 5. Respect operator pause, required dialogs, and session restrictions. 6. Check round status, collect availability, choices, and continuation in that order. 7. Recover any missing or contradictory state before another money request. See the checked [slot controller](https://docs.hizi.io/engine/sdk/game-flow.html). Use the vertical's action rules for other games. ## Next Steps - [buy-features](https://docs.hizi.io/engine/sdk/buy-features.html) - How to implement buy-feature support. - [Error Handling](https://docs.hizi.io/engine/sdk/error-handling.html) - Handle network and game errors. - [Types & Interfaces](https://docs.hizi.io/engine/sdk/types.html) - Full type reference for `IGameResult` and related types. --- # Error handling SDK calls return `TNetworkResponse`. Check `success` before accessing `result`. Catch exceptions from rendering, scenario decoding, and any custom transport separately. ```typescript const response = await placeBet({ backendURL, token, stake, config }); if (!response.success) { stopAutoplay(); blockPlay(); showError(response.error.code, response.error.message); // Reconcile the server state before permitting another money request. } else { await present(response.result); } ``` ## Error contract `IErrorResponse` contains a string `code`, a `message`, and optional `passThroughData`. `API_RETURNCODES` is a numeric enum. Convert its value to a string before comparison. ```typescript if (!response.success && response.error.code === String(API_RETURNCODES.REALITYCHECK)) { // Keep the next paid action blocked until the operator flow completes. } ``` `recoverableErrorCodes` already contains strings. The default network timeout is 30,000 milliseconds. Do not expose tokens, complete requests, or raw session data in logs. Retain safe diagnostic identifiers, the error code, operation, timestamp, and last known round hash. ## Controller and loader errors The controller and loader examples throw `SdkRequestError` for an SDK failure. It retains `code`, `passThroughData`, and the original error as `cause`. [Download the error helper](https://docs.hizi.io/engine/sdk/examples/sdk-error.ts). Save it beside the other example files. ```typescript import type { IErrorResponse } from '@hizi.io/engine-sdk'; // Keep SDK failures distinguishable from validation and renderer exceptions. export class SdkRequestError extends Error { readonly code: IErrorResponse['code']; readonly passThroughData: IErrorResponse['passThroughData']; constructor(error: IErrorResponse) { super(error.message, { cause: error }); this.name = 'SdkRequestError'; this.code = error.code; this.passThroughData = error.passThroughData; } } ``` ```typescript import { SdkRequestError } from './sdk-error'; try { await game.start(selectedStake); } catch (error) { stopAutoplay(); blockPlay(); if (error instanceof SdkRequestError) { handleOperatorError(error.code, error.message, error.passThroughData); } else { showLocalError(error); } } ``` The handlers are application components. Validate operator data before displaying a dialog or sending an acknowledgement. Keep play blocked until recovery completes. ## Recovery policy | Error | Action | | --- | --- | | `BALANCETOOLOW` | Stop autoplay. Refresh balance. Offer the approved cashier action. | | `BETLIMITREACHED`, `INVALIDPARAMETER` | Revalidate stakes and settings. Do not keep sending the rejected value. | | `NETWORKERROR`, malformed response, or lost connection | Treat the transaction outcome as unknown. Reconnect and load current state. | | `OPERATIONBEINGPROCESSED` | Keep actions blocked. Poll a safe state read with bounded delay. Do not submit another bet. | | `RATELIMITEXCEEDED` | Stop repeated requests. Use bounded backoff for safe reads. Reconcile any money action. | | `GENERICBETERROR`, `GENERICWINERROR`, `RNGFAILURE` | Display the failure. Determine round state before any new action. | | `SESSIONINVALID`, `NOTLOGGEDON` | Use the supplied refresh flow when allowed. Replace session data and restore the round. | | `PLAYERBLOCKED`, `COOLOFFPERIODACTIVE`, `RESPONSIBLEGAMINGLIMITREACHED`, `RESPONSIBLESGAMINGSESSIONEXPIRED`, or `REALITYCHECK` | Follow the operator's dialog and acknowledgement contract. Do not automatically retry. | | Unknown code or inconsistent state | Stop new play. Preserve the last confirmed state and offer support or exit. | The exported `recoverableErrorCodes` list classifies errors. It does **not** guarantee that retrying a money request is safe. A bet can commit before the connection fails. Neither `placeBet` nor `collect` exposes a general client idempotency key. Never turn a Retry button into an unconditional repeat of the last request. Use [recovery](https://docs.hizi.io/engine/sdk/recovery-and-session.html) to discover whether the round is open, complete, or awaiting a choice. A new paid bet requires a new player action. ## Operator errors and messages Honor `operatorHandlesErrors` and `translateErrors`. Forward required errors through [Operator Interface](https://docs.hizi.io/operator-interface/api/methods.html#error). Preserve `passThroughData` for the operator adapter. It can include a dialog, buttons, or an acknowledgement requirement. Do not reduce every error to a generic dismiss button. Render message text safely. Validate button URLs and allowed actions. Do not evaluate response content as code or insert arbitrary HTML. A dismissed dialog does not clear an uncertain transaction. Keep separate blocks for operator pause, session expiry, recovery, and responsible-gambling restrictions. ## Recovery UX Show the last confirmed board with a clear pending or reconnecting state. Avoid displaying an assumed loss, refund, or win while the result is unknown. Use bounded retries for safe reads. Offer support after repeated failure. Do not trap the player in an automatic login or reload loop. Cancel timers and close sockets on teardown. Prevent late replies from changing a replacement session's view. --- # Buy-Features A buy-feature lets a player pay a fixed price for direct entry into a bonus feature (for example, free spins). The player does not need to trigger the feature in the base game. ## How It Works The published game defines each available option. `loadConfig.config.buyFeatures` supplies its runtime price, return to player (RTP), and initial spin count. The current engine exposes the configured target price and effective feature RTP. Use these returned values. Do not reconstruct the price from base-game RTP or a remembered generator formula. Rebuild the purchase display when the session configuration changes. Some verticals use `featureToBuy` to select a normal bet table with price `1`. See that vertical's guide before treating every table as a bonus purchase. ## Getting Available Buy-Features The `loadConfig` response includes the buy-features: ```typescript const response = await loadConfig({ backendURL, token: sessionToken }); if (response.success) { const buyFeatures = response.result.config.buyFeatures; if (buyFeatures && buyFeatures.length > 0) { for (const feature of buyFeatures) { console.log(`Feature: ${feature.id}`); console.log(` Price: ${feature.price}x stake`); console.log(` Initial spins: ${feature.initialSpins}`); } } } ``` ## Buy-Feature Option Type Each entry in `config.buyFeatures` is `IBuyFeatureOption & { id: string }`: ```typescript // Base type interface IBuyFeatureOption { feature: string; // Name of the table whose entries the engine uses price: number; // Buy price as multiplier of stake featureRTP: number; // Feature RTP percentage initialSpins: number; // Spins awarded on initial trigger } // In config.buyFeatures, each entry also has an `id` field: // (IBuyFeatureOption & { id: string })[] ``` | Field | Type | Description | | -------------- | -------- | -------------------------------------------------------------------------------------------------- | | `id` | `string` | Feature identifier | | `feature` | `string` | Name of the table whose entries the engine uses in the engine configuration | | `price` | `number` | Buy price as a multiplier of stake. A price of `100` means the feature costs 100x the stake amount | | `featureRTP` | `number` | The expected return percentage when buying this feature | | `initialSpins` | `number` | Number of spins the engine awards on the initial trigger (before retriggers) | ## Purchasing a Feature Resolve [legal stakes and the full purchase cost](https://docs.hizi.io/engine/sdk/stakes-and-money.html) first. Require `packageBuyEnabled` under the approved policy for paid slot bonus purchases. Check `maxPackageStake` against the total debit. Confirm the cost with the player before sending the request. The current engine applies some purchase limits to the base stake only. Its acceptance does not replace your check of the complete debit. To buy a feature, call `placeBet` with `featureToBuy`. The engine computes the price from the `stake` and the configured `price` multiplier. The client only needs to pass the feature ID: ```typescript const feature = buyFeatures[0]; // e.g. { id: 'freespin', price: 100 } const currentStake = legalStakes[selectedStakeIndex]; // validated policy ladder const response = await placeBet({ backendURL, token: sessionToken, stake: currentStake, featureToBuy: feature.id, config, }); if (response.success) { const gameResult = response.result.result; // Render, finish timing, then inspect choices and collection before continuation. } ``` ::: tip `feature.price` is a multiplier of stake. Use it for UI display (`feature.price * currentStake`) to show the total cost. You do not need to send it back to the engine. ::: ::: tip `response.result.result.engineData.buyFeatureId` echoes back `feature.id` on this reply. It also echoes on every later reply for the round, including continuations and `collect`. See [Endpoints](https://docs.hizi.io/engine/sdk/endpoints.html#placebet) for the full field reference. ::: ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html) - Handle the multi-step round after purchasing. - [Gamble Features](https://docs.hizi.io/engine/sdk/gamble-features.html) - Other post-win features. - [Freeplays](https://docs.hizi.io/engine/sdk/free-plays.html) - Spending an operator-granted ticket instead of buying a feature. - [Endpoints](https://docs.hizi.io/engine/sdk/endpoints.html) - `placeBet` parameter reference. ## Resume and finish a purchase Read the bought ID from `response.result.result.engineData.buyFeatureId`. Read the original selected stake from `engineData.baseStake`. The RGS can store the full purchase debit in both `gameRoundInfo.stake` and `gameRoundInfo.baseStake`. Do not multiply payouts by that purchase debit. If a legacy bought round lacks the engine base stake, resolve its history before further play. Continue the existing round without repeating `featureToBuy` or `stake`. Render all awarded spins and retriggers. Respect choices, collect availability, exposure caps, and final round status. Apply the [recovery procedure](https://docs.hizi.io/engine/sdk/recovery-and-session.html) after an uncertain purchase response. Never resend the purchase automatically. --- # Gamble and collection A gamble risks an existing win. Collection takes the available win when the engine permits it. Both actions belong to the current round. ## Available actions Read `engineData.canCollect`, `playerChoice`, `inProgress`, and the configuration's wager lists. A positive `totalWin` alone does not permit collection or gambling. Apply `gameSettings.gambleEnabled` to elective slot gambles. If gambling is disabled, keep collection available. Do not automatically select a gamble to finish the round. A mandatory bonus choice can also use `playerChoice`. Classify feature options against `wagerFeatures` and `wagerStakeFeatures`. Preserve cash awards and other mandatory choices. Keep each option's original array index when hiding disallowed actions. Submit that original `playerChoiceIndex`, without a new stake or `featureToBuy`. ## Two wager models | Model | Meaning | | --- | --- | | `wagerFeatures` | The selected outcome multiplies the currently at-risk win. | | `wagerStakeFeatures` | The selected outcome specifies an absolute cashout multiplier of the original base stake. | The second model supports longer progressions such as Mines, Frogger, and Progressive. Their ordinary progression is a vertical-specific action. Do not assume the slot gamble permission disables the whole game. The engine decides eligibility using configured win limits and current state. `minWagerableWin` and `maxWagerableWin` are authoring amounts subject to currency conversion in engine checks. Use the offered actions instead of calculating eligibility from those fields alone. ## Built-in card gamble Current engines synthesize card gamble when configured. They offer `card_red`, `card_black`, `card_hearts`, `card_diamonds`, `card_clubs`, and `card_spades`. A fair 52-card draw determines the result. A correct colour pays 2× the at-risk win. A correct suit pays 4×. A miss loses the at-risk amount. Previously banked money remains protected. The returned scenario contains `suit` and `rank`. Render that card. Do not draw a local random card. Older custom wager tables can use other feature names and scenario contracts. After card entry, the engine can restrict later choices to the card family. Read the new options every time. Do not offer a cached ladder choice. ## Ladder gamble Read `config.ladderMultipliers` and `config.ladderLives`. Read current state from `engineData.ladderState`: ```typescript interface ILadderState { baseWin: number; // Win multiplier captured at ladder entry. currentRungIndex: number; lives: number; previousRungIndex?: number; } ``` The `ladder` choice advances one server step. Render the returned rung and lives. Rung payouts refer to `baseWin` and the original base stake. Do not multiply each new rung by the previous payout. Reaching the top can auto-collect. A bust can also close the round while paying protected money. Inspect the returned flags and balance. Do not call `collect` again after automatic closure. ## Full collection When `canCollect` is true, call `collect({ backendURL, token })`. Omit `amount` to collect the full available sum. Check the response. `amountCredited` is the amount credited when present. Replace the balance with the returned balance. `amountToCollect` in a collect reply is not a reliable new remainder; it can echo the requested amount. Current replies can include `result` and `gameRoundInfo`. Use them to determine whether the round stays open. A collect reply can contain a compressed scenario. After `reply.success`, call `decodeScenario(reply.result.result, config)` when that result exists. ## Bank part of a win Gate this function with `partialCollectEnabled`. Explain it as protecting part of the win inside the round, not paying it immediately. ```text base stake = 100 outstanding win = 1000 bank request = collect({ amount: 500 }) returned bankedWin = 5 immediate wallet credit = none outstanding win still includes the protected 500 ``` `engineData.bankedWin` is a multiplier. Multiply it by `engineData.baseStake` for display. The remaining at-risk amount excludes that protected portion. A later partial request adds to the protected amount. The engine rounds the protected multiplier down to two decimal places. Display the returned value, not your requested value. Reject a partial amount that rounds to a zero multiplier. The engine credits only that small amount, stamps `inProgress: false` and `canCollect: false` on the reply, and leaves the rest of the win outstanding. Your frontend then shows a settled round while the RGS round is still open. Reject an amount that would protect the entire remaining win: that takes the full-collect path. Offer explicit full collection without `amount` instead. Offer explicit full collection without `amount` instead. The slot controller checks both cases. Do not add the banked amount to balance. Do not subtract it from the amount sent for final collection. For final collection, omit `amount`. If a collect response omits updated result or round state, use `loadConfig` before another action. Never assume success means the round closed. Never send a zero collect amount. ## Required tests Exercise both wager models, every card option, ladder top and bust, banking followed by win or loss, and reload during a choice. Test a bought round's base stake and a disabled-gamble session. Reconcile a lost collect reply before permitting another request. See [game flow](https://docs.hizi.io/engine/sdk/game-flow.html), [money](https://docs.hizi.io/engine/sdk/stakes-and-money.html), and [recovery](https://docs.hizi.io/engine/sdk/recovery-and-session.html). --- # Freeplays Freeplays (free spins or tickets) are bets that an operator grants to a player. A player can spend a freeplay without a wallet debit. Each freeplay has a fixed stake, and optionally a fixed feature, set when the operator grants the package. ## How It Works Operators grant and revoke freeplay packages out-of-band, through the Operator API. The engine sends the currently available packages to your frontend as part of the normal session calls. There is no separate "claim" endpoint. Read `freePlaysAvailable` and spend a ticket with `placeBet`. ## Reading Available Freeplays `freePlaysAvailable` (an array of [`IFreePlayInfo`](https://docs.hizi.io/engine/sdk/types.html#ifreeplayinfo)) is included in: - [`login()`](https://docs.hizi.io/engine/sdk/endpoints.html#login) / [`refresh()`](https://docs.hizi.io/engine/sdk/endpoints.html#refresh): `IConnectReply.freePlaysAvailable` - [`loadConfig()`](https://docs.hizi.io/engine/sdk/endpoints.html#loadconfig): `ILoadConfigReply.freePlaysAvailable` ```typescript const response = await login({ loginURL, launchToken }); if (response.success) { const freePlays = response.result.freePlaysAvailable ?? []; } ``` ::: warning Not echoed by `placeBet` `IPlaceBetReply` does **not** carry `freePlaysAvailable`. See [Keeping the List in Sync](https://docs.hizi.io/engine/sdk/free-plays.html#keeping-the-list-in-sync) to update your local copy after you spend a ticket. ::: Each entry groups tickets that share a stake, a currency, and (optionally) a feature. A player can hold several entries at the same time: ```typescript interface IFreePlayInfo { currency: string; // ISO 4217 currency code stake: number; // Fixed stake amount for each free play count: number; // Number of free plays available feature?: string; // Optional feature to enter directly (e.g. "freespin") } ``` The RGS wire entry also carries a package `serial`, unique per operator. The SDK type omits it. Read it from the raw reply when your ticket picker must identify one package of several. ### Helpers Two helper functions summarize the array. You do not need to reduce the array yourself: ```typescript import { getFreePlaysRemaining, getFreePlayStake } from '@hizi.io/engine-sdk'; const totalRemaining = getFreePlaysRemaining(freePlays); // sum of `count` across all entries const firstStake = getFreePlayStake(freePlays); // first entry's `stake`, or undefined if none ``` ::: tip `getFreePlayStake` only reads the first entry. If a player can hold packages at more than one stake or feature, do not rely on this helper alone. Iterate `freePlaysAvailable` yourself and build a picker. ::: ## Choosing a Freeplay Sometimes more than one package is available, with different stakes, or a stake tied to a specific feature such as `"freespin"`. Present the options so the player can pick which one to spend: ```typescript const options = freePlays.map((fp, i) => ({ value: i, label: fp.feature ? `${fp.stake} · ${fp.feature} (${fp.count} left)` : `${fp.stake} (${fp.count} left)`, })); ``` ## Spending a Freeplay Call `placeBet` with `useTicket: true`, the ticket's own `stake`, and (if set) its `feature` as `useTicketFeatureType`: ```typescript const freePlay = freePlays[selectedIndex]; // e.g. { currency: 'EUR', stake: 100, count: 5, feature: 'freespin' } const response = await placeBet({ backendURL, token: sessionToken, stake: freePlay.stake, useTicket: true, useTicketFeatureType: freePlay.feature, config, }); if (response.success) { const gameResult = response.result.result; // Handle exactly like any other placeBet result - multi-step rounds, // player choices, and wager/collect flows all still apply. } ``` ::: tip Still a normal round A freeplay spin returns a normal `IGameResult`. It runs through the full [game flow](https://docs.hizi.io/engine/sdk/game-flow.html) (multi-step rounds, player choices, wager/collect). The only difference is that the engine does not debit the stake from the player's balance. ::: ## Keeping the List in Sync The reply type does not guarantee an updated ticket list. Reload with `loadConfig` before the next ticket. Refresh also supplies the list when session renewal is required. You can reduce a local count after a confirmed opening response for display. Never reduce it on a timeout or again for a continuation. Reconcile uncertain responses before another ticket request. The operator can grant or revoke tickets while the game is open. ## Ticket controls and summary Lock the ticket's stake, currency, and feature. Validate its availability and effective limits before the opening request. Never silently replace an invalid ticket with a paid bet. Apply the same timing, input, rule, and recovery controls as paid play. A granted feature ticket uses `useTicketFeatureType`. It does not require a paid `featureToBuy` purchase. Confirm the applicable jurisdiction policy for promotional tickets separately. Show the number of remaining tickets and the campaign win summary. Accumulate confirmed round winnings once per round identity. Do not add each cumulative feature result again. Finish all awarded feature steps before displaying the completed campaign. Reconcile the final ticket list and balance. Require a clear player action before switching to paid play. Send the appropriate Operator Interface [freeplay events](https://docs.hizi.io/operator-interface/api/methods.html#features). ## Next Steps - [Game Flow](https://docs.hizi.io/engine/sdk/game-flow.html) - The full game cycle for a freeplay spin. It is the same cycle as any other bet. - [Endpoints](https://docs.hizi.io/engine/sdk/endpoints.html) - `placeBet` parameter reference, including `useTicket` / `useTicketFeatureType`. - [Types](https://docs.hizi.io/engine/sdk/types.html#ifreeplayinfo) - Full `IFreePlayInfo` field reference. --- # RGS features and promotions The remote gaming server (RGS) handles accounts, transactions, tickets, and operator integration. The frontend displays and requests supported actions. ## Feature inventory | Capability | Frontend contract | | --- | --- | | Real, demo, and social play | Read session mode and currency. Apply `isSocial` and `hideDemoBalance`. Never simulate a real wallet locally. | | Wallet balance | Replace from returned `balance`. Use `updateBalance` after external deposits or wrapper requests. | | Engine bonus purchase | `config.buyFeatures` plus `placeBet({ stake, featureToBuy })`. See [purchases](https://docs.hizi.io/engine/sdk/buy-features.html). | | Operator freeplay tickets | `freePlaysAvailable` plus `useTicket` and optional `useTicketFeatureType`. See [freeplays](https://docs.hizi.io/engine/sdk/free-plays.html). | | Awarded feature spins | `engineData` or scenario `spinInfo`. These belong to the existing round. | | Card, ladder, and other wager features | Original `playerChoiceIndex` or permitted continuation. See [gamble](https://docs.hizi.io/engine/sdk/gamble-features.html). | | Partial collection | `collect({ amount })` can bank money without a wallet credit. | | Persistent progression | Load counters, replace runtime values, and process events once. See [progression](https://docs.hizi.io/engine/sdk/progression-counters.html). | | Within-round jackpots | Slot `singleGameRoundProgressionData`. It is not a shared progressive jackpot feed. | | Operator feature packages | Connect reply `featuresAvailable`. This is distinct from engine `buyFeatures`. | | Live discounts | `balance.discountInfo`. Display eligibility while sending the ordinary undiscounted stake. | | Operator promotions | Operator Interface free-round and promotion events. Use the negotiated adapter. | | Provably fair verification | `config.rng === 'pf'`, round `pf` data, and `pfVerify`. See [verification](https://docs.hizi.io/engine/provably-fair.html). | | Session refresh and history | Supplied refresh/history URLs and `loadConfig` recovery data. | | Server time and broadcasts | Crash events through a [shared request/broadcast socket](https://docs.hizi.io/engine/sdk/crash-transport.html). | ## Discounts The SDK exports `IDiscountInfo` and `IDiscountEntry`. A discount contains text, currency, stake tiers, and optional campaign validity. ```typescript interface DiscountTier { stake: number; absoluteDiscountAmount: number; spinsRemaining?: number; } // balance.discountInfo also carries text, currency, discounts[], campaignId, // validFrom, and validTo. ``` Match the current stake and currency. Check the validity interval and remaining eligibility. Show the full stake, discount, and effective debit when confirmed by the integration. Send the original stake to the engine. Do not subtract the discount in `placeBet`, win conversion, or rules. The operator applies its discount during settlement. The server balance remains authoritative. Replace discount information whenever a balance reply arrives. Do not assume a campaign survives refresh or its last available spin. See [Operator API discounts](https://docs.hizi.io/operator-api/appendix-e-discounts.html). ## Operator packages and shared jackpots `featuresAvailable` describes operator packages with `game`, `type`, `rtp`, `value`, and an optional `description`. It is not an alias for `config.buyFeatures`. The engine SDK has no generic purchase helper for those packages. It also has no shared-jackpot subscription or claim helper. Implement the operator's agreed protocol before exposing either function. Do not send an operator package type as `featureToBuy` without a documented mapping. A game can display a fixed or within-round jackpot from its scenario. Do not present that value as a live shared jackpot pool. State reset conditions and odds in the rules. ### Backend services and frontend extensions The Game Studio API carries jackpot data types (`jackpotContribution`, `jackpotPayout`) and common-draw round fields. The jackpot feature is specified but not live: the spec marks those fields "Not currently implemented" and the engine ignores them. The published Game Studio API has no player-storage service. None of this is an operation of the engine SDK. The audited engine request router does not expose generic `getJackpotInfo` or `confirmJackpotWin` operations to your frontend. A shared-jackpot deployment needs a supported backend bridge and its frontend contract. See the [Game Studio API](https://docs.hizi.io/game-studio-api/) for server integration. That contract must supply jackpot IDs, eligible stakes, currency units, current amounts, data age, and reconnection behavior. It must define prize notification, confirmation, credit timing, and replay handling. Display stale data as stale. Never count a jackpot notification as wallet credit before settlement confirms it. Common-draw games need a draw identifier, participation state, authoritative deadlines, result messages, and missed-event recovery. The creator's ordinary Bingo and Crash modes do not automatically supply a shared-draw service. Do not advertise a multiplayer or jackpot function solely because the RGS supports its backend primitives. Persist player state behind your own game server. Keep frontend preferences separate from persisted engine counters. Do not call private backend routes or embed server credentials in the game. ## Provably fair data Provably fair (PF) rounds expose commitment and replay data where the configured engine supports it. Preserve the live audit and revealed seed. Verification must compare both the random-number audit and the resulting scenarios. Do not show successful verification before those comparisons succeed. A missing seed is not a passing verification. Send original opening data and player actions in the correct order to `pfVerify`. PF data can appear as additional reply fields outside the SDK's declared result types. Validate those extensions. Follow the [PF guide](https://docs.hizi.io/engine/provably-fair.html) and each vertical's replay instructions. ## Functionality outside the frontend Game certification, jurisdiction approval, age verification, wallet settlement, exclusion checks, and ticket grants require platform or operator services. The frontend must honor their results and restrictions. There is no frontend API that can make an uncertified game approved or grant a missing permission. A complete integration includes those services and the [acceptance evidence](https://docs.hizi.io/engine/sdk/release-checklist.html). --- # Progression Counters Progression counters track player progress across gamerounds. When a counter reaches 100% (value 1.0), it triggers an award and resets. The counter carries over any excess value. Each option in the counter's `awards` array has one of two kinds: a **feature spin** award (`{ count, feature }`) or a **cash** award (`{ winMultiplier }` × stake). The two kinds can mix freely in the same array. This page covers how the engine accumulates, completes, and resets counters at runtime. For how to increment counters and configure them at generation time, see [Generator · Progression Counters](https://docs.hizi.io/engine/generator/progression-counters.html). > Do you need a jackpot that fills and pays within a single gameround instead of persisting across rounds for a player? That is a different, similarly-shaped mechanic. See [Single-Gameround Jackpots](https://docs.hizi.io/engine/games/slot.html#single-gameround-jackpots). ## Configuration Define counters in your game's `config.json`. Selection mode (`randomChoice` or `playerChoice`) is at the top level. Each option inside `awards` chooses its own kind by the field it carries. ### Feature spins only ```json { "progressionCounters": [ { "name": "scatter-collection", "onComplete": { "type": "randomChoice", "awards": [{ "count": 10, "feature": "freespins" }] }, "stakeSpecific": false } ] } ``` ### Cash only `winMultiplier` is in stake-multiplier units. When the counter completes, the engine pays `winMultiplier × stake` directly. ```json { "progressionCounters": [ { "name": "cashbox", "onComplete": { "type": "randomChoice", "awards": [ { "winMultiplier": 5, "weighting": 3 }, { "winMultiplier": 20, "weighting": 1 } ] }, "stakeSpecific": false } ] } ``` ### Mixed (feature + cash in the same awards array) A single counter completion can offer the player a choice between a feature spin and a cash payout. Alternatively, the counter can randomly pick between the two: ```json { "progressionCounters": [ { "name": "treasure", "onComplete": { "type": "playerChoice", "awards": [ { "count": 10, "feature": "freespins" }, { "winMultiplier": 25 } ] }, "stakeSpecific": false } ] } ``` For `playerChoice`, the client receives the option list in `engineData.playerChoice`. The client submits a `playerChoiceIndex` on the next `placeBet` call. The engine resolves the pick by its kind. A cash option adds `winMultiplier × stake` to `totalWin`. The engine then resumes any pending feature spin, or ends the round. A feature option starts the awarded feature spin. ### Fields | Field | Type | Description | | -------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | `name` | `string` | Counter identifier. It must match keys in entry `progressionAwards`. | | `onComplete` | `TProgressionAwarded` | The award that the engine triggers when the counter reaches 1.0. | | `onComplete.type` | `"randomChoice" \| "playerChoice"` | Whether the engine picks a weighted option or the player picks from a list. | | `onComplete.awards` | `TPlayerChoiceAward[]` / `TRandomChoiceAward[]` | Mixed list of feature-spin and cash options. Each option carries either `{ count, feature }` or `{ winMultiplier }`, plus an optional `weighting` for `randomChoice`. | | `stakeSpecific` | `boolean` | If `true`, each stake level maintains its own counter. | ::: warning `resetOnFeatureEnd` is not live at runtime The generator's `IProgressionCounterConfig` also carries an optional `resetOnFeatureEnd?: string[]` (feature names whose end would reset the counter to `0`). The engine briefly implemented this feature. The engine team then **removed it from the runtime**. The currently-deployed engine ignores the field entirely. Nothing resets the counter when a listed feature ends. `loadConfig` strips the field before it sends `config.progressionCounters` to the client. A counter always persists across rounds until it completes. The generator's KPI-math RTP calculation still accounts for `resetOnFeatureEnd`. It treats tagged increment mass as discarded at the reset boundary. So a config that sets this field can produce an RTP report that assumes reset behaviour. The live game never performs this reset behaviour. Do not set `resetOnFeatureEnd` on a shipping game's `progressionCounters` today. If you need a meter that fills and resets within a single feature run or gameround, use a different, already-supported mechanic. See [Single-Gameround Jackpots](https://docs.hizi.io/engine/games/slot.html#single-gameround-jackpots). ::: ## Entry-Level Setup During game generation, assign `progressionAwards` to entries that should increment counters: ```typescript await generator.addResult(scenario, { win, metaTags, featureAwards, weight, progressionAwards: { 'scatter-collection': { 0: 0.01 } }, // 1% increment, attributed to scenario step 0 }); ``` The inner key identifies the 0-based scenario step for the increment. Use `0` for a single-object scenario. For a multi-result scenario, use the step that produced the increment. See [Generator · Recording increments with `addResult()`](https://docs.hizi.io/engine/generator/progression-counters.html#recording-increments-with-addresult) for the full shape and how it interacts with entry matching. In the editor, use the **Progression Awards** section on each entry to set pairs of counter name and increment value. ## How It Works 1. **Round start**: The engine loads current counter values from player data. 2. **Each spin**: If the selected entry has `progressionAwards`, the engine increments the corresponding counters. 3. **Completion**: When a counter reaches 1.0 or more, the engine picks one option from the `onComplete.awards` array. Or the engine asks the player to pick one. The engine then dispatches the option by kind. A feature option merges spins into `spinInfo`. A cash option adds `winMultiplier × stake` to `totalWin`. The counter then resets and carries over any excess. 4. **Round end**: The engine saves updated counter values to player data. `engineData.progressionCounters` holds the counter values, keyed by counter name. This field appears only after at least one counter receives an increment. Before that, the engine omits the field entirely instead of sending `{}`: ```typescript const counters = gameResult.engineData.progressionCounters; // { "scatter-collection": 0.55 } // 55% progress ``` ## Client Integration ### loadConfig Response When a game has progression counters, `loadConfig` returns: ```json { "config": { "progressionCounters": [ { "name": "scatter-collection", "onComplete": { ... }, "stakeSpecific": false } ], "progressionCounterValues": { "scatter-collection": 0.55 } } } ``` ### placeBet Response Each `placeBet` response includes current counter values in `engineData`: ```json { "result": { "engineData": { "progressionCounters": { "scatter-collection": 0.56 }, "inProgress": true } } } ``` ## Progression Events `engineData.progressionCounters` carries each counter's **value** at the current step. On its own, this only lets a client read the fill level. It cannot tell which step delivered an increment or an award. `engineData.progressionEvents` provides that information. It has one entry per counter that changed on the current scenario step. The client can then animate the fill and play the award at the exact moment they happen. The client does not need to infer them from the end-of-round state. Each `placeBet` response carries the events for that step: ```typescript interface IProgressionEvent { counter: string; // Counter name (matches progressionCounters) delta: number; // Increment applied on this step value: number; // Counter value after this step's delta and any reset cashWin?: number; // Cash awarded on this step, in stake-multiplier units featureAwards?: TRandomChoiceFeatureAward[]; // Feature spins merged into spinInfo on this step playerChoiceRequested?: boolean; // A completion on this step needs player input } ``` `delta` and `value` are always present. The award fields appear only when the increment carries the counter across `1.0`. The fields also require that `onComplete` fires: - `cashWin`: cash paid by a resolved cash award. The value is already included in `totalWin`. Use it only to present the win. - `featureAwards`: feature spins merged into the round's `spinInfo`. - `playerChoiceRequested`: the completion needs the player to choose. The options also arrive on `engineData.playerChoice`. A single step can complete a counter more than once. This can happen with a large increment, or a high carried-in value. Every crossing on that step aggregates into one event. ```typescript for (const event of gameResult.engineData.progressionEvents ?? []) { animateCounter(event.counter, event.value); // fill to the new value if (event.cashWin) showJackpotWin(event.counter, event.cashWin * stake); if (event.featureAwards) showFeatureAward(event.counter); if (event.playerChoiceRequested) awaitPlayerChoice(); // options on engineData.playerChoice } ``` The events are scoped to the current step. They arrive on the `placeBet` continuation for the exact step where the counter changed. This includes a step inside a multi-step round, such as a free spin sequence. For example, an award that lands on the third free spin signals on that spin, not at round end. `progressionEvents` is absent or empty on steps where no counter changed. ## Example ### Feature counter A slot game where collecting 100 scatter symbols awards 10 free spins: 1. **Config**: Define a counter `scatter-collection` with `onComplete: { type: "randomChoice", awards: [{ count: 10, feature: "freespins" }] }`. 2. **Generation**: On entries where a scatter lands, set `progressionAwards: { "scatter-collection": { 0: 0.01 } }`. 3. **Gameplay**: After 100 scatter lands (100 x 0.01 = 1.0), the counter triggers 10 free spins. The counter then resets to 0. ### Cash counter A coin-collect mechanic where every 20 coin landings pays out 5× stake: 1. **Config**: Define a counter `coin-collect` with `onComplete: { type: "randomChoice", awards: [{ winMultiplier: 5 }] }`. 2. **Generation**: On entries where a coin lands, set `progressionAwards: { "coin-collect": { 0: 0.05 } }`. 3. **Gameplay**: After 20 coin lands (20 × 0.05 = 1.0), the counter pays `5 × stake` into `totalWin`. The counter then resets to 0. ### Mixed treasure-chest counter This is a "pick your prize" mechanic. Every 50 chest landings offers the player a choice between free spins and a flat cash payout: 1. **Config**: ```json { "name": "chest-pick", "onComplete": { "type": "playerChoice", "awards": [ { "count": 8, "feature": "freespins" }, { "winMultiplier": 30 } ] }, "stakeSpecific": false } ``` 2. **Generation**: On each chest land, set `progressionAwards: { "chest-pick": { 0: 0.02 } }`. 3. **Gameplay**: After 50 chest lands, the engine surfaces both options in `engineData.playerChoice`. The client renders both kinds (use the `isCashChoice` / `isFeatureChoice` guards). The client then posts `playerChoiceIndex`. The engine pays out based on the picked option's kind. ## Float Precision Counter arithmetic uses `Math.round(val * 1e6) / 1e6` to avoid floating-point drift. This keeps behavior consistent across rounds. ## Backward Compatibility - All progression fields are optional. Existing games without progression counters are unaffected. - The engine handles old output files without `progressionAwards` fields gracefully. - `progressionCounters` on `engineData` is optional. Clients that do not use it can ignore it. --- # Crash transport Crash requires requests and server broadcasts on the same WebSocket connection. The engine binds the live round to that connection. ## SDK limitation SDK 0.2.18 `enableWebSockets` exposes only `close` and `isConnected`. Its message handler ignores messages without a pending request ID. It discards Crash broadcasts. Do not read an imaginary `handler.socket` or open a second socket only for events. Use this transport for all requests in the Crash session. It replaces `enableWebSockets`. [Download the transport](https://docs.hizi.io/engine/sdk/examples/game-socket.ts). ```typescript import { API_RETURNCODES, defaultNetworkTimeout, type TNetworkResponse } from '@hizi.io/engine-sdk'; type Envelope = { requestId?: string; status?: number; headers?: Record; reply?: Record; event?: string; payload?: unknown; }; // One connection carries both requests and server events. This replaces enableWebSockets. export class GameSocket { private nextId = 0; private pending = new Map) => void; timer: ReturnType; }>(); private constructor(private socket: WebSocket, onEvent: (event: string, payload: unknown) => void) { socket.addEventListener('message', message => { const frame = typeof message.data === 'string' ? message.data : message.data instanceof ArrayBuffer ? new TextDecoder('utf-8').decode(message.data) : null; if (frame === null) return; let data: Envelope; try { data = JSON.parse(frame); } catch { return; } if (!data || typeof data !== 'object') return; // Crash events can arrive before the matching placeBet reply. if (typeof data.event === 'string') { onEvent(data.event, data.payload); return; } if (!data.requestId) return; const pending = this.pending.get(data.requestId); if (!pending) return; this.pending.delete(data.requestId); clearTimeout(pending.timer); if (typeof data.status === 'number' && data.status >= 200 && data.status < 300) { pending.resolve({ success: true, result: data.reply ?? {} }); return; } const headers = Object.fromEntries(Object.entries(data.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v])); let text = headers['x-h-error-msg']; const encoded = headers['x-h-error-msg-base64']; if (encoded) { try { text = new TextDecoder().decode(Uint8Array.from(atob(encoded), c => c.charCodeAt(0))); } catch { text = text ?? 'Invalid error text'; } } pending.resolve({ success: false, error: { code: String(headers['x-h-error-id'] ?? data.reply?.errorId ?? data.reply?.code ?? API_RETURNCODES.UNEXPECTED), message: String(text ?? data.reply?.errorText ?? data.reply?.message ?? 'Request failed'), ...(data.reply?.passThroughData === undefined ? {} : { passThroughData: data.reply.passThroughData }), } }); }); socket.addEventListener('close', () => this.failPending()); socket.addEventListener('error', () => this.failPending()); } static async connect(url: string, onEvent: (event: string, payload: unknown) => void): Promise { const socket = new WebSocket(url); // The router sends replies as binary. Without this the browser delivers a Blob, // String(blob) is "[object Blob]", and every reply and event is discarded. socket.binaryType = 'arraybuffer'; const transport = new GameSocket(socket, onEvent); await new Promise((resolve, reject) => { const timer = setTimeout(() => { socket.close(); reject(new Error('Socket connection timed out')); }, defaultNetworkTimeout); const failed = () => { clearTimeout(timer); reject(new Error('Socket connection failed')); }; socket.addEventListener('error', failed, { once: true }); socket.addEventListener('close', failed, { once: true }); socket.addEventListener('open', () => { clearTimeout(timer); socket.removeEventListener('error', failed); socket.removeEventListener('close', failed); resolve(); }, { once: true }); }); return transport; } async request(body: Record): Promise> { if (this.socket.readyState !== WebSocket.OPEN) return this.networkError(); const requestId = `frontend-${++this.nextId}`; return new Promise(resolve => { const timer = setTimeout(() => { this.pending.delete(requestId); resolve(this.networkError()); }, defaultNetworkTimeout); this.pending.set(requestId, { timer, resolve: value => resolve(value as TNetworkResponse) }); try { this.socket.send(JSON.stringify({ cmd: 'game', data: { body, requestId, method: 'POST', headers: { 'Content-Type': 'application/json' }, } })); } catch { clearTimeout(timer); this.pending.delete(requestId); resolve(this.networkError()); } }); } private networkError(): TNetworkResponse { return { success: false, error: { code: String(API_RETURNCODES.NETWORKERROR), message: 'Connection interrupted. Reconcile the round before another action.', } }; } private failPending(): void { for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.resolve(this.networkError()); } this.pending.clear(); } close(): void { this.failPending(); this.socket.close(); } } ``` ## Start a round ```typescript import { CRASH_EVENTS, crashPlaceBetData, crashCollectData, type IPlaceBetReply, type ICollectReply, } from '@hizi.io/engine-sdk'; import { GameSocket } from './game-socket'; const socket = await GameSocket.connect(connection.webSocketURL!, (event, payload) => { // Validate the event payload against the Crash types before rendering. // Retain roundHash, bet hashes, cashout states, and settlement events. crashView.handle(event, payload); }); const opening = crashPlaceBetData([{ stake: selectedStake, autoCashOutMultiplier: 2 }]); const response = await socket.request({ ...opening, op: 'placeBet', token: connection.token, stake: selectedStake, }); if (!response.success) { // Stop new actions. Reconnect and reconcile; do not repeat the opening bet. } // Manual cashout, only after the server identifies this bet: const cashout = crashCollectData(betHash); const collected = await socket.request({ ...cashout, op: 'collect', token: connection.token, }); ``` The event handler must exist before the opening request. Events can arrive before its reply. Route them by round and bet identity. Do not treat a late event from a previous round as the current result. The transport does not automatically retry or reconnect. On disconnection, disable actions. Open a new connection and use the [Crash resume request](https://docs.hizi.io/engine/games/crash.html#reconnecting-mid-round). Do not send another opening stake. Do not use `SlotSession` for Crash. A timer or animation freeze cannot pause its server round. Display a reconnecting state instead of a simulated live cashout value. ## Request and event differences Request replies contain `requestId`, HTTP-like `status`, `headers`, and `reply`. Broadcasts contain `event` and `payload`. Handle `roundStarted`, `roundResumed`, `roundTick`, `betCashedOut`, `roundEnded`, and `balanceUpdated`. Their types and state transitions appear in the [Crash guide](https://docs.hizi.io/engine/games/crash.html). A successful cashout uses the server's multiplier and amount. The local curve estimates presentation only. Do not calculate a payable cashout from the displayed multiplier. Before a new round, reconcile all submitted bets and the final balance. Do not assume `roundEnded` alone contains the complete wallet update. Use normal SDK `loadConfig` over HTTP before opening this transport, or send its raw operation through `request`. For a raw reply, apply `decodeScenario` with the returned config to resumed results. Keep one session owner for the connection and request lock. --- # Endpoints The hizi RGS platform provides all endpoints. The initial `login()` call returns the `backendURL` and `token`. The SDK sends game operations as JSON POST bodies to `backendURL`. Login and refresh use GET on their supplied URLs. Do not append operation names to `backendURL`. ## login Exchange the short-lived launch token for a session token and the URLs for all later calls. The game's launch URL provides the launch token. The response also carries the **operator's platform settings** (`gameSettings`). Your frontend must follow these settings, such as stake limits, enabled features, UI behaviour, and regulatory flags. **Request:** ```typescript await login({ loginURL, launchToken }); ``` **Response: `IConnectReply`** ```typescript { token: string; // Session token for subsequent API calls backendURL: string; // URL for game API calls refreshURL: string; // URL for refreshing the session logoutURL: string; // Logout URL webSocketURL?: string; // WebSocket URL (optional) balance?: IBalanceReply; // Player balance gameSettings?: IGameSettings; // Operator platform settings - read this! tokenData?: ITokenData; // Token metadata (playerId, gameId, language, currency, mode, currencyMultiplier, gameRound, …) freePlaysAvailable?: IFreePlayInfo[]; // Available free plays featuresAvailable?: { game: string; type: string; rtp: number; value: number; description?: string }[]; operatorProtocol?: number; // Operator protocol version in use (informational) } ``` ::: warning Read `gameSettings` before showing the game `gameSettings` values differ by operator and by jurisdiction. These values must control your UI. Your frontend must clamp and validate stakes. Your frontend must also respect every enabled and disabled flag. The engine validates financial requests, but it does not implement frontend controls or disclosures. Apply all frontend requirements explicitly. Typical fields to apply: - Use `minStake`, `maxStake`, and `maxPackageStake` to clamp the stake selector and buy-feature prices. - Use `customStakes`, `defaultStakeIndex`, and `forceDefaultStake` to override the game's own stake list. - Use `maxExposure` to cap the maximum win that you show or allow. - Use `autoplayEnabled`, `autoplayLossLimitRequired`, and `autoplayShowTotalStake` to control the autoplay UI. - Use `gambleEnabled`, `turboEnabled`, `stopEnabled`, `partialCollectEnabled`, and `packageBuyEnabled` to enable or disable feature buttons. - Use `displayRTP`, `displayXRTP`, `showExactRTP`, `displayWinOdds`, and `displayJackpotOdds` to control the disclosure of RTP and odds. - Use these fields to control presentation: `forceOrientation`, `disableFullScreenMobile`, `hideCompanyLogo`, `displayClock`, `abbreviateAmounts`, `displayNetPosition`, `hideDemoBalance`, and `displaySessionTimer`. - Use `historyURL`, `homeURL`, `topupURL`, `lossLimitURL`, `homeEnabled`, and `redirectTarget` for external navigation. - If `reportAnimationEnd` is `true`, call [`reportAnimationEnd`](https://docs.hizi.io/engine/sdk/endpoints.html#reportanimationend) after every win animation. - Use `operatorHandlesErrors`, `translateErrors`, `preventRedirect`, and `refreshDisabled` to handle errors and sessions. See [`IGameSettings`](https://docs.hizi.io/engine/sdk/types.html#igamesettings) for the full field list. Operators may also supply extra fields beyond the typed ones. The index signature `[key: string]: unknown` exposes these extra fields. ::: ::: tip Stake limits precedence Operator limits replace the corresponding game defaults. A nonempty `customStakes` list replaces the default ladder. The current engine custom-list path skips its separate min/max checks. Filter the selected ladder against effective player and jurisdiction limits yourself. See [stakes and money](https://docs.hizi.io/engine/sdk/stakes-and-money.html). ::: SDK `login` and `refresh` copy a fixed list of eleven top-level keys and discard the rest of the raw reply. `tokenData` is one of them, so `playerId`, `operatorId`, `gameId`, `language`, `currency`, `currencyMultiplier`, `mode` and any open `gameRound` all reach your frontend. Every other SDK operation returns the raw reply unchanged, so `loadConfig` gives back `gameRoundInfo` and `amountToCollect` straight after. Only `gameVariables.rcElapsed` and the singular `freePlayInfo` are lost for good. Do not replace `login` to recover them. See [what `login()` does not return](https://docs.hizi.io/engine/sdk/launch-and-loading.html#what-login-does-not-return). --- ## loadConfig Load the game configuration including available stakes, RTP, and buy-features. **Request:** ```typescript await loadConfig({ backendURL, token }); ``` Under the hood, this sends: ```json { "op": "loadConfig", "token": "" } ``` **Response: `ILoadConfigReply`** ```typescript { config: { gameCode: string; gameType?: string; // Game vertical; optional in older configurations stakes?: number[]; // Available stake amounts (optional) minStake?: number; // Effective minimum stake maxStake?: number; // Effective maximum stake calculatedMaxExposure?: number; // Effective monetary exposure limit ladderMultipliers?: number[]; // Built-in ladder gamble configuration ladderLives?: number; rtp: number; // Return to player percentage maxPayoutCap?: number; // Maximum payout cap (the engine does not set this on loadConfig) maxPayout?: number; // Highest payout one round can accumulate, as a multiplier of stake maxPayoutOdds?: number; // "1 in N" denominator for maxPayout (currently repeats maxWinOdds) maxWin?: number; // Highest win one spin can pay, as a multiplier of stake maxWinOdds?: number; // "1 in N" denominator for maxWin in ordinary play maxWagerableWin?: number; // Maximum win amount eligible for wagering minWagerableWin?: number; // Minimum win amount eligible for wagering version: string; // Engine version buyFeatures?: IBuyFeatureOption[]; // Available buy-features (with `id` added) wagerFeatures?: string[]; // Relative wager features (e.g. ["card_red", "card_black", ...]) wagerStakeFeatures?: string[]; // Absolute cashout wager features (e.g. mines/frogger steps) progressionCounters?: IProgressionCounterConfig[]; // Progression counter configs progressionCounterValues?: Record; // Current counter values (0.0–1.0) [key: string]: unknown; // Additional fields from loadConfig.json }; tokenData: ITokenData; // Updated token data balance?: IBalanceReply; // Player balance gameResult?: IGameResult; // Last game result (if resuming) gameRoundInfo?: IGameRoundInfo; // Game round info (if resuming) freePlaysAvailable?: IFreePlayInfo[]; // Available free plays previousResults?: IGameResult[]; // Previous game results amountToCollect?: number; // Uncollected amount } ``` **Key fields:** | Field | Description | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `config.gameType` | The current game vertical, including fixed-odds types. Older configs can omit it. Validate it against your frontend. | | `config.stakes` | Array of available stake amounts, in minor currency units. Optional. Some games use custom stakes set by the operator. | | `config.rtp` | Return to player percentage (for example, `95` for 95%) | | `config.maxPayout` | The four certified figures a game states are `maxPayout`, `maxPayoutOdds`, `maxWin` and `maxWinOdds`. Print them on the rules screen. A figure the game does not have is absent, never `0`. See [`ILoadConfigConfig`](https://docs.hizi.io/engine/sdk/types.html#iloadconfigconfig) | | `config.buyFeatures` | Available buy-feature options. See [buy-features](https://docs.hizi.io/engine/sdk/buy-features.html) | | `config.wagerFeatures` | Available wager feature names (absent if the game has no wager features). See [gamble-features](https://docs.hizi.io/engine/sdk/gamble-features.html) | | `config.progressionCounters` | Progression counter configuration. See [Progression Counters](https://docs.hizi.io/engine/sdk/progression-counters.html) | | `config.progressionCounterValues` | Current counter values per player (0.0–1.0 range) | | `gameResult` | The last game result if the player has an incomplete round to resume | --- ## placeBet Place a bet to receive a game result. You can also continue an in-progress round with this endpoint. **Request:** ```typescript // New spin await placeBet({ backendURL, token, stake, config }); // Continue multi-step round await placeBet({ backendURL, token, config }); // Buy feature await placeBet({ backendURL, token, stake, featureToBuy: featureId, config, }); ``` `config` is the object that `loadConfig()` returns (`configResponse.result.config`). Passing `config` enables automatic decompression of the `scenario` field. Passing `config` makes the SDK expand a minified `scenario` before it resolves. Without `config`, `scenario` stays minified. The option does nothing for a game that does not minify scenarios. Under the hood: ```json { "op": "placeBet", "token": "", "stake": 100, "featureToBuy": "freespin" } ``` **Options: `IPlaceBetOptions`** | Field | Type | Required | Description | | ---------------------- | ------------------------- | -------- | ------------------------------------------------------------------ | | `backendURL` | `string` | Yes | Backend URL from login response | | `token` | `string` | Yes | Session token | | `stake` | `number` | No | Stake amount. Required for the first call of a gameround. | | `useTicket` | `boolean` | No | Use a freeplay ticket | | `useTicketFeatureType` | `string` | No | Feature type of the ticket | | `featureToBuy` | `string` | No | Feature to buy (for example, `'freespin'`). The engine derives the price from the stake. | | `config` | `ILoadConfigConfig` | No | Config from `loadConfig()`. Required to decompress the `scenario` field. | | `playerChoiceIndex` | `number` | No | Index of the selected option when `engineData.playerChoice` is set | | `additionalData` | `Record` | No | Additional game-specific parameters | **Response: `IPlaceBetReply`** ```typescript { result: IGameResult; // The game result tokenData: ITokenData; // Updated token data balance?: IBalanceReply; // Updated balance gameRoundInfo?: IGameRoundInfo; // Game round status amountToCollect?: number; // Uncollected amount } ``` The key payload is `result`, an `IGameResult`: ```typescript { // Your scenario data (from hizi engine generator) scenario: Record; // Engine state engineData: { // entryIndex, scenarioInfo and spinInfo come from the fixed-odds (entries-DB) // pipeline. Rules-engine games such as blackjack and crash omit those three. // Every other field below, including inProgress and canCollect, applies to all games. entryIndex?: number; scenarioInfo?: IScenarioInfo; spinInfo?: ISpinInfo[]; playerChoice?: TPlayerChoiceAward[]; currentFeature?: string; nextFeature?: string; progressionCounters?: Record; canCollect?: boolean; inProgress: boolean; buyFeatureId?: string; // Opening purchase identifier baseStake?: number; // Base stake for the round, EXCLUDING buy-feature cost bankedWin?: number; // Protected win multiplier, still unpaid ladderState?: { baseWin: number; currentRungIndex: number; lives: number; previousRungIndex?: number }; version?: string; }; // Cumulative win (multiplier of stake) totalWin: number; } ``` ::: tip If the round opened with `featureToBuy`, every reply for that round echoes back which feature it was. This includes every continuation `placeBet` call and the closing `collect` call. The SDK path is `response.result.result.engineData.buyFeatureId` after checking `response.success`. Use this field to attribute the round's total win to the bought feature. You do not need to track the feature on the client side. The field is absent (`undefined`) for a round that did not open by buying a feature. Older engine versions also duplicated this field at the reply root as `buyFeatureId`. That copy no longer exists. Read the field from `engineData` instead. ::: **Decision logic after presentation and required timing:** | State | Action | | --- | --- | | Closed round with consistent terminal engine flags | Enable a new player-requested round. | | `engineData.canCollect === true` | Offer collect. Offer only permitted choices or explicit continuation. | | Nonempty `engineData.playerChoice` | Wait for a player choice. Preserve its original index. | | `engineData.inProgress === true`, without either condition above | Follow the vertical's continuation contract. | | Open round without a legal action, or contradictory state | Recover before another request. | The checked [slot controller](https://docs.hizi.io/engine/sdk/game-flow.html) implements this order. A positive `totalWin` does not authorize `collect`. An `inProgress` flag does not authorize an automatic gamble. Games without a collect phase normally settle automatically. Render their final balance and result. Do not collect a second time after settlement. --- ## collect Collect when the vertical offers cashout. Fixed-odds games signal this with `engineData.canCollect`. Crash uses its live bet hashes. Ordinary slot rounds without a collect phase settle automatically. A partial collect during an open gamble banks a protected amount. It keeps the round open and does not immediately credit the wallet. See [gamble and banking](https://docs.hizi.io/engine/sdk/gamble-features.html). **Request:** ```typescript // Collect full amount await collect({ backendURL, token }); // Bank part of an open gamble win, when permitted await collect({ backendURL, token, amount }); ``` Under the hood: ```json { "op": "collect", "token": "", "collectAmount": 5000 } ``` **Options: `ICollectOptions`** | Field | Type | Required | Description | | ---------------- | ------------------------- | -------- | ------------------------------------------------------------------ | | `backendURL` | `string` | Yes | Backend URL from login response | | `token` | `string` | Yes | Session token | | `amount` | `number` | No | Amount to collect. If you omit this field, the engine collects the full available amount | | `additionalData` | `Record` | No | Additional game-specific parameters (for example, the crash `betHash` from `crashCollectData()`) | **Response: `ICollectReply`** ```typescript { amountCredited?: number; // Actual credit, absent when only banking amountToCollect: number; // Amount that was requested balance?: IBalanceReply; // Updated balance tokenData: ITokenData; // Updated token data result?: IGameResult; // Updated state; can remain open after banking gameRoundInfo?: IGameRoundInfo; } ``` `collect` has no `config` option. If its reply contains a compressed result, call `decodeScenario(reply.result.result, config)` after checking success. Use the returned round status and engine flags to determine settlement. If they are missing, reload state. Do not treat `amountToCollect` on this reply as a guaranteed remaining balance. Its SDK contract describes the requested amount. Reload authoritative outstanding amounts before another partial collect. Omit `amount` for full collection. Do not subtract a previously banked amount. A successful response alone does not prove that the round closed. --- ## pfVerify Replay any past round from its revealed seeds. If the round had player actions, replay those too. `pfVerify` reruns the exact game logic that the live round used. It returns the full result and the per-call RNG audit. You can compare this audit against what you recorded at round time. This endpoint works only for provably-fair rounds. A provably-fair round runs with `config.rng === 'pf'`. The endpoint never touches the wallet, progression counters, or operator caps. It is pure compute. **Request:** ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, // current session token (auth + gameId routing) serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, // the stake the round opened with featureToBuy: 'freespin', // if the round opened on a buy feature initialData: { /* … */ }, // blackjack side bets bets: [ /* … */ ], // roulette / crash opening bets playerNumbers: [ /* … */ ], // keno picks actions: [ { playerChoiceIndex: 0 }, // fixed-odds wager choice { pickPosition: 12 }, // mines pick { action: { kind: 'hit' } }, // blackjack action (TRulesAction shape) ], }); ``` Under the hood, the SDK POSTs to `backendURL`: ```json { "op": "pfVerify", "token": "", "serverSeed": "…", "clientSeed": "…", "…": "…" } ``` **Options: `IPfVerifyOptions`** | Field | Type | Required | Description | | --- | --- | --- | --- | | `backendURL` | `string` | Yes | Backend URL from the login response. | | `token` | `string` | Yes | Session token. The engine uses it only to resolve the `gameId` to the game config. The endpoint makes no wallet calls. | | `serverSeed` | `string` | Yes | The revealed server seed from the round's `pf` block. | | `clientSeed` | `string` | Yes | The client seed for that round. | | `startNonce` | `number` | No | Defaults to `0`. Set it if your round opened mid-stream against an existing provably-fair session. | | `stake` | `number` | No | Required by games that need the stake to settle. Examples: blackjack settlement math, and Frogger or other fixed-odds wager games. | | `featureToBuy` | `string` | No | Mirror the round's opening `featureToBuy` value. Examples: the Mines mine count, or a Slot bonus buy. | | `initialData` | `Record` | No | Mirror the round's opening payload. Currently this is blackjack's `{ sideBets }`. | | `bets` | `unknown[]` | No | Roulette's `IRouletteBet[]` or Crash's `ICrashBet[]`. These are the round's opening bets. | | `playerNumbers` | `number[]` | No | Keno: the player numbers, if the round was opened with explicit picks. | | `actions` | `IPfVerifyAction[]` | No | Continuation actions, in order. The shape depends on the game. See the game's Provably Fair section under [Games](https://docs.hizi.io/engine/games/). | **Response: `IPfVerifyReply`** ```typescript { rngData: IRngDataEntry[]; // per-call audit, same shape as the live `pf.rngData` steps: IGameResult[]; // per-step game outcomes, terminal step last (length 1 for one-shots) } ``` Two fields, two checks: 1. **Audit replay**: compare `rngData` element by element against the round's saved `pf.rngData`. Rows of type `'int'` must match exactly. Rows of type `'double'` carry the raw HMAC float and must match exactly. Use a deep-equal over the array. A swapped server seed produces different draws. A passing comparison proves the revealed seed is the seed that the round committed to. 2. **Result replay**: compare each `steps[i].scenario` and `steps[i].totalWin` value to what the live round returned at the matching step. For one-shot games, this is just `steps[0]`. For multi-step rounds (blackjack, Mines, Hi-Lo), this is the full per-step chain. If both checks match, only `(serverSeed, clientSeed, actions)` determined the round. The engine could not have rigged the outcome. ::: tip `engineData` is preserved Each step is a full `IGameResult`. `steps[i].engineData` carries the engine's per-step flow-control state: `playerChoice` (fixed-odds wager-feature offerings), `inProgress`, and `nextFeature`. Use this data to verify the round's flow, not just the final outcome. For example, confirm that a fixed-odds round showed the expected wager choices. ::: ::: tip Defence-in-depth commit check For an explicit commitment check on top of the audit replay, compute `SHA-256(serverSeed)` on the client. Compare the result to the `serverSeedHash` you saved before the round opened. This takes one `crypto.subtle.digest` call. The engine does not need to return this value. ::: ::: tip Scope `pfVerify` skips the **operator-side** post-processing that the live `placeBet` call runs. This includes progression counters, `gameSettings.maxExposure` caps, and supplemental stake debits. You cannot derive these from the provably-fair commitment. Verification covers only the RNG draws and the game logic that uses them. ::: ::: warning Crash Crash player-cashout timing depends on the wall clock, not on the seed. `pfVerify` for Crash returns only the committed `scenario.maxMultiplier`. This is the entire seed-derived surface. The operator audits bet settlements. ::: See [Provably Fair](https://docs.hizi.io/engine/provably-fair.html) for the underlying HMAC-SHA512 protocol. That page also explains how the protocol derives the seeds from `(serverSeed, clientSeed, nonce)`. --- ## refresh Refresh an expired session token. Call this when API requests return `SESSIONINVALID` or `NOTLOGGEDON` errors. **Request:** ```typescript await refresh(refreshURL); ``` The initial `login()` response provides the `refreshURL` (`result.refreshURL`). **Response: `IConnectReply`** ```typescript { token: string; // New session token backendURL: string; // Backend URL (may change) refreshURL: string; // New refresh URL logoutURL: string; // Logout URL webSocketURL?: string; // WebSocket URL balance?: IBalanceReply; // Player balance gameSettings?: IGameSettings; // Platform settings tokenData?: ITokenData; // Token metadata freePlaysAvailable?: IFreePlayInfo[]; // Available free plays } ``` ::: tip After a successful refresh, update your stored `token`, `backendURL`, and `refreshURL` with the new values from the response. ::: --- ## reportAnimationEnd Notify the backend that the game animation has finished. Use this endpoint when the operator requires animation-end reporting (see `gameSettings.reportAnimationEnd`). **Request:** ```typescript await reportAnimationEnd({ backendURL, token }); ``` --- ## updateBalance Request an updated balance from the backend. **Request:** ```typescript await updateBalance({ backendURL, token }); ``` --- # Types & Interfaces ## Engine Types The `@hizi.io/engine-sdk` package exports these types: ```typescript import type { IGameResult, IScenarioInfo, ISpinInfo, IBuyFeatureOption, ILoadConfigConfig, IProgressionEvent, TPlayerChoiceAward, TPlayerChoiceFeatureAward, TPlayerChoiceCashAward } from '@hizi.io/engine-sdk'; ``` --- ### `IGameResult` This is the primary game result object that `placeBet` returns. It contains scenario data, engine state, and win information. ```typescript interface IGameResult { scenario: Record; engineData: { entryIndex?: number; scenarioInfo?: IScenarioInfo; spinInfo?: Array; playerChoice?: TPlayerChoiceAward[]; currentFeature?: string; nextFeature?: string; progressionCounters?: Record; progressionEvents?: IProgressionEvent[]; canCollect?: boolean; inProgress: boolean; bankedWin?: number; baseStake?: number; buyFeatureId?: string; version?: string; ladderState?: ILadderState; }; totalWin: number; } ``` | Field | Type | Description | | -------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- | | `scenario` | `Record` | Game-specific scenario data. The [hizi engine generator](https://docs.hizi.io/engine/generator/) stores this data during generation. | | `engineData` | `object` | Engine state tracking. See fields below | | `engineData.entryIndex` | `number` | Index of the selected entry in the entries table. This field applies only to fixed-odds games. Rules-engine games (blackjack, crash) omit it. | | `engineData.scenarioInfo` | `IScenarioInfo` | State of the progression through a multi-result scenario. This field applies only to fixed-odds games. Rules-engine games (blackjack, crash) omit it. | | `engineData.spinInfo` | `ISpinInfo[]` | Total awarded and used spins per feature. Remaining = `count - used`. | | `engineData.playerChoice` | `TPlayerChoiceAward[]` | Pending player choices. Each option is a `TPlayerChoiceFeatureAward` (spins) or `TPlayerChoiceCashAward` (win multiplier). Use the `isFeatureChoice` and `isCashChoice` type guards. | | `engineData.currentFeature` | `string` | The feature that produced this result. Absent during base game spins | | `engineData.nextFeature` | `string` | Name of the table the engine uses for the next spin. `undefined` when the round is ending | | `engineData.progressionCounters` | `Record` | Current progression counter values (0.0–1.0). See [Progression Counters](https://docs.hizi.io/engine/sdk/progression-counters.html) | | `engineData.progressionEvents` | `IProgressionEvent[]` | Per-step counter increments and awards that resolved this step (cash wins, feature awards, player choice requests). Use this data to animate counter fills and award delivery. | | `engineData.canCollect` | `boolean` | `true` when the player can call `collect()` to cash out. See [Gamble Features](https://docs.hizi.io/engine/sdk/gamble-features.html) | | `engineData.inProgress` | `boolean` | `true` if the round needs more `placeBet` calls to complete | | `totalWin` | `number` | Cumulative win amount as a multiplier of stake | --- ### `ILadderState` ```typescript interface ILadderState { baseWin: number; currentRungIndex: number; lives: number; previousRungIndex?: number; } ``` `baseWin` is the accumulated win multiplier when the ladder starts. The configured rung multiplier acts on this value. See [gamble](https://docs.hizi.io/engine/sdk/gamble-features.html). The result's `bankedWin` is an unpaid protected multiplier. Its `baseStake` is the original selected monetary stake. These values survive continuations and recovery. ### `IScenarioInfo` Tracks progression through a multi-result scenario. ```typescript interface IScenarioInfo { scenarioIndex: number; currentScenarioIndex: number; inProgress: boolean; } ``` | Field | Type | Description | | ---------------------- | --------- | ---------------------------------------------------------------------------- | | `scenarioIndex` | `number` | Which scenario was randomly selected from the entry's scenario pool | | `currentScenarioIndex` | `number` | Current step in the scenario (0-based). Increments with each `placeBet` call | | `inProgress` | `boolean` | `true` if the scenario has more steps to play | --- ### `ISpinInfo` Tracks remaining spins for a feature (e.g., free spins). ```typescript interface ISpinInfo { feature: string; count: number; used: number; } ``` | Field | Type | Description | | --------- | -------- | ------------------------------------------------ | | `feature` | `string` | Name of the table whose entries the engine uses | | `count` | `number` | Total spins **awarded** for this feature, not remaining spins. This count grows on a retrigger or re-entry. Remaining spins = `count - used` | | `used` | `number` | Spins already consumed, including the current spin | --- ### `IBuyFeatureOption` Describes an available buy-feature. ```typescript interface IBuyFeatureOption { feature: string; price: number; featureRTP: number; initialSpins: number; } ``` | Field | Type | Description | | -------------- | -------- | ------------------------------------------------- | | `feature` | `string` | Name of the table whose entries the engine uses | | `price` | `number` | Buy price as a multiplier of stake | | `featureRTP` | `number` | Expected return percentage for this feature | | `initialSpins` | `number` | Spins awarded on initial trigger | ::: tip In `ILoadConfigConfig.buyFeatures`, each entry is `IBuyFeatureOption & { id: string }`. The config adds the `id` field. The base type does not include this field. ::: --- ### `ILoadConfigConfig` Game configuration returned by `loadConfig`. ```typescript interface ILoadConfigConfig { gameCode: string; gameType?: string; symbolMap?: Record; scenarioSchema?: number; stakes?: number[]; minStake?: number; maxStake?: number; rtp: number; maxPayoutCap?: number; maxPayout?: number; maxPayoutOdds?: number; maxWin?: number; maxWinOdds?: number; maxWagerableWin?: number; minWagerableWin?: number; version: string; buyFeatures?: (IBuyFeatureOption & { id: string })[]; wagerFeatures?: string[]; wagerStakeFeatures?: string[]; progressionCounters?: IProgressionCounterConfig[]; progressionCounterValues?: Record; ladderMultipliers?: number[]; ladderLives?: number; calculatedMaxExposure?: number; [key: string]: unknown; } ``` | Field | Type | Description | | -------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------- | | `gameCode` | `string` | Unique game identifier | | `gameType` | `string` | Current game vertical, including fixed-odds games. Older configurations can omit it. | | `stakes` | `number[]` | Available stake amounts. This field is optional; some games use custom stakes that the operator defines. | | `rtp` | `number` | Return to player percentage (e.g., `95`) | | `maxPayoutCap` | `number` | Maximum payout cap (optional). The engine does not set this field on a `loadConfig` reply. Read `maxPayout` for the game's own maximum payout. | | `maxPayout` | `number` | Highest payout one round can accumulate, as a multiplier of stake (optional). Absent for a game with no entries table (blackjack, crash, bingo). | | `maxPayoutOdds` | `number` | The "1 in N" denominator for `maxPayout` (optional). This field currently repeats `maxWinOdds`. See the warning below. | | `maxWin` | `number` | Highest win one spin can pay, as a multiplier of stake (optional). This is not the round total. For that, read `maxPayout`. | | `maxWinOdds` | `number` | The "1 in N" denominator for `maxWin` in ordinary play (optional). Absent when only a buy feature reaches the top prize. | | `maxWagerableWin` | `number` | Maximum authoring amount eligible for wagering. The engine applies currency conversion during eligibility checks. | | `minWagerableWin` | `number` | Minimum authoring amount eligible for wagering. Use the offered actions after engine checks. | | `minStake` | `number` | Effective minimum stake in account units. The operator override replaces the game default. Otherwise, the engine converts the default. Optional when neither supplies a bound. | | `maxStake` | `number` | Effective maximum stake in account units. The operator override replaces the game default. Otherwise, the engine converts the default. Optional when neither supplies a bound. | | `version` | `string` | Engine version string | | `buyFeatures` | `(IBuyFeatureOption & { id: string })[]` | Available buy-feature options (each with an added `id` field) | | `wagerFeatures` | `string[]` | Names of wager features. For these features, the entry win is a relative multiplier on the at-risk win (for example, `["card_red", ...]`). | | `wagerStakeFeatures` | `string[]` | Names of wager-stake features. For these features, the entry win is an absolute cashout multiplier relative to stake (e.g., mines and frogger steps). | | `progressionCounters` | `IProgressionCounterConfig[]` | Progression counter configs. See [Progression Counters](https://docs.hizi.io/engine/sdk/progression-counters.html) | | `progressionCounterValues` | `Record` | Current counter values from player data (0.0–1.0) | | `symbolMap` | `Record` | Integer-to-symbol name mapping used for scenario decompression. Present when `scenarioSchema` is set. | | `scenarioSchema` | `number` | Schema version used to compress the scenario. When present, pass `config` to `placeBet` to decompress. | The index signature passes through additional fields from the engine's `loadConfig.json`. `maxPayout`, `maxPayoutOdds`, `maxWin` and `maxWinOdds` are the four figures a certified game must state. The generator computes them and the game config carries them. Print them on the game's rules screen. A figure that a game does not have is absent from the reply. It never arrives as `0`. Two cases produce an absent figure: - The game has no entries table (blackjack, crash, bingo). It carries none of the four. - Only a buy feature reaches the top prize. The game then carries `maxWin` with no `maxWinOdds` beside it, because an ordinary round never wins that prize. ::: warning `maxPayoutOdds` currently repeats `maxWinOdds` The generator derives one odds value, from the chance that an ordinary round hits `maxWin`. It writes that same value to both fields. Nothing computes the chance of the accumulated max-payout route. For a game whose tables accumulate wins over a round, `maxPayout` is larger than `maxWin`, so `maxPayoutOdds` understates how rare the maximum payout is. ::: --- ### `IProgressionCounterConfig` Configuration for a progression counter. ```typescript interface IProgressionCounterConfig { name: string; onComplete: TProgressionAwarded; stakeSpecific: boolean; } // Selection mode at the top level; each option is independently a feature // spin or a cash payout. The two kinds may be mixed in the same array. type TProgressionAwarded = | { type: 'playerChoice'; awards: TPlayerChoiceAward[] } | { type: 'randomChoice'; awards: TRandomChoiceAward[] }; type TPlayerChoiceAward = | TPlayerChoiceFeatureAward // { count, feature } | TPlayerChoiceCashAward; // { winMultiplier } type TRandomChoiceAward = | (TPlayerChoiceFeatureAward & { weighting?: number }) | (TPlayerChoiceCashAward & { weighting?: number }); ``` | Field | Type | Description | | --------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Counter identifier that matches keys in entry `progressionAwards` | | `onComplete` | `TProgressionAwarded` | Award granted when the counter reaches 1.0. Each option in the `awards` array is independently a feature spin or cash payout. | | `stakeSpecific` | `boolean` | If `true`, separate counter per stake level | > The engine sends the `playerChoice` array back to the client in > `engineData.playerChoice`. This array is typed as `TPlayerChoiceAward[]`. > Use the `isCashChoice` and `isFeatureChoice` type guards exported > from `@hizi.io/engine-sdk` to render each option correctly. ### `IProgressionEvent` Per-step progression activity in `engineData.progressionEvents`. One entry exists per counter that changed on the current scenario step. See [Progression Counters](https://docs.hizi.io/engine/sdk/progression-counters.html#progression-events). ```typescript interface IProgressionEvent { counter: string; // Counter name (matches progressionCounters) delta: number; // Increment applied on this step value: number; // Counter value after this step's delta and any reset cashWin?: number; // Cash awarded this step, in stake-multiplier units (already in totalWin) featureAwards?: TRandomChoiceFeatureAward[]; // Feature spins merged into spinInfo this step playerChoiceRequested?: boolean; // A completion on this step needs player input } ``` | Field | Type | Description | | ----------------------- | ----------------------------- | ------------------------------------------------------------------------------------ | | `counter` | `string` | Counter name, matching an entry in `progressionCounters`. | | `delta` | `number` | Increment applied to the counter on this step. | | `value` | `number` | Counter value after this step's increment and any reset. | | `cashWin` | `number` | Present when a cash award resolves on this step. This value is in stake-multiplier units. `totalWin` already includes it. | | `featureAwards` | `TRandomChoiceFeatureAward[]` | Present when the engine awards feature spins on this step. The engine merges these spins into `spinInfo`. | | `playerChoiceRequested` | `boolean` | Present when a completion this step needs player input (options on `engineData.playerChoice`). | --- ## Response Types These types come from `@hizi.io/engine-sdk`: ### `IPlaceBetReply` Response from `placeBet()`: ```typescript interface IPlaceBetReply { result: IGameResult; tokenData: ITokenData; balance?: IBalanceReply; gameRoundInfo?: IGameRoundInfo; amountToCollect?: number; } ``` Access the game result: ```typescript const gameResult = response.result.result; // IGameResult ``` | Field | Type | Description | | ----------------- | ---------------- | --------------------------------------------------- | | `result` | `IGameResult` | The game result with scenario data and engine state | | `tokenData` | `ITokenData` | Updated token data | | `balance` | `IBalanceReply` | Updated balance | | `gameRoundInfo` | `IGameRoundInfo` | Gameround status (stake, open/closed) | | `amountToCollect` | `number` | Uncollected amount from the round | --- ### `ICollectReply` A successful collect can keep the round open. Read engine flags and round status. Reload outstanding amounts after partial banking. Response from `collect()`: ```typescript interface ICollectReply { amountCredited?: number; amountToCollect: number; balance?: IBalanceReply; tokenData: ITokenData; result?: IGameResult; gameRoundInfo?: IGameRoundInfo; } ``` | Field | Type | Description | | ----------------- | -------- | ---------------------------------------- | | `amountCredited` | `number?` | Actual wallet credit. Absent when a partial collect only banks winnings. | | `amountToCollect` | `number` | Amount requested for collection | --- ### `ILoadConfigReply` Response from `loadConfig()`: ```typescript interface ILoadConfigReply { config: ILoadConfigConfig; tokenData: ITokenData; balance?: IBalanceReply; gameResult?: IGameResult; gameRoundInfo?: IGameRoundInfo; freePlaysAvailable?: IFreePlayInfo[]; previousResults?: IGameResult[]; amountToCollect?: number; } ``` | Field | Type | Description | | -------------------- | ------------------- | -------------------------------------------- | | `config` | `ILoadConfigConfig` | Game configuration | | `tokenData` | `ITokenData` | Token data | | `balance` | `IBalanceReply` | Player balance | | `gameResult` | `IGameResult` | Last game result (for round resumption) | | `gameRoundInfo` | `IGameRoundInfo` | Gameround info (for round resumption) | | `freePlaysAvailable` | `IFreePlayInfo[]` | Available freeplays | | `previousResults` | `IGameResult[]` | Previous game results | | `amountToCollect` | `number` | Uncollected amount from an in-progress round | --- ## Network Types From `@hizi.io/engine-sdk`: ### `TNetworkResponse` ```typescript type TNetworkResponse = TNetworkSuccess | TNetworkError; ``` ### `TNetworkSuccess` ```typescript type TNetworkSuccess = { result: T; success: true; }; ``` ### `TNetworkError` ```typescript type TNetworkError = { error: IErrorResponse; success: false; }; ``` ### `IErrorResponse` ```typescript interface IErrorResponse { code: string; // stringified API_RETURNCODES value or engine-specific id message: string; passThroughData?: unknown; } ``` | Field | Type | Description | | ----------------- | -------- | ------------------------------------------------------------------------------------- | | `code` | `string` | Machine-readable error code. Compare against `API_RETURNCODES` string values or `recoverableErrorCodes`. | | `message` | `string` | Human-readable error message (may be localized) | | `passThroughData` | `unknown`| Optional operator-specific data | --- ## Additional Reply Types ### `IConnectReply` `login()` and `refresh()` return the session identity in `tokenData` (`playerId`, `operatorId`, `gameId`, `language`, `currency`, `currencyMultiplier`, `mode`, `gameRound`). They copy eleven top-level keys and discard the rest of the raw reply. See [what `login()` does not return](https://docs.hizi.io/engine/sdk/launch-and-loading.html#what-login-does-not-return). Response from `login()` and `refresh()`: ```typescript interface IConnectReply { token: string; backendURL: string; refreshURL: string; logoutURL: string; webSocketURL?: string; balance?: IBalanceReply; gameSettings?: IGameSettings; tokenData?: ITokenData; freePlaysAvailable?: IFreePlayInfo[]; featuresAvailable?: { game: string; type: string; rtp: number; value: number; description?: string }[]; operatorProtocol?: number; } ``` | Field | Type | Description | | -------------------- | ----------------- | -------------------------------------- | | `token` | `string` | Session token for subsequent API calls | | `backendURL` | `string` | URL for game API calls | | `refreshURL` | `string` | URL for refreshing the session token | | `logoutURL` | `string` | URL for logging out | | `webSocketURL` | `string` | WebSocket URL (optional) | | `balance` | `IBalanceReply` | Player balance | | `gameSettings` | `IGameSettings` | Platform game settings | | `tokenData` | `ITokenData` | Token metadata | | `freePlaysAvailable` | `IFreePlayInfo[]` | Available freeplays | | `featuresAvailable` | `{ game: string; type: string; rtp: number; value: number; description?: string }[]` | Purchasable feature packages available to the player (distinct from per-game `buyFeatures`) | --- ## Supporting Types ### `IBalanceReply` ```typescript interface IBalanceReply { totalBalance: number; mode: string; currency: string; balances: IBalanceEntry[]; discountInfo?: IDiscountInfo; } ``` ### `IBalanceEntry` ```typescript interface IBalanceEntry { type: string; currency: string; amount: number; } ``` --- ### `IDiscountInfo` and `IDiscountEntry` These exported types describe `balance.discountInfo`. See [discounts](https://docs.hizi.io/engine/sdk/rgs-features.html#discounts) for fields and presentation requirements. ### `IGameSettings` See the [settings reference](https://docs.hizi.io/engine/sdk/settings-reference.html) for units, runtime variants, defaults, and required behavior. The SDK interface declares required fields, but runtime settings can be partial. Platform-level game settings that the operator provides: ```typescript interface IGameSettings { autoplayEnabled: boolean; autoplayLossLimitRequired: boolean; displayCoins: boolean; displayJackpotOdds: boolean; displayRTP: boolean; displayXRTP: boolean; forceOrientation: ForcedOrientation; gambleEnabled: boolean; historyURL: string; homeURL: string; loadMsg: string; maxExposure: number; maxStake: number; maxPackageStake: number; minStake: number; minLoadTime: number; minSpinTime: number; multipleInstancesAllowed: boolean; rcDisplayWinLoss: boolean; rcEnabled: boolean; rcInterval: number; stopEnabled: boolean; topupURL: string; turboEnabled: boolean; homeEnabled: boolean; dynamicMinSpinTime: boolean; redirectTarget: 'self' | 'parent' | 'top'; packageBuyEnabled: boolean; defaultStakeIndex: number; operatorHandlesErrors: boolean; displayClock: boolean; isSocial: boolean; displayCurrency: boolean; customStakes: number[]; lossLimitURL: string; partialCollectEnabled: boolean; hideCompanyLogo: boolean; abbreviateAmounts: boolean; showExactRTP: boolean; jurisdiction: string; forceDefaultStake: boolean; disableFullScreenMobile: boolean; displayPaytableOnEnterGame: boolean; doNotStoreSettings: string[]; sessionTimeoutInSeconds: number; displayNetPosition: boolean; hideDemoBalance: boolean; preventRedirect: boolean; refreshDisabled: boolean; displayWinOdds: DisplayWinOdds[]; displaySessionTimer: boolean | string; skipWinsEqualToOrLessThanStake: boolean; reportAnimationEnd: boolean; launcherType: 'reelLink' | 'online'; autoplayShowTotalStake: boolean; translateErrors: boolean; currencyToDisplay: string; [key: string]: unknown; } ``` ::: tip Additional fields Operators may provide additional settings fields beyond those listed above. The index signature `[key: string]: unknown` allows any extra properties. Inspect the `gameSettings` object at runtime to see all available fields for your operator. ::: --- ### `IGameRoundInfo` On a bought round `stake` carries the full purchase debit. `baseStake` records the underlying stake. Use `IGameResult.engineData.baseStake` for win multipliers. See [stakes and money](https://docs.hizi.io/engine/sdk/stakes-and-money.html). ```typescript interface IGameRoundInfo { hash: string; status: string; stake: number; baseStake: number; mode: string; currency: string; game: string; currencyMultiplier?: number; commonRoundHash?: string; } ``` --- ### `IGameState` ```typescript interface IGameState { hash: string; type: string; processedOn?: string; result?: { winAmount?: number; info: Record; }; amountWagered?: number; collected?: number; reason?: string; } ``` --- ### `ITokenData` Opaque token metadata. Structure varies by operator. ```typescript interface ITokenData { [key: string]: unknown; } ``` --- ### `IFreePlayInfo` See [Freeplays](https://docs.hizi.io/engine/sdk/free-plays.html) for how to read, choose and spend these. ```typescript interface IFreePlayInfo { currency: string; stake: number; count: number; feature?: string; } ``` | Field | Type | Description | | ---------- | -------- | ----------------------------------------- | | `currency` | `string` | Currency code for the freeplay | | `stake` | `number` | Stake amount for the freeplay | | `count` | `number` | Number of freeplays available | | `feature` | `string` | Feature type for the freeplay (optional) | --- ### `TPlayerChoiceFeatureAward` Describes one option in a player choice prompt: ```typescript type TPlayerChoiceFeatureAward = { count: number; feature: string; }; ``` | Field | Type | Description | | --------- | -------- | ------------------------------------------ | | `count` | `number` | Number of spins awarded | | `feature` | `string` | Target feature for the awarded spins | --- ### `WebSocketHandler` Returned by `enableWebSockets()`: ```typescript interface WebSocketHandler { close(): void; isConnected(): boolean; } ``` | Method | Description | | --------------- | ----------------------------------------------------------------- | | `close()` | Close the WebSocket connection. All API calls revert to HTTP. | | `isConnected()` | Check if the WebSocket is currently open | A dropped connection does **not** revert the SDK to HTTP. The next API call reopens the socket automatically (SDK 0.2.3 or later). Only `close()` switches back to HTTP. See [WebSocket Support](https://docs.hizi.io/engine/sdk/game-flow.html#websocket-support). --- ## Request Option Types ### `ILoginOptions` Options for `login()`: ```typescript interface ILoginOptions { loginURL: string; launchToken: string; } ``` | Field | Type | Description | | ------------ | -------- | ------------------------------------------- | | `loginURL` | `string` | The operator-provided login endpoint URL | | `launchToken`| `string` | Short-lived token from the launch URL query | --- ### `ISessionOptions` Base options for authenticated requests (`loadConfig`, `reportAnimationEnd`, `updateBalance`): ```typescript interface ISessionOptions { backendURL: string; token: string; } ``` | Field | Type | Description | | ------------ | -------- | --------------------------------- | | `backendURL` | `string` | Backend URL from login response | | `token` | `string` | Session token | --- ### `IPlaceBetOptions` Options for `placeBet()`. Extends `ISessionOptions`: ```typescript interface IPlaceBetOptions extends ISessionOptions { config?: ILoadConfigConfig; stake?: number; useTicket?: boolean; useTicketFeatureType?: string; featureToBuy?: string; playerChoiceIndex?: number; additionalData?: Record; } ``` | Field | Type | Description | | ---------------------- | ------------------------- | ------------------------------------------------------------------ | | `config` | `ILoadConfigConfig` | Config from `loadConfig()`. Required to decompress the `scenario` field. | | `stake` | `number` | Stake amount. Required for the first call of a gameround. | | `useTicket` | `boolean` | Use a freeplay ticket | | `useTicketFeatureType` | `string` | Feature type of the ticket | | `featureToBuy` | `string` | Feature to buy (e.g., `'freespin'`). Engine derives the price from the stake. | | `playerChoiceIndex` | `number` | Index of the selected option when `engineData.playerChoice` is set | | `additionalData` | `Record` | Additional game-specific parameters | --- ### `ICollectOptions` Options for `collect()`. Extends `ISessionOptions`: ```typescript interface ICollectOptions extends ISessionOptions { amount?: number; additionalData?: Record; } ``` | Field | Type | Description | | ---------------- | ------------------------- | ----------------------------------------------------------------- | | `amount` | `number` | Amount to collect. If you omit this field, the SDK collects the full available amount | | `additionalData` | `Record` | Additional game-specific parameters (e.g. crash `betHash` via `crashCollectData()`) | --- # Demo Open Fruits 95 ↗ --- # Game guides The creator supports 14 modes. Each guide defines its requests, returned scenario, player decisions, and settlement. Start with the shared [frontend guide](https://docs.hizi.io/engine/sdk/). Apply its loading, stakes, operator, jurisdiction, and recovery requirements to every vertical. | Creator mode | Guide | Round model | | --- | --- | --- | | `slot` | [Slots](https://docs.hizi.io/engine/games/slot.html) | One or more results, feature spins, choices, and optional gamble. | | `plinko` | [Plinko](https://docs.hizi.io/engine/games/plinko.html) | A selected row and risk table returns one bucket. | | `hilo` | [Hi/Lo](https://docs.hizi.io/engine/games/hilo.html) | Card choices, skips, and collect-or-continue. | | `overunder` | [Over/Under](https://docs.hizi.io/engine/games/overunder.html) | A threshold and direction select one roll outcome. | | `frogger` | [Frogger](https://docs.hizi.io/engine/games/frogger.html) | Successive survival steps with optional cashout. | | `mines` | [Mines](https://docs.hizi.io/engine/games/mines.html) | Player picks reveal tiles until cashout or a mine. | | `keno` | [Keno](https://docs.hizi.io/engine/games/keno.html) | Player selections and a draw settle together. | | `match` | [Match](https://docs.hizi.io/engine/games/match.html) | A numbered arrangement settles an EXACT or AT_LEAST match bet. | | `bingo` | [Bingo](https://docs.hizi.io/engine/games/bingo.html) | A draw resolves patterns across one or more tickets. | | `progressive` | [Progressive](https://docs.hizi.io/engine/games/progressive.html) | A configured step ladder offers cashout after survival. | | `crash` | [Crash](https://docs.hizi.io/engine/games/crash.html) | Server ticks and cashout events use the same live connection. | | `roulette` | [Roulette](https://docs.hizi.io/engine/games/roulette.html) | Multiple selections settle against one number. | | `blackjack` | [Blackjack](https://docs.hizi.io/engine/games/blackjack.html) | Hand actions can create extra debits before final settlement. | | `blank` | [Blank and custom](https://docs.hizi.io/engine/games/custom.html) | Imported configuration retains its game type and contract. | ## Shared implementation rules Validate the opening debit and any extra debit before submitting an action. Apply the effective policy for this player and vertical. A table selected through `featureToBuy` can be a normal bet variant with price `1`. Pass `config` to SDK `placeBet` for automatic scenario decoding. Decode collect results explicitly when required. Render authoritative results before enabling the next allowed action. Preserve original choice indexes and round identity. Never apply the slot continuation loop to a game that needs a player decision. Never infer the wallet credit from animation or a displayed cashout estimate. The [release checklist](https://docs.hizi.io/engine/sdk/release-checklist.html) covers acceptance cases for every mode. --- # Slot Game Responses This guide explains how to read the `scenario` field that `placeBet` returns for slot games. The shapes below target `@hizi.io/engine-sdk` 0.2.18. 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. Each API result contains one `IResult` scenario object. The generator can store an array, but the engine returns its current step. Cascades within one spin can also appear as the nested `cascadeData.steps` array. Implement the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) first. Use [rules and presentation](https://docs.hizi.io/engine/sdk/rules-and-presentation.html) to build the paytable from the effective configuration. ## 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; eventMultiplier?: 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>; ``` ```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; multipayResults?: IMultipayResult[]; winlineIndex?: number; progressionAwards?: Record; 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](https://docs.hizi.io/engine/sdk/response-handling.html#spininfo-and-nextfeature) 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 `multipayCount` records how many times an award pays. The parent's totals already contain those pays. Each pay can draw a different weighted result. `multipayResults` attributes each triggering pay to its positions and drawn outcome: ```typescript interface IMultipayResult { positions?: Array<{ column: number; row: number }>; winMultiplier?: number; spinMultiplierValue?: number; featureWin?: { featureName: string; featureCount: number }; progressionAwards?: Record; } ``` A pay's `winMultiplier` precedes the parent's `symbolMultiplierTotal`. Apply that multiplier only for the per-pay breakdown. Do not add the breakdown to the parent's win again. Missing pays can represent weighted outcomes that did not trigger. ### Payline identity For a winline win, use `winlineIndex` to select the exact effective line. Two wins can share starting positions but follow different lines. Resolve `config.winlines[scenario.featureName] ?? config.winlines.base` before indexing. These are the creator browser export's maps. An imported contract can use a different schema. See [rules and presentation](https://docs.hizi.io/engine/sdk/rules-and-presentation.html). ## 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](https://docs.hizi.io/engine/games/slot.html#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](https://docs.hizi.io/engine/games/slot.html#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 Animate every nested `cascadeData.steps` entry within the same response. A generator-level multi-result scenario uses separate API continuations. These are different levels. Do not send an extra request for each nested cascade. ::: ## 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. ### Collector, booster, and locked-cell state Use the returned snapshots and events. Do not reconstruct collector targets by comparing board images. | `IHoldAndWinData` field | Render | | --- | --- | | `collectorCollected` | Cumulative collector contribution, already in `totalMultiplier`. | | `collectorSweeps` | Collector landings on this respin only. Missing means no sweep. | | `boosterContributed` | Cumulative booster contribution, already in the total. | | `lockedCells[col][row]` | Cells that still cannot receive coins. Missing means no lock grid. | | `pendingCellModifiers` | Persistent modifiers on empty cells. They are not paid yet. | | `cellBoosterStamps` | Stamps created on this respin. Animate each stamp once. | | `cellBoosterContributed` | Cumulative applied cell boosts, already in the total. | ```typescript interface CollectorSweep { col: number; row: number; collected: number; multiplier?: number; from: Array<{ col: number; row: number }>; } interface PendingCellModifier { col: number; row: number; add: number; mult: number; } interface CellBoosterStamp { col: number; row: number; mode: 'add' | 'multiply'; value: number; resolved: boolean; } ``` A sweep's `from` cells identify the gathered coins. Read their values from the same snapshot's `multipliers` grid. Boosters resolve before collectors. `collected` already includes the sweep's optional multiplier. Collectors that land together use the same earlier snapshot. They do not gather each other on that respin. A collector from an earlier respin can be a target. A landing on an empty board still produces a zero-value sweep. A pending modifier transforms a future coin value `p` into `(p + add) * mult`. A stamp with `resolved: true` acts immediately. Otherwise, keep the marker until the returned state consumes it. Render the trigger board before respin events. Some creator payloads include a `holdAndWinEntryData` snapshot outside the current `IResult` declaration. Validate that extension against the published game. Do not require it on every game or expect collector sweeps on that entry snapshot. Keep the same grid, locked cells, banked total, and remaining lives when resuming. The feature's `ended` flag ends that feature. The engine's round state determines whether another feature or collection follows. ## Sticky Symbols During features with sticky symbols, `stuckSymbols` tracks which positions the engine locks: ```typescript type TStuckSymbols = Array>; // Same shape as board - null means the position is not stuck ``` ```typescript 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 feature - `bookSpinWin`: 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.collectorTargetData` lists 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` (on `collector` mode wins) lists the targets that a collector win actually gathers and the value it awards for each. - `scenario.collectorLevelData` is the level-progression state after this spin, when the slot has a `collectorLevelConfig`. ```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(); 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](https://docs.hizi.io/engine/sdk/progression-counters.html), which persist for a player across rounds. See [How this differs from Progression Counters](https://docs.hizi.io/engine/games/slot.html#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`. The round is then one stored multi-step scenario, described in [Single vs Multi-Result Scenarios](https://docs.hizi.io/engine/games/slot.html#single-vs-multi-result-scenarios) below. The engine returns one step per `placeBet` continuation, never an array. Each step carries 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 // One step per reply. Walk continuations while engineData.scenarioInfo reports inProgress. const step = gameResult.scenario 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 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 The API returns one scenario object per response. A multi-result entry advances through `engineData.scenarioInfo.currentScenarioIndex`. Its `inProgress` flag describes remaining entry steps. A consolidated round can include base play and several features. Read its `scenario.spinInfo`, `scenario.featureName`, and `scenario.nextFeature`. Use `engineData` as the fallback for games with runtime feature progression. A complete entry does not prove the whole round has settled. Inspect collect availability, player choices, and the round status. Follow the [slot controller](https://docs.hizi.io/engine/sdk/game-flow.html). ## Rendering a result The renderer receives an already decoded `IGameResult`. Route card and ladder gamble results to their specific views first. The fragment below handles a result with a slot board. `view` is your rendering adapter, not an SDK export. ```typescript import type { IGameResult, IResult } from '@hizi.io/engine-sdk'; async function present(result: IGameResult) { const slot = result.scenario as unknown as IResult; if (slot.cascadeData?.steps.length) { for (const step of slot.cascadeData.steps) { await view.presentCascade(step); } } else { await view.presentBoards(slot.boardStates, slot.board); await view.presentWins(slot.wins ?? [], slot.winAmount); } await view.presentFeatureState(slot); // The session controller decides the next legal action after timing and reports. } ``` Validate the scenario at your application boundary. Unknown symbols, missing boards, or unsupported required mechanics must block new play. Display a recoverable configuration error with the operator's approved behavior. Resolve rendering only after all required wins and transitions finish. Treat replay as presentation of saved state. Do not replay a debit, a wallet credit, or an operator round-start notification. ## 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.minStack`
`symbol.maxStack`
`symbol.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.multiplier`
`symbolMultiplierBehaviour` | 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.afterCascade`
`award.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 | `featureRandomEntryConfig`
`featureRandomEntryBaseWeighting` | 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 | `progressionCounters`
`awardResult.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 | `singleGameRoundProgression`
`awardResult.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](https://docs.hizi.io/engine/games/slot.html#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 | `maxPayout`
`consolidateRounds` | 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 tables. `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. | ## Player controls and disclosures Use [stakes and money](https://docs.hizi.io/engine/sdk/stakes-and-money.html) for stake buttons and purchases. Use [settings](https://docs.hizi.io/engine/sdk/settings-reference.html) for mute, turbo, stop, autoplay, and orientation. Suppress celebrations for returns no greater than total stakes when required. Keep the returned amount visible. Include the complete purchase cost when comparing a bought round with its return. Use [gamble and banking](https://docs.hizi.io/engine/sdk/gamble-features.html) for card and ladder gambles. Keep required acknowledgements and operator pause active across free features and recovery. Show rules for every configured item in the feature inventory below. Source the actual symbols, awards, limits, and probabilities from the published game. Do not infer them from a visual theme or reference title. ## 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`](https://docs.hizi.io/engine/sdk/endpoints.html#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](https://docs.hizi.io/engine/sdk/response-handling.html): the `IGameResult` structure and `engineData` fields. - [Plinko Responses](https://docs.hizi.io/engine/games/plinko.html): how to read plinko game responses. - [Types & Interfaces](https://docs.hizi.io/engine/sdk/types.html): the full SDK type reference. --- # Plinko Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to read the `scenario` field that `placeBet` returns for a plinko game. It also explains how to use the `loadConfig` data to map bucket indexes to multiplier values. ## Overview Plinko is a ball-drop game where a ball falls through rows of pegs into prize buckets. Each combination of row count and risk level (low/medium/high) produces a different set of bucket multipliers. The `minRows` and `maxRows` fields in the config give the row counts that the game offers. The shipped builder templates use 8 to 16 rows. The game uses buy features to let the player select a row/risk combination. ## The Plinko Scenario When `placeBet` returns a plinko result, `gameResult.scenario` contains: ```typescript interface IPlinkoScenario { bucket: number; // Bucket index the ball landed in (0 to rows) rows: number; // Number of rows played (minRows to maxRows) risk: string; // Risk level: "low" | "medium" | "high" multiplier: number; // Payout multiplier for this bucket } ``` ```typescript const response = await placeBet({ config, backendURL, token, stake, featureToBuy: 'rows_8_risk_low' }); if (response.success) { const { scenario, totalWin } = response.result.result; const plinko = scenario as IPlinkoScenario; console.log(`Rows: ${plinko.rows}, Risk: ${plinko.risk}`); console.log(`Ball landed in bucket ${plinko.bucket}`); console.log(`Multiplier: ${plinko.multiplier}x`); console.log(`Win: ${totalWin * stakeAmount}`); } ``` ## Bucket Multipliers from `loadConfig` The `loadConfig` response includes four plinko fields in the config: `minRows`, `maxRows`, `risks` and `multipliers`. `minRows` and `maxRows` give the lowest and the highest row count. `risks` lists the risk levels. Together they tell you which row/risk combinations exist. `multipliers` holds a pre-computed multiplier array for every combination. It is a flat object, not a nested one. Each key has the form `rows-{rows}-risk-{risk}`. Use this data to render the bucket labels in your UI before the player drops the ball. ```typescript const configResponse = await loadConfig({ backendURL, token }); if (configResponse.success) { const config = configResponse.result.config; const minRows = config.minRows as number; // e.g. 8 const maxRows = config.maxRows as number; // e.g. 16 const risks = config.risks as string[]; // ["low", "medium", "high"] const multipliers = config.multipliers as Record; // multipliers["rows-8-risk-low"] → [2.48, 1.48, 1.08, 0.91, 0.86, 0.91, 1.08, 1.48, 2.48] // multipliers["rows-8-risk-medium"] → [5.08, 1.99, 1.13, 0.83, 0.75, 0.83, 1.13, 1.99, 5.08] // multipliers["rows-8-risk-high"] → [9.97, 2.58, 1.14, 0.73, 0.63, 0.73, 1.14, 2.58, 9.97] // multipliers["rows-16-risk-high"] → [281.53, 46.44, 12.53, 4.6, 2.14, 1.21, ...] (17 buckets) } ``` Each array has `rows + 1` entries (one per bucket). Multipliers are symmetric. Edge buckets pay more than center buckets. The values above are the builder defaults for a 98% target RTP. Every game ships its own values, so read them from the config. ### Rendering bucket labels ```typescript function renderBuckets(rows: number, risk: string, multipliers: Record) { const mults = multipliers[`rows-${rows}-risk-${risk}`]; // mults.length === rows + 1 for (let i = 0; i < mults.length; i++) { renderBucketLabel(i, `${mults[i]}x`); } } ``` ## Selecting Row/Risk via Buy Features Every row/risk combination is a **buy feature**. There is no default basegame. Every `placeBet` call must include a `featureToBuy`. If a call does not include one, the engine rejects it with `PARAMETERMISSING` ("This game requires a buy feature. Provide `featureToBuy`."). ```typescript import { placeBet } from '@hizi.io/engine-sdk'; // Buy features are named like "rows_12_risk_high" const featureId = `rows_${selectedRows}_risk_${selectedRisk}`; const response = await placeBet({ config, backendURL, token, stake, featureToBuy: featureId, }); ``` ## Animating the Ball Drop The scenario gives you the final `bucket` index and the `rows` count. To animate the ball path, simulate the descent: ```typescript function animateBallDrop(rows: number, bucket: number) { // The ball makes `rows` binary decisions (left/right). // The bucket index equals the number of right turns. // Generate a random path that ends at the target bucket: const rights = bucket; // Total right turns needed const lefts = rows - bucket; // Total left turns needed // Shuffle to create a random-looking path to the known destination const moves = [...Array(rights).fill('right'), ...Array(lefts).fill('left')]; shuffleArray(moves); // Animate each peg bounce let x = 0; for (let row = 0; row < rows; row++) { if (moves[row] === 'right') x++; animateBounce(row, x); } // Ball lands in bucket at index x (=== bucket) } ``` ## Single-Result Game Plinko is a **single-result** game. Each `placeBet` call returns a complete result. `engineData.inProgress` is `false` after each spin. The game has no multi-step scenarios or continuation calls. ```typescript const response = await placeBet({ config, backendURL, token, stake, featureToBuy: featureId, }); if (response.success) { const { scenario, engineData, totalWin } = response.result.result; // engineData.inProgress === false - round is complete // No need to call placeBet again } ``` ## Complete Example ```typescript import { login, loadConfig, placeBet } from '@hizi.io/engine-sdk'; interface IPlinkoScenario { bucket: number; rows: number; risk: string; multiplier: number; } // After login and loadConfig... const config = configResponse.result.config; const multipliers = config.multipliers as Record; // Player selects 12 rows, high risk const rows = 12; const risk = 'high'; const featureId = `rows_${rows}_risk_${risk}`; // Show bucket multipliers before spin const mults = multipliers[`rows-${rows}-risk-${risk}`]; renderBuckets(mults); // [51.6, 10.26, 3.39, 1.55, 0.91, 0.67, 0.61, 0.67, 0.91, 1.55, 3.39, 10.26, 51.6] // Place the bet const response = await placeBet({ config, backendURL, token, stake, featureToBuy: featureId, }); if (response.success) { const plinko = response.result.result.scenario as IPlinkoScenario; const totalWin = response.result.result.totalWin; // Animate ball drop through 12 rows into bucket await animateBallDrop(plinko.rows, plinko.bucket); // Show result highlightBucket(plinko.bucket, `${plinko.multiplier}x`); showWin(totalWin * stake); } ``` ## Provably Fair Verification Plinko is one-shot: a single weighted entry draw lands the ball in its bucket. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to replay that draw from the committed seeds. ### Request ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, featureToBuy: 'rows_12_risk_high', // the round's row-count + risk package }); ``` ### Reading the response This is one-shot: `steps` has one entry. - `rngData`: one `int` row (the entry draw). Compare it to the live `pf.rngData` with an array-equal check. - `steps[0].scenario.bucket` (or whatever field your plinko build exposes): this value equals the live bucket. - `steps[0].totalWin`: this value equals the live multiplier. ```typescript if (!reply.success) return; const { rngData, steps } = reply.result; const rngOk = deepEqual(rngData, pf.rngData); const bucketOk = deepEqual(steps[0].scenario, liveScenario); ``` ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): the `IGameResult` structure and `engineData` fields. - [Slot Game Responses](https://docs.hizi.io/engine/games/slot.html): how to read hizi engine slot game responses. - [Buy Features](https://docs.hizi.io/engine/sdk/buy-features.html): how buy features work. --- # Frogger Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to read the `scenario` field that `placeBet` returns for a frogger-style (chicken-run / crash-walk) game. It also explains how to use the `loadConfig` data to display cashout multipliers. ## Overview Frogger is a multi-step wager game. The player places an initial bet at a chosen risk level. Then the player progresses through steps. At each step, the player can cash out or continue. If the player survives a step, the step multiplies the cashout value. If the player dies, the player loses everything. Each risk level has its own wager multiplier and cashout progression. The game uses **wager stake features** (`wagerStakeFeatures`). Each step is a separate feature. The entry's win is the absolute cashout multiplier for that step. If the player chooses to continue, the engine triggers the next step's feature automatically. ## The Frogger Scenario When `placeBet` returns a frogger result, `gameResult.scenario` contains: ```typescript interface IFroggerScenario { step: number; // Current step index (0 = initial bet) risk: string; // Risk level name: "low" | "medium" | "high" | "extreme" alive: boolean; // Whether the player survived this step cashoutMultiplier?: number; // Cashout value if the player collects here } ``` `cashoutMultiplier` is present only on a surviving entry. A death entry is `{ step, risk, alive: false }` and carries no multiplier. ### Step 0: Initial Bet The first `placeBet` call places the initial bet. This call must include a `featureToBuy` field that selects the risk level (see [Risk Selection via Buy Features](https://docs.hizi.io/engine/games/frogger.html#risk-selection-via-buy-features)). The result shows whether the player survived entry: ```typescript const response = await placeBet({ config, backendURL, token, stake, featureToBuy: `risk_${selectedRisk}` }); if (response.success) { const { scenario, engineData, totalWin } = response.result.result; const frogger = scenario as IFroggerScenario; if (frogger.alive) { // Player survived - show cashout offer console.log(`Cashout available: ${frogger.cashoutMultiplier}x`); // engineData.inProgress === true - player must wager or collect } else { // Instant death - player loses their bet console.log('Died on entry'); // engineData.inProgress === false - round is over } } ``` ### Subsequent Steps: Wager or Collect After the player survives, the game is **in progress** (`engineData.inProgress === true`). The player chooses one of these options: 1. **Collect**: cash out at the current multiplier 2. **Continue**: wager the current cashout for a higher multiplier ```typescript // Player wants to continue - place a wager bet const wagerResponse = await placeBet({ config, backendURL, token, stake, // Same stake as the original bet }); if (wagerResponse.success) { const { scenario, engineData, totalWin } = wagerResponse.result.result; const frogger = scenario as IFroggerScenario; if (frogger.alive) { // Survived - new cashout multiplier available console.log(`Step ${frogger.step}: ${frogger.cashoutMultiplier}x`); if (!engineData.inProgress) { // Final step - auto-cashout console.log(`Auto-cashout: ${totalWin}`); } } else { // Died - everything is lost console.log(`Busted at step ${frogger.step}`); // engineData.inProgress === false } } ``` To **collect** instead of continuing, use the collect/cashout mechanism rather than calling `placeBet` again. ### Collect Response: `deathStep` and `maxSteps` When the player collects, the engine copies the last step's `scenario` and adds two extra fields to it: ```typescript interface IFroggerCollectScenario { risk: string; step: number; // Step the player cashed out on alive: boolean; cashoutMultiplier?: number; // carried over from the collected step /** * The step the player *would have* busted on had they kept going - * `null` if the simulated run would have survived to the final step. */ deathStep: number | null; /** Maximum steps configured for this frogger table. */ maxSteps: number; } ``` `deathStep` is purely **cosmetic / informational**. It is a "what would have happened" readout for the UI (for example, "You cashed out at step 3. You would have busted on step 5!"). It is: - Simulated with `Math.random()`, not certified RNG. - Not part of certified gameplay. It does not affect the payout or the audit log. - `null` when the simulated run would have reached the final step without busting. `maxSteps` is the total number of progression steps configured for the table. Use it to render "cashed out at N of M" style displays. ```typescript const collectResponse = await collect({ backendURL, token }); if (collectResponse.success) { const { scenario, totalWin } = collectResponse.result.result; const frogger = scenario as IFroggerCollectScenario; if (frogger.deathStep === null) { showMessage(`Cashed out at step ${frogger.step}/${frogger.maxSteps} - you would have survived!`); } else { showMessage(`Cashed out at step ${frogger.step}/${frogger.maxSteps} - you would have busted on step ${frogger.deathStep}.`); } } ``` Note: `deathStep` and `maxSteps` are **only** present on the collect response. The `placeBet` scenario during normal play contains `{ risk, step, alive, cashoutMultiplier }`. ## Cashout Multipliers from `loadConfig` The `loadConfig` response includes the full cashout progression for each risk level. Use this to render the step ladder in your UI before the player starts. ```typescript const configResponse = await loadConfig({ backendURL, token }); if (configResponse.success) { const lc = configResponse.result.config; // lc.maxSteps - maximum number of progression steps // lc.risks - array of { name, steps, multipliers } // where multipliers is the cashout values for that risk per step // (index 0 is always 0, index N is the cashout multiplier at step N) const risks = lc.risks as Array<{ name: string; steps: number; multipliers: number[] }>; // risks[0] → { name: "low", steps: 10, multipliers: [0, 1.25, 1.5, 2, 2.5, 3, ...] } // risks[3] → { name: "extreme", steps: 5, multipliers: [0, 3, 9, 27, 81, 243] } } ``` ### Rendering the Step Ladder ```typescript function renderStepLadder(risk: { name: string; multipliers: number[] }) { // multipliers[0] = 0 (step 0 has no cashout) // multipliers[1] = cashout after surviving step 0 // multipliers[N] = cashout after surviving step N-1 for (let step = 1; step < risk.multipliers.length; step++) { renderStep(step, `${risk.multipliers[step]}x`); } } ``` ## How RTP Works in Frogger ### The house edge is taken once, on entry The engine applies the target RTP (for example, 95%) entirely at **step 0**, the initial bet. After that, every wager step is a **fair game**. Here is a concrete example with a **2x wager multiplier** and **95% target RTP**: | Step | Cashout | Survive chance | Cumulative RTP | | --------- | ------- | -------------- | -------------- | | 0 (entry) | N/A | **47.5%** | N/A | | 1 | 2x | 50% | 95% | | 2 | 4x | 50% | 95% | | 3 | 8x | 50% | 95% | | 4 | 16x | 50% | 95% | - **Step 0** survive chance = 95% ÷ 2.0 = **47.5%** (slightly below the "fair" 50%) - **Steps 1+** survive chance = **50%** (exactly fair, a coin flip for double-or-nothing) Step 0 is slightly harder than fair. This creates the 5% house edge. Once the player survives entry, every subsequent wager is neutral. The player no longer fights the house, only the odds. ### Why it's the same RTP at every cashout point No matter which step the player decides to cash out at, their expected return on the original bet is always 95%: - Cash out at step 1: 47.5% × 2x = **0.95x** - Cash out at step 2: 47.5% × 50% × 4x = **0.95x** - Cash out at step 5: 47.5% × 50%⁴ × 32x = **0.95x** This means players cannot gain an advantage by always cashing out early or by always going deep. The game is balanced at every step. ### With multiplier rounding When multiplier rounding is enabled, the builder rounds the cashout values at each step to clean numbers (for example, 2x, 4x, 8x instead of 1.97x, 3.88x, 7.65x). This happens once, when the game is generated. The builder then derives the survive weights from the rounded values, so the target RTP stays exact. Two things follow from this: - **Survive weights** at each step may vary slightly from the "fair" rate. - **Per-step win values** are absolute cashout multipliers (e.g. 1.25, 1.5, 2.0), not relative ratios. This avoids compounding rounding errors across many steps. The engine uses `wagerStakeFeatures` mode. Each entry's win IS the cashout multiplier at that step, applied directly to the stake. ## Risk Selection via Buy Features Every risk level is a **buy feature**. There is no default basegame. Every opening `placeBet` call must include a `featureToBuy` field. The engine rejects a call without one with `PARAMETERMISSING` ("This game requires a buy feature. Provide `featureToBuy`."). ```typescript import { placeBet } from '@hizi.io/engine-sdk'; // Buy features are named like "risk_low", "risk_medium", "risk_high", "risk_extreme" const featureId = `risk_${selectedRisk}`; const response = await placeBet({ config, backendURL, token, stake, featureToBuy: featureId, }); ``` `featureToBuy` is required only on the **opening** bet. The wager-stake continuations (step 1, step 2, and so on) are plain `placeBet` calls. The engine tracks the active feature across the round. ## Multi-Step Game Flow Frogger is a **multi-step wager game**. The typical flow: 1. **Initial bet**: call `placeBet` with `featureToBuy` for risk selection (required) 2. **Check result**: if `alive`, show the cashout offer; if dead, the round is over 3. **Player decides**: collect to end the round, or continue by calling `placeBet` again 4. **Repeat** until the player collects, dies, or reaches the final step (auto-cashout) ``` placeBet (step 0) → alive? → placeBet (step 1) → alive? → ... → collect or bust → dead? → round over ``` `engineData.inProgress` is `true` while the player has an active cashout to wager or collect. It becomes `false` when the player dies, collects, or hits the auto-cashout at the final step. ## Complete Example ```typescript import { login, loadConfig, placeBet } from '@hizi.io/engine-sdk'; interface IFroggerScenario { step: number; risk: string; alive: boolean; cashoutMultiplier?: number; // absent on a death entry } // After login and loadConfig... const config = configResponse.result.config; const risks = config.risks as Array<{ name: string; steps: number; multipliers: number[] }>; // Player selects "high" risk const selectedRisk = 'high'; const featureId = `risk_${selectedRisk}`; // Show the step ladder const risk = risks.find(r => r.name === selectedRisk)!; for (let i = 1; i < risk.multipliers.length; i++) { renderStep(i, `${risk.multipliers[i]}x`); } // Place initial bet with buy feature let response = await placeBet({ config, backendURL, token, stake, featureToBuy: featureId, }); // Game loop while (response.success) { const { scenario, engineData, totalWin } = response.result.result; const frogger = scenario as IFroggerScenario; if (!frogger.alive) { // Player died showBust(frogger.step); break; } if (!engineData.inProgress) { // Auto-cashout at final step showWin(totalWin * stake); break; } // Show cashout offer and wait for player decision const playerChoice = await showCashoutOffer(frogger.cashoutMultiplier); if (playerChoice === 'collect') { // Player collects - end the round showWin(totalWin * stake); break; } // Player continues - place next wager response = await placeBet({ config, backendURL, token, stake }); } ``` ## Provably Fair Verification Frogger sits on top of the fixed-odds pipeline. Each hop is a weighted entry draw. The wager-stake cascade is deterministic from the entries. There are no mid-round player choices that the seeds do not already imply. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to replay every hop from the committed seeds. ### Request ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, featureToBuy: 'risk_medium', // the round's risk tier actions: [{ playerChoiceIndex: 0 }, { playerChoiceIndex: 0 }], // one per continuation hop }); ``` `actions` drives the replay. Pass one `{ playerChoiceIndex: 0 }` for every continuation hop the live round played. The verifier chains each hop from the previous step's `nextFeature`, so the index value itself is ignored. Frogger surfaces no `playerChoice`. Without `actions` the reply contains the opening step only. ### Reading the response - `rngData`: one `int` row per hop (each entry draw). Compare it array-equal against the live `pf.rngData`. - `steps[i]`: the `IGameResult` after hop `i`, including `scenario.cashoutMultiplier`. `steps[0]` is the opening bet. Compare `steps[i]` against the live round's step `i`, in order. Send enough actions to make the two chains the same length. - `steps[steps.length - 1].totalWin`: the final cashout multiplier at the end of the round (or `0` if the frog died). The `deathStep` value shown by the live UI at collect time is **not** part of `pfVerify`'s output. It is `Math.random()`-driven cosmetic enrichment in `frogger.collect`, not PF-derived. ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): `IGameResult` structure and `engineData` fields. - [Plinko Responses](https://docs.hizi.io/engine/games/plinko.html): reads plinko game responses. - [Buy Features](https://docs.hizi.io/engine/sdk/buy-features.html): explains how buy features work. --- # Hi/Lo Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to read the `scenario` field that `placeBet` returns for a Hi/Lo card game. It also explains how to use the `loadConfig` data to display card odds and multipliers. ## Overview Hi/Lo is a multi-step wager game. The engine deals the player a card from an infinite deck (13 equally-likely values, Ace low through King high, 4 suits). The player chooses **Higher**, **Lower**, or **Skip**: - **Higher / Lower**: The player guesses the next card's rank. A win multiplies the cashout. A loss forfeits everything. - **Skip**: The player discards the card and receives a new one. The cashout multiplier does not change. The player can **collect** at any point between guesses. The game uses **wager features** (`wagerFeatures`). Each round's features multiply the accumulated `totalWin`. The basegame entry (`win: 1`) sets the initial stake as the base. Every subsequent feature multiplies from there. ## The Hi/Lo Scenario When `placeBet` returns a Hi/Lo result, `gameResult.scenario` contains: ```typescript interface IHiLoScenario { round: number; // Current round (0 = deal, 1+ = guesses) card: number; // Card value: 1 (Ace) through 13 (King) suit: string; // "hearts" | "diamonds" | "clubs" | "spades" action?: string; // "deal" (initial card) or "choose" (skip re-deal choice) guess?: string; // "higher" | "lower" | "skip" - present on guess results correct?: boolean; // Whether the guess was correct - present on higher/lower multiplier?: number; // Win multiplier - present on correct guesses } ``` The `card` and `suit` always represent the **drawn card**: - On deal/choose: the card the player sees and must decide on - On win/lose: the next card the engine drew (the result of the guess) - On skip: the new card the engine deals after the skip ## Game Flow ### Step 1: Initial Deal and Player Choice The first `placeBet` call deals a card. It presents the player's choices in `playerChoice`: ```typescript const response = await placeBet({ config, backendURL, token, stake: 100 }); if (response.success) { const { scenario, engineData } = response.result.result; const hilo = scenario as IHiLoScenario; // hilo.round === 0, hilo.card === 7, hilo.suit === "hearts" console.log(`Dealt: ${hilo.card} of ${hilo.suit}`); // engineData.playerChoice contains the available actions: // [ // { feature: "round_1_card_7_higher", count: 1 }, // { feature: "round_1_card_7_lower", count: 1 }, // { feature: "round_1_card_7_skip", count: 1 }, // ] } ``` ### Step 2: Player Guesses The player selects a choice by index, using `playerChoiceIndex`. Find the index by the feature name, not by a fixed position: ```typescript // Player chooses "higher" const higherIndex = engineData.playerChoice.findIndex(choice => choice.feature.endsWith('_higher')); const guessResponse = await placeBet({ config, backendURL, token, playerChoiceIndex: higherIndex, }); if (guessResponse.success) { const { scenario, engineData, totalWin } = guessResponse.result.result; const hilo = scenario as IHiLoScenario; if (hilo.correct) { // Won - scenario shows the drawn card console.log(`Correct! Drew ${hilo.card} of ${hilo.suit}, multiplier: ${hilo.multiplier}x`); console.log(`Cashout: ${totalWin}x`); // engineData.playerChoice has choices for the next card // engineData.canCollect === true - player can collect or continue } else { // Lost - scenario shows the card that beat you console.log(`Bust! Drew ${hilo.card} of ${hilo.suit}`); // engineData.inProgress === false - round is over } } ``` ### Skip: New Card, Same Multiplier When the player skips, the scenario shows the new card. The multiplier does not change: ```typescript // The skip choice is always the last entry in playerChoice const skipIndex = engineData.playerChoice.length - 1; const skipResponse = await placeBet({ config, backendURL, token, playerChoiceIndex: skipIndex, }); if (skipResponse.success) { const { scenario, engineData } = skipResponse.result.result; const hilo = scenario as IHiLoScenario; // hilo.guess === "skip" // hilo.card is the NEW card dealt console.log(`New card: ${hilo.card} of ${hilo.suit}`); // engineData.playerChoice has higher/lower/skip for the new card // totalWin is unchanged } ``` ### Collecting To cash out, call the collect endpoint instead of placing another bet: ```typescript const collectResponse = await collect({ backendURL, token }); // Player receives totalWin × stake ``` ## Loading Game Config The `loadConfig` response provides game parameters. It also provides the full odds and multiplier tables for round 1 (house edge) and round 2+ (fair): ```typescript const config = configResponse.result.config; config.gameType; // 'hilo' config.maxRounds; // Maximum guess rounds (e.g. 25) config.tiesWin; // Whether same-value cards count as a win config.strictExtremes; // When true, a tie loses on Ace + higher and on King + lower config.startingCardMin; // Minimum starting card value (e.g. 4) config.startingCardMax; // Maximum starting card value (e.g. 10) // Round 1 odds (house edge applied) config.round1Odds; // Array of { card, pHigher, pLower, higherMultiplier, lowerMultiplier } // Round 2+ odds (fair, 100% RTP) config.fairOdds; // Same structure, but with fair multipliers ``` ### Using the Odds Tables ```typescript interface IHiLoOdds { card: number; // 1 (Ace) through 13 (King) pHigher: number; // Probability of winning a "higher" guess pLower: number; // Probability of winning a "lower" guess higherMultiplier: number; // Payout multiplier for correct "higher" lowerMultiplier: number; // Payout multiplier for correct "lower" } const round1Odds = config.round1Odds as IHiLoOdds[]; const fairOdds = config.fairOdds as IHiLoOdds[]; // Show odds for current card (e.g. card value 7, round 1) const odds = round1Odds[6]; // 0-indexed: card 7 is index 6 console.log(`Higher: ${(odds.pHigher * 100).toFixed(1)}% → ${odds.higherMultiplier}x`); console.log(`Lower: ${(odds.pLower * 100).toFixed(1)}% → ${odds.lowerMultiplier}x`); // For round 2+, use fairOdds instead const fair = fairOdds[6]; console.log(`Fair higher: ${fair.higherMultiplier}x`); ``` ## How the Player Choice Works Hi/Lo uses `playerChoice` feature awards to present the higher/lower/skip options. The `playerChoiceIndex` in the `placeBet` request is a position in the awards array. The positions are not fixed. The builder adds the higher choice only when a higher guess can win. It adds the lower choice only when a lower guess can win. It always adds the skip choice last. | Choice | Feature pattern | | ------- | ---------------------------------- | | Higher | `round_{N}_card_{V}_higher` | | Lower | `round_{N}_card_{V}_lower` | | Skip | `round_{N}_card_{V}_skip` | ::: warning Do not hardcode an index. Read the `feature` name of each entry in `engineData.playerChoice`. Match on the suffix `_higher`, `_lower` or `_skip`. With the default `strictExtremes: true`, an Ace offers no lower choice and a King offers no higher choice. Skip is index 2 only for a card that offers both directions. ::: ## How RTP Works in Hi/Lo ### The house edge is taken once, on the first guess The engine applies the target RTP (for example, 98%) at **round 1** only. All subsequent guesses use **fair odds (100% RTP)**. For every card, the invariant holds: **P(win) × multiplier = effective RTP** | Round | Effective RTP | Multiplier formula | | ------- | ------------- | ------------------------- | | Round 1 | 98% (target) | `0.98 / P(win)` | | Round 2+ | 100% (fair) | `1.0 / P(win)` | ### Dynamic Odds: Strategy Does Not Change the EV The engine computes multipliers per card, based on the probability of winning that guess. Higher probability means lower multiplier. The expected value is identical for every card: | Card | Guess | P(win) | Multiplier | EV (Round 1) | | ---- | ------ | ------ | ---------- | ------------- | | A | Higher | 92.3% | 1.0617x | 0.98 | | 7 | Higher | 53.8% | 1.82x | 0.98 | | K | Lower | 92.3% | 1.0617x | 0.98 | | 7 | Lower | 53.8% | 1.82x | 0.98 | The default config sets `strictExtremes: true`. A tie then loses at exactly two cards: an Ace with "higher", and a King with "lower". So those two guesses win 12 times in 13, not always. **Skipping does not change the expected value.** It changes the variance (risk and reward), not the RTP. A player who skips to Aces wins almost every time. That player gains almost nothing. A player who plays middle cards wins less often. That player gets bigger payouts. The EV is the same either way. ### Multiplier rounding When the game config enables multiplier rounding, the builder snaps multipliers to clean numbers (for example, 1x, 1.5x, 2x). The builder does this once, at generate time. In the default `adjustMultipliers` mode the builder keeps the natural card weights. The rounded multiplier then moves the actual RTP off the target. In `adjustWeights` mode the builder adjusts the win and lose weights. Only that mode keeps the target RTP exact. ## Feature Structure Hi/Lo uses a layered feature structure: ``` basegame (regular feature, deal card, win=1) └─ playerChoice → round_1_card_7_higher | _lower | _skip round_1_card_7_higher (wagerFeature, resolves guess) ├─ win entries (per drawn card): multiplier, playerChoice → round_2_card_X_higher | _lower | _skip └─ lose entries (per drawn card): win=0, bust round_1_card_7_skip (wagerFeature, re-deals same round) └─ entries (per drawn card): win=1, playerChoice → round_1_card_X_higher | _lower | _skip round_N_card_V (wagerFeature, choice after skip re-deal) └─ single entry: win=1, playerChoice → round_N_card_V_higher | _lower | _skip ``` Key points: - **Basegame** is a regular feature, not a wager feature. `win: 1` sets `totalWin = 1× stake`. - **All round features** are wager features. Multipliers compound multiplicatively. - **Win and lose entries** each show the drawn card in the scenario. A win shows the card the player beat. A loss shows the card that beat the player. - **Win entries** include `playerChoice` for the next card. The player does not need another `placeBet` call. - **Skip entries** include `playerChoice` for the re-dealt card. The player does not need another `placeBet` call. - **Choice features** (`round_N_card_V`) are entered only through skip re-deals. The basegame skips them for the initial card. ## Complete Example ```typescript import { login, loadConfig, placeBet, collect } from '@hizi.io/engine-sdk'; interface IHiLoScenario { round: number; card: number; suit: string; guess?: string; correct?: boolean; multiplier?: number; } const CARD_LABELS: Record = { 1: 'A', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '10', 11: 'J', 12: 'Q', 13: 'K', }; // After login and loadConfig... // Deal initial card let response = await placeBet({ config, backendURL, token, stake: 100 }); // Game loop while (response.success) { const { scenario, engineData, totalWin } = response.result.result; const hilo = scenario as IHiLoScenario; if (hilo.guess && hilo.correct === false) { // Bust - show the card that beat the player showBust(CARD_LABELS[hilo.card], hilo.suit); break; } if (!engineData.inProgress) { // Auto-cashout at max rounds showWin(totalWin); break; } // Show current card and choices const choices = engineData.playerChoice; showCard(CARD_LABELS[hilo.card], hilo.suit); if (totalWin > 1) { showCashout(totalWin); // Player can collect } // Wait for player decision const decision = await getPlayerDecision(choices); if (decision === 'collect') { await collect({ backendURL, token }); showWin(totalWin); break; } // Continue with chosen action response = await placeBet({ config, backendURL, token, playerChoiceIndex: decision, // index into engineData.playerChoice }); } ``` ## Provably Fair Verification Hi/Lo is a multi-step game. Each guess (higher / lower / skip) draws a fresh card from the RNG. The verify path replays the round. It follows the same `playerChoice` decisions the player made live. ### Request Each continuation step contributes one entry in `actions`. The `playerChoiceIndex` field matches the offered choice (higher / lower / skip). ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, actions: [ { playerChoiceIndex: 0 }, // higher { playerChoiceIndex: 0 }, // higher { playerChoiceIndex: 2 }, // skip { playerChoiceIndex: 1 }, // lower - round ends here ], }); ``` ### Reading the response - `rngData`: one `int` row per drawn card. Compare it against the live `pf.rngData` array for equality. - `steps[0]`: the `IGameResult` for the initial deal. `steps[i + 1]`: the `IGameResult` after action `i`. Each step includes the revealed card in `scenario`. - `steps[steps.length - 1].totalWin`: equals the live terminal multiplier. The card sequence in the verified `steps[]` proves the engine could not have re-drawn a card to alter the outcome. ```typescript if (!reply.success) return; const { rngData, steps } = reply.result; const rngOk = deepEqual(rngData, pf.rngData); const terminalWinOk = steps[steps.length - 1].totalWin === lastLiveResult.totalWin; ``` ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): the `IGameResult` structure and `engineData` fields. - [Frogger Responses](https://docs.hizi.io/engine/games/frogger.html): another multi-step wager game with a collect-or-continue flow. - [Mines Responses](https://docs.hizi.io/engine/games/mines.html): a multi-step wager game with tile picking. --- # Mines Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to read the `scenario` field that `placeBet` returns for a mines-style game. It also explains how to read the grid size, the mine count and the cashout multiplier. ## Overview Mines is a multi-step wager game. The player places an initial bet with a chosen mine count. The player then reveals tiles on a grid. Each safe tile increases the cashout multiplier. If the player hits a mine, the player loses the entire stake. The player can cash out at any point between picks. The game uses **wager stake features** (`wagerStakeFeatures`). Each pick is a separate feature. The entry's win is the absolute cashout multiplier relative to the original stake. This avoids compounding rounding errors across many picks. ## Sending a Pick Position Every `placeBet` call for a mines game **must** include a `pickPosition` in `additionalData`. This tells the engine which tile the player selected: ```typescript // Initial bet - select mine count via featureToBuy + first tile pick await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'mines_10', additionalData: { pickPosition: 7 }, }); // Continuation - subsequent tile picks await placeBet({ config, backendURL, token, playerChoiceIndex: 0, additionalData: { pickPosition: 12 }, }); ``` ::: warning The engine returns an error if a call omits `pickPosition`. Every bet in a mines game requires a tile selection. ::: ## The Board Array The engine's game layer adds a `board` array to each `placeBet` response. This array shows the state of the full grid. Each element is one of the following: | Value | Meaning | | ---------- | ------------------------------------ | | `null` | Unknown. The engine has not revealed the tile. | | `'picked'` | Safe. The player picked this tile and survived. | | `'bomb'` | Mine. The player hit this tile, or the engine reveals it when the round ends. | ```typescript interface IMinesScenario { board: (null | 'bomb' | 'picked')[]; // Length = grid size (e.g. 25 for 5x5) currentMultiplier: number; // Current cashout multiplier mineCount: number; // Mines on this board } ``` ### Reading the Board ```typescript const response = await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'mines_10', additionalData: { pickPosition: 7 }, }); if (response.success) { const { scenario, engineData } = response.result.result; const { board, currentMultiplier, mineCount } = scenario as IMinesScenario; // board[7] is 'picked' (safe) or 'bomb' (mine hit) if (board[7] === 'picked') { console.log(`Safe! Cashout: ${currentMultiplier}x`); // engineData.canCollect === true - collect or continue } else { console.log('Boom!'); // All mine positions are revealed in the board } } ``` ### Game Over Reveal When the round ends (a mine hit, or auto-cashout on the final safe tile), the engine reveals all mine positions in the `board` array. Tiles that the player never picked remain `null`. If an unpicked tile contains a mine, it becomes `'bomb'`. ```typescript // After hitting a mine, the board might look like: // [null, 'picked', null, 'bomb', 'picked', null, null, 'bomb', null, ...] // ↑ safe ↑ mine ↑ safe ↑ the hit ``` ## Player Actions After a Safe Pick 1. **Collect**: cash out at the current multiplier. 2. **Continue**: reveal another tile for a higher multiplier. ```typescript // Player wants to continue const next = await placeBet({ config, backendURL, token, playerChoiceIndex: 0, additionalData: { pickPosition: 12 }, }); if (next.success) { const { board, currentMultiplier } = next.result.result.scenario as IMinesScenario; if (board[12] === 'picked') { console.log(`New cashout: ${currentMultiplier}x`); } else { console.log('Mine hit - round over'); } } ``` ## Loading Game Config A mines game ships no `loadConfig` block of its own. The generator emits only `rtp`, `gameType`, `features`, `wagerStakeFeatures`, `buyFeatures` and `featureWeights`. The publish step then adds an empty `loadConfig`. The reply carries no `gridSize`, `minMines` or `maxMines` field. `minMines` and `maxMines` are builder inputs only. Read the mine-count options from the buy features. Read the grid size, the mine count and the cashout multiplier from the scenario. ```typescript const config = configResponse.result.config; config.gameType; // 'mines' // Mine-count options: every buy-feature id is `mines_` const mineCounts = (config.buyFeatures ?? []).map(bf => Number(bf.id.slice('mines_'.length))); // After the first placeBet: scenario.board.length; // Grid size (e.g. 25 for 5x5) scenario.mineCount; // Mines on this board scenario.currentMultiplier; // Cashout multiplier after this pick ``` ::: tip A hand-authored config can supply `loadConfig.gridSize`. The engine reads it for the board reveal. If it is absent, the engine uses 25 tiles. ::: ## Mine Count Selection via Buy Features Every mine count is a **buy feature**. There is no default basegame. Every opening `placeBet` call must include a `featureToBuy`. If a call omits it, the engine returns `PARAMETERMISSING` ("This game requires a buy feature. Provide `featureToBuy`."). ```typescript import { placeBet } from '@hizi.io/engine-sdk'; // Player selects 10 mines - first pick at position 3 const response = await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'mines_10', additionalData: { pickPosition: 3 }, }); ``` The opening bet requires `featureToBuy`. Subsequent picks in the same round are plain `placeBet` calls. The engine tracks the active feature. ## Multi-Step Game Flow Mines is a multi-step wager stake game. The typical flow: 1. **Initial bet**: `placeBet` with `pickPosition` and `featureToBuy` for the mine count (required). 2. **Pick loop**: each `placeBet` call with `playerChoiceIndex: 0` and a new `pickPosition`. 3. **End**: the player collects, hits a mine, or reveals all safe tiles (auto-cashout). ``` placeBet (pick 1) → [safe] → placeBet (pick 2) → [safe] → ... → collect → [mine] → round ends (loss, mines revealed) ``` `engineData.canCollect` is `true` while the player has an active cashout. `engineData.inProgress` becomes `false` when the player hits a mine or reveals all safe tiles. ## How the Math Works The cashout multiplier at pick k is: `targetRTP / P(survive k picks)` Where `P(survive k picks) = product of (safeTiles - i) / (gridSize - i)` for i = 0 to k-1. No single pick carries the house edge. The builder applies the `targetRTP` factor once, inside every cashout multiplier. Each step's survive probability matches the fair odds exactly. The RTP therefore stays the same, regardless of when the player collects. ## Provably Fair Verification A Provably Fair (PF) mines round commits to every entry draw (one per pick) and to the seeded mine-reveal shuffle. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to make the engine replay the round and confirm both commitments. The engine seeds the bomb-reveal shuffle from the PF commitment itself, `(serverSeedHash, clientSeed)`. The verifier reproduces the same bomb layout without extra inputs. ### Request Replay every pick, including the opening one. Pass each step's `pickPosition` in `actions`, in order. The `playerChoiceIndex` of a continuation step only matters when the engine surfaces a `playerChoice` (for example, a risk pick mid-round). In the common case, use `playerChoiceIndex: 0`. ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, featureToBuy: 'mines_10', // every mines round opens on a buy feature actions: [ { pickPosition: 7 }, // opening pick { pickPosition: 12 }, // continuation { pickPosition: 18 }, // continuation that hit a mine - round ends here ], }); ``` ### Reading the response `steps[i].scenario` is an `IMinesScenario` for pick `i`, including the cumulative `board` and (on the terminal step) the revealed mines. Compare against the round's recorded scenarios: - `rngData`: compare it array-by-array against the live round's `pf.rngData`. Each pick contributes one `int` row (the weighted entry draw). - `steps[i].scenario.board`: compare it element-by-element against the live board at each step. Terminal-step bomb positions must match exactly. - `steps[steps.length - 1].totalWin` (multiplier): compare it to the live round's terminal `totalWin`. ```typescript if (!reply.success) return; // reply.error has the failure const { rngData, steps } = reply.result; const rngOk = rngData.every((row, i) => deepEqual(row, pf.rngData[i])); const terminal = steps[steps.length - 1]; const liveBoard = lastLiveResult.scenario.board; const boardOk = liveBoard.every((tile, i) => tile === terminal.scenario.board[i]); const cashoutOk = terminal.totalWin === lastLiveResult.totalWin; ``` --- # Keno Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to read the `scenario` field that `placeBet` returns for a keno game. It also explains how to use the `loadConfig` data to display the pay table. ## Overview Keno is a lottery-style game. The player picks 1–10 numbers from a grid of 40. The house draws random numbers. The engine pays the player based on how many picks match the draw. Each combination of pick count and risk level (low, medium, or high) has a different pay table. The grid size and draw count are configurable in the builder. Keno is a **single-result** game. Each `placeBet` returns a complete result. There are no multi-step wager chains. ## Sending Player Numbers Every keno round requires a `featureToBuy` (pick count and risk). See [Pick Count & Risk Selection via Buy Features](https://docs.hizi.io/engine/games/keno.html#pick-count-risk-selection-via-buy-features) for details. Send the player's chosen numbers via `additionalData`. If the request omits them, the engine auto-picks random numbers. ```typescript // Player selects their own numbers await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'picks_5_risk_medium', additionalData: { playerNumbers: [3, 7, 12, 25, 38] }, }); // Auto-pick - engine chooses the numbers await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'picks_5_risk_medium', }); ``` The number of `playerNumbers` must match the pick count for the selected feature. If the count does not match, the engine returns `INVALIDPARAMETER`. The engine auto-picks only when `playerNumbers` is absent or empty. ## The Keno Scenario The engine enriches each `placeBet` response with the full draw result: ```typescript interface IKenoScenario { picks: number; // How many numbers the player selected risk: string; // Risk level: "low" | "medium" | "high" matches: number; // How many player picks matched the draw multiplier: number; // Payout multiplier for this outcome playerNumbers: number[]; // Player's chosen numbers (1-indexed, sorted) drawnNumbers: number[]; // House-drawn numbers (1-indexed, sorted) gridSize: number; // Total numbers on the grid drawCount: number; // How many numbers the house draws } ``` ### Reading the Result ```typescript const response = await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'picks_5_risk_medium', additionalData: { playerNumbers: [3, 7, 12, 25, 38] }, }); if (response.success) { const scenario = response.result.result.scenario as IKenoScenario; console.log('Your numbers:', scenario.playerNumbers); // [3, 7, 12, 25, 38] console.log('House drew:', scenario.drawnNumbers); // [2, 7, 15, 19, 25, 28, 31, 33, 37, 40] console.log(`${scenario.matches} matches, ${scenario.multiplier}x`); // Highlight matches const drawnSet = new Set(scenario.drawnNumbers); for (const num of scenario.playerNumbers) { if (drawnSet.has(num)) { console.log(`${num} - MATCH`); } } } ``` ### Tile States for Rendering Each number on the grid falls into one of four states: | Player picked? | House drew? | State | Display | | -------------- | ----------- | ------- | ------------------ | | Yes | Yes | Match | Green highlight | | Yes | No | Miss | Blue (player pick) | | No | Yes | Drawn | Red highlight | | No | No | Neutral | Default | ## Pay Table from `loadConfig` The `loadConfig` response includes a `payTable` object containing multiplier arrays for every pick count and risk level: ```typescript const config = configResponse.result.config; const payTable = config.payTable as Record>; // payTable["3"]["low"] → [0, 1.5, 2, 4.5] // payTable["7"]["medium"] → [0, 0, 0, 3, 6.5, 17, 75, 530] config.gameType; // 'keno' config.gridSize; // 40 config.numberRange; // { start: 1, end: 40 } config.drawCount; // 10 config.minPicks; // 1 config.maxPicks; // 10 config.risks; // ["low", "medium", "high"] ``` Each array has `picks + 1` entries. Index 0 is the payout for 0 matches. Index 1 is the payout for 1 match. Later indexes follow the same pattern. An entry of 0 means no payout for that match count. `numberRange` gives the first and the last number printed on the grid. Read the grid labels from it. A grid does not always start at 1. ## Pick Count & Risk Selection via Buy Features Every combination of pick count and risk level is a **buy feature**. There is no default basegame. Every `placeBet` call must include a `featureToBuy`. The engine rejects a call without one with `PARAMETERMISSING` ("This game requires a buy feature. Provide `featureToBuy`."). ```typescript import { placeBet } from '@hizi.io/engine-sdk'; // Player selects 7 picks, medium risk, chooses their numbers const featureId = `picks_${selectedPicks}_risk_${selectedRisk}`; const response = await placeBet({ config, backendURL, token, stake, featureToBuy: featureId, additionalData: { playerNumbers: [2, 8, 15, 22, 29, 33, 40] }, }); ``` ## Single-Result Game Keno is a **single-result** game. `engineData.inProgress` is `false` after every result. The game does not need continuation calls or collect calls. ```typescript const response = await placeBet({ config, backendURL, token, stake, featureToBuy: 'picks_2_risk_low', additionalData: { playerNumbers: [5, 10] } }); if (response.success) { const { scenario, engineData, totalWin } = response.result.result; // engineData.inProgress === false - round is complete } ``` ## How the Math Works Keno uses the **hypergeometric distribution** to compute match probabilities. This distribution gives the probability of a fixed number of successes when the engine samples without replacement. The player picks `k` numbers from a grid of `N`. The house draws `D` numbers. The formula gives the probability of exactly `h` matches: ``` P(h | k, N, D) = C(k, h) × C(N − k, D − h) / C(N, D) ``` `C(n, r)` is the binomial coefficient (read as "n choose r"). The risk level controls the **payout shape**: - **Low risk**: frequent small wins. Payouts start from fewer matches. - **Medium risk**: a balance between frequency and size. The threshold is moderate. - **High risk**: rare but large wins. Payouts start from more matches. The builder auto-computes multipliers from the target RTP (return to player) and risk exponents. The builder has a "clean multipliers" option. When enabled, the builder snaps values to round numbers (for example, 1.5x, 5x, 75x). The builder then adjusts the entry weights to keep the target RTP. The engine reads the finished numbers from the config. ## Provably Fair Verification Keno is single-step. The PF (Provably Fair) chain commits to a single weighted entry draw. This draw fixes the match count. The engine seeds the cosmetic `playerNumbers` (when auto-picked) and `drawnNumbers` arrays from the PF commitment itself: `(serverSeedHash, clientSeed)`. This lets the engine and `pfVerify` reproduce the same values without extra inputs. ::: tip Send the player picks if they chose their own If the player typed their own `playerNumbers`, pass that array back to the verify call. If you omit it, the verifier auto-picks from the seed. Then the array does not match the live scenario. ::: ### Request ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, featureToBuy: 'picks_10_risk_high', // the round's `picks_N_risk_X` package playerNumbers: [3, 11, 17, 22, 29, 31, 38, 40, 41, 44], // omit for auto-pick }); ``` ### Reading the response Keno is one-shot. `steps` has one entry: - `rngData`: one `int` row (the entry weighted draw). It matches the live `pf.rngData` array exactly. - `steps[0].scenario.matches`: equals the live match count. - `steps[0].scenario.playerNumbers` / `drawnNumbers`: matches the live scenario element-wise. - `steps[0].totalWin`: equals the live multiplier. ```typescript if (!reply.success) return; const { rngData, steps } = reply.result; const matchOk = deepEqual(rngData, pf.rngData); const drawOk = deepEqual(steps[0].scenario.drawnNumbers, liveScenario.drawnNumbers); const winOk = steps[0].totalWin === liveResult.totalWin; ``` ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): the `IGameResult` structure and `engineData` fields. - [Plinko Responses](https://docs.hizi.io/engine/games/plinko.html): another single-result game with risk levels. - [Buy Features](https://docs.hizi.io/engine/sdk/buy-features.html): how buy features work. --- # Bingo Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to send a Bingo round to `placeBet`. It explains how to read the enriched `scenario` the engine returns. It explains how to use the `loadConfig` ruleset to render the paytable. The guide covers the boards the creator ships as templates: **Bingo 75**, **Bingo 90**, and **Bingo 80**. It also covers any other board that a config itself describes. ## Overview Bingo is a single-call, instant-win draw game. The player buys **1–6 tickets** at a chosen per-ticket stake. The engine generates the tickets. The engine draws a fixed number of balls from the pool. The engine daubs the matching numbers. The engine pays every completed pattern. The game has no continuation, no `playerChoice`, and no `collect` step. Each `placeBet` call returns a complete result. There is exactly one `bingo` game type. Every config describes its own board: grid, number pool, balls drawn, free cells, column bands, and the full list of prize patterns. The engine plays exactly what that config describes. "Bingo 75", "Bingo 90" and "Bingo 80" are **templates in the game creator**. They are ordinary configs. A designer can start from a template and edit it freely. A client reads the board shape from the scenario's own `rows`, `cols`, and `freeCellCount` fields. This works the same way for any config. The three boards the creator ships today: | | Bingo 75 | Bingo 90 | Bingo 80 | | --- | --- | --- | --- | | Number pool | 1–75 | 1–90 | 1–80 | | Balls drawn | 40 | 35 | 36 | | Ticket layout | 5×5, B-I-N-G-O column bands, free centre | 3×5, one flat pool | 4×4, one flat pool | | Numbers per ticket | 24 (+ free centre) | 15 | 16 | | Prize patterns | 11 | 13 | 10 | | Default pay mode | `sum` | `sum` | `highest` | | Top prize | ×5000 (full card) | ×5000 (full card) | ×5000 (coverall) | Any other board is equally valid. A config can state a 6×2 grid over a pool of 40 with its own patterns. The engine does not need to know about it in advance. Tickets are independent and identically distributed. Prizes are per ticket. Therefore RTP (return to player, the average share of stakes the game pays back over time) stays constant no matter how many tickets the player buys. ## Sending a Round Send the number of tickets in `additionalData`. The per-ticket stake is the top-level `stake` field. The engine generates the tickets and draws the balls on the server. The player does not choose any numbers. ```typescript import { placeBet, bingoTicketData } from '@hizi.io/engine-sdk'; const response = await placeBet({ config, backendURL, token, stake: 20, // per-ticket stake additionalData: bingoTicketData(3), // buy 3 tickets → total debit 60 }); ``` The helper `bingoTicketData(ticketCount)` returns `{ ticketCount }`. You may build that object inline instead. `ticketCount` defaults to `1` when omitted. It must be an integer in the range `1..maxTickets` (6 by default). Otherwise the engine returns `INVALIDPARAMETER`. ::: tip Per-ticket vs. total stake `stake` is the stake **per ticket**. The round debit equals `stake × ticketCount`. Each prize pays `multiplier × stake` per ticket. The maximum round exposure depends on `payMode`. Under `highest` mode only the top prize pays, so the ceiling is `5000 × stake × ticketCount` (Bingo 80). Under `sum` mode a full card completes every contained instance. The per-ticket ceiling is then the sum of all instance multipliers: **×7374.8** on Bingo 75 and **×7218.7** on Bingo 90. ::: ## The Bingo Scenario The engine enriches each `placeBet` response with the full round result: ```typescript interface IBingoScenario { gridSize: number; // size of the number pool rows: number; // ticket rows - render the ticket cols: number; // ticket columns from these two freeCellCount: number; // pre-daubed cells on a ticket drawCount: number; // balls drawn per round payMode: 'sum' | 'highest'; stakePerTicket: number; // smallest currency units ticketCount: number; // 1..maxTickets drawnNumbers: number[]; // balls drawn, in draw order tickets: IBingoTicketResult[]; totalStake: number; // stakePerTicket × ticketCount totalWin: number; // summed cash across all tickets } interface IBingoTicketResult { cells: number[]; // ticket numbers, row-major; free cells are 0 daubed: boolean[]; // parallel to cells: is each cell daubed? wins: IBingoPatternWin[]; // every completed pattern instance on this ticket ticketWin: number; // total cash won by this ticket } interface IBingoPatternWin { pattern: string; // stable pattern key, e.g. "fullCard", "line" instance: number; // which placement of the pattern matched multiplier: number; // payout multiplier for this pattern cells: number[]; // cell indices covered (row-major) - for highlighting winAmount: number; // multiplier × stakePerTicket } ``` ### Reading the Result ```typescript if (response.success) { const scenario = response.result.result.scenario as IBingoScenario; console.log('Balls:', scenario.drawnNumbers); scenario.tickets.forEach((ticket, i) => { console.log(`Ticket ${i + 1} won ${ticket.ticketWin}`); for (const win of ticket.wins) { console.log(` ${win.pattern} (${win.multiplier}x) → ${win.winAmount}`); } }); console.log('Total win:', scenario.totalWin); } ``` ### Cell layout and states `cells` is row-major: `index = row * scenario.cols + col`. The array is `scenario.rows * scenario.cols` cells long. Read `rows` and `cols` from the scenario to render the ticket. Do not assume a fixed grid width. Any pre-daubed free cell has `cells[i] === 0`. `scenario.freeCellCount` gives the number of free cells on a ticket (`0` for a board with no free space). Of the three creator templates, only **Bingo 75** has a free cell (the centre cell, `cells[12] === 0`, 25 cells total). **Bingo 90** (15 cells) and **Bingo 80** (16 cells) have none. | `cells[i]` | `daubed[i]` | Meaning | Display | | --- | --- | --- | --- | | `0` | `true` | Free cell (pre-daubed, if the board has one) | Star / pre-daubed | | number | `true` | Daubed (its ball was drawn) | Highlighted | | number | `false` | Not yet daubed | Default | To highlight a winning pattern, use `win.cells`. It lists the exact cell indices that instance covers. ## Contained Patterns Stack The engine evaluates every pattern **instance** independently. Overlapping patterns all pay. The amounts add together. Completing two full rows on a Bingo 90 ticket pays **both** single-line (`bingoRow`) prizes **and** the two-line (`twoLines`) prize. It also pays every shorter pattern inside those rows. Two adjacent rows complete 17 instances in total: six `rowThree`, four `square`, four `rowFour`, two `bingoRow` and one `twoLines`. Each instance is one entry in `wins`. Two non-adjacent rows (the top row and the bottom row) complete 13 instances, because no `square` fits. A full card also completes the frame, the diagonals, and every line it contains. Each of these pays. ## Paytable Multipliers come from the published paytable. A game can override them via `loadConfig.paytable` (keyed by pattern key). Prizes equal `multiplier × per-ticket stake`. ### Bingo 75 (creator template) | Pattern key | Name | Cells | Multiplier | | --- | --- | --- | --- | | `line` | Single Bingo (row/column off-centre) | 5 | ×0.3 | | `lineCentre` | Single Bingo Through Centre (through free centre) | 4 | ×0.35 | | `innerCorners` | Four Corners (inner corners) | 4 | ×1 | | `slimL` | Slim L | 7 | ×5 | | `x` | X (both diagonals) | 8 | ×10 | | `smallFrame` | Small Frame | 8 | ×20 | | `bigDiamond` | Large Diamond | 8 | ×25 | | `champagne` | Champagne Glass | 10 (incl. free centre) | ×100 | | `cornersFrame` | Four Corners and Small Frame | 12 | ×200 | | `bigFrame` | Large Frame | 16 | ×2000 | | `fullCard` | Full Card | 24 | ×5000 | ### Bingo 90 (creator template) | Pattern key | Name | Cells | Multiplier | | --- | --- | --- | --- | | `colThree` | Three in a Column | 3 | ×0.2 | | `rowThree` | Three in a Row (no diagonals) | 3 | ×0.2 | | `square` | Four in a Square (2×2) | 4 | ×0.3 | | `rowFour` | Four in a Row | 4 | ×1 | | `lForm` | L Shape | 5 | ×2.5 | | `bingoRow` | Bingo (full row) | 5 | ×10 | | `chevron` | Chevron | 5 | ×15 | | `plus` | Plus | 7 | ×25 | | `pyramid` | Pyramid | 8 | ×30 | | `centreBlock` | Centre Block (3×3) | 9 | ×100 | | `twoLines` | Two Lines | 10 | ×150 | | `hollow` | Hollow (border) | 12 | ×1500 | | `fullCard` | Full Card | 15 | ×5000 | ### Bingo 80 (creator template) No published spec exists for this board. The designers built its patterns and multipliers for this task. They tuned the paytable to land close to the RTP target of the other two templates under `highest` payMode. Its patterns overlap heavily by design. Because of this overlap, `sum` mode would push RTP well over 100%. | Pattern key | Name | Cells | Multiplier | | --- | --- | --- | --- | | `horizontalLine` | Any Line - Horizontal | 4 | ×0.35 | | `verticalLine` | Any Line - Vertical | 4 | ×0.3 | | `diagonal` | Diagonal | 4 | ×1 | | `fourCorners` | Four Corners | 4 | ×3 | | `centerSquare` | Center Square | 4 | ×5.5 | | `block2x2` | 2x2 Block | 4 | ×2.55 | | `letterT` | Letter T | 7 | ×16 | | `letterX` | Letter X | 8 | ×22 | | `frame` | Letter O / Frame | 12 | ×150 | | `coverall` | Coverall (Full House) | 16 | ×5000 | ## The `loadConfig` Ruleset A bingo game config ships an `IBingoLoadConfig`: ```typescript interface IBingoLoadConfig { // --- the board (all required: there is no preset to fall back on) --- rows: number; // ticket rows cols: number; // ticket columns gridSize: number; // number pool, 1..gridSize drawCount: number; // balls drawn per round; a direct RTP lever patterns: IBingoPatternConfig[]; // prize patterns (cell masks + multipliers) // --- optional --- freeCells?: number[]; // pre-daubed cell indices (default: none) columnRanges?: { start: number; end: number }[]; // 1 flat band, or exactly `cols` bands payMode?: 'sum' | 'highest'; // how completed patterns pay (default 'sum') paytable?: Record; // multiplier-only overrides by pattern key maxTickets?: number; // cap on tickets per round (defaults to 6) } ``` The whole board is data-driven. `patterns` defines the pattern set. The creator's pattern editor produces this set. `paytable` overrides individual multipliers on top of it. `drawCount` sets how many balls the engine draws from the fixed pool. This is a direct lever for RTP. `columnRanges` controls how the engine generates tickets. One range means a single flat draw for the whole ticket (Bingo 90 / 80). Exactly `cols` ranges means each column draws only from its own band (Bingo 75's B 1–15 … O 61–75). Each band must hold at least as many numbers as its column has drawable cells. The bands' widths must total no more than `gridSize`. The engine rejects a config that breaks either rule at bet time, not at config load. Bingo has no `loadConfig` handler of its own. The first `placeBet` returns `GAMECONFIGNOTSUPPORTED` with the reason in the message. Stake limits and the exposure cap come from `gameSettings` on the placeBet request, not from this config. The stake ladder and `minStake` apply to the **per-ticket** stake. `maxStake` caps the **whole round** (`stake × ticketCount`). ### Payout modes - **`sum`** (default): every completed pattern instance pays. The amounts add together (contained patterns stack, as described above). - **`highest`**: only the single highest-value completed instance on each ticket pays. The other instances still appear in `wins`, with `winAmount: 0`. The scenario's `payMode` field records which mode ran. ## Single-Result Game Bingo is a **single-result** game. `engineData.inProgress` is `false` after every result. The client needs no continuation or collect calls. ```typescript const { scenario, engineData, totalWin } = response.result.result; // engineData.inProgress === false - round is complete ``` ## How the Math Works The engine draws balls without replacement, uniformly, from the full pool (40 of 75, or 35 of 90). Consider a pattern instance that needs `k` specific ticket numbers (the free centre does not count). The probability that the engine draws all `k` numbers is the **hypergeometric coverage**: ``` P(all k drawn) = C(N − k, D − k) / C(N, D) ``` `N` is the pool size. `D` is the draw count. By linearity of expectation, the per-ticket RTP is the sum over every pattern instance of `P(all its cells drawn) × multiplier`. This sum does not depend on ticket count or on how patterns overlap. The Bingo 90 full card evaluates to **1 in 14,099,900.1**. This matches the client's displayed Hauptgewinnchance exactly. RTP is therefore a property of the paytable and the draw, not of any weighted entry pool. The default paytable lands at approximately **93.9% (Bingo 75)** and approximately **93.4% (Bingo 90)** in `sum` mode. Tune `loadConfig.paytable` or `drawCount` to reach a specific certified target. ### RTP in `highest` mode In `highest` mode, RTP is **not** the per-pattern sum. The engine pays one instance instead of all of them. RTP still has an exact closed form, so no simulation is needed. Every ticket number is equally likely to be drawn. Therefore the set of daubed cells is a uniformly random subset of the ticket's `c` non-free cells. The size of this subset is the hypergeometric daub count. So: > RTP = Σ over daubed-subsets `B` of the `c` cells `P(daubed = B) × maxMult(B)` `maxMult(B)` is the largest multiplier among patterns whose cells all lie in `B`. `P(daubed = B) = P(count = |B|) / C(c, |B|)`. This is a finite sum over `2^c` subsets (2¹⁶ for Bingo 80, 2¹⁵ for Bingo 90, 2²⁴ for Bingo 75). The engine exposes this calculation as `analyticRtpHighest(def)`. **Bingo 80** ships with `highest` payMode by default. Its patterns overlap heavily, so `sum` mode would run well over 100%. Bingo 80 evaluates to approximately **94.3%**, the closest of the three to the other templates' target. **Bingo 90** and **Bingo 75** normally ship `sum` mode. Run as a hypothetical under `highest` mode, their own paytables evaluate to **68.7%** and **81.3%** respectively. Both figures match a Monte-Carlo cross-check. ## Provably Fair Verification Bingo is one-shot. The ticket generation and the ball draw form a single certified-RNG call sequence. The round replays exactly from its seeds. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to reproduce the tickets, the draw, and the payouts. ::: tip Pass the ticket count Send the round's `stake` (per-ticket) and the `ticketCount` the player bought. The generated tickets and the draw are functions of the RNG sequence. The win amounts scale with the stake. ::: ### Request ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 20, // per-ticket stake from the round ticketCount: 3, // tickets bought on the round }); ``` ### Reading the response Bingo is one-shot. `steps` has one entry: - `rngData`: the certified draw calls (one `getDoubles` per ticket, then one for the balls). Check this array for an exact match against the live `pf.rngData`. - `steps[0].scenario.tickets` / `drawnNumbers`: check these for an element-wise match against the live scenario. - `steps[0].totalWin`: this equals the live round multiplier. ```typescript if (!reply.success) return; const { rngData, steps } = reply.result; const rngOk = deepEqual(rngData, pf.rngData); const drawOk = deepEqual(steps[0].scenario.drawnNumbers, liveScenario.drawnNumbers); const winOk = steps[0].totalWin === liveResult.totalWin; ``` ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): the `IGameResult` structure and `engineData` fields. - [Roulette Responses](https://docs.hizi.io/engine/games/roulette.html): another single-call draw game with no `.db`. - [Keno Responses](https://docs.hizi.io/engine/games/keno.html): another draw game built on hypergeometric coverage. --- # Match game responses Match draws a permutation of numbered balls into numbered slots. A match occurs when a ball lands in its own slot. ## Configuration and choices Read `config.minBalls`, `maxBalls`, `betModes`, and `bets`. Each bet contains: ```typescript interface MatchBet { balls: number; betMode: 'EXACT' | 'AT_LEAST'; targetK: number; table: string; winProbability: number; // Fraction, not a percentage. multiplier: number; } ``` Use the returned bets to build the selector. `EXACT` wins on exactly `targetK` matches. `AT_LEAST` wins on that count or more. A table can have an ID such as `balls_5_exact_2`. Match `bet.table` to a returned buy option. Do not construct IDs from labels. The creator uses a price-1 buy feature to select each ordinary bet. Apply the [vertical-specific permission mapping](https://docs.hizi.io/engine/sdk/stakes-and-money.html#feature-purchases). Lock ball count, mode, and target while the request runs. ## Request ```typescript const option = config.buyFeatures?.find(f => f.feature === selectedBet.table); if (!option) throw new Error('Selected table is unavailable'); const reply = await placeBet({ backendURL, token, config, stake: selectedStake, featureToBuy: option.id, }); if (!reply.success) throw new Error(reply.error.message); const result = reply.result.result; ``` Match resolves in one call. The engine enriches the selected outcome with an arrangement. The round closes automatically. Do not call `collect` for an ordinary completed Match round. ## Scenario The SDK does not export `IMatchScenario` in version 0.2.18. Define this local type and validate it at the renderer boundary: ```typescript interface IMatchScenario { balls: number; betMode: 'EXACT' | 'AT_LEAST'; targetK: number; matches: number; won: boolean; arrangement: number[]; matchedSlots: number[]; winProbability: number; multiplier: number; } ``` `arrangement[i]` is the ball in slot `i + 1`. Ball numbers and `matchedSlots` are one-based. Array indexes are zero-based. For example, `[1, 3, 2, 5, 4]` has one match, at slot 1. Animate that returned arrangement. Do not shuffle locally to choose the result. `multiplier` describes the bet's winning payout. On a loss, it can remain positive while `totalWin` is zero. Use `won` and the authoritative result. Convert the win using `engineData.baseStake`. Read per-bet odds and RTP from the supplied configuration. Rounding and outcome weights can make RTP differ between bets. ## Recovery and tests Restore a completed arrangement as history. Do not replay its bet. Test both modes, zero matches, maximum matches, a missing table, and a lost response. Verify that `matchedSlots` agrees with the displayed arrangement. A permutation cannot have exactly `balls - 1` matches. Do not offer an impossible target absent from the returned bet list. For provably fair rounds, use [PF verification](https://docs.hizi.io/engine/provably-fair.html) with the original stake and feature ID. Apply all [shared frontend requirements](https://docs.hizi.io/engine/sdk/), including stake limits, operator notifications, rules, and jurisdiction timing. --- # Progressive game responses The Progressive vertical is a sequence of survive-or-bust steps. Each surviving step increases the amount available for collection. This vertical is not a shared progressive jackpot. Its progression belongs to one game round. ## Configuration Read these fields directly from `loadConfig().result.config`: | Field | Meaning | | --- | --- | | `gameType` | `progressive`. | | `maxSteps` | Number of outcome steps before final automatic collection. | | `multipliers` | Cashout multipliers. Index `0` is zero; index `n + 1` corresponds to scenario step `n`. | | `surviveRates` | Per-step survival probabilities as fractions. Index `n` corresponds to scenario step `n`. | | `wagerStakeFeatures` | Continuation tables such as `step_1`. | | `rtp` | Return-to-player percentage for the supplied configuration. | Explain survival probabilities, payout growth, bust behavior, and maximum steps in the rules. Use the supplied values. Do not derive new probabilities from rounded display multipliers. ## Open and continue The opening bet uses the base game. It requires no `featureToBuy`. ```typescript const first = await placeBet({ backendURL, token, stake: selectedStake, config }); if (!first.success) throw new Error(first.error.message); // Render first.result.result before presenting Collect or Continue. ``` After a surviving result, inspect `canCollect`, `inProgress`, and any `playerChoice`. Require a deliberate player action to risk the win. ```typescript // Only after the player chooses Continue and the result permits continuation: const next = await placeBet({ backendURL, token, config }); // If playerChoice exists, send its original playerChoiceIndex instead. // Only while collection is offered: const collected = await collect({ backendURL, token }); ``` Do not send the stake again for a continuation. Do not automatically run every surviving step. The player must retain the offered cashout decision. ## Scenario ```typescript interface ProgressiveScenario { step: number; // Zero-based. alive: boolean; cashoutMultiplier?: number; // Present on a surviving step. } ``` This type is local to your frontend. SDK 0.2.18 does not export a Progressive scenario type. The engine uses absolute wager-stake multipliers. A 4× cashout after a 2× cashout is 4× the original stake, not 8×. A bust pays zero unless additional configured mechanics protect a banked amount. The final surviving step normally closes and credits automatically. Inspect returned round state before another action. The creator emits `step_1` through `step_(maxSteps - 1)`. Do not request a nonexistent `step_maxSteps` table. ## Recovery and tests Restore the last step, base stake, amount available, and pending choice. Test initial bust, middle bust, collection after each step, and final automatic closure. Test `maxSteps=1`, which has no continuation. Apply the [common requirements](https://docs.hizi.io/engine/sdk/) and the jurisdiction rules for this product classification. Do not apply slot bonus-buy restrictions to an ordinary Progressive opening bet. --- # Over/Under Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to read the `scenario` field that `placeBet` returns for an Over/Under game. It also explains how to use the `loadConfig` data to display odds and multipliers. ## Overview Over/Under is a single-roll wager game. The engine rolls a virtual 100-sided die (values 1–100). The player bets whether the result is **over** or **under** a chosen threshold. - **Under T**: wins if the roll is ≤ T. Win chance = T/100. - **Over T**: wins if the roll is > T. Win chance = (100 − T)/100. The payout multiplier scales inversely with win probability: **multiplier = RTP / P(win)**. RTP (Return to Player) is the average percentage of all stakes that the game pays back to players. Lower risk means smaller rewards. Higher risk means bigger wins. Each bet is a **buy feature** (a bet that the player buys directly, in one step). The player selects a bet and places it in a single `placeBet` call. The game has no multi-step flow. It has no collect step. ## The Over/Under Scenario When `placeBet` returns an Over/Under result, `gameResult.scenario` contains: ```typescript interface IOverUnderScenario { roll: number; // Die result: 1–100 threshold: number; // The chosen threshold betType: string; // "under" or "over" won: boolean; // Whether the bet was correct multiplier?: number; // Win multiplier - present on wins } ``` ## Game Flow ### Single-Step: Place Bet and Roll Each bet is a buy feature. The player picks a bet (`over_75`, `under_30`, etc.). The player calls `placeBet` with `featureToBuy`: ```typescript const response = await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'over_75', }); if (response.success) { const { scenario, totalWin } = response.result.result; const result = scenario as IOverUnderScenario; console.log(`Rolled: ${result.roll}`); if (result.won) { console.log(`Win! ${result.multiplier}x → payout ${totalWin}`); } else { console.log(`Loss`); } } ``` ::: tip The game resolves instantly. It has no collect step, no playerChoice, and no follow-up calls. One request produces one result. ::: ### Feature Naming Buy features follow the pattern `{direction}_{threshold}`: | Feature | Bet | Win condition | | ----------- | --------- | -------------- | | `under_1` | Under 1 | roll ≤ 1 | | `over_1` | Over 1 | roll > 1 | | `under_50` | Under 50 | roll ≤ 50 | | `over_50` | Over 50 | roll > 50 | | `under_99` | Under 99 | roll ≤ 99 | | `over_99` | Over 99 | roll > 99 | The game provides 198 bets (`under_1` through `under_99`, `over_1` through `over_99`) as buy features. ## Loading Game Config The `loadConfig` response provides the odds table: ```typescript const config = configResponse.result.config; config.diceMax; // 100 (die sides) config.odds; // Array of threshold odds ``` ### Using the Odds Table ```typescript interface IOverUnderOdds { threshold: number; // 1–99 pUnder: number; // P(roll <= threshold) pOver: number; // P(roll > threshold) underMultiplier: number; // Payout for correct "under" bet overMultiplier: number; // Payout for correct "over" bet } const odds = config.odds as IOverUnderOdds[]; // Show odds for threshold 75 const t75 = odds[74]; // 0-indexed: threshold 75 is index 74 console.log(`Under 75: ${(t75.pUnder * 100).toFixed(0)}% → ${t75.underMultiplier}x`); console.log(`Over 75: ${(t75.pOver * 100).toFixed(0)}% → ${t75.overMultiplier}x`); ``` ## How RTP Works The `rtpMode` setting in the builder controls the house edge in two ways: ### Adjust Multipliers (default) This mode keeps natural win probabilities. It reduces the multiplier to reach the target RTP. The house edge is visible in the payout. **P(win) × multiplier = target RTP** | Bet | P(win) | Multiplier (98% RTP) | EV | | -------- | ------ | -------------------- | ----- | | Under 50 | 50% | 1.96x | 0.98 | | Over 50 | 50% | 1.96x | 0.98 | | Under 98 | 98% | 1.0x | 0.98 | | Over 98 | 2% | 49.0x | 0.98 | ### Adjust Weights This mode uses a fair multiplier (1/P). It reduces the win probability to reach the target RTP. The house edge stays hidden in the win rate. **adjusted P(win) × fair multiplier = target RTP** | Bet | Multiplier | Adjusted P(win) | EV | | -------- | ---------- | --------------- | ----- | | Under 50 | 2.0x | 49% | 0.98 | | Over 50 | 2.0x | 49% | 0.98 | | Under 98 | 1.0204x | 96.04% | 0.98 | | Over 98 | 50.0x | 1.96% | 0.98 | Every bet has the same expected value, regardless of threshold or RTP mode. The player trades risk for reward. ### Multiplier rounding If you enable multiplier rounding, the builder snaps multipliers to clean numbers (e.g. 1x, 1.5x, 2x). The builder does this once, at generate time. In "Adjust Multipliers" mode, this may cause the actual RTP to deviate slightly from the target. In "Adjust Weights" mode, the builder adjusts weights to maintain the target RTP exactly. ## Feature Structure Over/Under uses standalone buy features. It has no basegame. Every `placeBet` call must include a `featureToBuy`. The engine rejects a call without one with `PARAMETERMISSING` ("This game requires a buy feature. Provide `featureToBuy`."). ``` under_50 (buy feature, resolves instantly) ├─ win entries (rolls 1–50): win = multiplier └─ lose entries (rolls 51–100): win = 0 over_75 (buy feature, resolves instantly) ├─ win entries (rolls 76–100): win = multiplier └─ lose entries (rolls 1–75): win = 0 ``` Key points: - **Every bet is a buy feature** that `featureToBuy` triggers. - **No wager features.** Each feature resolves directly to a final win amount. - **Win entries** show specific roll values from the winning range for visual variety. - **Lose entries** show specific roll values from the losing range. - **Single round.** One `placeBet` call gives an immediate result. ## Complete Example ```typescript import { login, loadConfig, placeBet } from '@hizi.io/engine-sdk'; interface IOverUnderScenario { roll: number; threshold: number; betType: string; won: boolean; multiplier?: number; } // After login and loadConfig... const odds = config.odds; // Player wants "Over 75" - show them the odds first const t75 = odds[74]; console.log(`Over 75: ${(t75.pOver * 100).toFixed(0)}% chance → ${t75.overMultiplier}x`); // Place bet and roll in one call const response = await placeBet({ config, backendURL, token, stake: 100, featureToBuy: 'over_75', }); if (response.success) { const result = response.result.result.scenario as IOverUnderScenario; const totalWin = response.result.result.totalWin; if (result.won) { console.log(`Rolled ${result.roll} - Win! ${result.multiplier}x → payout ${totalWin}`); } else { console.log(`Rolled ${result.roll} - Loss`); } } ``` ## Provably Fair Verification Over/Under is one-shot. A single entry draw (the random number the engine generates for the round) fixes the roll. The threshold and direction (from `featureToBuy`) decide the payout. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to replay the draw. ### Request ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, featureToBuy: 'over_50', // the round's threshold + direction package }); ``` ### Reading the response This is one-shot. `steps` has one entry. - `rngData`: one `int` row (the entry draw). Compare it to the live `pf.rngData`. The arrays must be equal. - `steps[0].scenario.roll`: this value equals the live roll value. - `steps[0].totalWin`: this value equals the live multiplier. ```typescript if (!reply.success) return; const { rngData, steps } = reply.result; const rollOk = steps[0].scenario.roll === liveScenario.roll; const winOk = steps[0].totalWin === liveResult.totalWin; ``` ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): `IGameResult` structure and `engineData` fields. - [Hi/Lo Responses](https://docs.hizi.io/engine/games/hilo.html): a multi-step card guessing game with cashout. - [Keno Responses](https://docs.hizi.io/engine/games/keno.html): a number selection game with instant resolution. --- # Roulette Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to send a roulette round to `placeBet`. It also explains how to read the enriched `scenario` object that the engine returns. Use the `loadConfig` ruleset to render payouts and validate bets on the client side. ## Overview Roulette is a wager game that resolves in a single `placeBet` call. The wheel is a European single-zero wheel with 37 pockets (0 to 36). The player places one or more bets. Each bet has its own type, selection, and stake. The engine draws a pocket. The engine settles every bet. The engine returns the result in a single response. The round has no continuation, no `playerChoice` step, and no `collect` step. - **One round = one `placeBet` call.** - **Round stake** is the sum of every bet's `stake`. Pass that sum as the top-level `stake` field. The engine validates that the two values match. - The engine returns **settlement** per bet in `scenario.settlement[]`. It also returns enriched pocket metadata (color, odd/even, dozen, column). ## Bet Types | Type | Selection shape | Winning pockets | Standard payout (total return) | | ----------- | ------------------------ | --------------- | ------------------------------ | | `straight` | `number` (0–36) | 1 | 36 | | `split` | `[number, number]` | 2 | 18 | | `street` | `number` (1–12, street index) | 3 | 12 | | `corner` | `[number, number, number, number]` | 4 | 9 | | `six_line` | `number` (1–11, six-line index) | 6 | 6 | | `red` | `null` / omit | 18 | 2 | | `black` | `null` / omit | 18 | 2 | | `odd` | `null` / omit | 18 | 2 | | `even` | `null` / omit | 18 | 2 | | `low_18` | `null` / omit (covers 1–18) | 18 | 2 | | `high_18` | `null` / omit (covers 19–36) | 18 | 2 | | `dozen` | `1` (1–12), `2` (13–24), `3` (25–36) | 12 | 3 | | `column` | `1` (column 1,4,7…), `2`, `3` | 12 | 3 | ::: tip The engine returns actual payouts in `loadConfig.payouts`. These may differ from the standard table. See [How RTP Works](https://docs.hizi.io/engine/games/roulette.html#how-rtp-works) below. ::: ### Selection Conventions - **Splits** must be adjacent on the layout. The engine accepts `[a, b]` where `a` and `b` share a row or column edge. This includes the 0-row splits `[0, 1]`, `[0, 2]`, and `[0, 3]`. Order does not matter. - **Streets** are indexed 1–12: street `s` covers pockets `{3s−2, 3s−1, 3s}`. Street 1 = `{1, 2, 3}`, street 12 = `{34, 35, 36}`. - **Corners** are 4-pocket squares anchored at the top-left pocket. The engine validates that the four pockets form a legal 2×2 block (not column 3, not crossing the table edge). - **Six-lines** are indexed 1–11: six-line `s` covers two adjacent streets, pockets `{3s−2 … 3s+3}`. ## The Roulette Scenario When `placeBet` returns a roulette result, `gameResult.scenario` contains: ```typescript interface IRouletteScenario { pocket: number; // Drawn pocket: 0–36 color: 'red' | 'black' | 'green'; // green = pocket 0 oddEven: 'odd' | 'even' | null; // null when pocket = 0 highLow: 'low_18' | 'high_18' | null; dozen: 1 | 2 | 3 | null; column: 1 | 2 | 3 | null; settlement: IRouletteBetSettlement[]; totalStake: number; // Sum of bet stakes totalWin: number; // Sum of all winAmounts, never capped } interface IRouletteBetSettlement { type: TRouletteBetType; selection?: number | [number, number] | [number, number, number, number] | 1 | 2 | 3 | null; stake: number; won: boolean; payout: number; // total return multiplier - stake × payout gives the bet's gross return winAmount: number; // stake × payout on a win, 0 on a loss. Not rounded } ``` ### Reading `winAmount` `winAmount` is the gross return of one bet: `stake × payout`. The engine does not round it. A `won: true` straight bet of 10 chips at standard payout 36 returns `winAmount = 10 × 36 = 360`. Under `adjust_payouts` the payouts are fractional, so `winAmount` is often fractional too. The same bet at the 98% payout 36.260 returns `winAmount = 362.60`. A losing bet has `winAmount = 0`. The engine does not return the stake separately, because it already debited the stake at `placeBet` time. The wallet receives one amount for the whole round, not one amount per bet. That amount is `Math.floor(gameResult.totalWin × stake)`. Treat every `winAmount` as a display value. Sum across the array: `scenario.totalWin === settlement.reduce((s, b) => s + b.winAmount, 0)`. ### `gameResult.totalWin` `gameResult.totalWin` is a **stake-relative multiplier**, not an amount. The engine credits `gameResult.totalWin × stake` to the player. For roulette specifically: ``` gameResult.totalWin = scenario.totalWin / scenario.totalStake ``` Use `scenario.totalWin` for display. Use `gameResult.totalWin` only when you compute the credited amount yourself. The `gameSettings.maxExposure` cap can break this identity. The engine clips `gameResult.totalWin` only. It never changes `scenario.totalWin`. See [Validation Rules](https://docs.hizi.io/engine/games/roulette.html#validation-rules). ## Game Flow ### Single-Step: Place All Bets and Spin ```typescript import { placeBet, rouletteAdditionalData, type IRouletteBet } from '@hizi.io/engine-sdk'; const bets: IRouletteBet[] = [ { type: 'straight', selection: 17, stake: 5 }, { type: 'red', stake: 20 }, { type: 'dozen', selection: 2, stake: 10 }, ]; const totalStake = bets.reduce((s, b) => s + b.stake, 0); // 35 const response = await placeBet({ config, backendURL, token, stake: totalStake, additionalData: rouletteAdditionalData(bets), // { bets } }); if (response.success) { const scenario = response.result.result.scenario as IRouletteScenario; console.log(`Pocket ${scenario.pocket} (${scenario.color})`); for (const s of scenario.settlement) { console.log(` ${s.type}${s.selection != null ? ` ${JSON.stringify(s.selection)}` : ''} - ${s.won ? `won ${s.winAmount}` : 'lost'}`); } console.log(`Total: staked ${scenario.totalStake}, won ${scenario.totalWin}`); } ``` The helper `rouletteAdditionalData(bets)` returns `{ bets }`. You may build that object inline instead. ::: warning The top-level `stake` must equal the sum of `bets[].stake`. If the values do not match, the engine returns `INVALIDPARAMETER` with `"Declared stake does not match bet total."` ::: ::: tip The round resolves instantly. It needs no `collect` step, no `playerChoice` step, and no follow-up calls. One request produces one fully settled result. ::: ## Loading Game Config `loadConfig.config` for a roulette game carries the ruleset. The SDK exports no type for it. Declare this interface in your own code: ```typescript interface IRouletteLoadConfig { wheelVariant: 'european'; useTargetRTP: boolean; // false = textbook payouts on a uniform wheel rtpMechanism: 'adjust_payouts' | 'adjust_odds'; targetRTP: number; // e.g. 0.98 enabledBets: Record; payouts: Record; // total return multipliers (stake × payout = win amount) } ``` The builder emits these six fields and no others. An older config can omit `useTargetRTP`. The engine then reads it as `true`. Stake limits and the exposure cap come from `gameSettings` on the `placeBet` request, not from this config. ### Using the Config Client-Side ```typescript const config = configResponse.result.config as IRouletteLoadConfig; // Render only the bet types this ruleset enables for (const type of Object.keys(config.enabledBets) as TRouletteBetType[]) { if (!config.enabledBets[type]) continue; const payout = config.payouts[type]; console.log(`${type}: ${payout.toFixed(3)}×`); } // Pre-validate the round stake before calling placeBet. // The limit is the effective maxStake on the loadConfig reply, not part of the ruleset. const maxStake = configResponse.result.config.maxStake as number | undefined; if (maxStake !== undefined && totalStake > maxStake) { throw new Error(`Stake ${totalStake} exceeds maxStake ${maxStake}`); } ``` ## How RTP Works The builder can reach the target RTP in two ways. RTP means return to player: the percentage of wagered money that the game returns to players over time. The `rtpMechanism` setting controls which way the builder uses. The `useTargetRTP` flag switches the whole mechanism on. When it is `false`, the wheel keeps uniform pocket weights and textbook payouts. The RTP is then the rules-derived 36/37, about 97.30%. ### `adjust_payouts` (default) Pocket weights stay uniform, at 1/37 each. The engine increases each bet type's payout so the RTP equals the target on every bet: ``` payout = (targetRTP × 37) / winningPockets ``` For 98% RTP: | Bet type | Winning pockets | Payout (total return) | EV | | ---------- | --------------- | --------------------- | ----- | | straight | 1 | 36.260 | 0.98 | | split | 2 | 18.130 | 0.98 | | street | 3 | 12.087 | 0.98 | | corner | 4 | 9.065 | 0.98 | | six_line | 6 | 6.043 | 0.98 | | red/black | 18 | 2.014 | 0.98 | | dozen/col | 12 | 3.022 | 0.98 | These payouts are slightly below the textbook values. This difference shows the house edge. ### `adjust_odds` Payouts stay at the textbook values (36 / 18 / 12 / 9 / 6 / 3 / 2). The engine reduces the pocket-0 weight so non-zero pockets cover `targetRTP` of the total weight: ``` weight(0) = round(1000000 × (1 − targetRTP)) weight(1..36) = floor((1000000 − weight(0)) / 36) ``` The 36 non-zero weights do not always divide evenly. The engine gives the remainder to the lowest pockets, one unit each. The builder previewer uses a smaller scale of 10000 and rounds each weight. Its preview odds can differ a little from the live wheel. Even-money, dozen, and column bets never include 0. These bets hit the textbook RTP exactly. Inside bets that include 0 (for example, straight on 0, or split `[0,1]`) have RTP below the target. This happens because the engine down-weights their winning pocket. The builder warns when this happens. The headline RTP shown in the previewer assumes a bet that does not include zero. ## Validation Rules The engine enforces the following at `placeBet` time. Any failure returns `INVALIDPARAMETER`: - `bets` is a non-empty array. - Every bet's `type` must be a member of `enabledBets`. Its value must be `true`. - Every bet's `stake` must be an integer. It must be greater than `0`. - Every bet's `selection` is shape-valid for its `type`. The engine checks splits and corners for adjacency. - `bets` holds at most 500 entries. A longer array returns `"Too many bets. Maximum 500."` - Top-level `stake` is an integer. It equals `sum(bets[].stake)` exactly. The engine compares the two values with no tolerance. The round stake has no ceiling in this config. The engine validates it against `gameSettings`, the same way as every other vertical. See [Stakes and money](https://docs.hizi.io/engine/sdk/stakes-and-money.html). `gameSettings.maxExposure` caps the round payout. The engine clips `gameResult.totalWin` so the credited amount lands at the cap. It leaves `scenario.totalWin` and every `settlement[].winAmount` unchanged. The two values then disagree. This cap is silent. The engine does not report it as an error. ## Pocket Color Reference ```typescript const RED_POCKETS = new Set([ 1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36, ]); // Black = 1..36 minus RED_POCKETS. Green = 0. ``` Each spin also provides the same metadata via `scenario.color`, `scenario.oddEven`, `scenario.dozen`, and `scenario.column`. Prefer these fields when you react to a result. Do not recompute them on the client. ## Complete Example ```typescript import { login, loadConfig, placeBet, rouletteAdditionalData, type IRouletteBet, type IRouletteScenario, } from '@hizi.io/engine-sdk'; // After login... IRouletteLoadConfig is the local interface declared above. const cfgResp = await loadConfig({ backendURL, token }); const config = cfgResp.result.config as IRouletteLoadConfig; // Player composes a round const bets: IRouletteBet[] = [ { type: 'straight', selection: 17, stake: 5 }, { type: 'split', selection: [4, 5], stake: 5 }, { type: 'corner', selection: [10, 11, 13, 14], stake: 5 }, { type: 'red', stake: 20 }, { type: 'dozen', selection: 2, stake: 10 }, ]; const totalStake = bets.reduce((s, b) => s + b.stake, 0); const maxStake = cfgResp.result.config.maxStake as number | undefined; if (maxStake !== undefined && totalStake > maxStake) throw new Error('Round stake too large'); const resp = await placeBet({ config, backendURL, token, stake: totalStake, additionalData: rouletteAdditionalData(bets), }); if (resp.success) { const scenario = resp.result.result.scenario as IRouletteScenario; console.log(`Wheel landed on ${scenario.pocket} (${scenario.color})`); for (const s of scenario.settlement) { if (s.won) { console.log(` ✓ ${s.type} - won ${s.winAmount} at ${s.payout}×`); } else { console.log(` ✗ ${s.type} - lost ${s.stake}`); } } console.log(`Net: ${scenario.totalWin - scenario.totalStake}`); } ``` ## Provably Fair Verification Roulette is one-shot. One weighted-index draw lands the pocket. A deterministic settlement step then pays out each bet. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to replay the pocket draw and the settlement from the round's seeds. ### Request Pass the round's stake and the exact `bets` array that the player submitted on the opening `placeBet` call. Both the pocket and the per-bet payouts are functions of those bets. ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 200, // total wagered (sum of bet stakes) bets: [ { type: 'straight', selection: 17, stake: 100 }, { type: 'red', stake: 100 }, ], }); ``` ### Reading the response This is a one-shot check. `steps` has one entry. - `rngData`: one `int` row (the weighted pocket draw). Compare it element by element against the live `pf.rngData`. - `steps[0].scenario.pocket`: equals the live pocket. - `steps[0].scenario.settlement`: matches the live per-bet `won`, `payout`, and `winAmount` values, element by element. - `steps[0].totalWin`: equals the live total. ```typescript if (!reply.success) return; const { steps } = reply.result; const pocketOk = steps[0].scenario.pocket === liveScenario.pocket; const settleOk = deepEqual(steps[0].scenario.settlement, liveScenario.settlement); ``` ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): the `IGameResult` structure and `engineData` fields. - [Over/Under Responses](https://docs.hizi.io/engine/games/overunder.html): another single-call wager game. - [Hi/Lo Responses](https://docs.hizi.io/engine/games/hilo.html): a multi-step playerChoice game with cashout. --- # Blackjack Game Responses Apply the shared [frontend requirements](https://docs.hizi.io/engine/sdk/) for loading, stakes, operator events, jurisdiction controls, and recovery. This guide explains how to send a blackjack round to `placeBet`. It explains how to read the `scenario` that the engine returns. It also explains how the `availableActions` in the scenario drive the multi-step action flow. ## Overview Blackjack is a **rules-based, multi-step** game. Unlike fixed-odds games, there is no entries database. The engine draws cards from a random number generator (RNG). It evaluates each action live. A round is a sequence of `placeBet` calls: 1. **Initial deal**: Send one `placeBet` call with the main stake and optional side bets. 2. **Action continuations**: Send one `placeBet` call per decision (hit, stand, double, split, insurance, even money) while `engineData.inProgress === true`. 3. **Settlement**: The final `placeBet` call ends the round. There is no separate `collect` call. `result.scenario` holds everything you need to render the round: engine state, dealer view, current hands, available actions, side bets, and totals. The field `engineData.inProgress` shows whether the round is still active. The engine returns rules switches once, via `loadConfig`. It does not repeat them on every `placeBet` call. ## Initial Deal Use `blackjackInitialData()` from `@hizi.io/engine-sdk` to send side bets with the opening bet. Omit side bets for a plain main-hand round. ```typescript import { placeBet, blackjackInitialData } from '@hizi.io/engine-sdk'; // Main hand only const first = await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackInitialData(), }); // Main hand + side bets const firstWithSides = await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackInitialData({ perfectPairs: { stake: 10 }, twentyOnePlusThree: { stake: 10 }, }), }); ``` The player posts side-bet stakes **in addition to** the main `stake`. The engine debits the full total. Both helpers return raw additional data. Put them in `additionalData`. Spreading them directly into SDK options drops `initialData` or `action` from the request. ## Action Continuations While the round is in progress, the engine surfaces the legal actions in `result.scenario.availableActions`. Build the next `placeBet` payload with `blackjackActionData()`. ```typescript import { placeBet, blackjackActionData } from '@hizi.io/engine-sdk'; // Hit on the current hand const next = await placeBet({ config, backendURL, token, stake: 100, // same base stake as the opening bet additionalData: blackjackActionData({ kind: 'hit' }), }); // Double - the engine will read supplementalStakeCost from the offered choice await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackActionData({ kind: 'double' }) }); // Split, insurance (with amount), even money await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackActionData({ kind: 'split' }) }); await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackActionData({ kind: 'insurance', amount: 50 }) }); await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackActionData({ kind: 'evenMoney' }) }); ``` Available action kinds: `hit`, `stand`, `double`, `split`, `insurance`, `evenMoney`. The scenario's `availableActions` array tells you which actions are currently legal. It also tells you any `supplementalStakeCost` the player must post to take them. ## The Blackjack Scenario `gameResult.scenario` is a single flat `IBlackjackScenario`. Settlement-only fields (`dealer.cards`, `dealer.total`, `hands[].outcome`, `hands[].payout`, side-bet `result`/`payout`, `insurance.won`/`payout`, `capped`/`uncappedTotalWin`) are absent mid-round. The engine adds these fields once `engineData.inProgress === false`. The engine drops the mid-round-only fields (`activeHand`, `phase`, `availableActions`) at settlement. The engine omits optional fields from the response when they would be empty, false, or a "no-op default". Almost no `false` flag or empty object reaches the wire. One exception exists: `insurance.won` carries `false` on a settled insurance bet that lost. `engineData.inProgress` is the source of truth for whether the round is still active. Mid-round, `phase` also tells you whether the next decision is about insurance or is a regular player turn. ```typescript interface IBlackjackScenario { dealer: { upcard: IBlackjackCard; peekState?: 'noBJ' | 'bj'; // present only when the dealer peeked cards?: IBlackjackCard[]; // settlement-only: full revealed hand total?: number; // settlement-only }; hands: IBlackjackHand[]; activeHand?: number; // index into `hands`; omitted once settled phase?: 'insurance_offered' | 'player_turn'; // omitted once settled availableActions?: IBlackjackChoice[]; // omitted when no decision pending sideBets?: { // omitted when no side bets placed perfectPairs?: IBlackjackPerfectPairsBet; twentyOnePlusThree?: IBlackjackTwentyOnePlusThreeBet; }; insurance?: IBlackjackInsuranceBet; evenMoneyTaken?: boolean; // present (true) only when even money was taken baseStake: number; totalStake: number; // base + every supplemental posted so far totalWin: number; // 0 mid-round, gross payout once settled capped?: boolean; // true when the operator's `maxExposure` clipped the payout uncappedTotalWin?: number; // pre-cap payout, present only when `capped` } interface IBlackjackHand { cards: IBlackjackCard[]; total: number; soft?: boolean; // omitted when false (no ace counted as 11) stake: number; isFromSplit?: boolean; isFromSplitAces?: boolean; busted?: boolean; stood?: boolean; doubled?: boolean; surrendered?: boolean; outcome?: 'win' | 'loss' | 'push' | 'blackjack' | 'bust' | 'surrender'; payout?: number; } interface IBlackjackChoice { action: 'hit' | 'stand' | 'double' | 'split' | 'insurance' | 'evenMoney'; supplementalStakeCost?: number; data?: Record; } interface IBlackjackPerfectPairsBet { stake: number; result?: 'mixed' | 'colored' | 'perfect' | 'loss'; payout?: number } interface IBlackjackTwentyOnePlusThreeBet { stake: number; result?: 'flush' | 'straight' | 'threeOfAKind' | 'straightFlush' | 'suitedTrips' | 'loss'; payout?: number } interface IBlackjackInsuranceBet { stake: number; won?: boolean; payout?: number } interface IBlackjackCard { rank: 'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K'; suit: 'S' | 'H' | 'D' | 'C'; } ``` The engine keeps ranks as strings (including `'10'` rather than `10`) so the frontend can render card faces directly. ### Multi-hand Rounds After a split, `hands` contains one entry per resulting hand. Each entry has its own `stake`, card list, and `total`. Once the round settles, each entry also has an `outcome`. `totalStake` is the sum of every posted stake in the round (main hand + splits + doubles + side bets + insurance). `totalWin` is the summed credit from all winning positions. ## Game Flow ``` placeBet (initial) → engineData.inProgress? ├─ false → terminal (dealer/player blackjack, all hands bust) - read settled scenario └─ true → scenario.availableActions presents legal actions └─ placeBet (action) → repeat until engineData.inProgress === false ``` `engineData.inProgress` is the gate for continuation calls. Mid-round, `scenario.phase` carries the finer-grained sub-state (`insurance_offered` vs `player_turn`). The engine drops the phase once the round settles. There is no `collect` call. The final `placeBet` call credits winnings automatically. ## Complete Example This action loop assumes a validated session, config, stake, and request lock. Apply operator blocks and cycle timing around it. `render`, `askPlayer`, and `mapToAction` are frontend adapters. ```typescript import { login, loadConfig, placeBet, blackjackInitialData, blackjackActionData, IBlackjackScenario } from '@hizi.io/engine-sdk'; let response = await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackInitialData(), }); while (response.success) { const { scenario, engineData } = response.result.result; const bj = scenario as IBlackjackScenario; if (!engineData.inProgress) { // Terminal - render dealer (full hand), all hands with outcomes, and payouts await render(bj); break; } await render(bj); const picked = await askPlayer(bj.availableActions); // your UI picks one // Recheck operator gates, legal action, balance, and supplementalStakeCost. response = await placeBet({ config, backendURL, token, stake: 100, additionalData: blackjackActionData(mapToAction(picked)), }); } ``` ## Provably Fair Verification Blackjack draws every card from the RNG on demand: opening deal, dealer peek, hit/double draws, split draws, dealer's hole and run-out. A provably fair (PF) round's `pf.rngData` field records every one of those calls. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to make the engine re-deal the round from its revealed seeds and your saved action list. This confirms that the cards, and therefore the payout, were never altered. ### Request Mirror the round's opening side bets in `initialData`. Mirror every player decision (in order) in `actions`. Each action is the raw `TRulesAction` object that your code sent on the live round. This is what `blackjackActionData()` would have built. There is no entry in `actions` for the initial deal itself. The engine does that automatically from the seeds. ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: pf.revealedServerSeed, clientSeed: pf.clientSeed, stake: 100, // base stake on the opening bet initialData: { sideBets: { perfectPairs: { stake: 10 }, twentyOnePlusThree: { stake: 10 } }, }, actions: [ { action: { kind: 'insurance', amount: 50 } }, // dealer Ace → declined or taken { action: { kind: 'hit' } }, { action: { kind: 'stand' } }, ], }); ``` ::: tip Insurance amount `{ kind: 'insurance' }` carries the `amount` the player wagered. Round it the same way the live round did (typically `baseStake / 2`). ::: ### Reading the response `steps[i].scenario` is the `IBlackjackScenario` after each action. It has the same shape that `placeBet` returned at that step. The terminal step (`steps[steps.length - 1]`, where `engineData.inProgress === false`) carries the fully settled scenario: dealer's revealed hand, every player hand's `outcome` / `payout`, side-bet results, insurance payout, capped totals. Two checks for a verified round: - `rngData` row-for-row matches the live `pf.rngData`. The engine draws cards as `int` rows. The dealer's peek also draws an `int` row. Every blackjack row is an `int` row. - `steps[i].scenario.dealer.cards` and each `hands[j].cards` match the live scenario at the same step. This proves the cards were not swapped. The terminal step's `totalWin` also matches. ```typescript if (!reply.success) return; // reply.error has the failure const { steps } = reply.result; const cardsOk = steps.every((step, i) => { const live = liveSteps[i].scenario as IBlackjackScenario; const verified = step.scenario as IBlackjackScenario; if (!deepEqual(live.dealer.cards, verified.dealer.cards)) return false; return live.hands.every((h, j) => deepEqual(h.cards, verified.hands[j].cards)); }); const terminal = steps[steps.length - 1]; const winOk = terminal.totalWin === lastLiveResult.totalWin; ``` ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): `IGameResult` structure and `engineData` fields. - [Crash Responses](https://docs.hizi.io/engine/games/crash.html): Another rules-based game, driven by websocket broadcasts rather than per-step actions. - [Types Reference](https://docs.hizi.io/engine/sdk/types.html): Full blackjack type surface (`TBlackjackAction`, `IBlackjackSideBets`, etc.). --- # Crash game responses Crash is a live game. The engine commits its bust point before the round starts. Server time controls the curve and all cashouts. Implement the shared [frontend requirements](https://docs.hizi.io/engine/sdk/). Use the [Crash transport](https://docs.hizi.io/engine/sdk/crash-transport.html) for requests and broadcasts on one connection. SDK 0.2.18 `enableWebSockets` discards these broadcasts. ## Opening a Round Validate each stake and the complete debit. Check whether the jurisdiction permits multiple simultaneous bets and automatic cashout targets. The following fragments assume an open `GameSocket` named `socket`. Its event handler must exist before the first request. ```typescript import { crashPlaceBetData, type IPlaceBetReply } from '@hizi.io/engine-sdk'; const bets = [ { stake: 100, autoCashOutMultiplier: 2.5 }, { stake: 100 }, ]; const response = await socket.request({ op: 'placeBet', token, ...crashPlaceBetData(bets), }); if (!response.success) { // Disable opening bets. Reconcile the session before another action. } ``` `crashPlaceBetData` returns `{ bets }`. SDK `additionalData` normally flattens this field into the request. With the raw transport, spread the helper directly. The total debit is the sum of the bet stakes. `autoCashOutMultiplier` must exceed `1`. Omit it for a manual bet. Do not implement automatic cashout by watching local animation ticks. A single manual bet can instead use `{ op: 'placeBet', token, stake: 100 }`. Lock the opening action before sending it. Enable the next round only after confirmed settlement and required cycle timing. ## Listening to the Curve The transport calls your handler with `(event, payload)`. Validate payloads against the exported `TCrashEvent` union before using them. | Event | Payload and action | | --- | --- | | `roundStarted` | `roundHash`, `betHashes` in submission order, `currentMultiplier: 0`, and optional `balance`. Bind the new round. | | `roundResumed` | `roundHash`, `startTime`, `currentMultiplier`, `betsSubmitted`, and `totalWinValue`. Replace local state with the restored state. | | `roundTick` | `currentMultiplier`. Update the visual curve. | | `betCashedOut` | `betHashes`, matching `winValues`, `winValue`, cumulative `totalWinValue`, `multiplier`, and `isAuto`. Mark those bets settled. | | `roundEnded` | `currentMultiplier`, plus `revealedServerSeed` on a provably fair round. `currentMultiplier` is absent when the round closes externally. The SDK's `ICrashRoundEndedPayload` does not declare `revealedServerSeed`, so read it from the raw frame. Stop the live curve and disable cashout. | | `balanceUpdated` | Cumulative `totalWinValue` and `winValues: { hash, value }[]`. Reconcile settled bets and refresh the authoritative balance. | Each event name also exists in `CRASH_EVENTS`. `betCashedOut` can occur several times. Replace cumulative `totalWinValue`; do not add it repeatedly. Most events do not include a round hash. Bind them to the active connection and round established by `roundStarted` or `roundResumed`. Ignore callbacks from an old connection after reconnecting. Events can arrive before the request reply. Do not reveal the bust point before `roundEnded`. Local interpolation estimates presentation only. It cannot establish a payable multiplier. ## Manual Cashout Use an unsettled bet hash from the server. Mark its request as pending before sending it. Prevent a second cashout request for the same bet. ```typescript import { crashCollectData, type ICollectReply } from '@hizi.io/engine-sdk'; const response = await socket.request({ op: 'collect', token, ...crashCollectData(betHash), }); ``` The helper accepts one hash or an array. The engine selects the multiplier when the request arrives. It rejects cashout after bust with `GAMEROUNDNOTACTIVE`. A successful manual cashout produces `betCashedOut` with `isAuto: false`. Reconcile both the reply and the event without displaying two credits. A lost reply does not authorize another cashout request without checking the bet state. ## Reconnecting Mid-Round The curve continues while the frontend is disconnected. Disable cashout and opening bets during recovery. Close the old transport and register the replacement handler. Refresh credentials when required. Load the authoritative round before deciding whether a live resume is necessary. For a confirmed open Crash round, send no new stake or bets: ```typescript const response = await socket.request({ op: 'placeBet', token }); ``` The engine returns the existing scenario and emits `roundResumed`. Restore settled bets, pending bets, the curve position, and cumulative winnings. Resume normal event handling. | Response | Required recovery | | --- | --- | | `GAMEROUNDALREADYSTARTED` | The live round can already be settled. Reload its result and balance before allowing another player-requested opening. | | `GAMEROUNDNOTACTIVE` | The live state is unavailable. Reconcile through loadConfig, history, or the operator. Do not assume wallet settlement. | | `DATASTRUCTUREWRONG` | State cannot be interpreted. Keep play blocked and report the operator error. | | Network error or timeout | Preserve uncertainty. Reconnect and reconcile; do not send another opening bet. | A resume can itself produce final settlement events. Continue receiving them during recovery. If state remains ambiguous, keep new play blocked. ## Scenario Shape ```typescript interface ICrashScenario { startTime: string; betsSubmitted: ICrashBet[]; betsCollected?: Record; } interface ICrashBet { hash: string; stake: number; autoCashOutMultiplier?: number; multiplierCollected?: number; amountCollected?: number; collected?: boolean; inProgress?: boolean; collectedAtinMS?: number; } ``` `startTime` is an ISO timestamp. `collectedAtinMS` measures milliseconds since that time. Stakes and collected amounts use minor currency units. Multipliers are decimal factors. The scenario describes accepted bets and cashouts. Do not reconstruct it from the currently displayed curve. ## Configuration Read `config.loadConfig` as the exported `ICrashLoadConfig` after validating its shape. | Field | Meaning | | --- | --- | | `startMultiplier` | Visual multiplier at curve start. | | `timerIntervalMs` | Server broadcast interval in milliseconds. | | `startGameDelayMs` | Delay before the curve rises. | | `gameRtp` | Return to player as a fraction, such as `0.95`. | | `minMultiplier`, `maxMultiplier` | Configured bounds on the drawn multiplier. `minMultiplier` is also the base factor of the curve itself. A config that raises `minMultiplier` rescales the whole curve. | | `growth` | Exponential growth per second. | | `postCashoutSpeedMultiplier` | Presentation speed factor after all bets cash out. | The engine can accelerate the remaining flight after every bet has settled. This does not create additional winnings. Do not confuse the configured `maxMultiplier` cap with the hidden bust point of a live round. ## Game Flow ```text validated player opening → request on the event connection → roundStarted → ticks → cashout events ↓ roundEnded → balanceUpdated → balance reconciliation ↓ timing and input gates → next player opening ``` Operator pause can block new actions. It cannot pause server time. Do not freeze the screen while presenting a cashout button as live. Agree the wrapper's behavior for active real-time rounds. The [release checklist](https://docs.hizi.io/engine/sdk/release-checklist.html) includes disconnection before cashout, events before replies, and repeated settlement messages. ## Provably Fair Verification Crash has one seed-derived surface: the committed `maxMultiplier`, which is the curve's bust point. (A seed-derived surface is the part of the outcome that the provably fair seeds determine.) Everything else is not derivable from the seeds. This includes the curve interpolation, the player's manual cashout time, and auto-cashout settlement. The engine tracks these by wall-clock time or by operator audit. Use [`pfVerify`](https://docs.hizi.io/engine/sdk/endpoints.html#pfverify) to confirm that the engine fixed the bust point before the round started. ::: warning Cashouts are not PF-derived A passing `pfVerify` call proves the engine could not have moved the bust point after it committed to that point. It does **not** verify that the engine settled your bet at the right multiplier. That check is a separate operator-audit concern (timestamps, multiplier-at-cashout reconciliation). ::: ### Request This request needs no actions and no bets. The seeds alone determine the round's `maxMultiplier`. ```typescript import { pfVerify } from '@hizi.io/engine-sdk'; const reply = await pfVerify({ backendURL, token, serverSeed: revealedServerSeed, // from the ROUND_ENDED broadcast clientSeed: pf.clientSeed, }); ``` ### Reading the response This is a one-shot check: `steps` has one entry. - `rngData`: one `double` row (the entropy that derives the multiplier). Compare it as an array against the live `pf.rngData`. - `steps[0].scenario.maxMultiplier`: equals the multiplier where the curve crashed on the live `ROUND_ENDED` event. ```typescript if (!reply.success) return; const { rngData, steps } = reply.result; const rngOk = deepEqual(rngData, pf.rngData); const bustOk = steps[0].scenario.maxMultiplier === liveCrashPoint; ``` If both values match, the engine committed to the bust point before the player placed any bets. ## Next Steps - [Response Handling](https://docs.hizi.io/engine/sdk/response-handling.html): the `IGameResult` structure and `engineData` fields. - [Blackjack Responses](https://docs.hizi.io/engine/games/blackjack.html): a rules-based game with a richer action tree. - [Types Reference](https://docs.hizi.io/engine/sdk/types.html): `ICrashScenario`, `ICrashBet`, `ICrashLoadConfig`, `TCrashEvent`, `CRASH_EVENTS`, `crashPlaceBetData`, `crashCollectData`. --- # Blank and imported game outcomes The creator's Blank mode imports or edits a custom outcome format. It is not a predefined game with one universal scenario type. An imported archive preserves its `gameType`. A Blank game without an imported type can use `blank`. The engine's fixed-odds path can also serve custom presentations such as instant wins or scratch cards. ## Required frontend contract Define these items with the archive author before implementing the renderer: | Contract item | Required content | | --- | --- | | Identity | Game type, archive version, and supported scenario schema. | | Presentation configuration | Symbol or item assets, dimensions, rules, paytable, and feature labels. | | Scenario schema | Every field, its type, units, optionality, and display meaning. | | Input | Required initial selections and any supported `additionalData`. | | Flow | Single result, stored scenario sequence, feature continuation, player choice, or collection. | | Money | Original base stake, purchase cost, win units, caps, and settlement behavior. | | Recovery | Fields sufficient to restore an interrupted presentation and pending decision. | | Compliance | Approved classification, messages, timing, probabilities, and certificate information. | Include this contract in the game's versioned frontend assets or documentation. Do not infer it from a filename or sample image. ## Fixed-odds request flow Call `placeBet` with the selected stake to open the round. Pass the loaded `config`. Render the returned `scenario` according to the agreed schema. A custom visual choice does not alter a server outcome unless the engine defines an input for it. An arbitrary `additionalData` field does not create new game logic. An array supplied to the generator becomes a sequence of scenario results. Use `engineData.scenarioInfo` and the current action state. Nested arrays within one scenario remain local presentation data. For a scratch-card presentation, reveal the committed result. Do not choose a new win when the player scratches another region. Implement reveal-all only where the approved product rules permit it. ## Reject unsupported contracts If `gameType` or a required scenario field is unknown, keep new play disabled. Display a configuration error. Do not fall back to a slot renderer or generate a local result. A custom contract still requires every [shared frontend function](https://docs.hizi.io/engine/sdk/): launch, settings, stakes, loading, operator events, recovery, errors, and jurisdiction behavior. --- # Appendix C - game settings parameters ## Definitions This appendix describes launch settings. See the [frontend reference](https://docs.hizi.io/engine/sdk/settings-reference.html) for the complete current field inventory. You use these settings when you launch a game. The settings control the features of that game. You can pass game settings in two ways. Use a predefined set of settings, called a profile, with the profile parameter of the getGameURL call. Or pass a valid JSON structure with the options parameter of this call. ## List of available game settings | | | | | | --- | --- | --- | --- | | *Setting* | *Type* | *Default value* | *Notes* | | autoplayEnabled | boolean | true | | | autoplayLossLimitRequired | boolean | false | | | displayCoins | boolean | true | | | displayJackpotOdds | boolean | false | | | displayRTP | boolean | false | | | displayXRTP | boolean | false | | | displayClock | boolean | true | If false, the game does not show a clock | | forceOrientation | none landscape portrait | none | Use one of the allowed values | | gambleEnabled | boolean | true | | | historyURL | string | not set | | | homeURL | string | not set | | | loadMsg | string | not set | | | maxExposure | number | not set | Maximum exposure, in 1/100 of base currency (see note below) | | maxStake | number | not set | Maximum stake, in 1/100 of base currency (see note below) | | minStake | number | not set | Minimum stake, in 1/100 of base currency (see note below) | | minLoadTime | number | not set | in milliseconds | | minSpinTime | number | not set | in milliseconds | | multipleInstancesAllowed | boolean | false | | | rcDisplayWinLoss | boolean | false | | | rcEnabled | boolean | false | | | rcInterval | number | not set | in milliseconds | | stopEnabled | boolean | true | | | topupURL | string | not set | | | turboEnabled | boolean | true | | | homeEnabled | boolean | true | If false, the game does not display a home button | | redirectTarget | 'self' \| 'parent' \| 'top' | top | Redirect target for the “Home” and “History” buttons | | dynamicMinSpinTime | boolean | false | Select an approved dynamic timing policy. The boolean does not define an averaging algorithm. See the frontend timing guide. | | defaultStakeIndex | number | Not set | Index for default stake (starts with 0) | | operatorHandlesErrors | boolean | false | If true, the game does not generate popups for errors. The game sends all errors to the frame with postMessage. | | customStakes | Array of numbers | Not set | An array of custom stakes available in the game, in 1/100 units of the game currency. The system does not apply a further multiplier to these values. Contact hizi.io support to activate this option for your operator account. Sample: [10000, 20000, 100000, 200000] | | abbreviateAmounts | boolean | true | Used for social casinos. It abbreviates large amounts in the balance or stake display, for example 1M instead of 1,000,000. Use this setting only for social casinos. | | showExactRTP | boolean | false | If true, the game shows the exact RTP (not rounded) in the frontend rules | | packageBuyEnabled | boolean | true | If true, the game allows feature package buy (if the game supports it). If false, the game session does not offer feature package buy. | | displaySessionTimer | boolean | false | This setting tells the game to display a session timer (for example, for UKGC) | | skipWinsEqualToOrLessThanStake | boolean | false | If true, the game does not celebrate small wins (for example, for UKGC) | | reportAnimationEnd | boolean | false | If true, the game sends the reportAnimationEnd call | | launcherType | 'reelLink' \| 'online' | Not set | Used for special integrations. Contact hizi.io support before you use this setting. | | autoplayShowTotalStake | boolean | false | Shows the total stake in the autoplay popup. Used for Brazilian legislation. | | translateErrors | boolean | false | Uses a standard translation for unexpected errors. Used for Brazilian legislation. | | currencyToDisplay | string | Not set | Use this string to display the currency. Mostly used for custom currencies or sweep stakes. | ::: tip `maxExposure`, `maxStake`, `minStake` and the currency multiplier You always specify these three values as integers in 1/100 of the **base** currency (no fractions). When a game launches in another currency, the system multiplies the value by that currency's multiplier. These values define the intended bounds. The current engine custom-stake path skips its separate min/max checks. Frontends must apply the effective bounds themselves. (By contrast, you give `customStakes` in 1/100 of the **game** currency. The system does not apply a multiplier to `customStakes`.) ::: --- ## Jurisdiction profiles Use the current [jurisdiction inventory](https://docs.hizi.io/engine/sdk/jurisdictions.html#platform-preset-inventory) for all 13 named Hizi presets. That reference records source revisions and frontend behavior. A preset contains platform defaults. It does not establish game certification or complete legal compliance. Apply the player's limits and the requirements for the product. For example, the UKGC preset needs player-specific stake caps and different timing for non-slot casino games. The [settings reference](https://docs.hizi.io/engine/sdk/settings-reference.html) lists every SDK field and additional Hizi settings. It defines the units and required controls. --- ## List of available game variables Game variables are settings that the system forwards to the game client. The system passes only known variables. If you pass another variable in the variables parameter of getGameURL, the API throws an invalid parameter error. | | | | | --- | --- | --- | | *Setting* | *Type* | *Notes* | | rcElapsed | number | Time elapsed for games with reality check enabled | --- --- # Appendix D - Freeplays ## Basics These calls allow the operator to grant freeplays to a player. A freeplay is like a free spin in slot games. The operator account must have this feature enabled. Contact customer support to check if the feature is enabled. The operator grants freeplays for a specific game and stake (amount and currency). The operator can set an expiry date. After this date, the granted freeplays are not available. Each time the operator grants a package to a player, the operator must provide a unique serial for the package. Calls with the same serial are idempotent. This means repeated assignPlayerFreerounds calls with the same serial do not change or assign information linked to that serial twice. When a player uses a freeplay, the doTransactions call includes additional information. The operator side needs no further changes when a player uses freeplays. The operator must accept debit transactions with an amount of zero and a serial. This serial must be known to the operator from a previous assignPlayerFreerounds call. --- ## assignPlayerFreerounds - assign a package of freeplays to a player The operator sends this call to hizi.io to grant freeplays for a given stake and game to a player. | | | | | | --- | --- | --- | --- | | **POST /assignPlayerFreerounds** | | | Assign a package of freeplays for a game and stake to a player | | *fields to sign (order is important)* | | | | | **secret + X-H-AUTH-ID + X-H-TIMESTAMP + playerId + gameCode + serial + currency + stake + count** | | | | | *mandatory request header fields* | | | | | **X-H-AUTH-ID, X-H-AUTH-SIG, X-H-TIMESTAMP** | | | | | *request Parameters (json payload)* | | | | | *attribute* | *type* | *mandatory* | *notes* | | **playerId** | string | YES | ID that identifies the player at the operator site | | **gameCode** | string | YES | The game ID of the game for which the operator grants the freeplays | | **serial** | string | YES | A unique identifier for this package. This call is idempotent for this parameter. The operator must generate a unique serial. The operator should use a UUID. If the player uses the freeplay in a later debit transaction, hizi.io sends this serial as a reference. | | **currency** | currency | YES | Currency for which the operator grants the freeplay | | **stake** | amount | YES | Stake in the game for which the operator grants the freeplay | | **count** | number | YES | Number of freeplays that the operator grants in this package | | **expires** | timestamp | NO | If set, the freeplay expires after this date. If not set, the expiry date defaults to 180 days after the call. | | *returned attributes* | | | | | *attribute* | *type* | *mandatory* | *notes* | | **serial** | string | YES | Serial of the created package (the same value the operator passed) | | **errorCode** | errorCode | NO | If an error occurs, the HTTP status is not 200. hizi.io can return additional error information. | ### Sample call to grant a freeplay package | | | | --- | --- | | *Sample call grant a freeplay package* | *Description* | | **POST /assignPlayerFreerounds** | Assign a package of freeplays | | *Sample payload* | *Description* | | { "playerId": "141ad0ffab554ba8b9022db6dfdc05ad", "gameCode": "game-oddoreven", "serial": "unique-value", "currency": "EUR", "stake": "100", "count": "4", "expires": "2020-12-07T00:00:00Z" } | This call creates 4 freeplays. They are not valid after 7-Dec-2020. The signature must include the fields shown in red. | | *Sample response (****success****)* | *Description* | | { "serial": "unique-value", "internalid": "4596dbe02be2483598f261b7f8c853cd", "count": 4, "used": 0, "expires": "2020-12-07T00:00:00.000Z" } | Information about the created package | --- ## revokePlayerFreerounds - delete freeplays from a player The operator sends this call to hizi.io to revoke (delete) previously granted freeplay packages. This call is idempotent. It always returns the same result when the operator sends it more than once. | | | | | | --- | --- | --- | --- | | **POST /revokePlayerFreerounds** | | | Revoke a package of freeplays granted to a player | | *fields to sign (order is important)* | | | | | **secret + X-H-AUTH-ID + X-H-TIMESTAMP + playerId + gameCode + serial** | | | | | *mandatory request header fields* | | | | | **X-H-AUTH-ID, X-H-AUTH-SIG, X-H-TIMESTAMP** | | | | | *request Parameters (json payload)* | | | | | *attribute* | *type* | *mandatory* | *notes* | | **playerId** | string | YES | ID that identifies the player at the operator site | | **gameCode** | string | YES | The game ID of the game for which the operator granted the freeplay. The operator must supply this ID so hizi.io transmits the correct information. | | **serial** | string | YES | A unique identifier for this package. The operator must have used this serial in a previous assignPlayerFreerounds call with the same gameCode and playerId. | | *returned attributes* | | | | | *attribute* | *type* | *mandatory* | *notes* | | **serialRevoked** | string | YES | Serial that hizi.io revoked (the same serial the operator passed in the body of this call) | | **errorCode** | errorCode | NO | If an error occurs, the HTTP status is not 200. hizi.io can return additional error information. | ### Sample call to revoke a freeplay package | | | | --- | --- | | *Sample call* | *Description* | | **POST /revokePlayerFreerounds** | Revoke a previously granted package of freeplays | | *Sample payload* | *Description* | | { "playerId": "141ad0ffab554ba8b9022db6dfdc05ad", "gameCode": "game-oddoreven", "serial": "dbf8907c-8002-4bdc-8804-3b1ec00d21be" } | This call revokes a package with the given information | | *Sample response (****success****)* | *Description* | | { "serialRevoked": "dbf8907c-8002-4bdc-8804-3b1ec00d21be" } | hizi.io revoked the package | --- ## getPlayerFreerounds - query freeplays granted to a player The operator sends this call to hizi.io to query available freeplay packages. hizi.io does not report used or expired packages. | | | | | | --- | --- | --- | --- | | **GET /getPlayerFreerounds** | | | Query freeplay packages | | *fields to sign (order is important)* | | | | | **secret + X-H-AUTH-ID + X-H-TIMESTAMP + playerId + gameCode + currency** | | | | | *mandatory request header fields* | | | | | **X-H-AUTH-ID, X-H-AUTH-SIG, X-H-TIMESTAMP** | | | | | *request Parameters (URI parameters)* | | | | | *attribute* | *type* | *mandatory* | *notes* | | **playerId** | string | YES | ID that identifies the player at the operator site | | **gameCode** | string | YES | The game ID of the game for which the operator queries the freeplays | | **currency** | currency | YES | Currency for which the operator queries the freeplays | | *returned attributes* | | | | | *attribute* | *type* | *mandatory* | *notes* | | **tickets** | Array of ticket structure | YES | Returns the available packages, with serial, currency, stake, and count for each | | **errorCode** | errorCode | NO | If an error occurs, the HTTP status is not 200. hizi.io can return additional error information. | ### Sample call to query available freeplay packages | | | | --- | --- | | *Sample call* | *Description* | | **GET /getPlayerFreerounds?playerId=141ad0ffab554ba8b9022db6dfdc05ad&gameCode=game-oddoreven¤cy=EUR** | Queries the freeplays available for a game, player, and currency | | *Sample response (****success****)* | *Description* | | { "tickets": [ { "serial": "unique-value", "currency": "EUR", "stake": 100, "availableTickets": 4 }, { "serial": "959e3491-4d90-429b-be21-d5bde520982c", "currency": "EUR", "stake": 100, "availableTickets": 2 }, { "serial": "dbf8907c-8002-4bdc-8804-3b1ec00d21be", "currency": "EUR", "stake": 500, "availableTickets": 1 } ] } | hizi.io returns three packages. Two packages have a stake of 100 (1 EUR). One package has a stake of 5 EUR. In total, 6 freeplays are available for spins with a stake of 1 EUR. 1 freeplay is available for spins with a stake of 5 EUR. | registerFreeroundTemplate [WORK IN PROGRESS - CONTACT SUPPORT FOR DETAILS] getFreeroundTemplate [WORK IN PROGRESS - CONTACT SUPPORT FOR DETAILS] removeFreeroundTemplate [WORK IN PROGRESS - CONTACT SUPPORT FOR DETAILS] --- --- # Appendix E - Discounts To implement live discounts, the operator sends discount information with every request that returns a balance in reply. Subsequent doTransactions calls apply this discount. The operator must check that the submitted discount is eligible. If the discount is not eligible, the operator must send an error back on debit requests and must not accept the call. To activate this functionality, contact hizi.io support. The operator can send an optional discountInfo object together with every balances object it returns in response to a call from the hizi API. This applies to getBalance and doTransactions calls. If a discount applies, the engine sends it in the gameTransaction structure in a debit transaction through doTransactions. The engine sends the original stake as amount. The operator must deduct the discountAmount when it debits the player's wallet. Example: a call with amount = 100 and discountAmount = 10 results in a debit of 90, in 1/100 of the base currency given in that call. If the discount is not eligible, the operator must deny the operation. The operator sends error code 33 back (DISCOUNTNOTELIGIBLE). See [Response Codes & Error Codes](https://docs.hizi.io/response-codes.html). The operator must not debit any amount. The operator must not start a game round. ### discountInfo This type describes the discount information available to the player. The operator can send it in every call that returns a balances object. | | | | | | | --- | --- | --- | --- | --- | | *attribute* | *type* | *allowed values* | *mandatory* | *notes* | | **text** | string | any | YES | Text to display in the frontend. Localize it based on the session. | | **currency** | currency | any 3 character currency code | YES | ISO 4217 3 letter currency code | | **discounts** | Array of discount entries defining a discount in absolute amount per stake | Array of discountEntry | YES | | | **campaignId** | string | any | YES | Campaign ID that identifies the discount. The API sends it back in debit calls. | | **validFrom** | timestamp | any valid value | NO | Timestamp when this campaign starts. If not present, the engine uses the current date. | | **validTo** | timestamp | any valid value | NO | Timestamp when this campaign ends (inclusive). If not present, the campaign is valid forever. | ### discountEntry | | | | | | | --- | --- | --- | --- | --- | | *attribute* | *type* | *allowed values* | *mandatory* | *notes* | | **stake** | amount | any | YES | Stake in 1/100 of base currency | | **absoluteDiscountAmount** | amount | any | YES | Discount in 1/100 of base currency to apply to the stake above | | **spinsRemaining** | number | any | NO | Number of remaining spins (bets) for this discount. For information only. | | | | | --- | --- | | *Samples* | *Description* | | { "text": "happy hour", "currency": "EUR", "discounts": [ { "stake": 10, "absoluteDiscountAmount": 1 }, { "stake": 100, "absoluteDiscountAmount": 10 } ] } | Example of discount information. It gives a discount of 1 cent for a stake of 0.10 EUR. It also gives a discount of 10 cents for a stake of 1 EUR. | --- --- # Operator Interface The Operator Interface is the standard interface for integrating game clients with operators and aggregators. It makes game integration seamless across every lobby it supports. It uses one event-based protocol for game state, errors, and player interactions. It includes built-in support for major aggregators such as EveryMatrix, Pariplay, and Relax. hizi writes the module in TypeScript and ships full type definitions with it. ## Next Steps - [Getting Started](https://docs.hizi.io/operator-interface/guide/getting-started.html): Log in to the @hoelle NPM registry. Install the module. - [Usage](https://docs.hizi.io/operator-interface/guide/usage.html): Import the module, set up `OperatorInterface`, and handle the messages it exchanges with the lobby. - [Configuration](https://docs.hizi.io/operator-interface/guide/configuration.html): Logging and the `setup` parameters. - [API Reference](https://docs.hizi.io/operator-interface/api/): The static methods and the events sent to the game. --- # API Reference This page is the API Reference for the Operator Interface. - [Methods](https://docs.hizi.io/operator-interface/api/methods.html) - The `OperatorInterface` class provides these static methods. - [Events](https://docs.hizi.io/operator-interface/api/events.html) - The Operator Interface sends these events to the game. --- # API Events ## Incoming Events (Operator to Game) The OperatorInterface sends these events to the game. Listen to them with `OperatorInterface.on()`. ### Game Control - `spinRequested`: Operator requests a spin. - `spinStopRequested`: Operator requests to stop the spin. - `stopRequested`: Operator requests to stop the game. - `stopAutoplayRequested`: Operator requests to stop autoplay. - `pauseRequested`: Operator requests to pause the game. - `resumeRequested`: Operator requests to resume the game. - `reloadRequested`: Operator requests to reload the game. ### Settings - `muteRequested`: Operator requests to mute the game. - `unmuteRequested`: Operator requests to unmute the game. - `toggleMuteRequested`: Operator requests to toggle mute. - `enableTurboRequested`: Operator requests to enable turbo mode. - `disableTurboRequested`: Operator requests to disable turbo mode. - `toggleTurboRequested`: Operator requests to toggle turbo mode. ### Stake Control - `increaseStakeRequested`: Operator requests to increase stake. - `decreaseStakeRequested`: Operator requests to decrease stake. - `updateStakeRequested`: Operator requests to update stake. ### UI - `showRulesDialogRequested`: Operator requests to show rules. - `hideRulesDialogRequested`: Operator requests to hide rules. - `showAutoplayDialogRequested`: Operator requests to show autoplay dialog. - `showErrorDialogRequested`: Operator requests to show error dialog. ### Other - `clearCookiesRequested` - `continueRequested` - `updateBalanceRequested` - `handleFreeRoundsUpdate` - `setPromoInfoResponse` - `getPromoInfoResponse` ## Operator Messages (Operator to OperatorInterface) These are the raw `postMessage` types that an operator wrapper sends to the game window (the `OperatorToOperatorInterfaceMessage` type). The OperatorInterface's generic message handler maps each type to the corresponding incoming event listed above. The handler then forwards the event to the game. See [Appendix A: Events](https://docs.hizi.io/operator-api/appendix-a-events.html) for the postMessage format. For most operators, the OperatorInterface reads the message string from `e.data.type`. For EveryMatrix, it reads the message string from `e.data.name`. For Lottoland, it reads the message string from `e.data.action`. For Playtech POP, it reads the message string from `_type`. The OperatorInterface reads the payload from `e.data.data`. | Message | Forwarded to the game as | | :--- | :--- | | `spin` (also `spinRequested`, `requestSpin`) | `spinRequested` | | `stop` (also `stopRequested`) | `stopRequested` | | `spinStop` (also `spinStopRequested`) | `spinStopRequested` | | `clearCookies` | `clearCookiesRequested` | | `stopAutospins` / `stopAutobet` | `stopAutoplayRequested` | | `stakeUp` | `increaseStakeRequested` | | `stakeDown` | `decreaseStakeRequested` | | `turbo` | no data → `toggleTurboRequested`; `true` → `enableTurboRequested`; `false` → `disableTurboRequested` | | `toggleMute` | no data → `toggleMuteRequested`; `true` → `muteRequested`; `false` → `unmuteRequested` | | `setAudio` | `true` → `muteRequested`; `false` → `unmuteRequested` (no data is ignored) | | `updateBalance` | `updateBalanceRequested` | | `showHelp` (also `paytableshow`, `showPaytable`) | `showRulesDialogRequested` | ::: warning Not routed by the generic handler The `OperatorToOperatorInterfaceMessage` type also declares `reload`, `pause`, `resume`, `mute`, `unmute`, and `stopAutoplay`. The generic postMessage handler does **not** map these strings. Operator-specific SDK callbacks emit the corresponding `*Requested` events instead. For example, Relax Gaming's `feim` callback maps `freeze` → `pauseRequested`, `unfreeze` → `resumeRequested`, `pauseAutoPlay` → `stopAutoplayRequested`, and `updateSettings` → `muteRequested`/`unmuteRequested`/`enableTurboRequested`/`disableTurboRequested`. Playtech POP's UCIP pause command also emits its event this way. The package declares `reloadRequested`. The package never emits it. ::: For messages that the game sends to the operator, see the notification methods (for example `gameReady`, `roundStarted`, `balanceUpdated`) in the [API Methods](https://docs.hizi.io/operator-interface/api/methods.html) reference. --- # API Methods The `OperatorInterface` class provides static methods to communicate with the operator. ## Setup ### setup Initializes the OperatorInterface. ```typescript static setup( operatorProtocol: OperatorProtocol, gameCode: string, currencyCode: string, languageCode: string, playerID: string, mode: string, lobbyURL: string, isMobile?: boolean ): Promise ``` **Parameters:** - `operatorProtocol`: The operator protocol to use. - `gameCode`: The game code. - `currencyCode`: The 3-letter ISO currency code. - `languageCode`: The 2-letter ISO language code. - `playerID`: The player ID. - `mode`: The game mode (e.g., "REAL", "DEMO"). - `lobbyURL`: The URL to the lobby. - `isMobile`: (Optional) Whether the game is in mobile mode. Defaults to `false`. ## Event Handling ### addEventListener Adds a listener to a given incoming event. ```typescript static addEventListener(event: OperatorInterfaceToGameMessage, callback: Callback): Callback ``` **Parameters:** - [`event`](https://docs.hizi.io/operator-interface/api/events.html#incoming-events-operator-to-game): The incoming event to listen for. - `callback`: The callback function to call. **Returns:** - The callback function. ### on Adds a listener to a given incoming event. `on` is an alias for `addEventListener`. ```typescript static on(event: OperatorInterfaceToGameMessage, callback: Callback): Callback ``` **Parameters:** - [`event`](https://docs.hizi.io/operator-interface/api/events.html#incoming-events-operator-to-game): The incoming event to listen for. - `callback`: The callback function to call. **Returns:** - The callback function. ### once Adds a listener to a given incoming event. The method removes the listener after the first call. ```typescript static once(event: OperatorInterfaceToGameMessage, callback: Callback): Callback ``` **Parameters:** - [`event`](https://docs.hizi.io/operator-interface/api/events.html#incoming-events-operator-to-game): The incoming event to listen for. - `callback`: The callback function to call. **Returns:** - The callback function. ### removeEventListener Removes a listener for a given event. ```typescript static removeEventListener(event: OperatorInterfaceToGameMessage, callback: Callback): void ``` **Parameters:** - [`event`](https://docs.hizi.io/operator-interface/api/events.html#incoming-events-operator-to-game): The incoming event. - `callback`: The callback function to remove. ## Game State ### gameReady Call this method after the game loads and initializes. ```typescript static gameReady(): void ``` ### loadProgress Call this method to update the casino with the loading progress. The casino displays the progress in a loading screen. ```typescript static loadProgress(progress: number): void ``` **Parameters:** - `progress`: A number between 0 and 1 representing the loading progress (1 = 100%). ### loadComplete Call this method when loading is complete. ```typescript static loadComplete(): void ``` ### gameDataLoaded Call this method when the game receives the loadConfig call. ```typescript static gameDataLoaded(): void ``` ### initialGameState Call this method before loading starts. It tells the operator whether the game will be muted. ```typescript static initialGameState(muted: boolean): void ``` **Parameters:** - `muted`: Whether the game is muted. ### retrievedStoredSettings Call this method after the game retrieves stored user settings, such as the mute state. ```typescript static retrievedStoredSettings(): void ``` ### gameUIUpdated Call this method when the buy feature menu opens or closes. Also call it when the splash screen appears or disappears, or when the game waits for user confirmation. ```typescript static gameUIUpdated(buyFeatureMenuOpen: boolean, splashScreenVisible: boolean, waitingForUserConfirmation: boolean): void ``` **Parameters:** - `buyFeatureMenuOpen`: Whether the buy feature menu is open. - `splashScreenVisible`: Whether the splash screen is visible. - `waitingForUserConfirmation`: Whether the game is waiting for user confirmation. ## Gameplay ### autoPlayStarted Call this method when the game starts autoplay. ```typescript static autoPlayStarted(): void ``` ### autoPlayFinished Call this method when the game ends autoplay. ```typescript static autoPlayFinished(): void ``` ### spinAnimationStarted Call this method when the spin animation starts. ```typescript static spinAnimationStarted(): void ``` ### spinAnimationStopped Call this method when the spin animation ends. ```typescript static spinAnimationStopped(): void ``` ### roundStarted Call this method when a new gameround starts. ```typescript static roundStarted(balance: number, stake: number): void ``` **Parameters:** - `balance`: The player's balance in minor units. - `stake`: The player's stake in minor units. ### roundEnded Call this method when a gameround ends. ```typescript static roundEnded(balance: number, stake: number, winAmount?: number, ticketsLeftAfter?: number): void ``` **Parameters:** - `balance`: The player's balance in minor units. - `stake`: The player's stake in minor units. - `winAmount`: (Optional) The player's win amount in minor units. Defaults to 0. - `ticketsLeftAfter`: (Optional) The number of tickets left after the gameround. Defaults to 0. ### balanceUpdated Call this method whenever the displayed balance changes. ```typescript static balanceUpdated(realBalance: number, demoBalance?: number, bonusBalance?: number, fudgeAmount?: number): void ``` **Parameters:** - `realBalance`: The real balance of the player. - `demoBalance`: (Optional) The demo balance of the player. Defaults to 0. - `bonusBalance`: (Optional) The bonus balance of the player. Defaults to 0. - `fudgeAmount`: (Optional) How much to fudge the displayed balance (if win animations have not yet finished). Defaults to 0. ### stakeUpdated Call this method to update the stake that the casino displays. ```typescript static stakeUpdated(stake: number): void ``` **Parameters:** - `stake`: The player's stake in minor units. ### cashWin Call this method to update the win amount that the casino displays. ```typescript static cashWin( winAmount: number, stake: number, winType?: 'normal' | 'big' | 'mega' | 'monster' | 'legendary', isFreePlayWin?: boolean, totalFreePlayWin?: number ): void ``` **Parameters:** - `winAmount`: The player's win amount in minor units. - `stake`: The player's bet amount in minor units. - `winType`: (Optional) The type of win. Defaults to 'normal'. - `isFreePlayWin`: (Optional) Whether the game awarded the win during freeplays. Defaults to `false`. - `totalFreePlayWin`: (Optional) The total win during freeplays. Defaults to 0. ### collected Call this method when the player collects a win. ```typescript static collected(collectedAmount: number): void ``` **Parameters:** - `collectedAmount`: The amount the player collected in minor units. ### cashier Call this method when the user runs out of funds, or attempts to spin with insufficient funds. ```typescript static cashier(): void ``` ### help Call this method when the user requests help. ```typescript static help(): void ``` ### history Call this method to navigate to the game history. ```typescript static history(): void ``` ### quit Call this method when the user presses the quit button. ```typescript static quit(): void ``` ### resume Call this method when the game regains focus. ```typescript static resume(): void ``` ### visibleSymbols Call this method to tell the operator which symbols are visible on the reels. ```typescript static visibleSymbols(visibleSymbols: number[][], stake: number): void ``` **Parameters:** - `visibleSymbols`: The visible symbols. - `stake`: The stake amount. ### winningSymbols Call this method to tell the operator which symbols appear in all winlines. ```typescript static winningSymbols(symbols: number[], stake: number): void ``` **Parameters:** - `symbols`: The winning symbols. - `stake`: The stake amount. ### lineCountUpdated Call this method when the number of winlines changes. ```typescript static lineCountUpdated(lineCount: number): void ``` **Parameters:** - `lineCount`: The number of winlines. ### cardGamble Call this method when the player places a card gamble bet. ```typescript static cardGamble(): void ``` ### ladderGamble Call this method when the player places a ladder gamble bet. ```typescript static ladderGamble(): void ``` ### gambleWin Call this method when the game awards a win after a gamble (bonus). ```typescript static gambleWin(winAmount: number, stake: number): void ``` **Parameters:** - `winAmount`: The player's win amount in minor units. - `stake`: The player's bet amount in minor units. ### fullRow Call this method when the game awards a full row winline. ```typescript static fullRow(rowCount: number, includesWilds: boolean, winlineSymbols: number[]): void ``` **Parameters:** - `rowCount`: The number of symbols in the winline. - `includesWilds`: Whether the winline includes a wild. - `winlineSymbols`: The symbols in the winline. ### scatterWin Call this method when scatters award cash. ```typescript static scatterWin(winAmount: number, scatterSymbolID: number, positions: number[][]): void ``` **Parameters:** - `winAmount`: The amount of cash the scatters awards. - `scatterSymbolID`: The ID of the symbol which awarded the scatter win. - `positions`: The position of the scatters on the reels. ## Features ### freeSpinsAwarded Call this method when the game awards the free spins feature. ```typescript static freeSpinsAwarded(awarded: number, stake: number, inFreeSpins: boolean): void ``` **Parameters:** - `awarded`: The number of free spins the game awarded. - `stake`: The player's bet amount in minor units. - `inFreeSpins`: Whether the game awarded the free spins during free spins. ### featuredStarted Call this method when the game enters a feature, such as free spins. ```typescript static featuredStarted(): void ``` ### featuredFinished Call this method when the game finishes a feature, such as free spins. ```typescript static featuredFinished(): void ``` ### freeplaysAwarded Call this method when the game enters freeplays. ```typescript static freeplaysAwarded(awarded: number, stake: number): void ``` **Parameters:** - `awarded`: The number of freeplays awarded. - `stake`: The stake of the freeplays in minor units. ### freeplaysSummary Call this method when freeplays end. ```typescript static freeplaysSummary(used: number, stake: number, totalWin: number): void ``` **Parameters:** - `used`: The number of freeplays used. - `stake`: The stake of the freeplays in minor units. - `totalWin`: The total win of the freeplays in minor units. ### getPromoInfo Call this method while the game is in freeplays. ```typescript static getPromoInfo(): void ``` ### setPromoInfo Call this method when the response contains `response.ticketInfo.promotions.data`. ```typescript static setPromoInfo(promoInfo: string): void ``` **Parameters:** - `promoInfo`: The promo info passed by the game engine. ### registerOption You can call this method to tell the operator that the commonUI can display specific game options. ```typescript static registerOption(option: RegisterOption): void ``` **Parameters:** - `option`: The game option to register. ### realityCheck Call this method to display the reality check dialog. ```typescript static realityCheck(elapsedMS: number, intervalMS: number, exitURL: string, historyURL: string): void ``` **Parameters:** - `elapsedMS`: The elapsed time in milliseconds. - `intervalMS`: The interval between reality checks in milliseconds. - `exitURL`: The URL to redirect to when the player exits the game. - `historyURL`: The URL to redirect to when the player wants to see their history. ### sendRealityCheckResponse Call this method when the player responds to the reality check dialog. ```typescript static sendRealityCheckResponse(backendURL: string, token: string, continued: boolean): Promise ``` **Parameters:** - `backendURL`: The backend URL received in the login call. - `token`: The token received in the last successful call. - `continued`: Whether the player chose to continue. **Returns:** - A promise that resolves to a `GameAPIReply` object on success, or an `ErrorResponse` on failure. Playtech (POP) requires this call after a bet fails with error 38 (REALITYCHECK). See [Playtech (POP) reality check](https://docs.hizi.io/operator-interface/operators/pop.html#reality-check). ## Audio & Turbo ### muted Call this method to tell the operator that the sound is now muted. ```typescript static muted(): void ``` ### unmuted Call this method to tell the operator that the sound is now unmuted. ```typescript static unmuted(): void ``` ### support Call this method when the user requests support. ```typescript static support(): void ``` ### turboEnabled Call this method to tell the operator that turbo is now enabled. ```typescript static turboEnabled(): void ``` ### turboDisabled Call this method to tell the operator that turbo is now disabled. ```typescript static turboDisabled(): void ``` ## Other ### error Call this method when the game shows an error. ```typescript static error(category: ErrorCategory, severity: ErrorSeverity, message: string, code?: string): void ``` **Parameters:** - `category`: The error category. - `severity`: The severity of the error. - `message`: The message to display. - `code`: (Optional, except required for Light and Wonder (LNW).) ### orientationUpdated Call this method when the orientation changes. ```typescript static orientationUpdated(): void ``` ### passThroughMessage Call this method with each message that the frontend receives. ```typescript static passThroughMessage(message: string): void ``` **Parameters:** - `message`: The message to forward to the operator. ### playerActivity Call this method when the player performs an action in the game. This allows the lobby to control session inactivity requirements for specific jurisdictions. Examples of actions that trigger this method: - Clicking the spin button - Changing the bet/stake amount - Interacting with game menus or settings - Clicking collect/gamble buttons - Any touch or click input during gameplay ```typescript static playerActivity(): void ``` --- # Configuration ## Logging Set the static `log` property to `true` to enable basic logging to the console. ```typescript OperatorInterface.log = true; ``` From version 1.4.5, the OperatorInterface enables this logging by default. ## Setup Parameters The `setup` method accepts the following parameters: | Parameter | Type | Description | | :--- | :--- | :--- | | `operatorProtocol` | `OperatorProtocol` | The operator protocol for communication (for example, `OperatorProtocol.GAMESGLOBAL`). | | `gameCode` | `string` | The identifier for the game. | | `currencyCode` | `string` | The 3-letter ISO currency code (for example, 'EUR'). | | `languageCode` | `string` | The 2-letter ISO language code (for example, 'en'). | | `playerID` | `string` | The unique identifier for the player. | | `mode` | `string` | The game mode (for example, "REAL", "DEMO"). | | `lobbyURL` | `string` | The URL to redirect to when exiting. | | `isMobile` | `boolean` | (Optional) States whether the game runs on a mobile device. The default value is `false`. | See [`setup`](https://docs.hizi.io/operator-interface/api/methods.html#setup) in the API reference for the full signature. --- # Getting Started ## Installation Log in to the @hoelle NPM registry. hizi provides the username and password. ```bash npm login --registry=https://code.hoelle.games:7082/ --scope=@hoelle ``` Install the module as a dependency. ```bash npm install -S @hoelle/operator-interface@latest ``` --- # Usage ## Basic Setup ```typescript // Import statements import { ErrorCategory, ErrorSeverity, OperatorInterface, OperatorInterfaceToGameMessage, OperatorProtocol, RegisterOption } from '@hoelle/operator-interface'; // Enable logging OperatorInterface.log = true; // Initialize OperatorInterface await OperatorInterface.setup( operatorProtocol, gameCode, currencyCode, languageCode, playerID, mode, lobbyURL, isMobile ); ``` ## Event Handling ### Listening for Events ```typescript // Listen for an incoming mute event and mute the game OperatorInterface.on('muteRequested', this.mute.bind(this)); ``` ### Broadcasting Events ```typescript // Broadcast an outgoing event OperatorInterface.spinAnimationStarted(); ``` --- # Operators Overview The Operator Interface supports integration with various operators and aggregators. Some operators require specific method calls to function correctly. ## Supported Operators - [EveryMatrix](https://docs.hizi.io/operator-interface/operators/everymatrix.html) - [GamesGlobal](https://docs.hizi.io/operator-interface/operators/gamesglobal.html) - [Lottoland](https://docs.hizi.io/operator-interface/operators/lottoland.html) - [Pariplay](https://docs.hizi.io/operator-interface/operators/pariplay.html) - [Playtech (POP)](https://docs.hizi.io/operator-interface/operators/pop.html) - [Relax Gaming](https://docs.hizi.io/operator-interface/operators/relax.html) - [Light and Wonder (LNW)](https://docs.hizi.io/operator-interface/operators/lnw.html) - [Whow Games](https://docs.hizi.io/operator-interface/operators/whow.html) - [ReelLink](https://docs.hizi.io/operator-interface/operators/reellink.html) ## Integration Requirements Each operator has a set of required method calls. See the specific operator guide for details. --- # EveryMatrix ## Required Methods EveryMatrix requires the following methods for integration: - [`error`](https://docs.hizi.io/operator-interface/api/methods.html#error) - [`loadComplete`](https://docs.hizi.io/operator-interface/api/methods.html#loadcomplete) - [`loadProgress`](https://docs.hizi.io/operator-interface/api/methods.html#loadprogress) - [`muted`](https://docs.hizi.io/operator-interface/api/methods.html#muted) - [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit) - [`roundEnded`](https://docs.hizi.io/operator-interface/api/methods.html#roundended) - [`roundStarted`](https://docs.hizi.io/operator-interface/api/methods.html#roundstarted) - [`stakeUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#stakeupdated) - [`unmuted`](https://docs.hizi.io/operator-interface/api/methods.html#unmuted) --- # GamesGlobal GamesGlobal uses the Bridge API protocol for communication between the game and the lobby. The game and the lobby exchange messages through `window.postMessage`. ## Bridge API Message Format Every message to the GamesGlobal lobby follows this structure: ```typescript { Id: string, // UUID for the message Name: string, // Message name (e.g., "bridge_api_ready") Body: object | null, Type: "REQUEST" | "NOTIFICATION" | "RESPONSE", Origin: "embeddedgame", SenderId: string // window.name } ``` ## Required Methods To integrate with GamesGlobal, use the following methods: - [`gameReady`](https://docs.hizi.io/operator-interface/api/methods.html#gameready) - Sends the `BRIDGE_API_READY` message to tell the lobby that the game is ready to communicate - [`playerActivity`](https://docs.hizi.io/operator-interface/api/methods.html#playeractivity) - Sends the `PLAYER_ACTIVITY` message when the player performs an action - [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit) - Sends the `CLOSE_APPLICATION` notification when the player requests to close the game ## Bridge API Messages ### BRIDGE_API_READY The game sends this message when it calls `gameReady()`. Send this message as soon as the game loads. The lobby does not send messages to the game before it receives this message. The lobby does not respond to any message before it receives this message. ### PLAYER_ACTIVITY The game sends this message when it calls `playerActivity()`. This message tells the lobby that the player performed an action. The lobby uses this information to control the rules for session inactivity in specific jurisdictions. The following actions trigger this message: - Clicking the spin button - Changing the bet/stake amount - Interacting with game menus or settings - Clicking collect/gamble buttons - Any touch or click input during gameplay ### CLOSE_APPLICATION The game sends this message when it calls `quit()`. This message tells the lobby that the player requested to close the game. The lobby closes the game. The lobby returns the player to the lobby. ## Usage Example ```typescript import { OperatorInterface, OperatorProtocol } from '@hoelle/operator-interface'; // Setup with GamesGlobal protocol await OperatorInterface.setup( OperatorProtocol.GAMESGLOBAL, 'game-code', 'EUR', 'en', 'player123', 'REAL', 'https://lobby.example.com' ); // Notify lobby that game is ready (sends BRIDGE_API_READY) OperatorInterface.gameReady(); // Notify lobby of player activity (sends PLAYER_ACTIVITY) OperatorInterface.playerActivity(); // Close the game (sends CLOSE_APPLICATION) OperatorInterface.quit(); ``` --- # Light and Wonder (LNW) ## Required Methods Light and Wonder requires the following methods for integration: - [`balanceUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#balanceupdated) - [`cashWin`](https://docs.hizi.io/operator-interface/api/methods.html#cashwin) - [`error`](https://docs.hizi.io/operator-interface/api/methods.html#error) - [`gameReady`](https://docs.hizi.io/operator-interface/api/methods.html#gameready) - [`getPromoInfo`](https://docs.hizi.io/operator-interface/api/methods.html#getpromoinfo) - [`loadProgress`](https://docs.hizi.io/operator-interface/api/methods.html#loadprogress) - [`passThroughMessage`](https://docs.hizi.io/operator-interface/api/methods.html#passthroughmessage) - [`realityCheck`](https://docs.hizi.io/operator-interface/api/methods.html#realitycheck) - [`registerOption`](https://docs.hizi.io/operator-interface/api/methods.html#registeroption) - [`resume`](https://docs.hizi.io/operator-interface/api/methods.html#resume) - [`roundEnded`](https://docs.hizi.io/operator-interface/api/methods.html#roundended) - [`roundStarted`](https://docs.hizi.io/operator-interface/api/methods.html#roundstarted) - [`setPromoInfo`](https://docs.hizi.io/operator-interface/api/methods.html#setpromoinfo) - [`spinAnimationStarted`](https://docs.hizi.io/operator-interface/api/methods.html#spinanimationstarted) - [`spinAnimationStopped`](https://docs.hizi.io/operator-interface/api/methods.html#spinanimationstopped) - [`stakeUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#stakeupdated) --- # Lottoland ## Required Methods Lottoland requires the following methods for integration: - [`cashier`](https://docs.hizi.io/operator-interface/api/methods.html#cashier) - [`history`](https://docs.hizi.io/operator-interface/api/methods.html#history) - [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit) --- # Pariplay ## Required Methods Integration with Pariplay requires the following methods: - [`balanceUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#balanceupdated) - [`cashier`](https://docs.hizi.io/operator-interface/api/methods.html#cashier) - [`loadProgress`](https://docs.hizi.io/operator-interface/api/methods.html#loadprogress) - [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit) - [`roundEnded`](https://docs.hizi.io/operator-interface/api/methods.html#roundended) - [`roundStarted`](https://docs.hizi.io/operator-interface/api/methods.html#roundstarted) --- # Playtech (POP) Playtech POP integration uses the UCIP (Unified Client Integration Protocol) for communication. The game and the wrapper exchange messages as JSON-stringified strings through `postMessage`. Playtech sets requirements for when events must trigger. Playtech also specifies when to use each call. These requirements can change. Find the latest documentation directly on the Playtech site: [https://pop-playtech.readme.io/docs/client-api](https://pop-playtech.readme.io/docs/client-api) (Contact the hizi integration team for the password) ## Required Methods The following methods are required for integration with Playtech POP: - [`autoPlayFinished`](https://docs.hizi.io/operator-interface/api/methods.html#autoplayfinished) - [`autoPlayStarted`](https://docs.hizi.io/operator-interface/api/methods.html#autoplaystarted) - [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit) - [`sendRealityCheckResponse`](https://docs.hizi.io/operator-interface/api/methods.html#sendrealitycheckresponse): see [Reality Check](https://docs.hizi.io/operator-interface/operators/pop.html#reality-check) ## Reality Check Playtech can reject a bet with a reality check. A regulation or the operator can enable this check. When it happens, the bet call fails with error **38 (REALITYCHECK)**. See [Response Codes](https://docs.hizi.io/response-codes.html). The reality check occurs only on a bet. It never occurs on a win, a cancellation, or the close of a game round. Playtech requires a dialog with two buttons: continue and stop. Do these steps: 1. Show the text from `passThroughData.clientMessage.msg` on the failed reply. Playtech sets this text. It can contain HTML links (``). Render them as links. 2. Show a continue button and a stop button. 3. When the player selects continue, call [`sendRealityCheckResponse`](https://docs.hizi.io/operator-interface/api/methods.html#sendrealitycheckresponse) with `continued: true`. Playtech then resets the reality check counter. The player can then place the bet again. 4. When the player selects stop, call [`sendRealityCheckResponse`](https://docs.hizi.io/operator-interface/api/methods.html#sendrealitycheckresponse) with `continued: false`. Then call [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit). The game must close itself. ::: warning If the game does not call `sendRealityCheckResponse`, Playtech does not reset the counter. The next bet then fails with the same reality check again. ::: A simpler fallback is permitted. Show the text with one "OK" button only. When the player selects "OK", call [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit). This closes the game, so the player experience is worse. The backend keeps the identifier of the reality check from Playtech (`actionId`). It sends this identifier back to Playtech when the game calls `sendRealityCheckResponse`. The game does not handle `actionId`. Do not confuse this flow with the [`realityCheck`](https://docs.hizi.io/operator-interface/api/methods.html#realitycheck) method. For POP, that method only opens the `responsible_gaming` page of the wrapper. ## Page Navigation If the game supports any of the following features, the game must call the corresponding method to delegate navigation to the wrapper. The wrapper enables page delegation when `w2gInitializationResponse` includes `openpage`. - [`cashier`](https://docs.hizi.io/operator-interface/api/methods.html#cashier): delegates the `deposit` page - [`help`](https://docs.hizi.io/operator-interface/api/methods.html#help): delegates the `help` page - [`history`](https://docs.hizi.io/operator-interface/api/methods.html#history): delegates the `game_history` page - [`realityCheck`](https://docs.hizi.io/operator-interface/api/methods.html#realitycheck): delegates the `responsible_gaming` page - [`support`](https://docs.hizi.io/operator-interface/api/methods.html#support): delegates the `support` page If the wrapper provides a `delegatePages` array in `w2gInitializationResponse`, the Operator Interface delegates only the listed pages. It silently ignores all other pages. If `delegatePages` is absent or empty, the Operator Interface delegates all pages. ## Event Listeners The Operator Interface emits the following events in response to messages from the POP wrapper: - `stopAutoplayRequested`: the wrapper sends an interrupt command - `pauseRequested`: the wrapper sends a pause command (if the `pause` feature is enabled) - `resumeRequested`: the wrapper sends an unpause command (if the `pause` feature is enabled) --- # ReelLink ## Required Methods Implement the following method to integrate the game with ReelLink: - [`gameReady`](https://docs.hizi.io/operator-interface/api/methods.html#gameready) Also implement the following event listeners. These listeners handle events that the ReelLink platform sends to the game: - [`spinRequested`](https://docs.hizi.io/operator-interface/api/methods.html#addeventlistener) - [`stopRequested`](https://docs.hizi.io/operator-interface/api/methods.html#addeventlistener) - [`stopAutoplayRequested`](https://docs.hizi.io/operator-interface/api/methods.html#addeventlistener) - [`updateBalanceRequested`](https://docs.hizi.io/operator-interface/api/methods.html#addeventlistener) When the game receives `updateBalanceRequested`, refresh the balance through the [engine SDK balance operation](https://docs.hizi.io/engine/sdk/endpoints.html#updatebalance). Check the response before updating the displayed balance. ## Mandatory Game Settings ReelLink passes the following settings to the game provider in the `connect`/`login` response. The game must respect these settings. | Setting | Type | Remarks | | --------------- | --------- | ------------------------------------------------------ | | `gambleEnabled` | `boolean` | Turns the gamble feature on or off | | `displayClock` | `boolean` | Turns the on-screen clock on or off | | `maxStake` | `number` | Maximum stake. The response can omit this field in some cases. | | `minStake` | `number` | Minimum stake. The response can omit this field in some cases. | --- # Relax Gaming ## Required Methods Implement the following methods to integrate with Relax Gaming: - [`autoPlayFinished`](https://docs.hizi.io/operator-interface/api/methods.html#autoplayfinished) - [`autoPlayStarted`](https://docs.hizi.io/operator-interface/api/methods.html#autoplaystarted) - [`balanceUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#balanceupdated) - [`cashWin`](https://docs.hizi.io/operator-interface/api/methods.html#cashwin) - [`cashier`](https://docs.hizi.io/operator-interface/api/methods.html#cashier) - [`error`](https://docs.hizi.io/operator-interface/api/methods.html#error) - [`featuredFinished`](https://docs.hizi.io/operator-interface/api/methods.html#featuredfinished) - [`featuredStarted`](https://docs.hizi.io/operator-interface/api/methods.html#featuredstarted) - [`history`](https://docs.hizi.io/operator-interface/api/methods.html#history) - [`initialGameState`](https://docs.hizi.io/operator-interface/api/methods.html#initialgamestate) - [`loadComplete`](https://docs.hizi.io/operator-interface/api/methods.html#loadcomplete) - [`loadProgress`](https://docs.hizi.io/operator-interface/api/methods.html#loadprogress) - [`muted`](https://docs.hizi.io/operator-interface/api/methods.html#muted) - [`quit`](https://docs.hizi.io/operator-interface/api/methods.html#quit) - [`roundEnded`](https://docs.hizi.io/operator-interface/api/methods.html#roundended) - [`roundStarted`](https://docs.hizi.io/operator-interface/api/methods.html#roundstarted) - [`stakeUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#stakeupdated) - [`turboDisabled`](https://docs.hizi.io/operator-interface/api/methods.html#turbodisabled) - [`turboEnabled`](https://docs.hizi.io/operator-interface/api/methods.html#turboenabled) - [`unmuted`](https://docs.hizi.io/operator-interface/api/methods.html#unmuted) --- # Whow Games ## Required Methods The following methods are required for integration with Whow Games: - [`autoPlayFinished`](https://docs.hizi.io/operator-interface/api/methods.html#autoplayfinished) - [`autoPlayStarted`](https://docs.hizi.io/operator-interface/api/methods.html#autoplaystarted) - [`cashWin`](https://docs.hizi.io/operator-interface/api/methods.html#cashwin) - [`freeSpinsAwarded`](https://docs.hizi.io/operator-interface/api/methods.html#freespinsawarded) - [`freeplaysAwarded`](https://docs.hizi.io/operator-interface/api/methods.html#freeplaysawarded) - [`fullRow`](https://docs.hizi.io/operator-interface/api/methods.html#fullrow) - [`gambleWin`](https://docs.hizi.io/operator-interface/api/methods.html#gamblewin) - [`lineCountUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#linecountupdated) - [`scatterWin`](https://docs.hizi.io/operator-interface/api/methods.html#scatterwin) - [`stakeUpdated`](https://docs.hizi.io/operator-interface/api/methods.html#stakeupdated) - [`turboDisabled`](https://docs.hizi.io/operator-interface/api/methods.html#turbodisabled) - [`turboEnabled`](https://docs.hizi.io/operator-interface/api/methods.html#turboenabled) - [`visibleSymbols`](https://docs.hizi.io/operator-interface/api/methods.html#visiblesymbols) - [`winningSymbols`](https://docs.hizi.io/operator-interface/api/methods.html#winningsymbols) --- # Response Codes & Error Codes ## Response Headers The API uses custom HTTP response header fields to report errors or information that is not specific to a request payload. | | | | --- | --- | | *Custom header field* | *Description* | | X-H-ERROR-ID | The error ID that caused the call to fail. See below for a list of possible values. If this field is present and the HTTP code is 200, the API treats the call as failed. The failure equals HTTP code 400. | | X-H-ERROR-MSG | A text description of the error that caused the call to fail. **This text is for diagnostics and logs only. It is not localized. Do not show it to the player.** For a player-facing message on responsible-gambling prompts (for example, error 11/38), use `passThroughData.clientMessage.msg` on the reply body instead. See [PassThroughData](https://docs.hizi.io/game-studio-api/data-structures.html#v2-gameapireply). | ## Error Reporting / Handling The API answers every request with an HTTP return code. This code represents the overall success or failure of the call. Custom header fields and the response payload can give more information. See the table below for the retry rules. **NOTE**: An HTTP return code of 200 usually means the call was successful. Ignore any error information in the response payload in this case. If the X-H-ERROR-ID response header field is present and the HTTP code is 200, the API treats the call as failed. The failure equals HTTP code 400. ### HTTP return codes | | | | | --- | --- | --- | | *Return code* | *Description* | *retry y/n* | | **Success codes: the response is in JSON format** | | | | 200 | The API received the request and authenticated it correctly. The operation succeeded without errors. **NOTE**: An HTTP code 200 means the call was successful. The API ignores any error reported in an errorCode object unless it also receives an **X-H-ERROR-ID**. | can be repeated (all calls are idempotent) | | **Error codes: the API received the request, but an error occurred during processing. Check the optional X-H-ERROR-ID and X-H-ERROR-MSG response header fields for details. The response may also contain more information.** | | | | 400 | The API could not process the request because of an error in the request payload. | do not retry | | 401 | The signature of the request was wrong. The API could not perform other authorisation. | do not retry | | 403 | The API authenticated the call, but the caller does not have permission to use the endpoint. | do not retry | | 404 | The API could not find the endpoint. | do not retry | | 405 | The method (GET/POST) is wrong or not allowed. | do not retry | | 429 | The caller exceeded the rate limit. If you get this error repeatedly, contact customer support. | retry after waiting some time | | 451 | The content is not available for legal reasons. | do not retry | | 4xx | Some other, unspecified error occurred. | do not retry | | **Error codes for an internal problem. Treat these as temporary.** | | | | 5xx | An unexpected error occurred. | retry the call | | 501 | The API does not implement this call. | do not retry | --- ### Error codes sent via X-H-ERROR-ID field or used in errorCode object The API can send the following error codes in the custom response header field. In most cases, you also get a text description of the error. The API also uses these values (column Return code) to identify known errors in an errorCode object. | | | | | --- | --- | --- | | *Errorcode* | *Return code* | *Description* | | **SUCCESS** | 0 | The call was successful. | | **PLAYERNOTFOUND** | 1 | The operator could not find the player. | | **PLAYERBLOCKED** | 2 | The player is blocked. | | **BALANCETOOLOW** | 10 | The player's balance is too low for a debit. | | **BETLIMITREACHED** | 11 | The bet limit is reached. The API cannot accept the bet. Game Studio API: if present, the reply body carries player-facing text in `passThroughData.clientMessage.msg`, not in this header. Whether this text is present depends on the operator's own decline message. Not every operator or account includes player-facing text, so the game studio must still have a static fallback ready for when it is absent. See [PassThroughData](https://docs.hizi.io/game-studio-api/data-structures.html#v2-gameapireply). | | **GAMEROUNDALREADYSTARTED** | 12 | The gameround already started (the operator already received an initial bet). The operator sends this error if it receives a second initial bet with a different transaction hash. | | **GAMEROUNDNOTACTIVE** | 13 | The gameround is not active. The operator sends this error if it knows the gameround hash but the gameround already closed. | | **GAMEROUNDNOTFOUND** | 14 | The operator did not find the gameround hash. The operator sends this error when it receives a transaction with an unknown gameround hash. Exception: an initial debit with **isFirstDebit** true indicates the start of a new gameround. | | **GAMEROUNDNOTSTARTED** | 15 | The gameround has not started. The operator sends this error in this case: it receives a debit with **isFirstDebit** false, or a credit call, it knows the gameround hash, but it did not receive the initial debit. This does not normally happen. It indicates a problem in the implementation. | | **TRANSACTIONCOUNTWRONG** | 16 | In a doTransaction call, transactionCount does not match the number of transactions passed. This does not normally happen. It indicates a problem in the implementation. | | **GAMEROUNDPLAYERMISMATCH** | 17 | There is a mismatch between the gameround ID and the user ID. The gameround hash does not belong to the given player ID. | | **CURRENCYINVALID** | 18 | The given currency is not valid, or it is not available for that player or game. | | **INVALIDPARAMETER** | 19 | The operator received an invalid value for a parameter. | | **TRANSACTIONAFTEREND** | 20 | In a gameTransaction package list, the operator received a debit or credit after an end transaction. | | **GAMEROUNDISLOCKED** | 21 | The gameround is locked, or the operator is retrying a credit call. The player cannot start new games until the gameround unlocks. After 24 hours, the system marks the gameround as stranded. Perform manual reconciliation in this case. The player can start new games after that time. | | **GAMEROUNDISCLOSING** | 22 | The gameround is closing, but the operator has not yet accepted the end gameround call. The player can start new games. | | **SPINNOTAVAILABLE** | 23 | The requested spin type (free or bonus) is not available. Handle this the same way as a low balance error. | | **DEBITSHOULDBECANCELLED** | 24 | A debit failed. Cancel it. (Internal EveryMatrix use only.) | | **COOLOFFPERIODACTIVE** | 25 | The player is in a cool-off period. | | **TRANSACTIONSARERETRYING** | 26 | The operator receives new transactions, but pending transactions still exist. (Internal error.) | | **ENDTRANSACTIONALREADYRECEIVED** | 27 | The operator receives new transactions, but it already processed the end transaction. The operator is retrying it. (Internal error.) | | **REGIONISBLOCKED** | 28 | The operator blocked the request for legal reasons. | | **NOTLOGGEDON** | 29 | The player is not logged in to the casino. | | **RESPONSIBLEGAMINGLIMITREACHED** | 30 | The player reached the bet limit. | | **RESPONSIBLESGAMINGSESSIONEXPIRED** | 31 | The session limit is reached. The session expired. | | **PLAYERNOAUTHENTICATED** | 32 | The casino does not know the player. | | **DISCOUNTNOTELIGIBLE** | 33 | The discount submitted in doTransactions is not eligible. | | **GENERICBETERROR** | 34 | An unexpected error occurred on a debit transaction. | | **GENERICWINERROR** | 35 | An unexpected error occurred on a credit transaction. | | **OPERATIONBEINGPROCESSED** | 36 | The operator is processing another transaction. | | **TRANSACTIONALREADYEXISTS** | 37 | The operator already processed the transaction. | | **REALITYCHECK** | 38 | The operator triggered a reality check. Game Studio API: for this code, `passThroughData.clientMessage.msg` always contains text. Use it for display, then retry the same call once the player acknowledges it. See [PassThroughData](https://docs.hizi.io/game-studio-api/data-structures.html#v2-gameapireply). | | **SIGNATUREWRONG** | 1001 | The signature is incorrect. | | **PAYLOADNOTJSON** | 1002 | The received payload is not valid JSON. | | **WRONGHEADERS** | 1003 | Header fields are missing or wrong. | | **PARAMETERMISSING** | 1004 | The request is missing a mandatory parameter. | | **INVALIDGAMEMODE** | 1005 | The request contains an invalid game mode. | | **GAMECODENOTFOUND** | 1006 | The operator could not find the game code, or the game code is not available. | | **GAMECONFIGNOTSUPPORTED** | 1007 | The requested game configuration is not supported. | | **SESSIONINVALID** | 1008 | The session is expired or invalid. The most frequent cause is a launch URL that the operator opened a second time, or opened after its validity time. Reload the game with a new launch URL from a new getGameURL call. See the [launch URL rules](https://docs.hizi.io/operator-api/operator-to-hizi.html#launch-url-rules). | | **RNGFAILURE** | 1009 | The RNG failed. | | **NETWORKERROR** | 1010 | A general network error occurred. | | **DATASTRUCTUREWRONG** | 1200 | The sent payload is wrong. | | **RATELIMITEXCEEDED** | 1300 | The caller exceeded a rate limit. The operator usually also reports this with HTTP status code 429. | | **UNEXPECTED** | -1 | An unexpected error occurred. The operator cannot specify it in more detail. | ---