Demostrablemente Justo

Los eventos del juego son la conversión de los floats generados aleatoriamente a un resultado concreto, el cual es específico para cada juego. Esto va desde el resultado de un lanzamiento de dados hasta el orden de las cartas de una baraja, o incluso la ubicación de cada bomba en un juego de Mines.

A continuación, se incluye una explicación detallada de cómo convertimos floats en eventos para cada uno de los juegos que tenemos en nuestra plataforma.

Blackjack, Hilo y Baccarat

En una baraja estándar, existen 52 resultados únicos posibles. En los juegos de Blackjack, Hilo y Baccarat de nuestra plataforma, usamos una cantidad ilimitada de barajas al generar el evento del juego, por lo que cada carta tiene la misma probabilidad de salir. Para calcularlo, multiplicamos cada float generado aleatoriamente por 52 y luego convertimos ese resultado en una carta específica.

// Index of 0 to 51 : ♦2 to ♣A
const CARDS = [ 
  ♦2, ♥2, ♠2, ♣2, ♦3, ♥3, ♠3, ♣3, ♦4, ♥4,  
  ♠4, ♣4, ♦5, ♥5, ♠5, ♣5, ♦6, ♥6, ♠6, ♣6, 
  ♦7, ♥7, ♠7, ♣7, ♦8, ♥8, ♠8, ♣8, ♦9, ♥9, 
  ♠9, ♣9, ♦10, ♥10, ♠10, ♣10, ♦J, ♥J, ♠J, 
  ♣J, ♦Q, ♥Q, ♠Q, ♣Q, ♦K, ♥K, ♠K, ♣K, ♦A, 
  ♥A, ♠A, ♣A 
]; 

// Game event translation
const card = CARDS[Math.floor(float * 52)];

La única diferencia entre estos juegos es que, en Hilo y Blackjack, hay un cursor de 13 para generar 52 eventos del juego posibles en los casos en los que es necesario repartir una gran cantidad de cartas al jugador, mientras que en Baccarat solo necesitamos generar 6 eventos del juego para cubrir la mayor cantidad posible de cartas jugables.

Bars

En un juego de Bars, se generan 30 floats aleatorios entre 0.0 y 1.0 y se usan como probabilidad para seleccionar la barra ganadora a partir de una tabla de pagos predeterminada. Esta tabla de pagos se puede encontrar en el pie de página del juego Bars y en la página de cálculo de verificación de Demostrablemente Justo.

Cases

En una apuesta de Cases, se genera un único float aleatorio entre 0.0 y 1.0, que se usa como probabilidad para seleccionar el case ganador a partir de una tabla de pagos predeterminada. Esta tabla de pagos se puede encontrar en el pie de página del juego Cases y en la página de cálculo de verificación de Demostrablemente Justo.

Drill

En Drill, usamos una fórmula inversa para convertir cada float aleatorio en un crash point. La ventaja de la casa (0.98) se divide por el complemento del float, lo que crea una distribución de probabilidad en la que los multiplicadores más altos se vuelven exponencialmente menos comunes. Si el float baja de 0.02, el drill se detiene de inmediato en 1.00x. Este proceso se repite tres veces para generar un crash point para cada drill.

// Game event translation with houseEdge of 0.98 (2%)
Multiplier = u < 0.02 ? 1 : min(0.98 / (1 - u), 2000000)

Tarot

En una apuesta de Tarot, se generan tres floats aleatorios entre 0.0 y 1.0: uno para cada carta de arcano, de izquierda a derecha. Cada float determina qué carta de arcano se selecciona a partir de una tabla de pagos predeterminada. La primera y la tercera carta se extraen de los Arcanos Menores (que se muestran como cartas grises), mientras que la segunda carta se extrae de los Arcanos Mayores (que se muestra como una carta dorada). Los Arcanos Menores y los Arcanos Mayores tienen, cada uno, sus propios rangos de asignación de probabilidades para cada nivel de dificultad. La tabla de pagos está disponible en el pie de página del juego Tarot y en la página de cálculo de verificación de Demostrablemente Justo. El multiplicador final se calcula multiplicando los multiplicadores de las tres cartas de arcano que se generan.

Packs

En una apuesta de Packs, se generan cinco floats aleatorios entre 0.0 y 1.0, uno por cada carta del sobre. Cada float se usa como probabilidad para seleccionar una carta a partir de una tabla de pagos predeterminada que contiene 240 cartas únicas con distintos multiplicadores y rarezas. Esta tabla de pagos se puede encontrar en el pie de página del juego Packs y en la página de cálculo de verificación de Demostrablemente Justo.

