Как работает 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. Поэтому случайное число преобразуется шаг за шагом в нужное распределение:

  1. Используется выборка с отбраковкой, но в легкой форме — только чтобы избежать вытягивания единственного максимально возможного числа, поскольку оно вот-вот будет увеличено на единицу.
  2. Число увеличивается на единицу (чтобы оно никогда не было нулем, что важно для деления на шаге 4).
  3. Оно уменьшается до десятичной дроби от 0 до 1 путем деления на максимально возможное значение.
  4. 0,99 (RTP Limbo) делится на эту дробь — именно этот шаг превращает равномерное распределение в такое, где небольшие множители экспоненциально более распространены, чем крупные.
  5. Результат округляется вниз до двух знаков после запятой.
  6. Наконец, он «обрезается» до допустимого диапазона Limbo: все, что меньше 1, становится 1 (минимальная ставка — 1,01, поэтому результаты ниже 1 — это всегда проигрыш), а все, что больше 1 000 000, становится 1 000 000 (максимальная выплата Limbo).

Блок-схема, показывающая, как доказуемо честная система Limbo от Cloudbet шаг за шагом преобразует случайное 32-битное число в итоговый множитель раунда.

После генерации множитель фиксируется и скрывается — его нельзя изменить после того, как вы сделали ставку. Решает раунд то, успеете ли вы (или нет) забрать выигрыш до его достижения.

Проверьте свои игры в 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);
})();

Разборы 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