Appearance
Roulette Game Responses
Apply the shared frontend requirements 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
placeBetcall. - Round stake is the sum of every bet's
stake. Pass that sum as the top-levelstakefield. 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 below.
Selection Conventions
- Splits must be adjacent on the layout. The engine accepts
[a, b]whereaandbshare 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
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:
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.totalStakeUse 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.
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<TRouletteBetType, boolean>;
payouts: Record<TRouletteBetType, number>; // 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) / 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 |
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:
betsis a non-empty array.- Every bet's
typemust be a member ofenabledBets. Its value must betrue. - Every bet's
stakemust be an integer. It must be greater than0. - Every bet's
selectionis shape-valid for itstype. The engine checks splits and corners for adjacency. betsholds at most 500 entries. A longer array returns"Too many bets. Maximum 500."- Top-level
stakeis an integer. It equalssum(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.
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 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: oneintrow (the weighted pocket draw). Compare it element by element against the livepf.rngData.steps[0].scenario.pocket: equals the live pocket.steps[0].scenario.settlement: matches the live per-betwon,payout, andwinAmountvalues, 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: the
IGameResultstructure andengineDatafields. - Over/Under Responses: another single-call wager game.
- Hi/Lo Responses: a multi-step playerChoice game with cashout.