Plinko에서 입증된 공정성이 작동하는 방식
Cloudbet에서 Plinko를 플레이하는 방법과, 공의 경로가 입증된 공정성을 갖추고 있는지 확인하는 방법을 알아보세요.
Plinko 플레이 방법
Plinko는 피라미드 모양으로 배치된 핀 사이로 공을 떨어뜨려 배당금을 노리는 확률 게임입니다. 공을 떨어뜨리기 전에 베팅 금액, 피라미드의 행(row) 수, 위험도를 설정합니다. 행 수가 많고 위험도가 높을수록 배수는 커지지만, 그만큼 도달하기는 어려워집니다. 공을 떨어뜨리면 각 행의 핀에 부딪히며 좌우로 튕기다가, 각기 고유한 배수를 가진 하단의 버킷 중 하나에 안착합니다. 많은 Plinko 게임에서 더 빠른 플레이를 위해 여러 개의 공을 동시에 떨어뜨릴 수 있습니다.
Cloudbet Originals Plinko
Cloudbet 자체 제작 Originals Plinko는 8, 10, 12, 16행 중 선택할 수 있으며, 네 가지 위험도(낮음, 중간, 높음, 극한)와 한 번에 1, 5, 10, 20, 50, 100개의 공을 떨어뜨리는 옵션을 제공합니다. RTP는 99%입니다. 선택 사항인 BOOST 기능(베팅 금액에 20% 추가 요금 부과)을 사용하면 특별한 2배, 5배, 10배 공 배수가 추가되어 최대 배수가 100,000배까지 올라가며, 총 당첨금은 $1,000,000로 제한됩니다.
입증된 공정성에 대한 일반적인 소개(서버 시드, 클라이언트 시드, 논스, 해싱, 결과 검증 방법)는 Cloudbet의 입증된 공정성 시스템을 참고하세요. 아래에서는 이것이 Plinko에 구체적으로 어떻게 적용되는지 설명합니다.
공의 경로가 버킷으로 바뀌는 과정
피라미드를 따라 내려가는 공의 경로는 핀에 부딪히는 일련의 과정이며, 각 부딪힘은 왼쪽 또는 오른쪽 중 하나입니다.

이 선택은 "0"(왼쪽) 또는 "1"(오른쪽)로 표현할 수 있습니다. "왼쪽, 왼쪽, 오른쪽"이라는 경로는 코드 "001"이 되며, 이를 따라가면 공은 버킷 #2에 안착합니다.

여기서 핵심 규칙은 다음과 같습니다. 경로는 오른쪽으로 부딪힌 횟수에 1을 더한 번호의 버킷에 안착합니다. 따라서 "100", "010", "001"처럼 "1"이 정확히 하나씩 있는 경로는 모두 버킷 #2(1 + 1)에 안착합니다.

이는 무작위 경로를 한 단계로 생성하고 인코딩할 수 있다는 의미입니다. 32비트 무작위 숫자를 가져와 처음 N비트(N은 행 수)를 읽고, 그중 "1"이 몇 개인지 셉니다. 여기에 1을 더하면 공이 안착하는 버킷이 결정됩니다. 버킷은 라운드가 시작되기 전에 서버에서 계산되어 고정되며, 라운드가 끝날 때까지 숨겨졌다가 공개됩니다.
직접 Plinko 게임의 입증된 공정성 검증하기
아래 코드는 Cloudbet의 Plinko 구현과 정확히 일치합니다. 서버 시드, 클라이언트 시드, 논스, 공의 개수, 행 수를 입력하고 의존성 하나를 설치한 다음 실행해 보세요.
npm install js-sha3
의존성을 설치한 후, 아래 코드 블록에 직접 데이터를 입력하세요.
// 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 myNumberOfBalls = 1; // ✍️YOUR_INPUT✍️: Enter your number of balls here
let myNumberOfRows = 12; // ✍️YOUR_INPUT✍️: Enter your number of rows 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 with a 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: Sample bits to determine ball paths
function pickBuckets(numOfRows, numOfBalls, rng) {
let bucketIndices = [];
for (let i = 0; i < numOfBalls; i++) {
const rawUint32 = rng.next().value;
let pathArray = rawUint32.toString(2).padStart(32, '0').split(""); // Convert the number into an array of bits
pathArray.splice(numOfRows); // Select the the first bits for the number of rows
bucketIndices.push(getBucketIndexFromPath(pathArray));
}
return bucketIndices;
}
// Calculates which bucket the ball would land in an array of 1's and 0's where 0 = leftwards, 1 = rightwards
// In principle it counts the amount of times the ball bounces rightwards
function getBucketIndexFromPath(pathArray) {
let rightCounter = 0;
for(let i = 0; i < pathArray.length; ++i) {
if (pathArray[i] === '1') {
rightCounter += 1;
} else {
rightCounter += 0;
}
}
return rightCounter + 1; // Add one so that the left-most bucket is bucket number one
}
// Example usage
(function main() {
let numOfRows = 12;
let numOfBalls = 1;
let nonce = 1;
if (usingCustomData) { numOfRows = myNumberOfRows; numOfBalls = myNumberOfBalls; nonce = myNonce; }
const { serverSeed, commitment } = generateServerSeed();
const clientSeed = getClientSeed();
const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
const numbersToGenerate = numOfBalls; // Only need to generate one number per ball
const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);
const bucketIndices = pickBuckets(numOfRows, numOfBalls, 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('📋 Number of rows:', numOfRows);
console.log('🏁 Buckets landed in (Bucket #1 is the leftern-most bucket):', bucketIndices);
})();
참고: Cloudbet에서 게임의 서버 시드를 공개하려면 먼저 새로운 서버 시드로 교체해야 합니다.
