Dice: Golden Ticket에서 입증된 공정성이 작동하는 방식
Cloudbet에서 Dice: Golden Ticket을 플레이하는 방법과, 모든 주사위 결과 및 Golden Ticket 추첨이 입증된 공정성을 갖추고 있는지 확인하는 방법을 알아보세요.
Dice: Golden Ticket 플레이 방법
Dice: Golden Ticket(DGT)은 Cloudbet의 클래식 Dice 게임과 동일하게 플레이하며, 여기에 한 가지 요소가 추가됩니다. 바로 일반 당첨금 위에 보너스 배수를 지급할 수 있는 숨겨진 보너스 목표값입니다.
기본 게임: Roll Over(2.00~99.99) 또는 Roll Under(0.01~98.00) 중 하나를 선택한 다음 베팅합니다. 주사위가 목표값 기준으로 내가 선택한 쪽에 떨어지면, 목표값에 따른 배당금으로 당첨됩니다(목표값이 좁을수록 배수가 커집니다).
Golden Ticket 보너스: 매 라운드마다 두 번째 숨겨진 값인 Golden Ticket도 보드에 함께 설정되며, 3배에서 200배 사이의 자체 보너스 배수를 갖습니다. 주사위 결과가 Golden Ticket의 위치에서 ±1.75 이내에 떨어지면, 추가 베팅 없이 즉시 자동으로 해당 배수만큼 보너스 상금을 받게 되며, 이는 일반 당첨금과 함께 누적됩니다.
Golden Ticket을 직접 겨냥할 수는 없습니다. 목표로 삼거나 영향을 줄 수 있는 대상이 아니며, 매 라운드마다 보드 어딘가에 항상 존재하고, 그 근처에 결과가 떨어지면 일반 플레이에 더해 보너스를 받는 방식입니다.
Cloudbet Originals Dice: Golden Ticket
Cloudbet 자체 제작 Originals DGT는 99%의 RTP, 최대 x6,730의 주사위 배수, 최대 $1,000,000의 당첨금을 제공합니다.
Dice: Golden Ticket(DGT)은 다른 모든 Cloudbet Originals와 동일한 입증된 공정성 기반을 사용합니다. 서버 시드, 클라이언트 시드, 논스, SHAKE256, 거부 샘플링 등 일반적인 메커니즘은 입증된 공정성 작동 방식을 참고하세요. 주사위 값 추출 방식도 일반 Dice와 동일하게 작동하며, DGT는 여기에 Golden Ticket 값과 배수를 추가로 더합니다. 아래에서는 이 시퀀스가 구체적으로 어떻게 DGT 결과로 변환되는지 설명합니다.
무작위 숫자를 게임 결과로 변환하기
DGT는 두 가지 값이 검증 가능한 무작위성에 의해 결정된다는 것을 증명해야 합니다. 바로 주사위 값과 Golden Ticket 값입니다.
과정은 간단합니다. 먼저 0에서 10000 사이(포함)의 정수 하나를 추출해 주사위의 최종 값으로 삼습니다. 그런 다음 175에서 9825 사이(포함)의 또 다른 정수를 추출해 Golden Ticket의 최종 값으로 삼습니다. 두 숫자 모두 100으로 나누어 소수점 둘째 자리까지 표현하며, 각각 0~100(주사위), 1.75~98.25(Golden Ticket) 범위를 갖게 됩니다.
Golden Ticket의 범위가 0~100이 아니라 1.75~98.25로 의도적으로 제한된 이유는, Golden Ticket이 어디에 위치하든 그 주변에 항상 폭 3.5의 "골든존"이 온전히 존재하도록 하기 위해서입니다. 만약 범위가 가장자리(0 또는 100)까지 이어진다면, 가장자리 근처에 위치한 Golden Ticket은 골든존이 좁아져 당첨 확률이 더 낮아지게 됩니다.
DGT에는 또 다른 독특한 특징이 있습니다. Golden Ticket의 배수 자체도 입증된 공정성 프로세스를 통해 생성됩니다. 간단히 말하면, 0에서 1 사이의 세 번째 무작위 숫자를 추출한 다음, 절단된 파레토 분포(truncated Pareto distribution)를 사용해 3배에서 200배 사이의 배수로 변환합니다.

