Как работает provably fair в Limbo
Вот как играть в Limbo на Cloudbet и как проверить, что каждый множитель честен (provably fair).
Как играть в Limbo
Limbo — быстрая и простая игра на множитель: перед каждым раундом вы задаете целевой множитель — насколько крупный выигрыш вы хотите получить. Сделайте ставку, после чего генерируется результирующий множитель. Если он оказывается равен вашей цели или выше, вы выигрываете ставку, умноженную на целевой множитель. Если он ниже вашей цели, вы проигрываете раунд.
Чем выше заданный вами целевой множитель, тем больше потенциальный выигрыш — но тем менее вероятно, что результат его достигнет. Вы можете задавать цель настолько низкой или высокой, насколько хотите, перед каждой ставкой.
Cloudbet Originals Limbo
Собственная игра Cloudbet Originals Limbo имеет RTP 99%, максимальный множитель x1 000 000 и принимает ставки от $0,01 до $20 000.
Случайные числа в Limbo генерируются тем же проверяемым способом, что и в любой другой игре Cloudbet Originals — о том, как Сид сервера, Сид клиента и Nonce объединяются в Сигнатуру раунда, а затем в поток случайных чисел через SHAKE256, читайте в системе provably fair Cloudbet. Эта страница описывает то, что специфично для Limbo: как это случайное число становится множителем, с которым вы соревнуетесь.
Как рассчитывается множитель
Limbo начинается со случайного 32-битного числа — сгенерированного с помощью SHAKE256, равномерно распределенного, то есть каждое значение от 0 до 2³²-1 равновероятно.
Однако это не та форма распределения, которая нам нужна. Множитель должен гораздо чаще принимать небольшие значения, чем огромные — вероятность выпадения 1,01x должна быть намного выше вероятности выпадения 100 000x. Поэтому случайное число преобразуется шаг за шагом в нужное распределение:
- Используется выборка с отбраковкой, но в легкой форме — только чтобы избежать вытягивания единственного максимально возможного числа, поскольку оно вот-вот будет увеличено на единицу.
- Число увеличивается на единицу (чтобы оно никогда не было нулем, что важно для деления на шаге 4).
- Оно уменьшается до десятичной дроби от 0 до 1 путем деления на максимально возможное значение.
- 0,99 (RTP Limbo) делится на эту дробь — именно этот шаг превращает равномерное распределение в такое, где небольшие множители экспоненциально более распространены, чем крупные.
- Результат округляется вниз до двух знаков после запятой.
- Наконец, он «обрезается» до допустимого диапазона Limbo: все, что меньше 1, становится 1 (минимальная ставка — 1,01, поэтому результаты ниже 1 — это всегда проигрыш), а все, что больше 1 000 000, становится 1 000 000 (максимальная выплата Limbo).

После генерации множитель фиксируется и скрывается — его нельзя изменить после того, как вы сделали ставку. Решает раунд то, успеете ли вы (или нет) забрать выигрыш до его достижения.
Проверьте свои игры в Limbo самостоятельно
Приведенный ниже код точно соответствует реализации Limbo на Cloudbet. Введите свои Сид сервера, Сид клиента и Nonce, чтобы воспроизвести собственный результат. Кстати: чтобы раскрыть Сид сервера игры на Cloudbet, нужно начать использовать другой Сид сервера.
Зависимости:
npm install js-sha3
Код:
// 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);
})();
