Przejdź do głównej zawartości

Provably Fair – implementacja

Jak Provably Fair jest zaimplementowane w kodzie?

Zakładając, że gra została zakończona i mamy niezhashowany seed serwera, seed klienta oraz nonce, poniżej wyjaśniamy, jak to działa.

Do wygenerowania wyników gry wymagane są trzy główne kroki.

  1. byteGenerator (generowanie losowych bajtów)

  2. generateFloats (konwersja bajtów na wartości zmiennoprzecinkowe (cyfry))

  3. Wartości zmiennoprzecinkowe na zdarzenia w grze (konwersja wartości zmiennoprzecinkowych na rzeczywiste zdarzenia w grach oryginalnych)

Funkcja ByteGenerator jako generator losowych bajtów

Funkcja byteGenerator służy jako generator losowych bajtów.

Wykorzystuje unikalne wartości clientSeed, serverSeed, nonce, oraz cursor do wygenerowania losowej i unikalnej wartości hashowanej SHA-256 przy użyciu funkcji kryptograficznej HMAC_SHA256.

Wygenerowana wartość SHA-256 ma rozmiar 32 bajtów. Aby zachować równowagę między wystarczająco losowym wynikiem gry a intensywnością obliczeń, 32 bajtów jest dzielone na 8 sekcje po 4 bajtów* każda, aby wygenerować każdy wynik gry.

W niektórych grach, w których wymagany jest więcej niż 8 wynik gry, wykorzystamy cursor. Kursor początkowo zaczyna się od 0 i zwiększa się do 1,2,3,4, aby spełnić wymagania dotyczące wyników,

W grach, w których nie potrzebujemy więcej niż 8 losowych wyników, wartość kursora nie jest zwiększana.

*4 bajtów danych zapewnia 2^32 (4,294,967,296) możliwych wyników, co stanowi wystarczająco dużą pulę losowości.

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 — funkcja do konwersji bajtów na wartości zmiennoprzecinkowe

Ta funkcja konwertuje szesnastkową wartość SHA-256 z bajtów na wartości zmiennoprzecinkowe, które są wykorzystywane w dalszych obliczeniach dla zdarzeń w grze.

Poniżej pokazano, jak szesnastkowa wartość SHA-256 jest konwertowana z bajtów na wartość zmiennoprzecinkową. Końcowy wynik, numArr, zawiera wszystkie możliwe wyniki wymagane przez grę. Jeśli wymagany jest tylko 1 wynik, lista będzie zawierać tylko jedną wartość.

Zwraca tablicę liczb od 0 do 1, gdzie count oznacza liczbę elementów w zwróconej tablicy.

Kod:

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

Wszystkie nasze gry oryginalne wykorzystują zarówno funkcje ByteGenerator, jak i GenerateFloats do generowania losowych wartości zmiennoprzecinkowych od 0 do 1. Od tego momentu każda gra wykorzystuje jednak unikalną procedurę do określenia zdarzenia w grze na podstawie wygenerowanej wartości zmiennoprzecinkowej.

Unikalna procedura zostanie szczegółowo wyjaśniona w sekcji Zdarzenia w grze.

Przykład z ilustracją

Poniżej pokażemy, jak dane wejściowe są wykorzystywane do wygenerowania zdarzenia w grze 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

Uwaga: W rzeczywistości, gdy gra jest aktywna, seed serwera jest hashowany, dlatego gracz ani operator nie mogą wyświetlić pojedynczego wyniku tego procesu w trakcie gry.

Ponieważ ten sam algorytm i te same funkcje są używane w trakcie gry oraz po jej zakończeniu, użytkownicy mogą zawsze zweryfikować wynik przy tych samych danych wejściowych.

Czy to odpowiedziało na twoje pytanie?