Comment le provably fair fonctionne dans Limbo

Voici comment jouer à Limbo sur Cloudbet, et comment vérifier que chaque multiplicateur est provably fair.

Comment jouer à Limbo

Limbo est un jeu de multiplicateur simple et rapide : avant chaque manche, vous définissez un multiplicateur cible — l'ampleur du gain que vous visez. Placez votre pari, puis un multiplicateur résultat est généré. S'il atteint ou dépasse votre cible, vous remportez votre multiplicateur cible multiplié par votre mise. S'il tombe en dessous de votre cible, vous perdez la manche.

Plus le multiplicateur cible que vous définissez est élevé, plus votre gain potentiel est important — mais moins il est probable que le résultat l'atteigne. Vous pouvez définir votre cible aussi bas ou aussi haut que vous le souhaitez avant chaque pari.

Cloudbet Originals Limbo

Le Limbo de Cloudbet Originals a un RTP de 99 %, un multiplicateur maximal de x1 000 000, et accepte des mises de 0,01 $ à 20 000 $.

Les nombres aléatoires de Limbo sont générés de la même façon vérifiable que chaque autre Cloudbet Original — consultez le système provably fair de Cloudbet pour savoir comment la Source du serveur, la Source du client et le Nonce se combinent en une Signature de manche puis en un flux de nombres aléatoires via SHAKE256. Cette page couvre ce qui est spécifique à Limbo : comment ce nombre aléatoire devient le multiplicateur contre lequel vous jouez.

Comment le multiplicateur est calculé

Limbo part d'un nombre aléatoire de 32 bits — généré par SHAKE256, uniformément distribué, ce qui signifie que chaque valeur entre 0 et 2³²-1 est également probable.

Ce n'est cependant pas la forme dont nous avons besoin. Un multiplicateur doit tomber sur de petites valeurs bien plus souvent que sur d'énormes valeurs — les chances d'obtenir un 1,01x doivent être bien plus élevées que celles d'obtenir un 100 000x. Le nombre aléatoire est donc remodelé, étape par étape, pour obtenir cette distribution :

  1. L'échantillonnage par rejet est utilisé, mais légèrement — uniquement pour éviter de tirer le plus grand nombre possible, puisque ce nombre est sur le point d'être incrémenté de un.
  2. Le nombre est incrémenté de un (afin qu'il ne soit jamais zéro, ce qui compte pour la division de l'étape 4).
  3. Il est ramené à une décimale entre 0 et 1, en le divisant par sa valeur maximale possible.
  4. 0,99 (le RTP de Limbo) est divisé par cette décimale — c'est cette étape qui transforme une répartition uniforme en une répartition où les petits multiplicateurs sont exponentiellement plus fréquents que les grands.
  5. Le résultat est arrondi à deux décimales par défaut.
  6. Enfin, il est « borné » à la plage valide de Limbo : tout ce qui est inférieur à 1 devient 1 (la mise minimale étant 1,01, les résultats inférieurs à 1 sont simplement une perte), et tout ce qui dépasse 1 000 000 devient 1 000 000 (le gain maximal de Limbo).

Une fois généré, le multiplicateur est fixe et caché — il ne peut pas être modifié après le placement de votre pari. Encaisser (ou non) avant qu'il ne soit atteint est ce qui détermine la manche.

Vérifiez vos propres parties de Limbo

Le code ci-dessous correspond exactement à l'implémentation de Limbo chez Cloudbet. Entrez votre Source du serveur, votre Source du client et votre Nonce pour reproduire votre propre résultat. Remarque : pour révéler la Source du serveur d'une partie sur Cloudbet, vous devez commencer à utiliser une autre Source du serveur.

Dépendances :

npm install js-sha3

Code :

