Ir al contenido principal

Provably Fair - Eventos del juego

Qué son los eventos del juego

Anteriormente, generamos números de punto flotante aleatorios entre 0 y 1 mediante las funciones generateFloats y byteGenerator. Los eventos del juego convierten estos números de punto flotante generados en resultados reales del juego. Esto ocurre de distintas maneras según el juego. En Plinko, los números de punto flotante ayudan a determinar la dirección de la caída de la bola. En Dice, ayudan a determinar el resultado de la tirada de dados.

A continuación, se ofrece una explicación detallada de cómo convertimos los números de punto flotante en eventos o resultados justos para cada juego original disponible en nuestra plataforma.


Tower

En Tower, el objetivo del juego es llegar a la cima de la torre y salvar a la princesa encontrando las llaves y evitando las manzanas envenenadas. Cada fila de la torre contiene una cantidad predefinida de casillas seguras (llaves) y manzanas envenenadas, según la dificultad seleccionada.

El evento del juego en Tower consiste en determinar la posición de las casillas seguras (o llaves) en cada fila.

Hay un total de 9 filas en la torre. La cantidad de columnas y la distribución de llaves y manzanas envenenadas varían según la dificultad elegida:

  • Fácil: 4 columnas - 3 llaves y 1 manzana envenenada por fila

  • Media: 3 columnas - 2 llaves y 1 manzana envenenada por fila

  • Difícil: 2 columnas - 1 llave y 1 manzana envenenada por fila

  • Experto: 3 columnas: - 1 llave y 2 manzanas envenenadas por fila

  • Maestro: 4 columnas - 1 llave y 3 manzanas envenenadas por fila

El resultado de cada fila se determina generando posiciones aleatorias para las llaves (casillas seguras) dentro de las columnas disponibles, según la configuración de dificultad.

const TOWER_DIFFICULTY_TO_CONFIG: Record<TowerDifficulty, DifficultyConfig> = {
[TowerDifficulty.EASY]: {
count: 3,
columns: 4,
},
[TowerDifficulty.MEDIUM]: {
count: 2,
columns: 3,
},
[TowerDifficulty.HARD]: {
count: 1,
columns: 2,
},
[TowerDifficulty.EXPERT]: {
count: 1,
columns: 3,
},
[TowerDifficulty.MASTER]: {
count: 1,
columns: 4,
},
};


// Generate floats to determine the positions of safe tiles (keys) for each row
const floats = generateFloats({
serverSeed,
clientSeed,
nonce,
cursor,
count,
});

// Get the tower configuration based on the selected difficulty
const towerConfig = TOWER_DIFFICULTY_TO_CONFIG[difficulty];
const results: number[][] = [];
const resultPerRow = towerConfig.count;

// Iterate through each row to determine the positions of safe tiles for that row
for (let i = 0; i < TOWER_TOTAL_ROWS.toNumber(); i++) {
const result: number[] = [];
const values = Array.from(Array(towerConfig.columns).keys());

// Get positions for safe tiles in the current row
for (let j = 0; j < resultPerRow; j++) {
const float = floats[i * resultPerRow + j];
if (float === undefined) {
break;
}

const position = Math.floor(float * (towerConfig.columns - j));
result.push(values.splice(position, 1)[0] ?? 0);
}

results.push(result);
}

return results;

// Example: when clientSeed='abc', serverSeed='def', nonce=1, difficulty='easy'
// results: [
// [0, 1, 2],
// [0, 2, 3],
// [0, 2, 3],
// [0, 1, 2],
// [0, 1, 3],
// [0, 1, 2],
// [1, 2, 3],
// [0, 1, 3],
// [0, 1, 3]
// ]
// Each sub-array represents the column indexes of safe tiles for that row
// (e.g., for the first row, safe tiles are in columns 0, 1, and 2)

Chicken

En Chicken, el objetivo es mover al pollo por tantos carriles como sea posible sin caer en el carril peligroso.

