O que são eventos do jogo
Anteriormente, geramos floats aleatórios entre 0 e 1 usando as funções generateFloats e byteGenerator. Os eventos do jogo traduzem esses floats gerados em resultados reais do jogo. Isso acontece de maneiras diferentes em jogos diferentes. No Plinko, os floats ajudam a determinar a direção da queda da bola. No Dice, os floats ajudam a determinar o resultado da rolagem dos dados.
A seguir, há uma explicação detalhada de como traduzimos floats em eventos ou resultados justos para cada jogo original que temos na nossa plataforma.
Tower
No Tower, o objetivo do jogo é chegar ao topo da torre e salvar a princesa encontrando as chaves e evitando as maçãs envenenadas. Cada linha da torre contém um número predefinido de blocos seguros (chaves) e maçãs envenenadas, dependendo da dificuldade selecionada.
O evento do jogo no Tower é determinar a posição dos blocos seguros (ou chaves) em cada linha.
Há um total de 9 linhas na torre. O número de colunas e a distribuição de chaves e maçãs envenenadas variam de acordo com a dificuldade escolhida:
Fácil: 4 colunas - 3 chaves e 1 maçã envenenada por linha
Médio: 3 colunas - 2 chaves e 1 maçã envenenada por linha
Difícil: 2 colunas - 1 chave e 1 maçã envenenada por linha
Especialista: 3 colunas: - 1 chave e 2 maçãs envenenadas por linha
Mestre: 4 colunas - 1 chave e 3 maçãs envenenadas por linha
O resultado de cada linha é determinado pela geração de posições aleatórias das chaves (blocos seguros) dentro das colunas disponíveis, com base na configuração de dificuldade.
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
No Chicken, o objetivo é mover a galinha por quantas faixas for possível sem pousar na faixa perigosa.
Cada passo bem-sucedido para frente aumenta o multiplicador de pagamento. O jogador pode sacar a qualquer momento antes de a galinha pular para a faixa perigosa. Se a galinha chegar à zona de perigo antes de o jogador sacar, a aposta é perdida.
O principal evento do jogo no Chicken é a determinação da faixa perigosa — a faixa específica em que a galinha perderá.
Há 4 dificuldades no Chicken. O número de faixas que a galinha pode atravessar varia dependendo da dificuldade selecionada:
Fácil: 19 faixas
Médio: 17 faixas
Difícil: 15 faixas
Especialista: 10 faixas
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
No Plinko, os jogadores podem escolher entre uma pirâmide com altura de 8 a 16. Conforme a bola é solta do topo, ela cairá para a esquerda ou para a direita de 8 a 16 vezes, sempre que atingir um pino no tabuleiro de Plinko. O evento do jogo no Plinko é determinar se a bola cai para a esquerda ou para a direita em cada nível, representado por 0 e 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
No Dice, uma rolagem de dados pode ter valores entre 0.00 e 100.00. O evento do jogo no Dice é determinar esse valor. Fazemos isso usando (floats * 10,001) / 100). Como incluímos valores de até 2 casas decimais e o número 0, há um total de 10,000 + 1 = 10,001 resultados possíveis (incluindo casas decimais).
// 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
No Limbo, o objetivo do jogo é adivinhar um multiplicador >1. Se o resultado do jogo for maior que o multiplicador, o usuário vence o jogo. O evento do jogo é gerar um valor float acima de 1 e que segue uma distribuição 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
No Keno, o evento do jogo exige a identificação de 10 posições únicas de gemas no tabuleiro de keno. Para isso, geramos 10 floats diferentes entre 1 e 40., representando cada localização no tabuleiro de keno. Em seguida, um é usado para lidar com floats duplicados e retornar as 10 localizações finais das 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
No Mines, o evento do jogo exige a identificação da posição das minas no tabuleiro de minas com 25 blocos. Com base na seleção do usuário de um número de minas, geramos uma lista de números inteiros de 0 a 24 para representar os 25 espaços no tabuleiro.
// 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
Nossa Roulette é baseada na roda europeia de ‘0’ zero, que tem 37 bolsos diferentes, de 0 a 36. O evento do jogo na roulette é calcular, entre os 37 bolsos, em qual a bola cairá.
// 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
No Wheel, o usuário seleciona o número de segmentos da roda. Dependendo do segmento em que o ponteiro parar, o usuário ganhará o multiplicador daquele segmento. O evento do jogo é determinar o 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
No blackjack, o jogo é jogado com um baralho ilimitado. Portanto, cada rodada sempre terá a mesma probabilidade. O evento do jogo no Blackjack é determinar a carta comprada quando o jogador ou o banqueiro compra uma carta do baralho.
// 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
No Hilo, o jogo é jogado com um baralho ilimitado. Portanto, cada rodada sempre terá a mesma probabilidade. O objetivo do jogo é adivinhar se a próxima carta terá valor maior ou menor que a carta atual. O evento do jogo no Hilo é determinar qual carta um usuário compra em seguida (se comprar).
// 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
Há 2 partes no cálculo do resultado de um jogo Crash
Hash do bloco Bitcoin
Seed
Hash do Bitcoin
Os detalhes estão neste tweet:
O hash do bloco 779588 pode ser encontrado no explorador de blocos BTC (https://btcscan.org/block/000000000000000000033ab71b5fa7edcafe388e5f49719f1de8e1f8d0670460) e corresponde ao hash mostrado no tweet.
Seed
O processo de geração de seeds para 10 milhões de jogos Crash é projetado para garantir justiça e transparência, impedindo qualquer possibilidade de manipulação por Shuffle. Veja como as seeds são geradas e verificadas:
Geração da seed inicial: A seed do jogo de número 10 milhões é escolhida aleatoriamente por Shuffle.
Hash sequencial para geração de seeds: Para gerar as seeds de todos os outros jogos, usamos um método de hashing reverso:
A seed de cada jogo é o resultado do hash da seed do jogo seguinte. Especificamente, o hash do 9,999,999º jogo é o hash SHA256 do hash do 10º milionésimo jogo.
Esse encadeamento continua de modo que a seed de cada jogo é derivada do hash do jogo seguinte, culminando na seed do primeiro jogo, que é derivada do hash do segundo jogo.
Verificação da integridade da seed:
Ao publicar o valor em hash do hash usado no primeiro jogo, os jogadores podem verificar de forma independente a integridade do hash de todos os jogos até o 10º milionésimo jogo.
Usando esse método, se você souber o hash de qualquer jogo específico, poderá verificar as seeds de todos os jogos anteriores. Por exemplo, se você souber a seed do jogo 1323185, poderá aplicar hash a essa seed para verificar se ela corresponde à seed do jogo 1323184, e assim por diante, até o primeiro jogo.
Por exemplo, no jogo 1323185 (https://shuffle.com/games/originals/crash?md-id=1323185&modal=crashGame), o hash é 2f824ebb9548a4d24d042ecab390acd1dc931048f696dc03c738937852825ac0
No jogo 1323184 (https://shuffle.com/games/originals/crash?md-id=1323184&modal=crashGame), o hash é 20f42ccc46fed8e005767f85eb4bd5d42d3bd2a72b01ae77dcc7071bec280265
O hash do jogo 1323184 é o hash SHA256 do hash de 1323185. Você pode usar uma calculadora SHA256 online, como https://xorbin.com/tools/sha256-hash-calculator para verificar isso
Transparência e segurança:
Ao usar um hash de bloco BTC futuro, que está fora do controle de Shuffle, e publicar a versão em hash da seed do primeiro jogo, os usuários podem comprovar que os resultados do jogo Crash são justos e não foram adulterados.
Slide
O Slide usa o mesmo processo de provably fair e de seeds do Crash. A geração das seeds foi feita publicamente no x.com, e a publicação pode ser encontrada aqui.
O hash do bloco 952294 pode ser encontrado no explorador de blocos BTC (https://btcscan.org/block/00000000000000000000a382760ff19cdb7f9f22147b9123f3e471fb3c6d129a) e corresponde ao hash mostrado no tweet.
Baccarat
No Baccarat, o jogo é jogado com um baralho ilimitado. Portanto, cada rodada sempre terá a mesma probabilidade. O evento do jogo no Baccarat é determinar as cartas distribuídas às mãos do Jogador e do Banqueiro na distribuição inicial e em quaisquer compras adicionais, seguindo as regras padrão de compra do Baccarat.
As cartas são distribuídas na seguinte ordem a partir da sequência de floats gerada:
Primeira carta do Jogador - índice do float 0
Primeira carta do Banqueiro - índice do float 1
Segunda carta do Jogador - índice do float 2
Segunda carta do Banqueiro - índice do float 3
Terceira carta do Jogador (se comprada) - índice do float 4
Terceira carta do Banqueiro (se comprada)
índice do float 4 - se o jogador não comprar a terceira carta
índice do float 5 - se o jogador comprar a terceira 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
No Blitz, o jogo é jogado com um baralho ilimitado. Portanto, cada rodada sempre terá a mesma probabilidade. O evento do jogo no Blitz é determinar as cartas distribuídas ao jogador de acordo com a quantidade de compras escolhida e avaliar se todas as cartas compradas são ú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
No Coinflip, o objetivo é prever corretamente o lado em que uma moeda lançada cairá. O evento do jogo no Coinflip é determinar o resultado (CARA ou COROA) de cada lançamento de moeda. O Coinflip tem três modos — Clássico (um único lançamento), Clássico Progressivo (lance repetidamente e saque a qualquer momento) e Alvo (escolha um número de lançamentos e o número mínimo de previsões corretas necessário para vencer) —, mas os três determinam os resultados dos lançamentos da mesma forma.
Cada lançamento é determinado gerando um float e mapeando-o para 0 ou 1, exatamente como o Plinko mapeia seus floats para quedas à esquerda/direita:
// 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ássico — um lançamento é gerado (contagem = 1). O jogador vence se o lado selecionado corresponder ao lançamento.
Clássico Progressivo — os lançamentos são gerados um de cada vez à medida que o jogador continua, mas o resultado de cada lançamento é definido desde o início da rodada: o N-ésimo lançamento sempre é derivado ao gerar os lançamentos de 1 até N e selecionar o último. Portanto, revelar os lançamentos um a um conforme o jogador avança não altera qual teria sido o resultado de cada lançamento. O jogador pode sacar após qualquer lançamento bem-sucedido; a rodada termina no primeiro erro.
Alvo — o jogador escolhe um número total de lançamentos (até 20) e um número mínimo de previsões corretas necessário para vencer. Todos os lançamentos são gerados em um único lote (contagem = totalFlips), e a rodada é vencida se o número de lançamentos que corresponderem à previsão do jogador for, no mínimo, o mínimo escolhido.
Floor is Lava
Objetivo: Atravesse uma grade de 7×7 (49 blocos) em três níveis consecutivos sem pisar em um bloco que se transforma em lava.
Cada nível ocorre em “rodadas” de tamanho fixo — um lote de blocos cai na lava junto a cada rodada — e você pode sacar após sobreviver a qualquer rodada.
Geração do tabuleiro
Antes do início da rodada, o layout dos blocos dos três níveis é definido em uma única shuffle comprovadamente justa: uma permutação aleatória dos números inteiros 0..146 (49 blocos × 3 níveis) é selecionada sem reposição pelo gerador com seed e, em seguida, dividida por nível:
// 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
Gerar uma permutação de valores 147 em vez de três 49s separados de shuffle valores é apenas um truque de eficiência — como exatamente 49 dos 147 valores ficam na faixa de cada nível, cada nível ainda termina com uma ordenação independente e uniformemente aleatória de suas próprias 49 peças.
Rodadas: como a lava cai
A ordenação de 49 blocos de cada nível é dividida em partes consecutivas e sem sobreposição: um número fixo de blocos por rodada (dropsPerRound), para um número fixo de rodadas (roundsPerLevel), e o que sobra no final são os blocos que sobrevivem a todo o nível:
tilesDroppedInRound(level, dropsPerRound, roundIndex) = levelBoard.slice(roundIndex * dropsPerRound, (roundIndex + 1) * dropsPerRound)
tilesThatSurviveLevel(level, survivingCount) = levelBoard.slice(-survivingCount)
Como a shuffle já definiu a ordem dos blocos antecipadamente, o conjunto de lava da rodada não é escolhido em reação à sua escolha — é simplesmente a parte da lista pré-shuffled que está “prevista” para esta rodada.
Em cada escolha, você indica um bloco (selectedTile). Esse bloco não pode já pertencer a um lote de lava de uma rodada anterior neste nível. A rodada é então resolvida comparando seu único bloco com o lote inteiro de lava da rodada (selectedTile):
isLoss = droppedTiles.includes(selectedTile)
Se o seu bloco for um dos blocos que caem nesta rodada, a partida termina. Caso contrário, você sobreviveu à rodada — independentemente de quantos outros blocos caíram junto com ele.
Dificuldade configuração
Quatro dificuldades equilibram o tamanho da rodada com a quantidade de rodadas. Todas ainda somam o tabuleiro completo de 49 blocos (dropsPerRound × roundsPerLevel + survivingTiles = 49):
Dificuldade | Quedas por rodada | Rodadas por nível | Blocos sobreviventes |
Fácil | 7 | 6 | 7 |
Médio | 11 | 4 | 5 |
Difícil | 23 | 2 | 3 |
Tóxico | 48 | 1 | 1 |
Progressão de níveis e do jogo
Um jogo tem 3 níveis, cada um com seu próprio tabuleiro de 49 blocos com shuffled independente. Sobreviver a todas as rodadas de um nível o conclui e leva você ao tabuleiro do próximo nível na primeira rodada; concluir o terceiro nível finaliza o jogo.
Saque
Você pode sacar após sobreviver a qualquer rodada — sua primeira escolha bem-sucedida já libera essa opção.
Multiplicador
multiplier = edgeMultiplier * 49^(levelsCleared+1) / (tilesRemainingInLevel * survivingTiles^levelsCleared)
levelsCleared é o número de níveis totalmente concluídos até o momento, tilesRemainingInLevel diminui pela quantidade de quedas da rodada a cada vez que você sobrevive a uma rodada no nível atual, e survivingTiles é a contagem de sobreviventes por nível da dificuldade, conforme a tabela acima.
Este é um preço de probabilidades justas: o inverso da probabilidade acumulada de alcançar exatamente esse ponto (níveis concluídos × rodadas sobrevividas) sob uma distribuição uniforme e aleatória dos blocos, ajustado pela vantagem da casa.


