Case Battles

En las Case Battles, la equidad real se consigue combinando la criptografía del servidor con la imprevisibilidad de la blockchain de EOS. Así, ni los jugadores ni la casa pueden influir en el resultado una vez que la batalla está montada.

Cómo funciona
  1. Inicialización:Cuando se crea una batalla, generamos un Server Seed Pair único formado por una semilla privada y una pública. El Public Server Seed se muestra a todos los participantes al instante. Esto funciona como un compromiso: no podemos cambiar la semilla privada subyacente una vez revelada la pública.
  2. El bloqueo:En cuanto todos los jugadores se unen a la batalla, asignamos a la partida un EOS Block Number futuro. Como ese bloque todavía no se ha minado en la blockchain de EOS, su hash es completamente desconocido para todos, nosotros incluidos.
  3. El resultado:Cuando se mina el bloque designado, usamos su Block Hash único combinado con nuestro Private Server Seed para calcular el resultado. Como el hash del bloque determina el resultado y era imposible de predecir, se garantiza que la partida sea justa.
Parámetros de entrada

Nuestro sistema genera el resultado de las rondas de batallas de cajas aplicando un hash a la combinación de estos datos de entrada:

  • Private Server Seed: Un valor de 16 bytes generado aleatoriamente (se revela después de la partida).
  • EOS Block Hash: El identificador único del bloque de EOS asignado. Es la fuente de entropía externa.
  • Public Server Seed: El hash SHA-256 de la semilla privada, visible desde el principio.
  • Rondas: El número de rondas de la batalla.
  • Jugadores: El número de participantes.

Verifícalo tú mismo
Cuando termina la batalla se revela el Private Server Seed. Puedes verificar el resultado por tu cuenta con el fragmento de código de abajo. Al introducir las semillas y el hash del bloque, generarás exactamente el mismo resultado, lo que demuestra que la partida funcionó tal y como se describe.

    /**
 * 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)]}`,
  )
}

  

Cookies en este sitio

Algunas cookies son necesarias para mantener tu sesión abierta y que los juegos funcionen. Con tu permiso también las usaríamos para medir el rendimiento del sitio y de nuestros anuncios. Puedes cambiar tu respuesta cuando quieras — consulta nuestra política de privacidad