Cada paso exitoso hacia adelante aumenta el multiplicador de pago. El jugador puede retirar sus ganancias en cualquier momento antes de que el pollo salte al carril peligroso. Si el pollo llega a la zona peligrosa antes de que el jugador retire sus ganancias, se pierde la apuesta.

El evento principal del juego en Chicken es determinar el carril peligroso: el carril específico en el que el pollo perderá.

Hay 4 dificultades en Chicken. La cantidad de carriles que el pollo puede cruzar varía según la dificultad seleccionada:

  • Fácil: 19 carriles

  • Media: 17 carriles

  • Difícil: 15 carriles

  • Experto: 10 carriles

const TOTAL_LANES = 20

// Number of lanes that the chicken can cross for each difficulty
const DIFFICULTY_TO_VALID_LANE_COUNT: Record<ChickenDifficulty, number> = {
[ChickenDifficulty.EASY]: 19,
[ChickenDifficulty.MEDIUM]: 17,
[ChickenDifficulty.HARD]: 15,
[ChickenDifficulty.EXPERT]: 10,
};

// Number of gameResults generated depends on the difficulty
// Example with Hard difficulty, we generate 20 - 15 = 5 numbers
// and the smallest number in the array is the danger lane
const gameResults = generateFloats({
serverSeed,
clientSeed,
nonce,
cursor: 0,
count: TOTAL_LANES - DIFFICULTY_TO_VALID_LANE_COUNT[difficulty] // difficulty selected for the game
}).map((val, index) => Math.floor(val * (TOTAL_LANES - index)));

// Danger lane is determined by the smallest number in the game results array
const dangerLane = Math.min(...gameResults)

// Example: Hard difficulty - gameResults = [10, 4, 6, 15, 8]
// Danger lane will be on 4 (this is zero based index)
// Lane 0 - 3 are safe and lane 4 will be the danger lane
// The chicken will lose upon reaching lane 4 if not cashed out earlier

Plinko

En Plinko, los jugadores pueden elegir una pirámide con una altura de 8 a 16. A medida que la bola se suelta desde la parte superior, caerá a la izquierda o a la derecha de 8 a 16 veces, cada vez que golpee una clavija en el tablero de Plinko. El evento del juego en Plinko consiste en determinar si la bola cae a la izquierda o a la derecha en cada nivel, representado por 0 y 1.

// Create multiple floats between 0 and 1 
// Example: floats = [0.8320531346835196, 0.8531235849950463, 0.18227359023876488, 0.774284637067467, 0.18627392360940576, 0.6012540922965854, 0.608081541955471, 0.46018488449044526] when clientSeed='abc', serverSeed='def', nonce=1, count=8
// count refers to the number of plinko rows
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor, count });

// Example: [1, 1, 0, 1, 0, 1, 1, 0] Each 1 represents a drop to the right whereas 0 represents a drop to the left
const dropDirections = floats.map(val => Math.floor(val * 2));

// This sums up all the values in the array to get the final position of the ball from the left.

// Example: finalPositionFromLeft = 5 (the ball will land on the 6th slot from the left)
const finalPositionFromLeft = dropDirections.reduce((cur, acc) => cur + acc, 0)

Dice

En Dice, una tirada de dados puede tener un valor entre 0.00 y 100.00. El evento del juego en Dice consiste en determinar este valor. Lo hacemos utilizando (floats * 10,001) / 100). Como incluimos valores de hasta 2 decimales y el número 0, hay un total de 10,000 + 1 = 10,001 resultados posibles (incluidos los decimales).

// Creates an array of random numbers 
// Example: With clientSeed = 'abc', serverSeed = 'def', nonce = 1, floats = [0.43053962965495884]
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: 1 });

// Converts the value in the array to a number between 0.00 to 100.00 and return the value

// Example: resultValue = [43.05]
const resultValue = floats.map(val => Math.floor(floats * 10001) / 100);
return resultValue[0];
// Example: 43.05 is returned as the result of the game

Limbo

En Limbo, el objetivo del juego es adivinar un multiplicador >1. Si el resultado del juego es mayor que el multiplicador, el usuario gana el juego. El evento del juego consiste en generar un valor de punto flotante superior a 1 y que siga una distribución de curva hiperbólica.

