게임 이벤트란?
앞서 generateFloats 및 byteGenerator 함수를 사용해 0에서 1 사이의 무작위 부동소수점 값(float)을 생성했습니다. 게임 이벤트는 이렇게 생성된 float 값을 실제 게임 결과로 변환하는 과정이며, 변환 방식은 게임마다 다릅니다. 예를 들어 Plinko에서는 float 값으로 공이 떨어지는 방향을 결정하고, Dice에서는 주사위 굴림의 결과를 결정합니다.
아래에서는 플랫폼의 모든 Shuffle Originals 게임에서 float 값을 공정한 이벤트 또는 결과로 변환하는 방법을 자세히 설명합니다.
Tower
Tower의 목표는 열쇠를 찾고 독사과를 피해 탑 꼭대기에 올라 공주를 구하는 것입니다. 탑의 각 줄에는 선택한 난이도에 따라 미리 정해진 수의 안전 타일(열쇠)과 독사과가 있습니다.
Tower의 게임 이벤트는 각 줄에서 안전 타일(열쇠)의 위치를 결정하는 것입니다.
탑은 총 9줄로 구성됩니다. 열의 수와 열쇠 및 독사과의 배치는 선택한 난이도에 따라 다음과 같이 달라집니다:
쉬움: 4열 - 줄마다 열쇠 3개, 독사과 1개
보통: 3열 - 줄마다 열쇠 2개, 독사과 1개
어려움: 2열 - 줄마다 열쇠 1개, 독사과 1개
전문가: 3열 - 줄마다 열쇠 1개, 독사과 2개
마스터: 4열 - 줄마다 열쇠 1개, 독사과 3개
각 줄의 결과는 난이도 설정에 따라 사용 가능한 열 안에서 열쇠(안전 타일)의 위치를 무작위로 생성하여 결정됩니다.
const TOWER_DIFFICULTY_TO_CONFIG: Record<TowerDifficulty, DifficultyConfig> = {
[TowerDifficulty.EASY]: {
count: 3,
columns: 4,
},
[TowerDifficulty.MEDIUM]: {
count: 2,
columns: 3,
},
[TowerDifficulty.HARD]: {
count: 1,
columns: 2,
},
[TowerDifficulty.EXPERT]: {
count: 1,
columns: 3,
},
[TowerDifficulty.MASTER]: {
count: 1,
columns: 4,
},
};
// Generate floats to determine the positions of safe tiles (keys) for each row
const floats = generateFloats({
serverSeed,
clientSeed,
nonce,
cursor,
count,
});
// Get the tower configuration based on the selected difficulty
const towerConfig = TOWER_DIFFICULTY_TO_CONFIG[difficulty];
const results: number[][] = [];
const resultPerRow = towerConfig.count;
// Iterate through each row to determine the positions of safe tiles for that row
for (let i = 0; i < TOWER_TOTAL_ROWS.toNumber(); i++) {
const result: number[] = [];
const values = Array.from(Array(towerConfig.columns).keys());
// Get positions for safe tiles in the current row
for (let j = 0; j < resultPerRow; j++) {
const float = floats[i * resultPerRow + j];
if (float === undefined) {
break;
}
const position = Math.floor(float * (towerConfig.columns - j));
result.push(values.splice(position, 1)[0] ?? 0);
}
results.push(result);
}
return results;
// Example: when clientSeed='abc', serverSeed='def', nonce=1, difficulty='easy'
// results: [
// [0, 1, 2],
// [0, 2, 3],
// [0, 2, 3],
// [0, 1, 2],
// [0, 1, 3],
// [0, 1, 2],
// [1, 2, 3],
// [0, 1, 3],
// [0, 1, 3]
// ]
// Each sub-array represents the column indexes of safe tiles for that row
// (e.g., for the first row, safe tiles are in columns 0, 1, and 2)
Chicken
Chicken의 목표는 위험 차선을 밟지 않고 닭이 최대한 많은 차선을 건너가게 하는 것입니다.
한 칸씩 전진에 성공할 때마다 지급 배수가 올라갑니다. 플레이어는 닭이 위험 차선으로 뛰어들기 전이라면 언제든지 캐시아웃할 수 있습니다. 플레이어가 캐시아웃하기 전에 닭이 위험 구간에 도달하면 해당 베팅은 패배 처리됩니다.
Chicken의 핵심 게임 이벤트는 위험 차선, 즉 닭이 패배하게 되는 특정 차선을 결정하는 것입니다.
Chicken에는 4가지 난이도가 있으며, 선택한 난이도에 따라 닭이 건널 수 있는 차선 수가 다음과 같이 달라집니다:
쉬움: 차선 19개
보통: 차선 17개
어려움: 차선 15개
전문가: 차선 10개
const TOTAL_LANES = 20
// Number of lanes that the chicken can cross for each difficulty
const DIFFICULTY_TO_VALID_LANE_COUNT: Record<ChickenDifficulty, number> = {
[ChickenDifficulty.EASY]: 19,
[ChickenDifficulty.MEDIUM]: 17,
[ChickenDifficulty.HARD]: 15,
[ChickenDifficulty.EXPERT]: 10,
};
// Number of gameResults generated depends on the difficulty
// Example with Hard difficulty, we generate 20 - 15 = 5 numbers
// and the smallest number in the array is the danger lane
const gameResults = generateFloats({
serverSeed,
clientSeed,
nonce,
cursor: 0,
count: TOTAL_LANES - DIFFICULTY_TO_VALID_LANE_COUNT[difficulty] // difficulty selected for the game
}).map((val, index) => Math.floor(val * (TOTAL_LANES - index)));
// Danger lane is determined by the smallest number in the game results array
const dangerLane = Math.min(...gameResults)
// Example: Hard difficulty - gameResults = [10, 4, 6, 15, 8]
// Danger lane will be on 4 (this is zero based index)
// Lane 0 - 3 are safe and lane 4 will be the danger lane
// The chicken will lose upon reaching lane 4 if not cashed out earlier
Plinko
Plinko에서 플레이어는 높이 8~16의 피라미드를 선택할 수 있습니다. 공이 맨 위에서 떨어지면 Plinko 보드의 핀에 부딪힐 때마다 왼쪽 또는 오른쪽으로 떨어지며, 이 과정이 8~16회 반복됩니다. Plinko의 게임 이벤트는 각 단계에서 공이 왼쪽과 오른쪽 중 어느 쪽으로 떨어지는지 결정하는 것이며, 이는 각각 0과 1로 표시됩니다.
// Create multiple floats between 0 and 1
// Example: floats = [0.8320531346835196, 0.8531235849950463, 0.18227359023876488, 0.774284637067467, 0.18627392360940576, 0.6012540922965854, 0.608081541955471, 0.46018488449044526] when clientSeed='abc', serverSeed='def', nonce=1, count=8
// count refers to the number of plinko rows
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor, count });
// Example: [1, 1, 0, 1, 0, 1, 1, 0] Each 1 represents a drop to the right whereas 0 represents a drop to the left
const dropDirections = floats.map(val => Math.floor(val * 2));
// This sums up all the values in the array to get the final position of the ball from the left.
// Example: finalPositionFromLeft = 5 (the ball will land on the 6th slot from the left)
const finalPositionFromLeft = dropDirections.reduce((cur, acc) => cur + acc, 0)
Dice
Dice에서 주사위를 굴린 값은 0.00에서 100.00 사이입니다. Dice의 게임 이벤트는 이 값을 결정하는 것으로, (floats * 10,001) / 100) 공식을 사용합니다. 소수점 둘째 자리까지의 값과 숫자 0을 포함하므로, 가능한 결과는 총 10,000 + 1 = 10,001가지입니다(소수 포함).
// Creates an array of random numbers
// Example: With clientSeed = 'abc', serverSeed = 'def', nonce = 1, floats = [0.43053962965495884]
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: 1 });
// Converts the value in the array to a number between 0.00 to 100.00 and return the value
// Example: resultValue = [43.05]
const resultValue = floats.map(val => Math.floor(floats * 10001) / 100);
return resultValue[0];
// Example: 43.05 is returned as the result of the game
Limbo
Limbo의 목표는 1보다 큰(>1) 배수를 예측하는 것입니다. 게임 결과가 선택한 배수보다 크면 사용자가 승리합니다. Limbo의 게임 이벤트는 1보다 크면서 쌍곡선 분포를 따르는 float 값을 생성하는 것입니다.
// Generate float between 0 and 1
// Example: floats = [0.43053962965495884], float = 0.43053962965495884,
// when clientSeed='abc', serverSeed='def', nonce=1
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: 1 });
const float = floats[0]
// generate limbo game event results with house edge (1%)
// using a hyperbolic 1/x curve
// multiplying by this and dividing by (MAX_VALUE + 1) prevents division by 0 error
const MAX_VALUE = 2 ** 24;
const rtp = 0.99;
// Example: rawValue = 2.298905
const rawValue = (rtp * MAX_VALUE) / (MAX_VALUE * float + 1)
// resultValue = 2.29
const resultValue = (0.99 * MAX_VALUE / (MAX_VALUE * float +1)).toFixed(2,ROUND_DOWN);
return resultValue;
Keno
Keno의 게임 이벤트는 Keno 보드 위에서 서로 겹치지 않는 보석 위치 10개를 정하는 것입니다. 이를 위해 Keno 보드의 각 위치를 나타내는 1~40 사이의 서로 다른 값 10개를 생성합니다. 그런 다음 반복문을 사용해 중복된 값을 처리하고 최종 보석 위치 10개를 반환합니다.
// Generate 10 different floats as there are 10 positions that are selected when the game is played
// Example: floats = [18, 26, 7, 22, 31, 10, 23, 8, 24, 13] when clientSeed='abc', serverSeed='def', nonce=1
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: 10 }) .map((val, index) => Math.ceil(val * (40 - index)))
// array used to store the position of the gems const gemLocations: number[] = [];
// this loop helps to prevent duplicate gem positions by skipping positions that already contain a gem
for (const val of floats) { let currentGem = val;
let i = 0;
while (i <= currentGem) {
if (gemLocations.includes(i)) {
currentGem++;
}
i++;
}
gemLocations.push(currentGem);
}
// returns the location of the gem on the board
// Example: [18, 27, 7, 24, 35, 11, 28, 9, 31, 16] return gemLocations;
Mines
Mines의 게임 이벤트는 25칸으로 이루어진 Mines 보드에서 지뢰의 위치를 정하는 것입니다. 사용자가 선택한 지뢰 수에 따라, 보드의 25칸을 나타내는 0~24 사이의 정수 목록을 생성합니다.
// Generate 10 different floats
// Example: [20, 20, 4] when clientSeed='abc', serverSeed='def', nonce=1, numberOfBombs=3
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor: 0, count: numberOfBombs })
.map((val, index) => Math.floor(val * (25 - index)));
// array used to store the position of the bombs
const mineLocations: number[] = [];
// this loop helps to prevent duplicate bomb positions by skipping positions that already contain a mine
for (const val of floats) {
let currentMine = val;
let i = 0;
while (i <= currentMine) {
if (mineLocations.includes(i)) {
currentMine++;
} i++; } mineLocations.push(currentMine); }
// returns the location of the bombs on the board
// Example: [20, 21, 4]
return mineLocations;
Roulette
Shuffle의 Roulette은 '0'이 하나뿐인 유럽식 휠을 기반으로 하며, 0부터 36까지 총 37개의 포켓이 있습니다. Roulette의 게임 이벤트는 37개의 포켓 중 공이 들어갈 포켓을 계산하는 것입니다.
// Generate a float between 0 and 1
// Example: floats = [0.43053962965495884]
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor: 0, count: 1 });
// generate an integer from 0 to 37 to represent the pocket to land on
// Example: resultValue = 15
const resultValue = floats.map(val => Math.floor(val * 37))[0];
return resultValue
Wheel
Wheel에서 사용자는 휠의 구간 수를 선택합니다. 포인터가 멈춘 구간에 따라 사용자는 해당 구간의 배수를 획득합니다. Wheel의 게임 이벤트는 포인터가 멈출 구간을 결정하는 것입니다.
// Generate a float between 0 and 1
// Example: floats = [0.43053962965495884]
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:1 });
// Get an integer representing the number of segments set in the game
// Example: resultSegment = 8
const resultSegment = floats.map(val => Math.floor(val * numberOfSegments))[0];
return resultSegment
Blackjack
Blackjack은 무제한 카드 덱으로 진행되므로 매 턴의 확률이 항상 동일합니다. Blackjack의 게임 이벤트는 플레이어나 딜러가 덱에서 카드를 뽑을 때 어떤 카드가 나올지 결정하는 것입니다.
// Generate a float between 0 and 1
// Example: floats = [0.43053962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (52 elements are generated but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });
const CARDS = [ ♦2, ♥2, ♠2, ♣2, ♦3, ♥3, ♠3, ♣3, ♦4, ♥4, ♠4, ♣4, ♦5, ♥5, ♠5, ♣5, ♦6, ♥6, ♠6, ♣6, ♦7, ♥7, ♠7, ♣7, ♦8, ♥8, ♠8, ♣8, ♦9, ♥9, ♠9, ♣9, ♦10, ♥10, ♠10, ♣10, ♦J, ♥J, ♠J, ♣J, ♦Q, ♥Q, ♠Q, ♣Q, ♦K, ♥K, ♠K, ♣K, ♦A, ♥A, ♠A, ♣A
];
// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));
// Example: cards = [♠7, ♠10, ♥4, ♥9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;
Hilo
Hilo는 무제한 카드 덱으로 진행되므로 매 턴의 확률이 항상 동일합니다. 게임의 목표는 다음 카드가 현재 카드보다 높은지 낮은지 맞히는 것입니다. Hilo의 게임 이벤트는 사용자가 다음에 카드를 뽑을 경우 어떤 카드가 나올지 결정하는 것입니다.
// Generate a float between 0 and 1
// Example: floats = [0.43053962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (an unlimited number of cards can be drawn but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });
const CARDS = [
♦2, ♥2, ♠2, ♣2, ♦3, ♥3, ♠3, ♣3, ♦4, ♥4, ♠4, ♣4, ♦5, ♥5, ♠5, ♣5, ♦6, ♥6, ♠6, ♣6, ♦7, ♥7, ♠7, ♣7, ♦8, ♥8, ♠8, ♣8, ♦9, ♥9, ♠9, ♣9, ♦10, ♥10, ♠10, ♣10, ♦J, ♥J, ♠J, ♣J, ♦Q, ♥Q, ♠Q, ♣Q, ♦K, ♥K, ♠K, ♣K, ♦A, ♥A, ♠A, ♣A
];
// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));
// Example: cards = [♠7, ♠10, ♥4, ♥9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;
Crash
Crash 게임의 결과 계산은 다음 두 부분으로 이루어집니다.
비트코인 블록 해시
시드
비트코인 해시
자세한 내용은 아래 트윗에서 확인하실 수 있습니다:
블록 779588의 해시는 BTC 블록 탐색기(https://btcscan.org/block/000000000000000000033ab71b5fa7edcafe388e5f49719f1de8e1f8d0670460)에서 확인할 수 있으며, 트윗에 표시된 해시와 일치합니다.
시드
Crash 게임 1,000만 회분의 시드 생성 과정은 공정성과 투명성을 보장하고 Shuffle이 결과를 조작할 가능성을 원천적으로 차단하도록 설계되었습니다. 시드의 생성 및 검증 방식은 다음과 같습니다:
초기 시드 생성: 10,000,000번째 게임의 시드는 Shuffle이 무작위로 선택합니다.
순차 해싱을 통한 시드 생성: 나머지 모든 게임의 시드는 역방향 해싱 방식으로 생성합니다:
각 게임의 시드는 바로 다음 게임의 시드를 해싱한 결과입니다. 구체적으로, 9,999,999번째 게임의 해시는 10,000,000번째 게임 해시의 SHA256 해시입니다.
이러한 연쇄 과정이 계속 이어져 각 게임의 시드는 다음 게임의 해시를 해싱하여 도출되며, 최종적으로 첫 번째 게임의 시드는 두 번째 게임의 해시로부터 도출됩니다.
시드 무결성 검증:
첫 번째 게임에 사용된 해시의 해시값을 공개하므로, 플레이어는 10,000,000번째 게임까지 모든 게임 해시의 무결성을 직접 검증할 수 있습니다.
이 방식을 사용하면 특정 게임의 해시를 알고 있을 때 그 이전의 모든 게임 시드를 검증할 수 있습니다. 예를 들어 게임 1323185의 시드를 알고 있다면, 이 시드를 해싱하여 게임 1323184의 시드와 일치하는지 확인할 수 있으며, 같은 방식으로 첫 번째 게임까지 거슬러 올라가며 검증할 수 있습니다.
예를 들어 게임 1323185(https://shuffle.com/games/originals/crash?md-id=1323185&modal=crashGame)의 해시는 2f824ebb9548a4d24d042ecab390acd1dc931048f696dc03c738937852825ac0입니다.
게임 1323184(https://shuffle.com/games/originals/crash?md-id=1323184&modal=crashGame)의 해시는 20f42ccc46fed8e005767f85eb4bd5d42d3bd2a72b01ae77dcc7071bec280265입니다.
게임 1323184의 해시는 게임 1323185 해시의 SHA256 해시입니다. https://xorbin.com/tools/sha256-hash-calculator 등의 온라인 SHA256 계산기를 사용해 직접 확인해 보실 수 있습니다.
투명성과 보안:
Shuffle이 통제할 수 없는 미래의 BTC 블록 해시를 사용하고 첫 번째 게임 시드의 해시값을 공개함으로써, 사용자는 Crash 게임 결과가 공정하며 조작되지 않았음을 증명할 수 있습니다.
Slide
Slide는 Crash와 동일한 증명 가능한 공정성(Provably Fair) 및 시드 생성 과정을 사용합니다. 시드 생성은 x.com에서 공개적으로 진행되었으며, 해당 게시물은 여기에서 확인하실 수 있습니다.
블록 952294의 해시는 BTC 블록 탐색기(https://btcscan.org/block/00000000000000000000a382760ff19cdb7f9f22147b9123f3e471fb3c6d129a)에서 확인할 수 있으며, 트윗에 표시된 해시와 일치합니다.
Baccarat
Baccarat은 무제한 카드 덱으로 진행되므로 매 턴의 확률이 항상 동일합니다. Baccarat의 게임 이벤트는 최초 딜과 표준 Baccarat 드로우 규칙에 따른 추가 드로우에서 플레이어(Player) 핸드와 뱅커(Banker) 핸드에 배분되는 카드를 결정하는 것입니다.
카드는 생성된 float 시퀀스에서 다음 순서대로 배분됩니다:
플레이어의 첫 번째 카드 - float 인덱스 0
뱅커의 첫 번째 카드 - float 인덱스 1
플레이어의 두 번째 카드 - float 인덱스 2
뱅커의 두 번째 카드 - float 인덱스 3
플레이어의 세 번째 카드(드로우하는 경우) - float 인덱스 4
뱅커의 세 번째 카드(드로우하는 경우)
float 인덱스 4 - 플레이어가 세 번째 카드를 드로우하지 않은 경우
float 인덱스 5 - 플레이어가 세 번째 카드를 드로우한 경우
// Generate a float between 0 and 1
// Example: floats = [0.40353962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (an unlimited number of cards can be drawn but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });
const CARDS = [
♠2, ♥2, ♦2, ♣2, ♠3, ♥3, ♦3, ♣3, ♠4, ♥4,
♦4, ♣4, ♠5, ♥5, ♦5, ♣5, ♠6, ♥6, ♦6, ♣6,
♠7, ♥7, ♦7, ♣7, ♠8, ♥8, ♦8, ♣8, ♠9, ♥9,
♦9, ♣9, ♠10, ♥10, ♦10, ♣10, ♠J, ♥J, ♦J,
♣J, ♠Q, ♥Q, ♦Q, ♣Q, ♠K, ♥K, ♦K, ♣K, ♠A,
♥A, ♦A, ♣A
];
// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));
// Example: cards = [♠7, ♠10, ♥4, ♠9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;
Blitz
Blitz는 무제한 카드 덱으로 진행되므로 매 턴의 확률이 항상 동일합니다. Blitz의 게임 이벤트는 플레이어가 선택한 드로우 횟수만큼 플레이어에게 배분되는 카드를 결정하고, 뽑힌 카드가 모두 서로 다른지 판정하는 것입니다.
// Generate a float between 0 and 1
// Example: floats = [0.43053962965495884, 0.6645898742135614, 0.17454026662744582, 0.5729992990382016, 0.850033157505095] (an unlimited number of cards can be drawn but only 5 are shown here for brevity)
const floats = generateFloats({serverSeed, clientSeed, nonce, cursor, count:52 });
const CARDS = [
♠2, ♥2, ♣2, ♦2, ♠3, ♥3, ♣3, ♦3, ♠4, ♥4,
♣4, ♦4, ♠5, ♥5, ♣5, ♦5, ♠6, ♥6, ♣6, ♦6,
♠7, ♥7, ♣7, ♦7, ♠8, ♥8, ♣8, ♦8, ♠9, ♥9,
♣9, ♦9, ♠10, ♥10, ♣10, ♦10, ♠J, ♥J, ♣J, ♦J,
♠Q, ♥Q, ♣Q, ♦Q, ♠K, ♥K, ♣K, ♦K, ♠A,
♥A, ♣A, ♦A
];
// Get an integer 0 to 51, representing the index of the cards
// Example: cardFloats = [22, 34, 9, 29, 44]
const cardFloats = floats.map(val => Math.floor(val * 52));
// Example: cards = [♠7, ♠10, ♥4, ♠9, ♦K]
const cards = cardFloats.map(val => CARDS[val])
return cards;
Coinflip
Coinflip의 목표는 던진 동전이 어느 면으로 떨어질지 정확히 맞히는 것입니다. Coinflip의 게임 이벤트는 각 동전 던지기의 결과, 즉 HEADS(앞면) 또는 TAILS(뒷면)를 결정하는 것입니다. Coinflip에는 Classic(한 번 던지기), Classic Progressive(연속으로 던지며 원하는 시점에 캐시아웃), Target(던질 횟수와 승리에 필요한 최소 적중 횟수를 선택)의 세 가지 모드가 있으며, 세 모드 모두 같은 방식으로 던지기 결과를 도출합니다.
각 던지기의 결과는 float 값을 생성한 뒤 이를 0 또는 1로 매핑하여 결정되며, 이는 Plinko에서 float 값을 왼쪽/오른쪽 낙하로 매핑하는 방식과 똑같습니다:
// Generate floats to determine each flip outcome
// Example: floats = [0.43053962965495884, 0.6645898742135614, ...] when clientSeed='abc', serverSeed='def', nonce=1
const floats = generateFloats({ serverSeed, clientSeed, nonce, cursor, count });
// Each float maps to 0 or 1, which maps to HEADS or TAILS
// Example: [0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1]
const flipValues = floats.map(val => Math.floor(val * 2));
const flipResults = flipValues.map(val => (val === 0 ? 'HEADS' : 'TAILS'));
// Example: [HEADS, TAILS, HEADS, TAILS, TAILS, HEADS, TAILS, HEADS, TAILS, HEADS,
// TAILS, HEADS, TAILS, HEADS, HEADS, TAILS, HEADS, HEADS, HEADS, TAILS]
return flipResults;
Classic — 던지기 1회가 생성됩니다(count = 1). 플레이어가 선택한 면이 던지기 결과와 일치하면 승리합니다.
Classic Progressive — 플레이어가 계속 진행할 때마다 던지기가 한 번씩 생성되지만, 각 던지기의 결과는 라운드가 시작될 때 이미 정해져 있습니다. N번째 던지기는 항상 1번부터 N번까지의 던지기를 생성한 뒤 마지막 값을 취하는 방식으로 도출되므로, 플레이어가 진행하면서 결과를 하나씩 공개하더라도 각 던지기의 결과는 달라지지 않습니다. 플레이어는 던지기에 성공할 때마다 캐시아웃할 수 있으며, 처음으로 예측이 빗나가면 라운드가 종료됩니다.
Target — 플레이어가 총 던지기 횟수(최대 20회)와 승리에 필요한 최소 적중 횟수를 선택합니다. 모든 던지기 결과는 한 번에 일괄 생성되며(count = totalFlips), 플레이어의 예측과 일치하는 던지기 횟수가 선택한 최소 횟수 이상이면 라운드에서 승리합니다.
Floor is Lava
목표: 용암으로 변하는 타일을 밟지 않고 연속된 세 레벨에 걸쳐 7×7 그리드(타일 49개)를 건너는 것입니다.
각 레벨은 고정된 크기의 '라운드'로 진행되며, 라운드마다 일정 수의 타일이 한꺼번에 용암으로 변합니다. 어느 라운드든 살아남은 후에는 캐시아웃할 수 있습니다.
보드 생성
게임이 시작되기 전, 세 레벨 전체의 타일 배치가 증명 가능한 공정성(Provably Fair) 방식의 셔플 한 번으로 정해집니다. 시드 기반 생성기에서 정수 0..146(타일 49개 × 3레벨)의 무작위 순열을 비복원 추출한 뒤, 이를 레벨별로 나눕니다:
// Draw a random permutation of 0..146 without replacement
// Example: raw = [103, 12, 140, 76, 3, ...]
const boards = [[], [], []];
for (const value of raw) {
boards[Math.floor(value / 49)].push(value % 49); // bucket = level, residue = tile index }
// boards[0], boards[1], boards[2] are each a uniformly random ordering of tiles 0..48
49개 값의 셔플을 세 번 따로 하는 대신 147개 값의 순열을 한 번 뽑는 것은 효율을 위한 방법일 뿐입니다. 147개 값 중 정확히 49개씩이 각 레벨의 구간에 속하므로, 각 레벨은 여전히 자체 49개 타일에 대해 독립적이고 균일한 무작위 순서를 갖게 됩니다.
라운드: 용암이 떨어지는 방식
각 레벨의 49개 타일 순서는 연속적이고 서로 겹치지 않는 묶음으로 나뉩니다. 라운드당 고정된 수의 타일(dropsPerRound)씩 고정된 라운드 수(roundsPerLevel)만큼 나누며, 마지막에 남는 타일이 해당 레벨 전체에서 살아남는 타일이 됩니다:
tilesDroppedInRound(level, dropsPerRound, roundIndex) = levelBoard.slice(roundIndex * dropsPerRound, (roundIndex + 1) * dropsPerRound)
tilesThatSurviveLevel(level, survivingCount) = levelBoard.slice(-survivingCount)
셔플을 통해 타일 순서가 미리 확정되므로, 각 라운드의 용암 타일은 플레이어의 선택에 따라 정해지는 것이 아니라 미리 섞어 둔 목록에서 해당 라운드 '차례'에 해당하는 구간으로 정해집니다.
매번 선택할 때마다 타일 하나(selectedTile)를 지정합니다. 이 타일은 현재 레벨의 이전 라운드에서 이미 용암으로 변한 타일이어서는 안 됩니다. 이후 선택한 타일 하나를 해당 라운드의 용암 타일 전체(selectedTile)와 비교하여 라운드 결과가 결정됩니다:
isLoss = droppedTiles.includes(selectedTile)
선택한 타일이 이번 라운드에 용암으로 변한 타일 중 하나라면 게임이 종료됩니다. 그렇지 않다면 함께 변한 다른 타일이 몇 개이든 상관없이 해당 라운드에서 살아남은 것입니다.
난이도 설정
네 가지 난이도는 라운드당 용암 타일 수와 라운드 수를 서로 다르게 조합합니다. 어느 난이도든 합계는 전체 49개 타일 보드와 같습니다(dropsPerRound × roundsPerLevel + survivingTiles = 49):
난이도 | 라운드당 용암 타일 수 | 레벨당 라운드 수 | 생존 타일 수 |
쉬움 | 7 | 6 | 7 |
보통 | 11 | 4 | 5 |
어려움 | 23 | 2 | 3 |
독성 | 48 | 1 | 1 |
레벨 및 게임 진행
한 게임은 3개 레벨로 구성되며, 각 레벨에는 독립적으로 섞인 49개 타일 보드가 있습니다. 한 레벨의 모든 라운드에서 살아남으면 해당 레벨을 클리어하고 다음 레벨 보드의 첫 번째 라운드로 이동합니다. 세 번째 레벨을 클리어하면 게임이 완료됩니다.
캐시아웃
어느 라운드든 살아남은 후에는 캐시아웃할 수 있으며, 첫 번째 선택에 성공하는 즉시 캐시아웃이 가능해집니다.
배수
multiplier = edgeMultiplier * 49^(levelsCleared+1) / (tilesRemainingInLevel * survivingTiles^levelsCleared)
levelsCleared는 지금까지 완전히 클리어한 레벨 수입니다. tilesRemainingInLevel은 현재 레벨에서 라운드를 하나 통과할 때마다 해당 라운드의 용암 타일 수만큼 줄어들며, survivingTiles는 위 표에 나온 난이도별 레벨당 생존 타일 수입니다.
이는 공정 확률에 기반한 배당으로, 타일이 균일하게 무작위로 배치된다는 가정하에 정확히 해당 지점(클리어한 레벨 × 생존한 라운드)에 도달할 누적 확률의 역수에 하우스 엣지를 반영한 값입니다.


