Blackjack
Unser Blackjack mischt für jede Hand einen frischen Kartenschuh mit 8 Decks über einen transparenten Client-Seed-Mechanismus. So steht die Reihenfolge aller Karten schon vor der ersten ausgeteilten Karte fair fest und kann während der Hand nicht mehr verändert werden.
So funktioniert's- Der Aufbau:Bevor du eine Hand startest, wird ein
Server Seederzeugt. Den Hash dieses Seeds bekommst du sofort von uns. Er wirkt wie ein digitaler Fingerabdruck und beweist, dass die gesamte Reihenfolge im Kartenschuh bereits feststeht, bevor die erste Karte ausgeteilt wird. - Dein Einfluss:Du lieferst einen
Client Seed. Dieser Wert wirkt mit unserem Server Seed zusammen, um den Kartenschuh zu mischen. Wenn du deinen Client Seed änderst, änderst du damit das Zufallsmuster künftiger Hände. - Das Spiel: Die Seeds erzeugen 416 Zufallszahlen (eine pro Karte im Kartenschuh mit 8 Decks), von denen jede die nächste Karte aus den verbleibenden ungemischten Karten zieht. Die Karten werden dann streng der Reihe nach von oben aus dem Kartenschuh ausgeteilt: deine erste Karte, die offene Karte des Dealers, deine zweite Karte, die verdeckte Karte des Dealers und danach jedes Ziehen, jedes Splitten und jede Karte des Dealers. Da der komplette Kartenschuh vor dem Austeilen feststand, können wir keine Karte je nach deiner Spielweise ändern.
Die Reihenfolge im Kartenschuh ergibt sich aus dem Hash der Kombination aus:
- Server Seed: Ein zufällig erzeugter 32-Byte-Wert.
- Client Seed: Eine zufällige Zeichenfolge, die du kontrollierst.
- Bet Nonce: Die fortlaufende Nummer des Spiels, das mit dem aktuellen Paar gespielt wurde.
- 8 Decks, vor jeder Hand gemischt — Kartenzählen bringt nichts.
- Der Dealer hält bei allen 17.
- Blackjack zahlt 3:2.
- Verdoppeln mit den ersten beiden Karten, auch nach dem Splitten.
- Einmal Splitten bei jedem Paar gleichwertiger Karten; gesplittete Asse erhalten je eine Karte.
Überprüf es selbst
Du kannst die Fairness jeder Blackjack-Hand überprüfen, indem du dein Seed-Paar rotierst (dadurch wird der aktive Server Seed offengelegt) und das Mischen direkt hier rekonstruierst. Gib den offengelegten Server Seed, deinen Client Seed und die Nonce der Hand ein — die exakte Reihenfolge im Kartenschuh und damit jede ausgeteilte Karte wird in deinem Browser reproduziert.
Das Verifizierungstool nutzt denselben Algorithmus wie der Code unten.
/**
* This module provides functions for generating random rolls in a gaming system.
* The randomness is achieved using a combination of server seed, client seed, and EOS block hash.
*/
import { createHmac } from 'node:crypto'
/**
* Roll function to generate random numbers
* @param seed
* @param entropy
* @param nonce
* @param rollCount
* @returns returns an array of floats ranging from 0 (inclusive) to 1 (inclusive)
*/
function roll({
seed,
entropy,
nonce,
rollCount,
}: {
seed: string
entropy: string
nonce: number
rollCount: number
}): number[] {
const chunk = (arr: number[], n: number): number[][] =>
arr
.slice(0, ((arr.length + n - 1) / n) | 0)
.map((_, i) => arr.slice(n * i, n * i + n))
function* byteGenerator({
seed,
entropy,
nonce,
cursor,
}: {
seed: string
entropy: string
nonce: number
cursor: number
}): Generator {
// Setup cursor variables
let currentRound = Math.floor(cursor / 32)
let currentRoundCursor = cursor
currentRoundCursor -= currentRound * 32
// Generate outputs until cursor requirement fulfilled
while (true) {
// HMAC function used to output provided inputs into bytes
const hmac = createHmac('sha256', seed)
hmac.update(`${entropy}:${nonce}:${currentRound}`)
const buffer = hmac.digest()
// Update cursor for next iteration of loop
while (currentRoundCursor < 32) {
yield Number(buffer[currentRoundCursor])
currentRoundCursor += 1
}
currentRoundCursor = 0
currentRound += 1
}
}
const generateFloats = ({
seed,
entropy,
nonce,
cursor,
count,
}: {
seed: string
entropy: string
nonce: number
cursor: number
count: number
}): number[] => {
// Random number generator function
const rng = byteGenerator({ seed, entropy, nonce, cursor })
// Declare bytes as empty array
const bytes: number[] = []
// Populate bytes array with sets of 4 from RNG output
while (bytes.length < count * 4) {
bytes.push(rng.next().value)
}
// Return bytes as floats using lodash reduce function
return chunk(bytes, 4).map(bytesChunk =>
bytesChunk.reduce((result: number, value: number, i: number) => {
const divider = 256 ** (i + 1)
const partialResult = value / divider
return result + partialResult
}, 0),
)
}
function getRolls(
seed: string,
entropy: string,
nonce: number,
count: number,
) {
return generateFloats({
seed,
entropy,
nonce,
cursor: 0,
count,
})
}
return getRolls(seed, entropy, nonce, rollCount)
}
function rollMany({
seed,
entropy,
rollCount,
manyCount,
}: {
seed: string
entropy: string
rollCount: number
manyCount: number
}): number[][] {
let nonce: number = 0
return Array.from({ length: manyCount }, () => {
nonce++
return roll({
seed,
entropy,
nonce,
rollCount,
})
})
}
/**
* Helper function to get the result of a roulette round.
*
* @param seed - The server seed (unhashed), which is generated at game initialization.
* @param eosBlockHash - The EOS block hash used as an additional input for randomness.
* @returns The result of the roulette round. The result can be 'RED', 'BLACK', or 'GREEN'.
* @example
* getRouletteOutcome({
* seed: 'bc6dd40a3d992f6af19dec8a3b7143fe7f28b105434c567869de4f74de077384',
* eosBlockHash:
* '164dfa0a77db58df5c2454c8b6fc4c697e2e3bc0d5dc16a4f20092ebfe0304f2',
* });
*/
function getRouletteOutcome({
seed,
eosBlockHash,
}: {
seed: string
eosBlockHash: string
}): string {
const result = roll({
seed,
entropy: eosBlockHash,
nonce: 1,
rollCount: 1,
})
const pfOutcome = result[0]
const outcome = Math.floor(pfOutcome * 15)
/**
* The roulette wheel is divided into 15 slots:
* - 0 is GREEN
* - 1-7 are RED
* - 8-14 are BLACK
* - 1 and 14 are RED_BAIT and BLACK_BAIT respectively. Both the color and bait win.
*/
let rouletteResult: 'GREEN' | 'RED' | 'RED_BAIT' | 'BLACK' | 'BLACK_BAIT'
if (outcome === 0) {
rouletteResult = 'GREEN'
}
else if (outcome <= 7 && outcome >= 1) {
if (outcome === 1) {
rouletteResult = 'RED_BAIT'
}
else {
rouletteResult = 'RED'
}
}
else if (outcome <= 14 && outcome >= 8) {
if (outcome === 14) {
rouletteResult = 'BLACK_BAIT'
}
else {
rouletteResult = 'BLACK'
}
}
else {
throw new Error('Invalid roll value')
}
return rouletteResult
}
/**
* Helper function to get a Mines board.
*
* @param seed The server seed (unhashed), which is saved on your profile. The unhashed version is provided once you rotate your account active seed pair.
* @param nonce Be nonce of seed pair
* @param entropy Client seed
* @param mineCount Number of mines
* @returns a 2D array representing the Mines board
* @example
* getMinesOutcome({
* seed: '655c850574b58409c124d6b266e4e8e618732a07a2e00454cdb59de6f711540c',
* nonce: 10,
* entropy: '47eb564l52y',
* mineAmount: 10,
* });
*/
const GRID_WIDTH = 5 as const
const GRID_HEIGHT = 5 as const
function getMinesOutcome({
seed,
nonce,
entropy,
mineAmount,
}: {
seed: string
nonce: number
entropy: string
mineAmount: number
}): { type: string, uncovered: boolean }[][] {
const result = roll({
seed,
entropy,
nonce,
rollCount: mineAmount,
})
const newBoard: { type: string, uncovered: boolean }[][] = Array.from(
{ length: 5 },
() =>
Array(5).fill({
type: 'SAFE',
uncovered: false,
}) as { type: string, uncovered: boolean }[],
)
const minePositions = generateRandomMinesPositions(result)
for (const minePosition of minePositions) {
const { row, col } = minePosition
newBoard[row][col] = { type: 'MINE', uncovered: false }
}
return newBoard
}
function generateRandomMinesPositions(
randomFloats: number[],
): { row: number, col: number }[] {
const potentialPositions: { row: number, col: number }[] = []
for (let x = 0; x < GRID_WIDTH; x++) {
for (let y = 0; y < GRID_HEIGHT; y++) {
const newPosition = { row: x, col: y }
potentialPositions.push(newPosition)
}
}
const randomPositions = randomFloats.map((randomFloat) => {
const randomIndex = Math.floor(randomFloat * potentialPositions.length)
const [randomPosition] = potentialPositions.splice(randomIndex, 1)
return randomPosition
})
return randomPositions
}
/**
* Helper function to get case ticket
*
* @param seed The server seed (unhashed), which is saved on your profile. The unhashed version is provided once you rotate your account active seed pair.
* @param nonce Be nonce of seed pair
* @param entropy Client seed
* @param rollCount Number of cases you open (1-4)
* @returns a array representing the case ticket per roll
* @example
* getCaseUnboxOutcome({
* seed: '9424a6362c807cceb6aa89bc9dcafc433994543deed9e7c807dda880e0dd7b25',
* nonce: 0,
* entropy: '47eb564l52y',
* rollCount: 1,
* });
*/
function getCaseUnboxOutcome({
seed,
nonce,
entropy,
rollCount,
}: {
seed: string
nonce: number
entropy: string
rollCount: number
}): { roll: number, ticket: number }[] {
const MAX_TICKET_COUNT = 100000
const result: number[] = roll({
seed,
entropy,
nonce,
rollCount,
})
const tickets: { roll: number, ticket: number }[] = []
for (let i = 0; i < rollCount; i++) {
tickets.push({
roll: i,
ticket: result[i] * MAX_TICKET_COUNT,
})
}
return tickets
}
/**
* Helper function to get case battle outcome
* @param seed
* @param eosBlockHash
* @param rounds
* @param players
* @returns returns an array of each player's ticket per round. The ticket can be bound to the item of the case that is being played in the round.
* @example
* getCaseBattleOutcome({
* seed: '73884c92de9eba6ede0734cc2423a1e3a95af39ac50efca45cbf91f666263fe9',
* eosBlockHash:
* '168ccd760261a8da6728fd2f52daa08eae33339b49fcca863c5bc258bf576d23',
* rounds: 5,
* players: 3,
* }),
*/
function getCaseBattleOutcome({
seed,
eosBlockHash,
rounds,
players,
}: {
seed: string
eosBlockHash: string
rounds: number
players: number
}): { round: number, player: number, ticket: number }[] {
const result: number[][] = rollMany({
seed,
entropy: eosBlockHash,
rollCount: rounds,
manyCount: players,
})
const MAX_TICKET_COUNT = 100000
const totalRounds: number = rounds * players
const tickets: { round: number, player: number, ticket: number }[] = []
for (let i = 0; i < totalRounds; i++) {
const round: number = Math.floor(i / players)
const player: number = i % players
const ticket: number = result[player][round] * MAX_TICKET_COUNT
tickets.push({ round: round + 1, player: player + 1, ticket })
}
return tickets
}
/**
* Helper function to get Plinko outcome
*
* The result is array for each ball and its outcome on the board. The outcome is the slot number where the ball ends up (starting from index 0).
*
* @param seed The server seed (unhashed), which is saved on your profile. The unhashed version is provided once you rotate your account active seed pair.
* @param nonce Be nonce of seed pair
* @param entropy Client seed
* @param rowAmount Number of rows in the Plinko game
* @param rollCount Plinko row amount multiplied with the ball count
* @returns a array representing the case ticket per roll
* @example
* getPlinkoOutcome({
* seed: 'e6d924a5a7d13850cc213abd65935104f18cbd28115b8eb444331613fbd5f3c6',
* nonce: 0,
* entropy: '47eb564l52y',
* rowAmount: 12,
* ballCount: 1,
* }),
*/
function getPlinkoOutcome({
seed,
nonce,
entropy,
rowAmount,
ballCount,
}: {
seed: string
nonce: number
entropy: string
rowAmount: number
ballCount: number
}): number[] {
const rollCount: number = rowAmount * ballCount
const result: number[] = roll({
seed,
entropy,
nonce,
rollCount,
})
const trimmedResults: number[] = result.map(float =>
Number(float.toFixed(2)),
)
// Initialize an empty array to store the game results
const gameResults: number[] = []
// Loop over the number of balls
for (let ballIndex = 0; ballIndex < ballCount; ballIndex++) {
const sliceStart: number = ballIndex * rowAmount
const sliceEnd: number = (ballIndex + 1) * rowAmount
const ballNumbers: number[] = trimmedResults.slice(sliceStart, sliceEnd)
let outcome: number = 0 // Which slot the ball ends up in
for (let i = 0; i < rowAmount; i++) {
if (ballNumbers[i] > 0.5) {
outcome++
}
}
gameResults.push(outcome)
}
// Return the array of game results
return gameResults
}
/**
* Helper function to reconstruct the Blackjack shoe.
*
* The 8-deck shoe (416 cards) is shuffled from a single roll before any card
* is dealt. Cards are then dealt strictly in order from the top of the shoe:
* your first card, the dealer's upcard, your second card, the dealer's hole
* card, and then every hit, split card and dealer draw.
*
* @param seed The server seed (unhashed), which is saved on your profile. The unhashed version is provided once you rotate your account active seed pair.
* @param nonce Be nonce of seed pair
* @param entropy Client seed
* @returns the full shoe in deal order, as human-readable cards
* @example
* getBlackjackShoe({
* seed: '655c850574b58409c124d6b266e4e8e618732a07a2e00454cdb59de6f711540c',
* nonce: 10,
* entropy: '47eb564l52y',
* });
*/
function getBlackjackShoe({
seed,
nonce,
entropy,
}: {
seed: string
nonce: number
entropy: string
}): string[] {
const DECK_COUNT = 8
const SHOE_SIZE = 52 * DECK_COUNT
const result: number[] = roll({
seed,
entropy,
nonce,
rollCount: SHOE_SIZE,
})
// The ordered shoe is [0..415] mapped onto cards 0-51; every float picks
// (and removes) one card from the remaining unshuffled cards.
const remaining: number[] = Array.from(
{ length: SHOE_SIZE },
(_, i) => i % 52,
)
const shoe: number[] = result.map((randomFloat) => {
const randomIndex = Math.floor(randomFloat * remaining.length)
const [card] = remaining.splice(randomIndex, 1)
return card as number
})
// Decode card numbers: rank = card % 13 (Ace first), suit = floor(card / 13)
const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
const SUITS = ['♠', '♥', '♦', '♣']
return shoe.map(
card => `${RANKS[card % 13]}${SUITS[Math.floor(card / 13)]}`,
)
}
