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

Đây là cách chơi Pump trên Cloudbet, và cách kiểm tra xem bơm lỗi của mỗi vòng có phải là provably fair hay không.

Cách chơi Pump

Pump là một trò chơi "được ăn cả, ngã về không": bạn bơm một thứ gì đó (một quả bóng bay trong phiên bản của Cloudbet) để tăng hệ số nhân sau mỗi lần bơm. Rút tiền bất cứ lúc nào để chốt khoản thắng của bạn — nhưng nếu bơm quá đà, nó sẽ nổ, và bạn mất tiền cược. Trước khi bắt đầu, bạn thường chọn một mức độ khó để cân bằng giữa việc bạn có thể an toàn đi được bao xa và tốc độ tăng của hệ số nhân.

Sau khi đặt cược, nhấn Pump để bơm quả bóng bay từng bước một. Mỗi lần bơm thành công làm tăng khoản thưởng tiềm năng của bạn. Bạn có thể nhấn Rút Tiền sau bất kỳ lần bơm nào để nhận hệ số nhân hiện tại — hoặc tiếp tục bơm để có hệ số lớn hơn. Nếu lần bơm tiếp theo của bạn trùng với bơm lỗi của vòng đó, quả bóng bay sẽ nổ ngay lập tức và vòng chơi kết thúc mà không có khoản thưởng nào.

Cloudbet Originals Pump

Pump Originals của Cloudbet có năm mức độ khó — Dễ, Trung bình, Khó, Chuyên gia và Bậc thầy — kiểm soát số lượng bơm bị lỗi trong tổng số 25 lần bơm của vòng chơi (những lần bơm làm nổ quả bóng bay). Trò chơi có tỷ lệ RTP 99%, mức thắng tối đa 1.000.000 $, và chấp nhận cược từ 0,01 $. Chế độ Autobet cho phép bạn cấu hình số tiền cược, số lần bơm mỗi vòng, giới hạn lãi/lỗ và số vòng chơi tự động — chế độ chơi thủ công ("Pump chờ THAO TÁC của BẠN") luôn có sẵn.

Để biết cơ chế chung của provably fair — server seed, client seed, nonce, hashing, và cách Round Signature được tạo ra — xem hướng dẫn provably fair của Cloudbet. Dưới đây là cách chuỗi số ngẫu nhiên đó trở thành kết quả cụ thể của một vòng Pump.

Biến số ngẫu nhiên thành sơ đồ trò chơi

25 lần bơm của Pump được đánh nhãn từ A đến Y để chúng ta có thể theo dõi chúng:

Sơ đồ thể hiện 25 máy bơm của Pump được đánh dấu từ A đến Y, dùng để theo dõi việc chọn máy bơm lỗi theo cơ chế công bằng có thể chứng minh của Cloudbet.

Tất cả các lần bơm ban đầu đều không lỗi. Sử dụng số đầu tiên trong chuỗi số ngẫu nhiên của trò chơi, chúng tôi chọn một lần bơm không lỗi theo chỉ số và biến nó thành lỗi:

Ví dụ minh họa thuật toán công bằng có thể chứng minh của Pump: máy bơm lỗi đầu tiên được chọn ngẫu nhiên trong số 25 máy bơm còn hoạt động tốt.

Lần bơm đó sau đó bị loại khỏi danh sách các lần bơm không lỗi, để lượt chọn tiếp theo chỉ lấy từ những gì còn lại:

Sơ đồ cho thấy hệ thống công bằng có thể chứng minh của Pump từ Cloudbet loại bỏ máy bơm lỗi đã chọn khỏi danh sách trước khi rút máy bơm tiếp theo.

Việc này lặp lại cho đến khi đạt đủ số lượng bơm lỗi theo mức độ khó của vòng chơi (trung bình = 3 được minh họa ở đây):

Ví dụ minh họa thuật toán công bằng có thể chứng minh của Pump chọn máy bơm lỗi thứ hai trong số các máy bơm còn hoạt động tốt còn lại.

Sau khi mọi lần bơm lỗi của vòng chơi đã được chọn xong, các số đã dùng để chọn chúng không còn cần thiết nữa — những gì còn lại chỉ là những lần bơm nào bị lỗi:

Sơ đồ cho thấy toàn bộ bố cục máy bơm lỗi của một vòng chơi trước khi phép tính công bằng có thể chứng minh của Cloudbet đơn giản hóa nó thành một máy bơm duy nhất có ý nghĩa.

Vì các lần bơm được kích hoạt lần lượt theo thứ tự (A, rồi B, rồi C…), chỉ có lần bơm lỗi đầu tiên mà người chơi chạm đến mới thực sự quan trọng — bất kỳ lần nào sau đó sẽ không bao giờ được kích hoạt. Vì vậy sơ đồ được đơn giản hóa chỉ còn một lần bơm liên quan đó:

Sơ đồ thể hiện bố cục máy bơm lỗi công bằng có thể chứng minh của Pump được đơn giản hóa chỉ còn máy bơm lỗi đầu tiên mà người chơi thực sự sẽ chạm tới.

Chi tiết kỹ thuật

Chúng tôi gọi số lượng bơm không lỗi là P. Các byte ngẫu nhiên từ SHAKE256 được chia thành các số nguyên không dấu 4 byte (0 đến 2³²−1). Để chọn công bằng lần bơm nào bị lỗi, chúng tôi sử dụng rejection sampling đối với P, sau đó lấy phần dư (+1) làm chỉ số của lần bơm không lỗi để biến thành lỗi. Nếu vẫn cần thêm bơm lỗi, lần bơm đã chọn sẽ bị loại khỏi danh sách, P giảm đi 1, và một số mới được lấy mẫu — lặp lại cho đến khi đạt đủ số lượng bơm lỗi của vòng chơi.

Sơ đồ bơm lỗi cuối cùng được tạo ra và lưu ở phía máy chủ trước khi vòng chơi bắt đầu, và không thể bị thay đổi sau đó mà không phá vỡ Commitment đã được chia sẻ với người chơi. Mỗi lần người chơi nhấn bơm chỉ đơn giản là được đối chiếu với sơ đồ đã cam kết trước này.

Tự kiểm chứng

Chúng tôi đã viết một chương trình Node.js nhỏ chính xác với cách triển khai Pump của Cloudbet — nhập Server Seed, Client Seed, Nonce và mức độ khó của bạn để tái tạo lại kết quả vòng chơi của bạn. (Lưu ý: để công bố Server Seed của một vòng trên Cloudbet, bạn cần đã làm mới sang một cặp seed mới.)

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

Cài đặt thư viện duy nhất cần thiết trước:

npm install js-sha3

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