Skip to content

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:

  1. Initial deal — one placeBet with the main stake and optional side bets.
  2. Action continuations — one placeBet per decision (hit, stand, double, split, insurance, even money) while engineData.inProgress === true.
  3. Settlement is terminal at the end of the final placeBet. There is no separate collect call.

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.

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 },
  }),
});

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().

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 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.

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';
}

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 === 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 phase is dropped once the round settles. There is no collect call — the final placeBet 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 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.

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 — 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:

  • rngData row-for-row matches the live pf.rngData (cards are drawn as int rows; peek decisions add double rows).
  • steps[i].scenario.dealer.cards and each hands[j].cards match the live scenario at the same step — that's the proof the cards weren't swapped — and the terminal step's totalWin 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 HandlingIGameResult structure and engineData fields.
  • Crash Responses — Another rules-based game, driven by websocket broadcasts rather than per-step actions.
  • Types Reference — Full blackjack type surface (TBlackjackAction, IBlackjackSideBets, etc.).