Как работает provably fair в Plinko

Вот как играть в Plinko на Cloudbet и как проверить, что путь каждого шарика честен (provably fair).

Как играть в Plinko

Plinko — игра на удачу, в которой вы бросаете шарик вниз по пирамиде из штырьков и наблюдаете, как он отскакивает к выплате. Перед броском вы задаете сумму ставки, количество рядов в пирамиде и уровень риска. Чем больше рядов задействовано и выше риск, тем больше возможные множители — но тем сложнее их получить. После броска шарик отскакивает влево или вправо от каждого ряда штырьков, пока не попадет в одну из ячеек внизу, у каждой из которых свой множитель. Во многих играх Plinko можно бросать сразу несколько шариков для более быстрой игры.

Cloudbet Originals Plinko

Собственная игра Cloudbet Originals Plinko позволяет выбрать 8, 10, 12 или 16 рядов, четыре уровня риска (Low, Medium, High, Extreme) и 1, 5, 10, 20, 50 или 100 шариков за бросок. RTP игры — 99%. Дополнительная функция BOOST (доплата 20% к ставке) добавляет специальные множители шариков 2x, 5x и 10x, повышая максимальный множитель до 100 000x — с ограничением общей выплаты в $1 000 000.

Общее введение в provably fair — сиды сервера, сиды клиента, nonce, хеширование и то, как проверить результат — смотрите в системе provably fair Cloudbet. Ниже описано, как это применяется конкретно к Plinko.

Как путь шарика превращается в ячейку

Путь каждого шарика вниз по пирамиде — это серия отскоков от штырьков, и каждый отскок либо влево, либо вправо.

Диаграмма, показывающая, как каждый отскок в доказуемо честной игре Plinko от Cloudbet кодируется как выбор влево или вправо.

Этот выбор можно закодировать как «0» (влево) или «1» (вправо). Путь вида «влево, влево, вправо» превращается в код «001», и следуя ему, шарик попадает в ячейку №2.

Пример, показывающий путь шарика Plinko, закодированный как последовательность нулей и единиц в доказуемо честной системе Cloudbet, в результате чего шарик попадает в лунку №2.

Вот ключевая закономерность: путь попадает в ячейку, номер которой соответствует количеству отскоков вправо плюс один. Поэтому «100», «010» и «001» — все с ровно одной «1» — попадают в ячейку №2 (1 + 1).

Диаграмма, показывающая несколько разных доказуемо честных траекторий шарика Plinko, которые математически попадают в одну и ту же лунку.

Это означает, что случайный путь можно сгенерировать и закодировать за один шаг: взять 32-битное случайное число, прочитать его первые N бит (где N — количество рядов) и посчитать, сколько из этих бит равны «1». Прибавить единицу — и это ячейка, в которую попадает шарик. Ячейка рассчитывается и фиксируется на сервере до начала раунда, скрыта до его окончания, а затем раскрывается.

Проверьте честность (provably fair) своих игр в Plinko

Приведенный ниже код точно соответствует реализации Plinko на Cloudbet. Введите свои Сид сервера, Сид клиента, Nonce, количество шариков и количество рядов, установите единственную зависимость и запустите его.

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           = 1;                  // ✍️YOUR_INPUT✍️: Enter your Nonce here
let myNumberOfBalls   = 1;                  // ✍️YOUR_INPUT✍️: Enter your number of balls here
let myNumberOfRows    = 12;                 // ✍️YOUR_INPUT✍️: Enter your number of rows 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 with a 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: Sample bits to determine ball paths
function pickBuckets(numOfRows, numOfBalls, rng) {
  let bucketIndices = [];
  for (let i = 0; i < numOfBalls; i++) {
    const rawUint32 = rng.next().value;
    let pathArray = rawUint32.toString(2).padStart(32, '0').split("");  // Convert the number into an array of bits
    pathArray.splice(numOfRows);                                        // Select the the first bits for the number of rows
    bucketIndices.push(getBucketIndexFromPath(pathArray));
  }
  return bucketIndices;
}

// Calculates which bucket the ball would land in an array of 1's and 0's where 0 = leftwards, 1 = rightwards
// In principle it counts the amount of times the ball bounces rightwards
function getBucketIndexFromPath(pathArray) {
  let rightCounter = 0;
  for(let i = 0; i < pathArray.length; ++i) {
    if (pathArray[i] === '1') {
      rightCounter += 1;
    } else {
      rightCounter += 0;
    }
  }
  return rightCounter + 1; // Add one so that the left-most bucket is bucket number one
}

// Example usage
(function main() {
  let numOfRows = 12;
  let numOfBalls = 1;
  let nonce = 1;
  if (usingCustomData) { numOfRows = myNumberOfRows; numOfBalls = myNumberOfBalls; nonce = myNonce; }

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

  const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
  const numbersToGenerate = numOfBalls; // Only need to generate one number per ball
  const rng = createRandomNumberSequence(numbersToGenerate, roundSignature); 

  const bucketIndices = pickBuckets(numOfRows, numOfBalls, 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('📋 Number of rows:', numOfRows);
  console.log('🏁 Buckets landed in (Bucket #1 is the leftern-most bucket):', bucketIndices);
})();

Примечание: чтобы раскрыть Сид сервера игры на Cloudbet, сначала нужно перейти на новый Сид сервера.

Разборы provably fair по играм:

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