Roulette Game Responses
This guide explains how to send a roulette round to placeBet, how to read the enriched scenario returned by the engine, and how to use the loadConfig ruleset to render payouts and validate bets client-side.
Overview
Roulette is a single-call wager game on a European single-zero wheel (37 pockets, 0–36). The player composes one or more bets, each with its own type, selection, and stake. The engine draws a pocket, settles every bet, and returns the result in a single response. There is no continuation, no playerChoice, and no collect step.
- One round = one
placeBetcall. - Round stake is the sum of every bet's
stake— pass that sum as the top-levelstakefield; the engine validates that they match. - Settlement is returned per-bet in
scenario.settlement[], plus 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
Actual payouts are returned in loadConfig.payouts and may differ from the standard table — see How RTP Works below.
Selection Conventions
- Splits must be adjacent on the layout. The engine accepts
[a, b]whereaandbshare a row or column edge, including the 0-row splits[0, 1],[0, 2],[0, 3]. Order doesn't matter. - Streets are indexed 1–12: street
scovers 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
scovers two adjacent streets, pockets{3s−2 … 3s+3}.
The Roulette Scenario
When placeBet returns a roulette result, gameResult.scenario contains:
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 (capped by maxRoundWin)
}
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 full amount credited on a win
winAmount: number; // stake × payout on a win, 0 on a loss
}Reading winAmount
winAmount is the total amount credited on a win — stake × payout. A won: true straight bet of 10 chips at standard payout 36 returns winAmount = 10 × 36 = 360. A losing bet has winAmount = 0 (the stake is not returned separately — it was already debited at placeBet time).
Sum across the array: scenario.totalWin === settlement.reduce((s, b) => s + b.winAmount, 0) (capped if maxRoundWin > 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.totalStakeUse scenario.totalWin for display and gameResult.totalWin only when computing the credited amount yourself.
Game Flow
Single-Step — Place All Bets and Spin
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({
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) simply returns { bets }; you may build that object inline.
WARNING
The top-level stake must equal the sum of bets[].stake. The engine returns INVALIDPARAMETER with "Declared stake does not match bet total." if they disagree.
TIP
The round resolves instantly — no collect step, no playerChoice, no follow-up calls. One request in, one fully-settled result out.
Loading Game Config
loadConfig.config for a roulette game carries the ruleset:
interface IRouletteLoadConfig {
wheelVariant: 'european';
rtpMechanism: 'adjust_payouts' | 'adjust_odds';
targetRTP: number; // e.g. 0.98
enabledBets: Record<TRouletteBetType, boolean>;
payouts: Record<TRouletteBetType, number>; // total return multipliers (stake × payout = win amount)
maxRoundStake: number;
maxRoundWin: number; // 0 = uncapped
}Using the Config Client-Side
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
if (totalStake > config.maxRoundStake) {
throw new Error(`Stake ${totalStake} exceeds maxRoundStake ${config.maxRoundStake}`);
}How RTP Works
The target RTP can be reached two ways, controlled by rtpMechanism in the builder:
adjust_payouts (default)
Pocket weights stay uniform (1/37 each); each bet type's payout is bumped so RTP equals the target on every bet:
payout = (targetRTP × 37) / winningPocketsFor 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 |
The house edge is visible in the slightly-non-textbook payouts.
adjust_odds
Payouts stay at the textbook values (36 / 18 / 12 / 9 / 6 / 3 / 2). The pocket-0 weight is reduced so non-zero pockets cover targetRTP of the total weight:
weight(0) = round(10000 × (1 − targetRTP))
weight(1..36) = round(10000 × targetRTP / 36)Even-money / dozen / column bets (which never include 0) hit the textbook RTP exactly. Inside bets that include 0 (e.g. straight on 0, split [0,1]) under-RTP because their winning pocket is down-weighted; the builder warns when this happens. The headline RTP shown in the previewer assumes a non-zero-including bet.
Validation Rules
The engine enforces the following at placeBet time. Any failure returns INVALIDPARAMETER:
betsis a non-empty array.- Every bet's
typeis a member ofenabledBetsand istrue. - Every bet's
stakeis finite and> 0. - Every bet's
selectionis shape-valid for itstype. Splits and corners are checked for adjacency. sum(bets[].stake) ≤ maxRoundStake.- Top-level
stake === sum(bets[].stake)(within1e-9).
maxRoundWin > 0 caps scenario.totalWin to that value (silent — not an error).
Pocket Color Reference
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.The same metadata is also available on each spin via scenario.color, scenario.oddEven, scenario.dozen, and scenario.column — prefer those when reacting to a result rather than recomputing client-side.
Complete Example
import {
login, loadConfig, placeBet,
rouletteAdditionalData,
type IRouletteBet,
type IRouletteScenario,
type IRouletteLoadConfig,
} from '@hizi.io/engine-sdk';
// After login...
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);
if (totalStake > config.maxRoundStake) throw new Error('Round stake too large');
const resp = await placeBet({
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, then a deterministic settle pays out each bet. Use 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 the player submitted on the opening placeBet — both the pocket and the per-bet payouts are functions of those bets.
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
One-shot — steps has one entry.
rngData— oneintrow (the weighted pocket draw). Array-equal against the livepf.rngData.steps[0].scenario.pocket— equals the live pocket.steps[0].scenario.settlement— element-wise match against the live per-betwon/payout/winAmount.steps[0].totalWin— equals the live total.
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 —
IGameResultstructure andengineDatafields. - Over/Under Responses — Another single-call wager game.
- Hi/Lo Responses — Multi-step playerChoice game with cashout.