// Generate float between 0 and 1 
// Example: floats = [0.43053962965495884], float = 0.43053962965495884,
// when clientSeed='abc', serverSeed='def', nonce=1
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: 1 });
const float = floats[0]
// generate limbo game event results with house edge (1%)
// using a hyperbolic 1/x curve
// multiplying by this and dividing by (MAX_VALUE + 1) prevents division by 0 error

const MAX_VALUE = 2 ** 24;
const rtp = 0.99;

// Example: rawValue = 2.298905
const rawValue = (rtp * MAX_VALUE) / (MAX_VALUE * float + 1)

// resultValue = 2.29
const resultValue = (0.99 * MAX_VALUE / (MAX_VALUE * float +1)).toFixed(2,ROUND_DOWN);
return resultValue;

Keno

En Keno, el evento del juego requiere identificar 10 ubicaciones únicas de gemas en el tablero de keno. Para lograrlo, generamos 10 números de punto flotante diferentes entre 1 y 40, que representan cada ubicación en el tablero de keno. Luego se utiliza un método para gestionar los números de punto flotante duplicados y devolver las 10 ubicaciones finales de las gemas.

// Generate 10 different floats as there are 10 positions that are selected when the game is played 
// Example: floats = [18, 26, 7, 22, 31, 10, 23, 8, 24, 13] when clientSeed='abc', serverSeed='def', nonce=1

const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: 10 }) .map((val, index) => Math.ceil(val * (40 - index)))

// array used to store the position of the gems const gemLocations: number[] = [];

// this loop helps to prevent duplicate gem positions by skipping positions that already contain a gem
for (const val of floats) { let currentGem = val;
let i = 0;
while (i <= currentGem) {
if (gemLocations.includes(i)) {
currentGem++;
}
i++;
}

gemLocations.push(currentGem);
}

// returns the location of the gem on the board
// Example: [18, 27, 7, 24, 35, 11, 28, 9, 31, 16] return gemLocations;

Mines

En Mines, el evento del juego requiere identificar la ubicación de las minas en el tablero de 25 casillas. Según la selección del usuario de una cantidad de minas, generamos una lista de números enteros de 0 a 24 para representar los 25 espacios del tablero.

// Generate 10 different floats 
// Example: [20, 20, 4] when clientSeed='abc', serverSeed='def', nonce=1, numberOfBombs=3
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: numberOfBombs })
.map((val, index) => Math.floor(val * (25 - index)));

// array used to store the position of the bombs
const mineLocations: number[] = [];

// this loop helps to prevent duplicate bomb positions by skipping positions that already contain a mine
for (const val of floats) {
let currentMine = val;
let i = 0;

while (i <= currentMine) {
if (mineLocations.includes(i)) {
currentMine++;
} i++; } mineLocations.push(currentMine); }

// returns the location of the bombs on the board
// Example: [20, 21, 4]
return mineLocations;

Roulette

Nuestra Roulette se basa en la rueda europea de un solo ‘0’, que tiene 37 casillas diferentes, desde 0 hasta 36. El evento del juego en Roulette consiste en calcular, entre las 37 casillas, en cuál caerá la bola.

// Generate a float between 0 and 1 
// Example: floats = [0.43053962965495884]
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor: 0, count: 1 });

// generate an integer from 0 to 37 to represent the pocket to land on
// Example: resultValue = 15
const resultValue = floats.map(val => Math.floor(val * 37))[0];
return resultValue

Wheel

En Wheel, el usuario selecciona la cantidad de segmentos que tendrá la rueda. Según el segmento en el que se detenga el indicador, el usuario ganará el multiplicador de ese segmento. El evento del juego consiste en determinar el segmento.

// Generate a float between 0 and 1 
// Example: floats = [0.43053962965495884]
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:1 });

// Get an integer representing the number of segments set in the game
// Example: resultSegment = 8
const resultSegment = floats.map(val => Math.floor(val * numberOfSegments))[0];

return resultSegment

Blackjack

