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

Вот как играть в Keno на Cloudbet и как проверить, что каждый розыгрыш является provably fair.

Как играть в Keno

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

Cloudbet Originals Keno

Собственная игра Cloudbet Originals Keno использует игровое поле из 40 чисел: вы выбираете от 1 до 10 чисел, а игра разыгрывает 10. RTP составляет 99 %, ставки принимаются от $0.01. Чем меньше чисел вы выбрали, тем выше волатильность и крупнее выплата за попадание; чем больше чисел выбрано, тем чаще выигрыши, но ниже выплата за попадание. Доступна функция Auto Bet для автоматизации серии раундов.

Розыгрыш в Keno формируется тем же проверяемым способом, что и в любой другой игре Cloudbet Original — подробнее о том, как Server Seed, Client Seed и Nonce объединяются в Round Signature, а затем в последовательность случайных чисел через SHAKE256, смотрите в нашем обзоре provably fair. Эта страница описывает то, что специфично именно для Keno: как эта последовательность случайных чисел превращается в фактический набор разыгранных чисел.

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

Игровое поле Keno пронумеровано от 1 до 40. Десять из этих 40 чисел должны быть разыграны по одному, без повторения какого-либо числа.

Схема, показывающая provably fair поле Keno от Cloudbet — 40 пронумерованных ячеек, каждая со своим индексом для честного выбора чисел.

Количество ещё не разыгранных чисел мы называем P. P начинается со значения 40 и уменьшается на 1 после каждого розыгрыша числа.

Пример работы provably fair алгоритма розыгрыша Keno: первое число случайным образом выбирается из всех 40 позиций на поле.

Для каждого из 10 розыгрышей из вывода SHAKE256 берётся случайное 4-байтовое (32-битное) число в диапазоне от 0 до 4 294 967 295. Чтобы честно выбрать среди оставшихся P чисел, мы применяем rejection sampling (см. наше руководство по rejection sampling), а затем берём результат по модулю P, чтобы получить позицию следующего разыгранного числа. Если нужно разыграть ещё числа, выбранное число удаляется из пула, P уменьшается на 1, и процесс повторяется.

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

Это продолжается, пока не будут разыграны все 10 чисел.

4. Финальное provably fair поле Keno с выделенными всеми 10 случайно и проверяемо выпавшими числами.

5. Блок-схема provably fair расчёта Keno от Cloudbet: случайные байты и rejection sampling определяют позицию каждого выпавшего числа на поле.

Игровой процесс

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

Код для независимой проверки результатов Keno на Cloudbet

Введите свои Server Seed, Client Seed и Nonce, чтобы проверить собственные результаты. Установите единственную зависимость и запустите код самостоятельно, чтобы воспроизвести результат своей игры.

Примечание: чтобы раскрыть Server Seed завершённой игры на Cloudbet, вы должны уже начать новый раунд (это переключает игру на новую пару seed).

Dependencies:

npm install js-sha3

The code:

// 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 mine positions to the mine positions 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
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
  }
}

// Step 4: Pick a number of unique positions
function pickUniquePositions(numOfPicks, totalPositions, rng) {
  const available = Array.from({ length: totalPositions }, (_, i) => i);
  const picks = [];

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

    picks.push(available[index]);
    available.splice(index, 1); // Remove chosen pump from list of non-faulty pumps
  }
  return picks;
}
// 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() {
  let nonce = 1;
  if (usingCustomData) { nonce = myNonce; }
  const numOfHits = 10;
  const totalPositions = 40;

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

  const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
  const numbersToGenerate = totalPositions * 2; // We need at least one random number per position, but there is an improbable possibility  we'll need two
  const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);

  let hits = pickUniquePositions(numOfHits, totalPositions, rng);
  hits = hits.map((value)=> value + 1 );
  hits.sort((a, b)=>a-b);

  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(`🎯 Hits:`, hits);
})();