Blackjack Game Responses
This guide explains how to send a blackjack round to placeBet, how to read the scenario returned by the engine, and how the availableActions exposed 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 DB — the engine draws cards from the RNG and evaluates each action live. A round is a sequence of placeBet calls:
- Initial deal — one
placeBetwith the main stake and optional side bets. - Action continuations — one
placeBetper decision (hit, stand, double, split, insurance, even money) whileengineData.inProgress === true. - Settlement is terminal at the end of the final
placeBet. There is no separatecollectcall.
Everything you need to render the round — engine state, dealer view, current hands, available actions, side bets, totals — lives flat on result.scenario. Whether the round is still active is signalled by engineData.inProgress. Rules switches are returned once via loadConfig and are not echoed on every placeBet.
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.
import { placeBet, blackjackInitialData } from '@hizi.io/engine-sdk';
// Main hand only
const first = await placeBet({
backendURL,
token,
stake: 100,
...blackjackInitialData(),
});
// Main hand + side bets
const firstWithSides = await placeBet({
backendURL,
token,
stake: 100,
...blackjackInitialData({
perfectPairs: { stake: 10 },
twentyOnePlusThree: { stake: 10 },
}),
});Side-bet stakes are posted in addition to the main stake — the engine debits the full total.
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().
import { placeBet, blackjackActionData } from '@hizi.io/engine-sdk';
// Hit on the current hand
const next = await placeBet({
backendURL,
token,
stake: 100, // same base stake as the opening bet
...blackjackActionData({ kind: 'hit' }),
});
// Double — the engine will read supplementalStakeCost from the offered choice
await placeBet({ backendURL, token, stake: 100, ...blackjackActionData({ kind: 'double' }) });
// Split, insurance (with amount), even money
await placeBet({ backendURL, token, stake: 100, ...blackjackActionData({ kind: 'split' }) });
await placeBet({ backendURL, token, stake: 100, ...blackjackActionData({ kind: 'insurance', amount: 50 }) });
await placeBet({ backendURL, token, stake: 100, ...blackjackActionData({ kind: 'evenMoney' }) });Available action kinds: hit, stand, double, split, insurance, evenMoney. The scenario's availableActions array tells you which are currently legal and 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 and populated once engineData.inProgress === false. Mid-round-only fields (activeHand, phase, availableActions) are dropped at settlement.
Optional fields are omitted from the response when they would be empty / false / a "no-op default" — the wire never carries false flags or empty objects. engineData.inProgress is the source of truth for whether the round is still active; mid-round, phase further tells you whether the next decision is insurance-related or a regular player turn.
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 maxRoundWin 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<string, unknown>;
}
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';
}Ranks are kept 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 with its own stake, card list, total, and (when settled) outcome. totalStake is the sum of every posted stake in the round (main hand + splits + doubles + side bets + insurance), and 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 === falseengineData.inProgress is the gate for continuation calls; mid-round, scenario.phase carries the finer-grained sub-state (insurance_offered vs player_turn). The phase is dropped once the round settles. There is no collect call — the final placeBet credits winnings automatically.
Complete Example
import { login, loadConfig, placeBet, blackjackInitialData, blackjackActionData, IBlackjackScenario } from '@hizi.io/engine-sdk';
let response = await placeBet({
backendURL,
token,
stake: 100,
...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
render(bj);
break;
}
const picked = await askPlayer(bj.availableActions); // your UI picks one
response = await placeBet({
backendURL,
token,
stake: 100,
...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 PF round's pf.rngData records every one of those calls. Use pfVerify to have the engine re-deal the round from its revealed seeds plus your saved action list and confirm the cards (and therefore the payout) were never altered.
Request
Mirror the round's opening side bets in initialData and every player decision (in order) in actions. Each action is the raw TRulesAction object that was sent on the live round — what blackjackActionData() would have built. There is no entry in actions for the initial deal itself; the engine does that automatically from the seeds.
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' } },
],
});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 — same shape 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:
rngDatarow-for-row matches the livepf.rngData(cards are drawn asintrows; peek decisions adddoublerows).steps[i].scenario.dealer.cardsand eachhands[j].cardsmatch the live scenario at the same step — that's the proof the cards weren't swapped — and the terminal step'stotalWinmatches.
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 —
IGameResultstructure andengineDatafields. - Crash Responses — Another rules-based game, driven by websocket broadcasts rather than per-step actions.
- Types Reference — Full blackjack type surface (
TBlackjackAction,IBlackjackSideBets, etc.).