Cada uno de los 5 floats se asigna de manera independiente a una carta usando rangos de probabilidad acumulada, lo que da la posibilidad de recibir cartas iguales de la misma rareza o multiplicador al abrir un mismo sobre.

Darts

En una apuesta de Darts, se generan dos floats aleatorios entre 0.0 y 1.0, que se usan como distancia y rotación para posicionar el dardo en la diana. El valor de rotación se interpreta como 0.0 en la parte superior de la diana, 0.5 en la parte inferior y 1.0 volviendo a la parte superior. El valor de distancia se normaliza con sqrt(float) / 2 y representa la distancia desde el centro, de 0.0 a 0.5, donde 0.0 es el centro y 0.5 es el borde de la diana de dardos.

Flip

Un juego de Flip genera 20 eventos de juego independientes, en donde cada uno representa el resultado de un lanzamiento de moneda. Para cada lanzamiento, si el valor del float es menor o igual a 0.5, el resultado es “tails”; de lo contrario, es “heads”.

Snakes

En una apuesta de Snakes, se generan 10 lanzamientos de dados con valores enteros entre 1 y 6. Los valores se suman por ronda, para un total de 5 pares de lanzamientos de dados de seis caras por ronda.

Diamonds

Al jugar Diamonds, hay 7 resultados posibles en forma de gemas. Para lograrlo, multiplicamos cada float generado por 7 para luego convertirlo en la gema correspondiente. Luego, al jugador se le reparten 5 gemas.

// Index of 0 to 6 : green to blue
const GEMS = [ green, purple, yellow, red, cyan, pink, blue ];

// Game event translation
const gem = GEMS[Math.floor(float * 7)];

Dice Roll

En nuestra versión de Dice, cubrimos un rango posible de 00.00 a 100.00, lo que da un total de 10.001 resultados posibles. La conversión a evento del juego se realiza multiplicando el float por la cantidad de resultados posibles y luego dividiendo por 100, para que el número que resulte se ajuste a los límites del rango de dados indicado.

// Game event translation
const roll = (float * 10001) / 100;

Primedice

Primedice usa el mismo mecanismo de lanzamiento que Dice, cubriendo un rango posible de 00.00 a 100.00, lo que da un total de 10.001 resultados posibles. La conversión a evento del juego se realiza multiplicando el float por la cantidad de resultados posibles y luego dividiendo por 100, para que el número resultante quede dentro del rango de dados establecido. La diferencia con Primedice es que permite condiciones de apuesta más complejas, como obtener un resultado entre dos números, obtener un resultado fuera de un rango u obtener un resultado entre dos pares de números.

// Game event translation
const roll = (float * 10001) / 100;

Limbo

En Limbo, usamos un proceso de dos etapas. Primero, tomamos el float y lo multiplicamos tanto por el multiplicador máximo posible como por la ventaja de la casa. Después, para generar un evento del juego con una distribución de probabilidad, dividimos el multiplicador máximo posible por el resultado del primer paso, lo que crea el evento del juego en forma de crash point.

// Game event translation with houseEdge of 0.99 (1%)
const floatPoint = 1e8 / (float * 1e8) * houseEdge;

// Crash point rounded down to required denominator
const crashPoint = Math.floor(floatPoint * 100) / 100;

// Consolidate all crash points below 1
const result = Math.max(crashPoint, 1);

Plinko

En Plinko, el resultado que se genera se basa en el recorrido de la bola al caer. El evento del juego determina la dirección de la bola en cada nivel del recorrido. Los jugadores pueden elegir entre 8 y 16 filas de pines, lo que define cuántos eventos del juego se necesitan para generar un recorrido completo de arriba hacia abajo. Como solo hay dos direcciones posibles (izquierda o derecha), la conversión se realiza multiplicando cada float por 2.

// Index of 0 to 1 : left to right
const DIRECTIONS = [ left, right ];

// Game event translation
const direction = CARDS[Math.floor(float * 2)];

Roulette

Roulette se basa en la versión europea del juego de ruleta, donde la ruleta tiene 37 casillas posibles, del 0 al 36. El evento del juego se calcula multiplicando el float por 37 y luego convirtiendo ese resultado en la casilla correspondiente.

// Index of 0 to 36
const POCKETS = [ 
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 
  20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 
  30, 31, 32, 33, 34, 35, 36
];
  
// Game event translation
const pocket = POCKETS[Math.floor(float * 37)];

Keno

En los juegos tradicionales de Keno, se seleccionan 10 eventos del juego posibles en forma de aciertos sobre un tablero. Para lograrlo, multiplicamos cada float por la cantidad de casillas únicas posibles que existen. Una vez que se marca un acierto, esa casilla no se puede volver a elegir, lo que modifica el tamaño del conjunto de resultados posibles. Esto se hace restando 1 a la cantidad máxima de resultados posibles en cada iteración, por cada resultado de evento del juego que se genere usando el float correspondiente.

