BATTLE 21
Tactical BlackJackClassic BlackJackPokerHow to PlayPracticeAbout Us
Sign in
TacticalClassicPokerPractice

PROVABLY FAIR

Verify fairness

Check the server seed commitment and reconstruct the cards yourself.

What you can see while playing

Open Provably Fair in the lower-right corner. It shows the SHA-256 commitment to the hidden server seed before the cards are known, plus the relevant player seeds and deck or hand identifier. Cash Game also shows its Table ID. Copy the commitment if you want an independent record of what the server promised.

When the seed is revealed

For Tactical Blackjack, Classic Blackjack and Heads-up Poker, open the finished match in Match history to copy the revealed server seed. In Cash Game, your played hand seeds become available in Match history after you leave that table; they are not revealed to you while you remain seated. Hash the revealed seed text with SHA-256 and compare it with the earlier commitment.

How the cards are reconstructed

Tactical and Classic each shuffle a six-deck shoe using the server seed, both player seeds and the deck number. Heads-up Poker shuffles a fresh 52-card deck for each hand using those seeds and the hand number. Cash Game uses a fresh server seed for each 52-card hand and the Table ID; its hand number identifies the record but does not change the shuffle. The number of players dealt in determines how the shuffled cards are grouped into hands, not their order.

Check it yourself

Enter the values from Match history below and calculate the deck. The code alongside the calculator contains the values you entered; Copy code gives you a standalone program you can run locally and compare with the displayed result.

Reconstruct a deck

Deck calculation code

The complete shuffle and dealing algorithm, with the values entered on the left. Copy it to check a deck independently.

(async () => {
// Battle21 deck calculator (modern matches). Run in a browser console or Node 20+.
// Example: await calculateDeck({ game: 'poker_cash', serverSeed: '...', tableId: '...' })
// game: 'tactical', 'classic', 'poker_headsup', or 'poker_cash'
// serverSeed: 64-character revealed hexadecimal server seed.
// player1Seed/player2Seed: exact revealed strings for non-cash games.
// deckNumber: positive shoe/hand number for non-cash games. Cash hands have
// independent server seeds; their hand number is an identifier, not an input.
// tableId: exact Cash Game table ID. playersDealt: 2–6 for Cash Game output.

const textEncoder = new TextEncoder();
const suits = ['♠', '♥', '♦', '♣'];
const ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];

function fromHex(hex) {
  if (!/^[a-f0-9]{64}$/.test(hex)) throw Error('Use a 64-character lowercase hex seed.');
  return Uint8Array.from(hex.match(/../g), byte => parseInt(byte, 16));
}
function toHex(bytes) {
  return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
}
async function hmac(keyBytes, data) {
  const key = await crypto.subtle.importKey('raw', keyBytes,
    { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
  return new Uint8Array(await crypto.subtle.sign('HMAC', key, data));
}
async function calculateDeck({ game, serverSeed, player1Seed = '', player2Seed = '',
  deckNumber = 1, tableId = '', playersDealt = 2 }) {
  serverSeed = serverSeed.trim().toLowerCase();
  let keyBytes = fromHex(serverSeed);
  if (!['tactical', 'classic', 'poker_headsup', 'poker_cash'].includes(game))
    throw Error('Unknown game mode.');
  if (game !== 'poker_cash') {
    if (!Number.isSafeInteger(deckNumber) || deckNumber < 1)
      throw Error('Deck number must be a positive integer.');
    if (!player1Seed || !player2Seed) throw Error('Both player seeds are required.');
    const label = game === 'poker_headsup'
      ? 'battle21-poker-hand-v1:' + deckNumber
      : 'battle21-' + game + '-deck-v1:' + deckNumber;
    keyBytes = await hmac(keyBytes, textEncoder.encode(label));
  } else if (!/^[0-9a-f-]{36}$/i.test(tableId) ||
      !Number.isInteger(playersDealt) || playersDealt < 2 || playersDealt > 6) {
    throw Error('Cash Game needs its table ID and 2–6 dealt players.');
  }
  const version = game === 'poker_cash' ? 'battle21-poker-cash-hand-v3'
    : game === 'poker_headsup' ? 'battle21-poker-52-v2' : 'battle21-shoe-v2';
  const context = textEncoder.encode(JSON.stringify(game === 'poker_cash'
    ? [version, tableId]
    : [version, player1Seed, player2Seed, deckNumber]));

  // Six ordered decks for Blackjack, one ordered deck for Poker.
  const deck = [];
  const copies = game === 'tactical' || game === 'classic' ? 6 : 1;
  for (let copy = 0; copy < copies; copy++)
    for (const suit of suits) for (const rank of ranks)
      deck.push({ id: copies === 6 ? copy + '-' + suit + '-' + rank
        : suit + '-' + rank, suit, rank });

  // HMAC-SHA256 stream: context followed by an unsigned big-endian 64-bit
  // counter. Read each digest in big-endian 32-bit words. Rejection sampling
  // avoids modulo bias; Fisher–Yates consumes one accepted value per swap.
  let counter = 0, pool = new Uint8Array(0), offset = 0;
  async function randomInt(limit) {
    const accepted = Math.floor(4294967296 / limit) * limit;
    let value;
    do {
      if (offset + 4 > pool.length) {
        const input = new Uint8Array(context.length + 8);
        input.set(context);
        new DataView(input.buffer).setBigUint64(context.length, BigInt(counter++), false);
        pool = await hmac(keyBytes, input);
        offset = 0;
      }
      value = new DataView(pool.buffer, pool.byteOffset + offset, 4)
        .getUint32(0, false);
      offset += 4;
    } while (value >= accepted);
    return value % limit;
  }
  for (let i = deck.length - 1; i > 0; i--) {
    const j = await randomInt(i + 1);
    [deck[i], deck[j]] = [deck[j], deck[i]];
  }
  const visible = deck.map(card => card.suit + '-' + card.rank);
  if (game === 'poker_headsup') return {
    firstDealtHand: [visible[0], visible[2]],
    secondDealtHand: [visible[1], visible[3]],
    communityCards: [visible[5], visible[6], visible[7], visible[9], visible[11]],
  };
  if (game === 'poker_cash') return {
    dealtHands: Array.from({ length: playersDealt }, (_, i) =>
      [visible[i], visible[i + playersDealt]]),
    communityCards: [2 * playersDealt + 1, 2 * playersDealt + 2,
      2 * playersDealt + 3, 2 * playersDealt + 5, 2 * playersDealt + 7]
      .map(i => visible[i]),
  };
  return visible;
}

const result = await calculateDeck({
  "game": "tactical",
  "serverSeed": "",
  "player1Seed": "",
  "player2Seed": "",
  "deckNumber": 1,
  "tableId": "",
  "playersDealt": 2
});
console.log(JSON.stringify(result, null, 2));
return result;
})();
Your wellbeing matters to us.

If gambling no longer feels under control, stop playing and seek independent help.

Get help from GambleAware
BATTLE 21

You can contact us at [email protected]

ExploreAbout UsHow to PlayFAQPracticeVerify Fairness
LegalTermsPrivacy Policy
Player safetyResponsible Gaming

© 2026 Battle 21. All rights reserved.

Play responsibly.