En Blackjack, el juego se juega con una baraja ilimitada. Por lo tanto, cada turno siempre tendrá la misma probabilidad. El evento del juego en Blackjack consiste en determinar la carta que se roba si el jugador o la banca roba una carta de la baraja.

// Generate a float between 0 and 1 
// Example: floats = [0.43053962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (52 elements are generated but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });

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
];

// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));

// Example: cards = [♠7, ♠10, ♥4, ♥9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;

Hilo

En Hilo, el juego se juega con una baraja ilimitada. Por lo tanto, cada turno siempre tendrá la misma probabilidad. El objetivo del juego es adivinar si la siguiente carta tiene un valor mayor o menor que la carta actual. El evento del juego en Hilo consiste en determinar qué carta roba el usuario a continuación (si roba una).

// Generate a float between 0 and 1 
// Example: floats = [0.43053962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (an unlimited number of cards can be drawn but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });

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
];

// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));


// Example: cards = [♠7, ♠10, ♥4, ♥9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;

Crash

Hay 2 partes en el cálculo del resultado de un juego de Crash

  • Hash de bloque de Bitcoin

  • Semilla

Hash de Bitcoin

Los detalles están en este tuit:

El hash del bloque 779588 se puede encontrar en el explorador de bloques de BTC (https://btcscan.org/block/000000000000000000033ab71b5fa7edcafe388e5f49719f1de8e1f8d0670460) y coincide con el hash que se muestra en el tuit.

Semilla

El proceso para generar semillas para 10 millones de juegos de Crash está diseñado para garantizar la imparcialidad y la transparencia, evitando cualquier posibilidad de manipulación por parte de Shuffle. Así se generan y verifican las semillas:

  1. Generación inicial de semillas: La semilla del juego número 10 millones se elige aleatoriamente por Shuffle .

  2. Hash secuencial para la generación de semillas: Para generar las semillas de todos los demás juegos, utilizamos un método de hash inverso:

    • La semilla de cada juego es el resultado de aplicar hash a la semilla del juego siguiente. Específicamente, el hash del juego número 9,999,999 es el hash SHA256 del hash del juego número 10 millones.

    • Este encadenamiento continúa de modo que la semilla de cada juego se deriva de aplicar hash al hash del juego siguiente, culminando con la semilla del primer juego, que se deriva del hash del segundo juego.

  3. Verificación de la integridad de las semillas:

    • Al publicar el valor con hash del hash utilizado en el primer juego, los jugadores pueden verificar de forma independiente la integridad del hash de todos los juegos hasta el juego número 10 millones.

    • Con este método, si conocen el hash de cualquier juego específico, pueden verificar las semillas de todos los juegos anteriores. Por ejemplo, si conocen la semilla del juego 1323185, pueden aplicar hash a esta semilla para verificar que coincide con la semilla del juego 1323184, y así sucesivamente hasta llegar al primer juego.

  4. Transparencia y seguridad:

    • Al utilizar un hash de bloque de BTC futuro que está fuera del control de Shuffle y publicar la versión con hash de la semilla del primer juego, los usuarios pueden comprobar que los resultados de los juegos de Crash son justos y no fueron manipulados.

Slide

Slide utiliza el mismo proceso de provably fair y semillas que Crash. La generación de semillas se realizó públicamente en x.com, y la publicación se puede encontrar aquí.

El hash del bloque 952294 se puede encontrar en el explorador de bloques de BTC (https://btcscan.org/block/00000000000000000000a382760ff19cdb7f9f22147b9123f3e471fb3c6d129a) y coincide con el hash que se muestra en el tuit.

Baccarat

En Baccarat, el juego se juega con una baraja ilimitada. Por lo tanto, cada turno siempre tendrá la misma probabilidad. El evento del juego en Baccarat consiste en determinar las cartas repartidas tanto a la mano del Jugador como a la de la Banca durante el reparto inicial y cualquier carta adicional, siguiendo las reglas estándar de robo de Baccarat.

Las cartas se reparten en el siguiente orden a partir de la secuencia de números de punto flotante generada:

  1. Primera carta del Jugador - índice de punto flotante 0

  2. Primera carta de la Banca - índice de punto flotante 1

  3. Segunda carta del Jugador - índice de punto flotante 2

  4. Segunda carta de la Banca - índice de punto flotante 3

  5. Tercera carta del Jugador (si roba) - índice de punto flotante 4

  6. Tercera carta de la Banca (si roba)

    1. índice de punto flotante 4 - si el jugador no robó la tercera carta

    2. índice de punto flotante 5 - si el jugador roba la tercera carta

// Generate a float between 0 and 1
// Example: floats = [0.40353962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (an unlimited number of cards can be drawn but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });

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
];

// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));

// Example: cards = [♠7, ♠10, ♥4, ♠9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;

Blitz

En Blitz, el juego se juega con una baraja ilimitada. Por lo tanto, cada turno siempre tendrá la misma probabilidad. El evento del juego en Blitz consiste en determinar las cartas repartidas al jugador según la cantidad de cartas que haya elegido robar y evaluar si todas las cartas robadas son únicas.

// Generate a float between 0 and 1
// Example: floats = [0.43053962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (an unlimited number of cards can be drawn but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });

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
];

// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));

// Example: cards = [♠7, ♠10, ♥4, ♠9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;

Coinflip

En Coinflip, el objetivo es acertar el lado en el que cae una moneda lanzada. El evento del juego en Coinflip consiste en determinar el resultado (CARA o CRUZ) de cada lanzamiento. Coinflip tiene tres modos: Clásico (un solo lanzamiento), Clásico Progresivo (lanzar repetidamente y retirar las ganancias en cualquier momento) y Objetivo (elegir una cantidad de lanzamientos y el número mínimo de aciertos necesarios para ganar). Sin embargo, los tres determinan los resultados de los lanzamientos de la misma forma.

Cada lanzamiento se determina generando un número de punto flotante y asignándolo a 0 o 1, exactamente como Plinko asigna sus números de punto flotante a caídas a la izquierda o a la derecha:

// Generate floats to determine each flip outcome
// Example: floats = [0.43053962965495884, 0.6645898742135614, ...] when clientSeed='abc', serverSeed='def', nonce=1
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor, count });

// Each float maps to 0 or 1, which maps to HEADS or TAILS
// Example: [0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]
const flipValues = floats.map(val => Math.floor(val * 2));
const flipResults = flipValues.map(val => (val === 0 ? 'HEADS' : 'TAILS'));


// Example: [HEADS, TAILS, HEADS, TAILS, TAILS, HEADS, TAILS, HEADS, TAILS, HEADS,
// TAILS, HEADS, TAILS, HEADS, HEADS, TAILS, HEADS, HEADS, HEADS, TAILS]
return flipResults;


Clásico: se genera un lanzamiento (cantidad = 1). El jugador gana si el lado que eligió coincide con el lanzamiento.

Clásico Progresivo: los lanzamientos se generan de uno en uno a medida que el jugador continúa, pero el resultado de cada lanzamiento queda establecido desde el inicio de la ronda: el lanzamiento número N siempre se obtiene generando los lanzamientos de 1 a N y tomando el último. Por lo tanto, revelar los lanzamientos uno por uno a medida que el jugador avanza no cambia cuál habría sido el resultado de un lanzamiento determinado. El jugador puede retirar sus ganancias después de cualquier lanzamiento exitoso; la ronda termina con el primer fallo.

Objetivo: el jugador elige una cantidad total de lanzamientos (hasta 20) y un número mínimo de aciertos necesarios para ganar. Todos los lanzamientos se generan como un solo lote (cantidad = totalFlips), y se gana la ronda si la cantidad de lanzamientos que coincide con la elección del jugador es al menos el mínimo seleccionado.

Floor is Lava

Objetivo: Cruzar una cuadrícula de 7×7 (49 casillas) a lo largo de tres niveles consecutivos sin pisar una casilla que se convierta en lava.