// Index of 0 to 39 : 1 to 40
const SQUARES = [ 
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
  21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
  31, 32, 33, 34, 35, 36, 37, 38, 39, 40
];
  
const hit = SQUARES[Math.floor(float * 40)];

Se utiliza la implementación del algoritmo de mezcla de Fisher-Yates para evitar que se generen aciertos duplicados.

Mines

En Mines, el juego se genera con 24 eventos del juego independientes, en forma de minas en el tablero. Cada float se multiplica por la cantidad de casillas únicas que queden disponibles en el tablero. Esto se hace restando 1 a la cantidad de casillas restantes en cada iteración, por cada resultado de evento del juego que se genere usando el float correspondiente. La ubicación de la mina se marca usando una posición en la cuadrícula, de izquierda a derecha y de arriba hacia abajo.

Se utiliza la implementación del algoritmo de mezcla de Fisher-Yates para evitar que se generen aciertos duplicados. Se usan entre 1 y 24 resultados de eventos del juego, según la configuración que se escoja.

Moles

En una apuesta de Moles, el jugador selecciona entre 1 y 6 topos en un tablero de 7 agujeros. En cada ronda, las posiciones de los topos se generan mediante una mezcla de Fisher-Yates en los 7 agujeros, consumiendo un float por topo. Se generan nuevas posiciones en cada ronda, con una partida que dura hasta 8 rondas (1 topo) o 9 rondas (2-6 topos).

Pump

En Pump, el juego se genera con 24 eventos independientes, en forma de explosiones en cada ronda. Cada float se multiplica por la cantidad de rondas restantes. Esto se hace restando 1 a la cantidad de rondas restantes por cada pump.

Se utiliza la implementación del algoritmo de mezcla de Fisher-Yates para evitar que se generen aciertos duplicados. Se usan entre 1 y 24 resultados de eventos del juego, según la configuración que se escoja.

Chicken

En Chicken, el juego se genera con 20 eventos del juego independientes, que representan una muerte en cada ronda. Cada float se multiplica por la cantidad de rondas restantes. Esto se hace restando 1 a la cantidad de rondas restantes en cada paso.

Se utiliza la implementación del algoritmo de mezcla de Fisher-Yates para evitar que se generen aciertos duplicados. Se usan entre 1 y 20 resultados de eventos del juego, según la configuración que se escoja.

Rock Paper Scissors

Las rondas de Rock Paper Scissors se generan de forma independiente para hacer posible una cantidad ilimitada de rondas, excepto en autobet, que está limitado a un máximo de 20 rondas. Para cada ronda, se genera un número entre 0 y 2 y se asigna a rock (0), paper (1) o scissors (2).

Video Poker

En Video Poker, el juego implica 52 eventos del juego independientes, en forma de cartas en una baraja. Cada float se multiplica por la cantidad de cartas posibles que quedan en la baraja. Esto se hace restando 1 a la cantidad de cartas restantes en cada iteración, por cada resultado de evento del juego que se genere usando el float correspondiente.

// Index of 0 to 51 : ♦2 to ♣A
const CARDS = [ 
  ♦2, ♥2, ♠2, ♣2, ♦3, ♥3, ♠3, ♣3, ♦4, ♥4,  
  ♠4, ♣4, ♦5, ♥5, ♠5, ♣5, ♦6, ♥6, ♠6, ♣6, 
  ♦7, ♥7, ♠7, ♣7, ♦8, ♥8, ♠8, ♣8, ♦9, ♥9, 
  ♠9, ♣9, ♦10, ♥10, ♠10, ♣10, ♦J, ♥J, ♠J, 
  ♣J, ♦Q, ♥Q, ♠Q, ♣Q, ♦K, ♥K, ♠K, ♣K, ♦A, 
  ♥A, ♠A, ♣A 
]; 

// Game event translation
const card = CARDS[Math.floor(float * 52)];

Wheel

El número del evento del juego se calcula multiplicando el float por la cantidad de resultados posibles dentro del segmento. Luego se usa para determinar el resultado del evento del juego como un multiplicador.