// If you are running this yourself, then you can input your own data here
// You can then compare:
// 1) Cloudbet's provided Commitment to the Commitment outputted here. This proves that the Server Seed was not altered during gameplay.
// 2) Cloudbet's provided game outcome to the game outcome outputted here. This proves that your game's outcome was determined solely by the pre-game seeds and was not altered afterwards.
let myServerSeed      = 'your_server_seed'; // ✍️YOUR_INPUT✍️: Enter your (unhashed) Server Seed here
let myClientSeed      = 'your_client_seed'; // ✍️YOUR_INPUT✍️: Enter your Client Seed here
let myNonce           = 0;                  // ✍️YOUR_INPUT✍️: Enter your Nonce here
const usingCustomData = false;              // Change this to true if you are using your own data

// Load libraries
const crypto = require('crypto');
const { sha3_256, shake256 } = require('js-sha3');

// Step 1: Generate a Server Seed and Commitment
// And generate a Client Seed - either by player input or by the player's browser
function generateServerSeed() {
  let seed = crypto.randomBytes(32).toString('hex');
  if (usingCustomData) { seed = myServerSeed; }
  const commitment = sha3_256(seed);
  return { 'serverSeed': seed, 'commitment': commitment };
}
function getClientSeed() {
  let seed = crypto.randomBytes(32).toString('hex');
  if (usingCustomData) { seed = myClientSeed; }
  return seed;
}

// Step 2: Create a Round Signature with SHA3 using a Server Seed, Client Seed and Nonce
function createRoundSignature(serverSeed, clientSeed, nonce) {
  return sha3_256(`${serverSeed}:${clientSeed}:${nonce}`);
}

// Step 3: Create a SHAKE256 stream generator from a Round Signature.
// To simplify this example program, this function doesn't create a stream, but instead a sequence of custom length.
function* createRandomNumberSequence(numbersToGenerate, roundSignature) {
  const numOfBytes = 4 * numbersToGenerate;           // 4 bytes (32 bits) per random number we generate
  const byteStream = shake256.create(8 * numOfBytes); // shake takes bits as input
  byteStream.update(roundSignature);                  // Use the Round Signature to seed the generator
  const buf = Buffer.from(byteStream.digest());       // The Buffer class lets us package every 4 bytes into one 32-bit integer
  for (let i = 0; i < numbersToGenerate; i++) {
    yield buf.readUInt32BE(i * 4);                    // Read buf in 4-byte steps
  }
  throw new Error("No more numbers in the random number sequence! Fix: make 'numbersToGenerate' larger.");
}

// Step 4: Draw from the random number sequence to calculate the game result
function getGameResult(rng) {
  let randomInt32 = rng.next().value;               // 'randomInt32' ranges from 0 and 4294967295
  while (randomInt32 == 0xFFFFFFFF) { randomInt32 = rng.next().value; } // This can basically be ignored

  const fraction = (randomInt32 + 1) / 0xFFFFFFFF;  // 'fraction' ranges from 0 to 1
  const inverse = 0.99 / fraction;                  // 'inverse' ranges from 0 to (basically) infinity
  let crashPoint = Math.floor(inverse * 100) / 100; // Cut off everything past the second decimal place
  // We force the crashpoint to range from 1 to 1,000,000
  if (crashPoint < 1.0) {
    crashPoint = 1.0;
  }
  if (crashPoint > 1000000) {
    crashPoint = 1000000;
  }

  return crashPoint;
}

// Example usage
(function main() {
  let nonce = 1;
  if (usingCustomData) { nonce = myNonce; }

  const { serverSeed, commitment } = generateServerSeed();
  const clientSeed = getClientSeed();

  const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
  const numbersToGenerate = 1 * 2; // We need 1 random number, but there is an improbable case we'll need two
  const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);

  const value = getGameResult(rng);

  console.log('📝 Commitment (shown before the game):', commitment);
  console.log('🔒 Server Seed (revealed after the game):', serverSeed);
  console.log('🎲 Client Seed:', clientSeed);
  console.log('🔢 Nonce:', nonce);
  console.log('🔁 Round Signature (can be verified after the game):', roundSignature);
  console.log('⛓️‍💥 Break Point:', value);
})();

Plus d'explications sur les jeux provably fair :

  1. Provably fair Mines
  2. Provably fair Pump
  3. Provably fair Plinko
  4. Provably fair Dice
  5. Provably fair Dice: Golden Ticket
  6. Provably fair Roulette
  7. Provably fair Keno