Cada nivel se desarrolla en «rondas» de tamaño fijo: en cada ronda, un lote de casillas cae en la lava a la vez, y pueden retirar sus ganancias después de sobrevivir a cualquier ronda.

Generación del tablero

Antes de que comience la ronda, la distribución de casillas de los tres niveles se genera en un solo shuffle Provably Fair: se genera, sin reemplazo, una permutación aleatoria de los números enteros 0..146 (49 casillas × 3 niveles) con el generador basado en semillas y luego se divide por nivel:

// Draw a random permutation of 0..146 without replacement 
// Example: raw = [103, 12, 140, 76, 3, ...]
const boards = [[], [], []];
for (const value of raw) {
boards[Math.floor(value / 49)].push(value % 49); // bucket = level, residue = tile index }
// boards[0], boards[1], boards[2] are each a uniformly random ordering of tiles 0..48

Generar una permutación de 147 valores en lugar de tres 49 de shuffle valores por separado es solo un truco de eficiencia; dado que exactamente 49 de los 147 valores se encuentran dentro del rango de cada nivel, cada nivel termina teniendo un ordenamiento independiente y uniformemente aleatorio de sus propias 49 fichas.

Rondas: cómo cae la lava

El ordenamiento de 49 casillas de cada nivel se divide en grupos consecutivos y sin superposición: una cantidad fija de casillas por ronda (dropsPerRound), durante una cantidad fija de rondas (roundsPerLevel), y las casillas restantes al final son las que sobreviven todo el nivel:

tilesDroppedInRound(level, dropsPerRound, roundIndex) = levelBoard.slice(roundIndex * dropsPerRound, (roundIndex + 1) * dropsPerRound) 

tilesThatSurviveLevel(level, survivingCount) = levelBoard.slice(-survivingCount)

Dado que el shuffle ya fijó el orden de las casillas desde el inicio, el conjunto de lava de la ronda no se elige como reacción a su selección: es el segmento de la lista pre-shuffleda que corresponde a esa ronda.

En cada selección, eligen una casilla (selectedTile). Esa casilla no debe pertenecer ya a un lote de lava de una ronda anterior en este nivel. Luego, la ronda se resuelve comparando su única casilla con el lote de lava completo de la ronda (selectedTile):

isLoss = droppedTiles.includes(selectedTile) 

Si su casilla es una de las que caen en esta ronda, la partida termina. De lo contrario, sobreviven la ronda, sin importar cuántas otras casillas hayan caído junto con ella.

Dificultad configuración

Cuatro dificultades equilibran el tamaño de la ronda con la cantidad de rondas. Todas suman el tablero completo de 49 casillas (dropsPerRound × roundsPerLevel + survivingTiles = 49):

Dificultad

Caídas por ronda

Rondas por nivel

Casillas supervivientes

Fácil

7

6

7

Media

11

4

5

Difícil

23

2

3

Tóxica

48

1

1

Progresión de niveles y del juego

Un juego tiene 3 niveles, cada uno con su propio tablero de 49 casillas generado de forma independiente con shuffle. Sobrevivir todas las rondas de un nivel lo completa y los lleva al tablero del siguiente nivel en la primera ronda; completar el tercer nivel finaliza el juego.

Retiro de ganancias

Pueden retirar sus ganancias después de sobrevivir a cualquier ronda; su primera selección exitosa ya lo habilita.

Multiplicador

multiplier = edgeMultiplier * 49^(levelsCleared+1) / (tilesRemainingInLevel * survivingTiles^levelsCleared) 

levelsCleared es la cantidad de niveles completados por completo hasta el momento, tilesRemainingInLevel se reduce según la cantidad de caídas de la ronda cada vez que sobreviven una ronda dentro del nivel actual, y survivingTiles es la cantidad de casillas supervivientes por nivel de la dificultad, según la tabla anterior.

Este es un precio con probabilidades justas: el inverso de la probabilidad acumulada de haber llegado a ese punto exacto (niveles completados × rondas sobrevividas) con una distribución uniforme y aleatoria de las casillas, ajustado por la ventaja de la casa.

¿Ha quedado contestada tu pregunta?