Case Opening

Our system generates the result for each case opening by hashing the combination of these 3 inputs.

The case opening result is determined by taking your active seed pair from your account and using these values in our roll generation logic.

  • Client Seed: a random string of 12 characters.
  • Server Seed: a SHA-256 hash of 16 cryptographically secure random bytes.
  • Bet Nonce: Amount of bets made with this pair.

For independent verification, you may replicate the results of any round from previous days by using this code and copying the input below.

    /**
 * 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
}