기술적 세부사항
SHAKE256으로 생성된 무작위 바이트는 0에서 2³²-1 사이의 4바이트 부호 없는 정수로 분할됩니다. 주사위 값은 10001가지 결과(0~10000, 포함)를 가지므로 거부 샘플링에서 P = 10001을 사용하며, Golden Ticket 값은 9651가지 결과(175~9825, 포함)를 가지므로 P = 9651을 사용합니다. (이 단계가 왜 중요한지는 입증된 공정성 페이지를 참고하세요.)
주사위 값과 Golden Ticket 값 선택: 주사위의 무작위 숫자를 10001로 나눈 나머지를 구해 값으로 삼습니다. Golden Ticket의 무작위 숫자도 동일하게 9651로 나눈 나머지를 구한 다음, 175를 더합니다(따라서 175~9825 범위). 두 값 모두 100으로 나눕니다.
Golden Ticket 배수 선택 (통계에 대한 기본 지식이 있으면 이해에 도움이 됩니다): 무작위 32비트 숫자를 추출한 다음 그 최댓값으로 나누어 0에서 1 사이에 고르게 분포된 값을 만듭니다. 이 값을 절단된 파레토 분포(하한 3, 상한 200, 알파 1.343 — 배수의 기댓값이 9가 되도록 선택되어 DGT의 99% RTP 목표를 유지)로 변환한 다음, 소수점 둘째 자리로 반올림합니다.
게임 진행
주사위 값과 Golden Ticket 값 모두 서버 측에서 생성되어 라운드가 끝날 때까지 숨겨지며, 플레이가 시작된 이후에는 감지되지 않고 변경될 수 없습니다. 베팅 후에는 선택한 기준값이 주사위 값과 비교되고, Golden Ticket과 주사위 값 사이의 거리가 1.75의 "골든존" 기준으로 확인되어 보너스 당첨 여부가 결정됩니다.
독립 검증을 위한 코드
Cloudbet의 DGT 구현과 정확히 일치합니다. 서버 시드, 클라이언트 시드, 논스를 입력해 직접 게임을 검증해 보세요. (Cloudbet에서 게임의 서버 시드를 공개하려면 이미 새로운 서버 시드로 넘어간 상태여야 합니다.)
의존성:
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
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) {
// Dice
// Ranges from 0 to 100, with two decimal places
const diceMinValue = 0;
const diceMaxValue = 10000;
let diceValue = sampleFromRange(diceMinValue, diceMaxValue, rng);
diceValue = diceValue / 100; // Now the value ranges from 0 to 100
// Golden Ticket
// Ranges from 1.75 to 98.25 due to fairness
// (If it landed on 99, for example, then the chance of the dice landing near
// it would be smaller than the guarantee of 3.5%)
const gtMinValue = 175;
const gtMaxValue = 9825;
let goldenTicketValue = sampleFromRange(gtMinValue, gtMaxValue, rng);
goldenTicketValue = goldenTicketValue / 100;
// Golden Ticket Multiplier
const multiplier = getRandomNumberTruncatedPareto(rng); // Complicated maths, can be ignored ;)
return { diceValue, goldenTicketValue, multiplier };
}
// Sample a number using the truncated Pareto distribution
function getRandomNumberTruncatedPareto(rng) {
// Get a uniformly distributed number between 0 and 1
let randomInt32 = rng.next().value;
const fraction = randomInt32 / 0xFFFFFFFF;
// Truncated Pareto distribution. The expected value is 9.00
const m0 = 3;
const m1 = 200;
const alpha = 1.343;
const r = Math.pow(m0 / m1, alpha);
const inner = 1 - fraction * (1 - r);
let randomNumber = m0 / Math.pow(inner, 1 / alpha);
randomNumber = Math.round(randomNumber * 100) / 100; // Round to 2 decimal places
return randomNumber;
}
// Sample a number between 'min' to 'max' (inclusive) using Rejection Sampling
function sampleFromRange(min, max, rng) {
const P = max - min + 1;
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 + min;
}
// 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 = 3 * 2; // We need 3 random numbers, however due to rejection sampling we may need more
const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);
const { diceValue, goldenTicketValue, multiplier } = 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('🎯 Dice landed on:', diceValue);
console.log('🎫 Golden ticket landed on:', goldenTicketValue);
console.log('⭐ Multiplier:', multiplier);
})();
