Passar para o conteúdo principal

Provably Fair - Eventos do jogo

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:

  1. Geração da seed inicial: A seed do jogo de número 10 milhões é escolhida aleatoriamente por Shuffle.

  2. 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.

  3. 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.

  4. 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:

  1. Primeira carta do Jogador - índice do float 0

  2. Primeira carta do Banqueiro - índice do float 1

  3. Segunda carta do Jogador - índice do float 2

  4. Segunda carta do Banqueiro - índice do float 3

  5. Terceira carta do Jogador (se comprada) - índice do float 4

  6. Terceira carta do Banqueiro (se comprada)

    1. índice do float 4 - se o jogador não comprar a terceira carta

    2. í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.

Respondeu à sua pergunta?