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

Вот как играть в Pump на Cloudbet и как проверить, что «бракованный» насос в каждом раунде выбирается честно (provably fair).

Как играть в Pump

Pump — игра «испытай удачу»: вы накачиваете что-либо (в версии Cloudbet — воздушный шар), и с каждой накачкой растет множитель. Заберите выигрыш в любой момент, чтобы зафиксировать его, — но если зайти слишком далеко, шар лопнет, и вы потеряете ставку. Перед началом обычно выбирается уровень сложности, который уравновешивает то, как далеко можно безопасно зайти, и то, насколько быстро растет множитель.

Сделав ставку, нажимайте Pump, чтобы накачивать шар по одному шагу за раз. Каждая успешная накачка увеличивает потенциальную выплату. Вы можете нажать Забрать после любой накачки, чтобы забрать текущий множитель, — или продолжить накачивать ради большего. Если следующая накачка окажется «бракованной» для этого раунда, шар мгновенно лопается, и раунд завершается без выплаты.

Cloudbet Originals Pump

Собственная игра Cloudbet Originals Pump имеет пять уровней сложности — Easy, Medium, Hard, Expert и Master, — определяющих, сколько из 25 накачек раунда являются «бракованными» (то есть лопающими шар). RTP игры — 99%, максимальный выигрыш — $1 000 000, ставки принимаются от $0,01. Режим Autobet позволяет настроить сумму ставки, количество накачек за раунд, лимиты прибыли/убытка и количество раундов для автоматической игры — ручная игра («Pump ждет ВАШЕГО ввода») также всегда доступна.

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

Как случайные числа превращаются в раскладку игры

25 накачек Pump обозначены буквами от A до Y, чтобы можно было их отслеживать:

Диаграмма, показывающая 25 насосов Pump, обозначенных от A до Y, используемых для отслеживания доказуемо честного выбора неисправного насоса Cloudbet.

Изначально все накачки не бракованные. С помощью первого числа из случайной последовательности игры мы выбираем небракованную накачку по индексу и делаем ее бракованной:

Практический пример доказуемо честного алгоритма Pump: первый неисправный насос выбирается случайным образом из всех 25 исправных насосов.

Затем эта накачка удаляется из списка небракованных, поэтому следующий выбор происходит только из оставшихся:

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

Это повторяется, пока не будет достигнуто количество бракованных накачек для уровня сложности раунда (на примере показан medium = 3):

Практический пример, показывающий, как доказуемо честный алгоритм Pump выбирает второй неисправный насос из оставшихся исправных насосов.

Как только выбраны все бракованные накачки раунда, числа, использованные для их выбора, больше не нужны — остается только то, какие накачки бракованные:

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

Поскольку накачки срабатывают по одной, по порядку (A, затем B, затем C…), значение имеет только первая бракованная накачка, до которой доходит игрок — все, что после нее, никогда не срабатывает. Поэтому раскладка упрощается до этой единственной релевантной накачки:

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

Технические детали

Количество небракованных накачек мы называем P. Случайные байты из SHAKE256 разбиваются на 4-байтные целые числа без знака (от 0 до 2³²−1). Чтобы честно выбрать, какая накачка станет бракованной, мы используем выборку с отбраковкой относительно P, а затем берем остаток (+1) как индекс небракованной накачки, которую нужно сделать бракованной. Если требуется еще бракованных накачек, выбранная накачка удаляется из списка, P уменьшается на 1, и вытягивается новое число — так повторяется, пока не будет достигнуто нужное количество бракованных накачек раунда.

Итоговая раскладка бракованных накачек генерируется и сохраняется на сервере до начала раунда, и ее нельзя изменить впоследствии, не нарушив уже переданный игроку Коммитмент. Каждая накачка, которую нажимает игрок, сверяется с этой заранее зафиксированной раскладкой.

Проверьте сами

Мы написали небольшую программу на Node.js, точно соответствующую реализации Pump на Cloudbet — введите свои Сид сервера, Сид клиента, Nonce и уровень сложности, чтобы воспроизвести результат своего раунда. (Примечание: чтобы раскрыть Сид сервера раунда на Cloudbet, вы должны уже перейти на новую пару сидов.)

// 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 myDifficultyLevel = 'easy';             // ✍️YOUR_INPUT✍️: Enter your difficulty level here (easy, medium, hard, expert, or master)
const usingCustomData = false;              // Change this to true if you are using your own data

// These are the possible difficulty levels (and how many faulty pumps there are per difficulty level)
const faultyPumpsPerDiff = {
  'easy':       1,
  'medium':     3,
  'hard':       5,
  'expert':     9,
  'master':     12
};

// 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); // shaker256 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: Generate indices of the faulty pumps
function pickUniquePositions(numOfFaultyPumps, totalPumps, rng) {
  const available = Array.from({ length: totalPumps }, (_, i) => i);
  const faultyPumps = [];

  for (let i = 0; i < numOfFaultyPumps; ++i) {
    const index = sample(available.length, rng);

    faultyPumps.push(available[index]);
    available.splice(index, 1); // Remove chosen pump from list of non-faulty pumps
  }
  return faultyPumps;
}
// Rejection sampling for fair results
function sample(P, rng) {
  const maxAcceptable = Math.floor(0x100000000 / P) * P;

  // Rejection sampling
  let rand;
  do {
    rand = rng.next().value;
  } while (rand >= maxAcceptable);
  // Modulo operation
  const sampled_number = rand % P;

  return sampled_number;
}

// Example usage
(function main() {
  const totalPumps = 25;
  let difficultyLevel = 'easy';
  let nonce = 1;
  if (usingCustomData) { difficultyLevel = myDifficultyLevel; nonce = myNonce; }

  const numOfFaultyPumps = faultyPumpsPerDiff[difficultyLevel];

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

  const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
  const numbersToGenerate = 12 * 2;                 // We have at most 12 faulty pumps, however due to rejection sampling we may need more numbers
  const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);

  const faultyPumps = pickUniquePositions(numOfFaultyPumps, totalPumps, rng);
  let firstFaultyPump = Math.min(...faultyPumps);  // Select the smallest/first faulty pump
  firstFaultyPump += 1;                            // Increment the index so that it starts with 1, 2, 3, ... instead of 0, 1, 2

  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('🎯 Difficulty level:', difficultyLevel);
  console.log('💣 First faulty pump (zero-indexed, i.e., 1, 2, 3, ...):', firstFaultyPump);
})();

Установите единственную зависимость сначала:

npm install js-sha3

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

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