Appearance
Blackjack Game Responses
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:
- Initial deal: Send one
placeBetcall with the main stake and optional side bets. - Action continuations: Send one
placeBetcall per decision (hit, stand, double, split, insurance, even money) whileengineData.inProgress === true. - Settlement: The final
placeBetcall ends the round. There is no separatecollectcall.
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({
backendURL,
token,
stake: 100,
...blackjackInitialData(),
});
// Main hand + side bets
const firstWithSides = await placeBet({
backendURL,
token,
stake: 100,
...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.
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({
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 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". 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 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 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';
}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 === falseengineData.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
typescript
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 provably fair (PF) round's pf.rngData field records every one of those calls. Use 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' } },
],
});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:
rngDatarow-for-row matches the livepf.rngData. The engine draws cards asintrows. It addsdoublerows for peek decisions.steps[i].scenario.dealer.cardsand eachhands[j].cardsmatch the live scenario at the same step. This proves the cards were not swapped. The terminal step'stotalWinalso 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:
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.).