Как Provably Fair реализуется в коде?
Предположим, что игра завершена и у нас есть нехешированный серверный сид, клиентский сид и nonce. Вот как это работает.
Для генерации результатов игры потребуется выполнить три основных шага.
byteGenerator (генерация случайных байтов)
generateFloats (преобразование байтов в числа с плавающей точкой)
Числа с плавающей точкой в игровые события (преобразование чисел с плавающей точкой в фактические события в оригинальных играх)
Функция ByteGenerator как генератор случайных байтов генератор
Функция byteGenerator служит генератором случайных байтов.
Она использует уникальные значения clientSeed, serverSeed, nonce, а также cursor для генерации случайного и уникального хешированного значения SHA-256 с помощью криптографической функции HMAC_SHA256.
Сгенерированное значение SHA-256 имеет размер 32 байта. Чтобы обеспечить баланс между достаточной случайностью игрового результата и вычислительной нагрузкой, 32 байта делятся на 8 блоков по 4 байта* для генерации каждого игрового результата.
В некоторых играх, где требуется более 8 игровых результатов, мы используем cursor. Изначально значение курсора равно 0, затем оно увеличивается до 1, 2, 3, 4, чтобы получить необходимое количество результатов.
В играх, где не требуется более 8 случайных результатов, значение курсора не увеличивается.
*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 содержит все возможные результаты, необходимые для игры. Если требуется только один результат, список будет содержать только одно значение.
Функция возвращает массив чисел от 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;
}
Во всех наших оригинальных играх используются обе функции 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
Примечание: в действительности, пока игра активна, серверный сид хешируется, поэтому игрок и оператор не могут просматривать отдельный результат этого процесса во время игры.
Поскольку во время игры и после её завершения используются один и тот же алгоритм и функции, пользователи всегда могут проверить результат при одинаковых входных данных.
