Jak działa provably fair w grze Limbo
Oto jak grać w Limbo na Cloudbet oraz jak sprawdzić, że każdy mnożnik jest provably fair.
Jak grać w Limbo
Limbo to szybka, prosta gra na mnożnik: przed każdą rundą ustawiasz docelowy mnożnik — jak dużej wygranej chcesz. Postaw zakład, a następnie generowany jest mnożnik wynikowy. Jeśli osiągnie on Twój cel lub go przekroczy, wygrywasz swój docelowy mnożnik pomnożony przez zakład. Jeśli wynik jest niższy niż Twój cel, przegrywasz rundę.
Im wyższy docelowy mnożnik ustawisz, tym większa potencjalna wygrana — ale tym mniej prawdopodobne, że wynik go osiągnie. Możesz ustawić swój cel dowolnie nisko lub wysoko przed każdym zakładem.
Cloudbet Originals Limbo
Własna gra Limbo od Cloudbet Originals ma RTP na poziomie 99%, maksymalny mnożnik x1 000 000 oraz przyjmuje zakłady od 0,01 $ do 20 000 $.
Liczby losowe w Limbo są generowane w taki sam weryfikowalny sposób, jak w każdej innej grze Cloudbet Originals — zobacz system provably fair Cloudbet, aby dowiedzieć się, jak Seed serwera, Seed klienta i Nonce łączą się w Sygnaturę rundy, a następnie w strumień liczb losowych za pomocą SHAKE256. Ta strona opisuje to, co jest specyficzne dla Limbo: jak ta liczba losowa staje się mnożnikiem, z którym rywalizujesz.
Jak obliczany jest mnożnik
Limbo zaczyna od losowej liczby 32-bitowej — generowanej przez SHAKE256, rozłożonej jednostajnie, co oznacza, że każda wartość z zakresu od 0 do 2³²-1 jest tak samo prawdopodobna.
To jednak nie jest kształt rozkładu, którego potrzebujemy. Mnożnik powinien znacznie częściej przyjmować małe wartości niż ogromne — szanse na 1,01x powinny być znacznie wyższe niż szanse na 100 000x. Dlatego losowa liczba jest przekształcana krok po kroku w ten rozkład:
- Stosowane jest próbkowanie odrzucające, ale w lekkiej formie — jedynie po to, aby uniknąć wylosowania pojedynczej największej możliwej liczby, ponieważ liczba ta zaraz zostanie zwiększona o jeden.
- Liczba jest zwiększana o jeden (dzięki czemu nigdy nie jest zerem, co ma znaczenie przy dzieleniu w kroku 4).
- Jest ona skalowana do wartości dziesiętnej z zakresu od 0 do 1, poprzez podzielenie przez jej maksymalną możliwą wartość.
- 0,99 (RTP Limbo) jest dzielone przez tę wartość dziesiętną — to właśnie ten krok zamienia jednostajny rozkład w taki, w którym małe mnożniki są wykładniczo częstsze niż duże.
- Wynik jest zaokrąglany w dół do dwóch miejsc po przecinku.
- Na koniec jest on „przycinany” do prawidłowego zakresu Limbo: wszystko poniżej 1 staje się 1 (minimalny zakład to 1,01, więc wyniki poniżej 1 są zawsze przegraną), a wszystko powyżej 1 000 000 staje się 1 000 000 (maksymalna wypłata w Limbo).

Po wygenerowaniu mnożnik jest ustalony i ukryty — nie można go zmienić po postawieniu zakładu. To, czy zdążysz się wypłacić przed jego osiągnięciem, decyduje o wyniku rundy.
Zweryfikuj własne gry Limbo
Poniższy kod jest zgodny z implementacją Limbo na Cloudbet. Wprowadź swój Seed serwera, Seed klienta i Nonce, aby odtworzyć własny wynik. Uwaga: aby ujawnić Seed serwera danej gry na Cloudbet, musisz zacząć korzystać z innego Seeda serwera.
Zależności:
npm install js-sha3
Kod:
// 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);
})();
