Как работает 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, чтобы можно было их отслеживать:

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

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

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

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

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

Технические детали
Количество небракованных накачек мы называем 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
