可验证公平在代码中是如何实现的?
假设游戏已经结束,并且我们已获得未哈希的服务器种子、客户端种子和 Nonce(随机数),其工作原理如下。
生成游戏结果主要需要三个步骤。
byteGenerator(生成随机字节)
generateFloats(将字节转换为浮点数(数字))
浮点数转换为游戏事件(将浮点数转换为 Shuffle Original 游戏中的实际事件)
ByteGenerator 函数:随机字节生成器
byteGenerator 函数用作随机字节生成器。
它接收 clientSeed、serverSeed、nonce 和 cursor 的唯一值,并通过加密函数 HMAC_SHA256 生成一个随机且唯一的 SHA-256 哈希值。
生成的 SHA-256 值大小为 32 字节。为了在游戏结果的充分随机性与计算量之间取得平衡,这 32 字节会被拆分为 8 段,每段 4 字节*,用于生成每个游戏结果。
在某些需要超过 8 个游戏结果的游戏中,我们会使用 cursor。cursor 的初始值为 0,并会依次增加到 1、2、3、4,以满足所需的结果数量。
对于所需随机结果不超过 8 个的游戏,cursor 的值不会递增。
*4 字节数据可产生 2^32(4,294,967,296)种可能的结果,这一范围足以保证随机性。
function* byteGenerator({ serverSeed, clientSeed, nonce, cursor }: ByteGeneratorInterface) {
// Setup cursor variables let currentRound = Math.floor(cursor / 32);
let currentRoundCursor = cursor;
currentRoundCursor -= currentRound * 32;
// Generate outputs until cursor requirement fullfilled
while (true) {
// HMAC function used to output provided inputs into bytes
const hmac = crypto.createHmac('sha256', serverSeed);
hmac.update(`${clientSeed}:${nonce}:${currentRound}`);
const buffer = hmac.digest();
// Update curser for next iteration of loop
while (currentRoundCursor < 32) {
yield Number(buffer[currentRoundCursor]);
currentRoundCursor += 1;
}
currentRoundCursor = 0;
currentRound += 1;
}
}generateFloats 函数:将字节转换为浮点数
该函数将 SHA-256 十六进制值从字节转换为浮点数,以便进一步用于游戏事件的计算。
下面展示了 SHA-256 十六进制值如何从字节转换为浮点数。最终输出 numArr 包含游戏所需的全部结果。如果只需要 1 个结果,列表中将只包含一个值。
该函数返回一个由 0 到 1 之间的数字组成的数组,count 表示返回数组中的元素个数。
代码:
// Convert the hash output from the rng byteGenerator to floats
export function generateFloats({
serverSeed,
clientSeed,
nonce,
cursor,
count,
}: GenerateFloatsInterface) {
// Random number generator function
const rng = byteGenerator({ serverSeed, clientSeed, nonce, cursor });
// Declare bytes as empty array
const bytes = [];
// Populate bytes array with sets of 4 from RNG output
while (bytes.length < count * 4) {
bytes.push(rng.next().value!);
}
// Return bytes as floats using lodash reduce function
const numArr = chunk(bytes, 4).map(bytesChunk =>
bytesChunk.reduce((result, value, i) => {
const divider = 256 ** (i + 1);
const partialResult = value / divider;
return result + partialResult;
}, 0),
);
return numArr;
}
我们所有的 Shuffle Original 游戏都会同时使用 ByteGenerator 和 generateFloats 函数来生成 0 到 1 之间的随机浮点数。但从这一步开始,每款游戏都会通过各自独特的流程,根据生成的浮点数确定游戏事件。
这些独特流程将在游戏事件中详细说明。
图解示例
下面我们将演示如何使用输入值生成一次骰子(Dice)游戏事件。
Input Values: Given some random input values
serverSeed, clientSeed, nonce, cursor
Step 1: byteGenerator creates a SHA-256 byte
"a3f4e0ac7c7e8e9b5f16106c6b1d14e87c2c5a8d59b1d1c6a0b5f3e5a7d4c9a8”
Step 2: 256 bytes is split into 8 equal set of 32 bytes
"a3f4e0ac”, “7c7e8e9b”, “5f16106c”, “6b1d14e8”,
“7c2c5a8d”, “59b1d1c6”, “a0b5f3e5”, “a7d4c9a8”
Step 3: Each set is broken down into 2 bytes each
(only first 2 sets is shown)
set 1: a3-f4-e0-ac
set 2: 7c-7e-8e-9b
......
Step 4: Each of the 2 bytes represent a number from 0 to 255
set 1: [163, 244, 224, 172]
set 2: [124, 126, 142, 155]
......
Step 5: Using the formula in the code to generate numArr
Float 1 = (163 / (256^1)) + (244 / (256^2)) + (224 / (256^3))
+ (172 / (256^4)) = 0.64045528601
Float 2 = (124 / (256^1)) + (126 / (256^2)) + (142 / (256^3))
+ (155 / (256^4)) = 0.48630610737 Float 3 ......
Step 6: Output the floats in a list
numArr = [0.64045528601, 0.48630610737, ...... ]
*** Game Event Generation ***
Step 7: The numArr output will be used to generate game events,
depending on the game requirement
Example 7: Using dice as an example.
The game event is generated using the first numArr value.
const resultValue = floats.map(val => Math.floor(floats * 10001) / 100);
resultValue = Math.floor(0.64045528601 * 10001) / 100
= 64.05
注意:实际游戏进行时,服务器种子处于哈希状态,因此玩家和运营方在游戏过程中都无法查看此流程的各项输出。
由于游戏过程中和游戏结束后使用的是相同的算法和函数,只要输入相同,用户随时都可以自行验证结果。
