Skip to content

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:

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

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

  • rngData row-for-row matches the live pf.rngData. The engine draws cards as int rows. It adds double rows for peek decisions.
  • steps[i].scenario.dealer.cards and each hands[j].cards match the live scenario at the same step. This proves the cards were not swapped. The terminal step's totalWin also 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: IGameResult 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.).