跳转到主要内容

可验证公平 - 游戏事件

什么是游戏事件

此前,我们已使用 generateFloats 和 byteGenerator 函数生成了 0 到 1 之间的随机浮点数(float)。游戏事件负责将这些浮点数转换为实际的游戏结果,不同游戏的转换方式各不相同。例如,在 Plinko 中,浮点数用于决定小球下落的方向;在 Dice 中,浮点数用于决定掷骰的结果。

下文将详细说明我们如何在平台上的每款 Shuffle Originals 游戏中,将浮点数转换为公平的事件或结果。


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 且服从双曲线分布的浮点数。

// 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 个互不重复的宝石位置。为此,我们会生成 10 个介于 1 到 40 之间的不同数值,分别代表 Keno 面板上的各个位置,然后通过循环处理重复值,返回最终的 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 面板上的位置。我们会根据用户选择的地雷数量,生成一组 0 到 24 之间的整数,用来表示面板上的 25 个格子。

// 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

我们的 Roulette 采用欧式单“0”轮盘,共有 37 个格位,编号从 0 到 36。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)上查到,且与推文中显示的哈希一致。

种子

1000 万局 Crash 游戏的种子生成流程旨在确保公平与透明,杜绝 Shuffle 进行任何操纵的可能。种子的生成与验证方式如下:

  1. 初始种子生成:第 1000 万局游戏的种子由 Shuffle 随机选定。

  2. 通过连续哈希生成种子:其余所有游戏的种子均采用反向哈希的方式生成:

    • 每局游戏的种子都是对下一局游戏的种子进行哈希运算的结果。具体来说,第 9,999,999 局游戏的哈希是第 1000 万局游戏哈希的 SHA256 哈希值。

    • 这一链式过程依次进行,每局游戏的种子都由下一局游戏的哈希经哈希运算得出,直至第一局游戏的种子由第二局游戏的哈希得出。

  3. 种子完整性验证:

    • 我们公开了第一局游戏所用哈希的哈希值,因此玩家可以自行验证直至第 1000 万局为止所有游戏哈希的完整性。

    • 通过这种方法,只要知道任意一局游戏的哈希,就能验证此前所有游戏的种子。例如,如果您知道第 1323185 局的种子,就可以对该种子进行哈希运算,验证其是否与第 1323184 局的种子一致,并依此类推,一直追溯到第一局。

  4. 透明与安全:

    • 通过使用 Shuffle 无法控制的未来 BTC 区块哈希,并公开第一局游戏种子的哈希值,用户可以证明 Crash 游戏结果公平且未经篡改。

Slide

Slide 采用与 Crash 相同的可验证公平机制和种子生成流程。种子生成已在 x.com 上公开进行,相关帖子请见此处。

区块 952294 的哈希可在 BTC 区块浏览器(https://btcscan.org/block/00000000000000000000a382760ff19cdb7f9f22147b9123f3e471fb3c6d129a)上查到,且与推文中显示的哈希一致。

Baccarat

Baccarat 使用无限副牌进行,因此每一轮的概率始终相同。Baccarat 的游戏事件是确定在首轮发牌以及按标准 Baccarat 补牌规则进行的任何补牌中,发给闲家(Player)和庄家(Banker)的牌。

牌会按以下顺序从生成的浮点数序列中依次发出:

  1. 闲家第一张牌 - 浮点数索引 0

  2. 庄家第一张牌 - 浮点数索引 1

  3. 闲家第二张牌 - 浮点数索引 2

  4. 庄家第二张牌 - 浮点数索引 3

  5. 闲家第三张牌(如补牌)- 浮点数索引 4

  6. 庄家第三张牌(如补牌)

    1. 浮点数索引 4 - 闲家未补第三张牌时

    2. 浮点数索引 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(选择抛掷次数以及获胜所需的最少猜中次数),但三种模式得出抛掷结果的方式完全相同。

每次抛掷的结果都是先生成一个浮点数,再将其映射为 0 或 1,与 Plinko 将浮点数映射为向左/向右下落的方式完全相同:

// 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——生成一次抛掷(count = 1)。如果玩家选择的一面与抛掷结果一致,即获胜。

Classic Progressive——玩家每继续一次就生成一次抛掷,但每次抛掷的结果在本局开始时就已确定:第 N 次抛掷始终通过生成第 1 至第 N 次抛掷并取最后一个结果得出,因此随着玩家推进而逐次揭晓结果,并不会改变任何一次抛掷原本的结果。玩家可在任意一次成功抛掷后兑现;一旦猜错,本局即告结束。

Target——玩家选择抛掷总次数(最多 20 次)以及获胜所需的最少猜中次数。所有抛掷会一次性批量生成(count = totalFlips),若与玩家猜测一致的抛掷次数不少于所选的最少次数,则本局获胜。

Floor is Lava

目标:在连续三个关卡中穿过 7×7 网格(49 格),途中不能踩到变成岩浆的格子。

每个关卡由固定规模的“回合”组成——每回合会有一批格子同时陷入岩浆——在任意回合存活后,您都可以兑现。

面板生成

本局开始前,三个关卡的格子布局会通过一次可验证公平的洗牌一并确定:从基于种子的生成器中无放回地抽取整数 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

抽取一次包含 147 个值的排列,而不是分别进行三次 49 个值的洗牌,只是为了提高效率——由于 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 则是上表中对应难度的每关存活格数。

这是基于公平赔率的定价:即在格子均匀随机分布的前提下,恰好到达该进度(已通过关卡 × 已存活回合)的累计概率的倒数,再按庄家优势进行调整。

这是否解答了您的问题?