// Index per payout configuration
const PAYOUTS = {
  '10': {
    low: [ 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0 ],
    medium: [ 0, 1.9, 0, 1.5, 0, 2, 0, 1.5, 0, 3 ],
    high: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 9.9 ]
  },
  '20': {
    low: [
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
    ],
    medium: [ 
      1.5, 0, 2, 0, 2, 0, 2, 0, 1.5, 0, 
      3, 0, 1.8, 0, 2, 0, 2, 0, 2, 0 
    ],
    high: [ 
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
      0, 0, 0, 0, 0, 0, 0, 0, 0, 19.8 
    ]
  },
  '30': {
    low: [ 
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
    ],
    medium: [
      1.5, 0, 1.5, 0, 2, 0, 1.5, 0, 2, 0, 
      2, 0, 1.5, 0, 3, 0, 1.5, 0, 2, 0,
      2, 0, 1.7, 0, 4, 0, 1.5, 0, 2, 0
    ],
    high: [
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 29.7
    ]
  },
  '40': {
    low: [
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
    ],
    medium: [
      2, 0, 3, 0, 2, 0, 1.5, 0, 3, 0,
      1.5, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0,
      1.5, 0, 2, 0, 2, 0, 1.6, 0, 2, 0,
      1.5, 0, 3, 0, 1.5, 0, 2, 0, 1.5, 0
    ],
    high: [
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 39.6
    ]
  },
  '50': {
    low: [
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
      1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0
    ],
    medium: [
      2, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0,
      1.5, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0,
      1.5, 0, 2, 0, 1.5, 0, 2, 0, 2, 0,
      1.5, 0, 3, 0, 1.5, 0, 2, 0, 1.5, 0,
      1.5, 0, 5, 0, 1.5, 0, 2, 0, 1.5, 0
    ],
    high: [
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 49.5
    ]
  }
};

// Game event translation
const spin = PAYOUTS[segments][risk][float * segments];

Crash

Para este juego, usamos el menú de Demostrablemente Justo basado en salt hash.

Slide

Para este juego, usamos el menú de Demostrablemente Justo basado en salt hash.

Zoo

Para este juego, usamos el menú de Demostrablemente Justo basado en salt hash.

Scarab Spin / Tome of Life

El número del evento del juego se calcula multiplicando el float por la cantidad de resultados posibles en el rodillo. Los primeros 4 rodillos tienen una longitud de 30 resultados posibles, mientras que el último rodillo tiene 41. El evento del juego determina la posición de parada central para cada rodillo. Este juego consta de 5 números de evento del juego, salvo en el caso de una ronda de bonificación, donde se generan más.

Blue Samurai

La tragamonedas Blue Samurai tiene 3 tipos de giros diferentes: regulares, de bonificación y especiales.

Para los giros regulares y de bonificación, se generan 18 floats de 0 a 1 a partir de tu hash. A diferencia de la tragamonedas Scarab Spin, que tiene rodillos fijos, la tragamonedas Blue Samurai tiene rodillos dinámicos, lo que implica que cada símbolo se genera a partir del float correspondiente que se le asignó.

Usamos muestreo aleatorio ponderado para asignar cada float a su casilla correspondiente, en el mismo orden, recorriendo los rodillos de arriba hacia abajo y de izquierda a derecha. Cada símbolo tiene su propia probabilidad fija de aparecer en una casilla, y los 2 rodillos externos tienen un conjunto de probabilidades distinto al de los 3 rodillos internos. Para más información sobre cómo se seleccionan los símbolos, revisa el algoritmo de selección proporcional a la aptitud (fitness proportionate selection algorithm).

Los giros especiales son un poco diferentes. Para empezar, solo se toman 12 floats de tu hash, ya que los rodillos externos se deshabilitan. Entre cada giro especial, cualquier símbolo de samurái queda fijo en su lugar durante el resto del juego, y el pago es el recuento final de samuráis. Esto significa que, si por ejemplo queda 1 samurái fijo en el primer giro, técnicamente solo necesitaríamos 11 floats para el giro siguiente. Por simplicidad en el modelo Demostrablemente Justo, generamos 12 floats cada vez y, si el float asignado a una casilla tiene un samurái fijo de un giro anterior, ese float no se usa.

Dragon Tower

En Dragon Tower, el juego se genera con 9 eventos del juego independientes, representados por los niveles de la torre. Para cada nivel, generamos una cantidad de huevos según la dificultad, y también hay un rango de casillas posibles donde puede estar el huevo, representado como un número entero.

Luego, cada float generado se convierte a números enteros para determinar la ubicación del huevo en cada fila. Por ejemplo: un nivel de dificultad fácil se representaría así: [0, 1, 3]; los huevos estarían en las casillas 1, 2 y 4.

// count represents the number of eggs
// size represents the number of possible squares

const LEVEL_MAP = {
  easy: { count: 3, size: 4 },
  medium: { count: 2, size: 3 },
  hard: { count: 1, size: 2 },
  expert: { count1, size: 3 },
  master: { count: 1, size: 4 },
}

Se utiliza la implementación del algoritmo de mezcla de Fisher-Yates para evitar que haya huevos duplicados en una misma fila.