Cómo funciona provably fair en Limbo
Así se juega a Limbo en Cloudbet, y así puedes comprobar que cada multiplicador es provably fair.
Cómo jugar a Limbo
Limbo es un juego de multiplicador rápido y sencillo: antes de cada ronda, fijas un multiplicador objetivo —qué tan grande quieres que sea tu premio—. Realizas tu apuesta y después se genera un multiplicador resultado. Si iguala o supera tu objetivo, ganas tu objetivo multiplicado por tu apuesta. Si queda por debajo, pierdes la ronda.
Cuanto más alto fijes el multiplicador objetivo, mayor será tu ganancia potencial, pero menos probable será que el resultado lo alcance. Puedes fijar tu objetivo tan bajo o tan alto como quieras antes de cada apuesta.
Limbo de Cloudbet Originals
El Limbo propio de Cloudbet Originals tiene un RTP del 99 %, un multiplicador máximo de x1 000 000 y acepta apuestas de 0,01 $ a 20 000 $.
Los números aleatorios de Limbo se generan de la misma forma verificable que en el resto de Cloudbet Originals; consulta el sistema provably fair de Cloudbet para saber cómo se combinan la Semilla del servidor, la Semilla del cliente y el Nonce en una Firma de ronda y luego en un flujo de números aleatorios mediante SHAKE256. Esta página cubre lo específico de Limbo: cómo ese número aleatorio se convierte en el multiplicador contra el que compites.
Cómo se calcula el multiplicador
Limbo parte de un número aleatorio de 32 bits —generado por SHAKE256, distribuido uniformemente, es decir, cada valor entre 0 y 2³²-1 tiene la misma probabilidad—.
Sin embargo, esa no es la forma que necesitamos. Un multiplicador debe caer en valores pequeños con mucha más frecuencia que en valores enormes: las probabilidades de un 1,01x deben ser mucho mayores que las de un 100 000x. Por eso el número aleatorio se transforma, paso a paso, en esa distribución:
- Se usa el muestreo por rechazo, pero de forma leve, solo para evitar extraer el mayor número posible, ya que está a punto de incrementarse en uno.
- El número se incrementa en uno (para que nunca sea cero, lo cual importa para la división del paso 4).
- Se reduce a un decimal entre 0 y 1, dividiéndolo entre su valor máximo posible.
- Se divide 0,99 (el RTP de Limbo) entre ese decimal; este es el paso que convierte una distribución uniforme en una donde los multiplicadores pequeños son exponencialmente más comunes que los grandes.
- El resultado se redondea hacia abajo a dos decimales.
- Por último, se «recorta» al rango válido de Limbo: cualquier valor por debajo de 1 se convierte en 1 (la apuesta mínima es 1,01, así que los resultados por debajo de 1 son siempre una pérdida), y cualquier valor por encima de 1 000 000 se convierte en 1 000 000 (el pago máximo de Limbo).

Una vez generado, el multiplicador queda fijo y oculto; no se puede cambiar después de realizada tu apuesta. Retirarte (o no) antes de alcanzarlo es lo que decide la ronda.
Verifica tus propias partidas de Limbo
El código de abajo es fiel a la implementación de Limbo de Cloudbet. Introduce tu Semilla del servidor, Semilla del cliente y Nonce para reproducir tu propio resultado. Por cierto: para revelar la Semilla del servidor de una partida en Cloudbet, debes empezar a usar otra Semilla del servidor.
Dependencias:
npm install js-sha3
Código:
// 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);
})();
