Cách provably fair hoạt động trong Limbo

Đây là cách chơi Limbo trên Cloudbet, và cách kiểm tra xem mỗi hệ số nhân có phải là provably fair hay không.

Cách chơi Limbo

Limbo là một trò chơi hệ số nhân đơn giản, nhịp độ nhanh: trước mỗi vòng, bạn đặt một hệ số nhân mục tiêu — mức thắng bạn đang nhắm tới. Đặt cược, sau đó một hệ số nhân kết quả được tạo ra. Nếu nó đạt đến hoặc vượt qua mục tiêu của bạn, bạn thắng hệ số nhân mục tiêu nhân với tiền cược của bạn. Nếu nó rơi xuống dưới mục tiêu, bạn thua vòng đó.

Hệ số nhân mục tiêu bạn đặt càng cao, khoản thắng tiềm năng càng lớn — nhưng khả năng kết quả đạt đến đó càng thấp. Bạn có thể đặt mục tiêu thấp hay cao tùy ý trước mỗi lần cược.

Cloudbet Originals Limbo

Limbo Originals của Cloudbet có tỷ lệ RTP 99%, hệ số nhân tối đa x1.000.000, và chấp nhận cược từ 0,01 $ đến 20.000 $.

Các số ngẫu nhiên của Limbo được tạo ra theo cùng cách có thể kiểm chứng như mọi Original khác của Cloudbet — xem hệ thống provably fair của Cloudbet để biết cách Server Seed, Client Seed và Nonce kết hợp thành một Round Signature rồi thành một chuỗi số ngẫu nhiên qua SHAKE256. Trang này trình bày những gì đặc thù của Limbo: số ngẫu nhiên đó trở thành hệ số nhân bạn đang chạy đua như thế nào.

Cách tính hệ số nhân

Limbo bắt đầu từ một số ngẫu nhiên 32 bit — được tạo bởi SHAKE256, phân bố đều, nghĩa là mọi giá trị từ 0 đến 2³²-1 đều có khả năng xảy ra như nhau.

Tuy nhiên, đó không phải là hình dạng phân bố chúng ta cần. Một hệ số nhân nên rơi vào các giá trị nhỏ thường xuyên hơn nhiều so với các giá trị lớn — xác suất của một hệ số 1,01x phải cao hơn nhiều so với xác suất của một hệ số 100.000x. Vì vậy số ngẫu nhiên được biến đổi lại, từng bước, thành phân phối đó:

  1. Rejection sampling được sử dụng, nhưng ở mức nhẹ — chỉ để tránh lấy đúng số lớn nhất có thể, vì số đó sắp được cộng thêm một.
  2. Số đó được cộng thêm một (để nó không bao giờ bằng 0, điều này quan trọng cho phép chia ở bước 4).
  3. Nó được chia nhỏ thành một số thập phân từ 0 đến 1, bằng cách chia cho giá trị tối đa có thể của nó.
  4. 0,99 (RTP của Limbo) được chia cho số thập phân đó — đây là bước biến một phân bố đều thành một phân bố mà các hệ số nhân nhỏ phổ biến hơn theo cấp số nhân so với các hệ số nhân lớn.
  5. Kết quả được làm tròn xuống còn hai chữ số thập phân.
  6. Cuối cùng, nó được "giới hạn" trong phạm vi hợp lệ của Limbo: bất kỳ giá trị nào dưới 1 sẽ trở thành 1 (mức cược tối thiểu là 1,01, nên các kết quả dưới 1 đều chỉ là thua cược), và bất kỳ giá trị nào trên 1.000.000 sẽ trở thành 1.000.000 (mức thắng tối đa của Limbo).

Sơ đồ luồng cho thấy hệ thống công bằng có thể chứng minh của Limbo từ Cloudbet biến đổi từng bước một số ngẫu nhiên 32-bit thành hệ số nhân cuối cùng của vòng chơi.

Sau khi được tạo ra, hệ số nhân được cố định và giấu kín — nó không thể bị thay đổi sau khi bạn đặt cược. Việc rút tiền (hoặc không) trước khi đạt đến hệ số đó là điều quyết định kết quả vòng chơi.

Tự kiểm chứng các ván Limbo của bạn

Đoạn mã dưới đây chính xác với cách triển khai Limbo của Cloudbet. Nhập Server Seed, Client Seed và Nonce của bạn để tái tạo lại kết quả của riêng bạn. Lưu ý: để công bố Server Seed của một ván trên Cloudbet, bạn cần bắt đầu sử dụng một Server Seed khác.

Các thư viện cần cài đặt:

npm install js-sha3

Mã nguồn:

// 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);
})();

Các bài giải thích khác về 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