Gry niestandardowe - Provably Fair
Jak system jest używany w grach niestandardowych
Każdy zakład w YEET Original zaczyna się od tzw. „round seed”. Round seed to HMAC-SHA256 twojego client seed oraz nonce zakładu, zakodowany przy użyciu server seed wybranego przez YEET. Gra następnie losuje każdą wartość potrzebną do danego zakładu na podstawie tego round seed. Server seed jest pokazywany w formie hasha przed rozpoczęciem gry i ujawniany po zresetowaniu pary seedów, dzięki czemu możesz ponownie obliczyć round seed każdego przeszłego zakładu i sprawdzić, czy jego wynik nie został zmieniony po obstawieniu.
Inne weryfikatory mogą nie pasować
Strony weryfikacyjne stworzone dla innych kasyn mogą generować liczby losowe w inny sposób, więc sprawdzanie zakładu YEET w takich miejscach może dać inny wynik. Weryfikuj zakłady YEET w zakładce Weryfikacja (Verify) w oknie uczciwości (Fairness) danej gry lub za pomocą kodu na tej stronie.
Generowanie liczb losowych
Każdy zakład generowany jest w dwóch krokach. Po pierwsze, twoja para seedów i nonce zakładu tworzą jeden round seed. Po drugie, round seed generuje ciąg losowych bajtów, z którego gra odczytuje bezstronne liczby całkowite. Te same dane wejściowe zawsze dają te same liczby, co sprawia, że wynik jest weryfikowalny.
1. Dane wejściowe
- Losowy 64-znakowy ciąg szesnastkowy wygenerowany przez nasz serwer
- Ciąg znaków, który można modyfikować w dowolnym momencie
- Licznik, który zwiększa się z każdą rozegraną grą
- Wskaźnik (Cursor) — ostatnia część komunikatu round seed. Zawsze wynosi 0.
2. Round seed
Round seed to HMAC-SHA256 zakodowany przy użyciu server seed (jako 32 bajty) dla tekstu clientSeed:nonce:0. Wynikiem jest 32 bajtów zapisanych jako 64 znaki szesnastkowe. Jeden zakład ma dokładnie jeden round seed.
async function deriveGameSeed(serverSeed, clientSeed, nonce) {
const seedBytes = hexToBytes(serverSeed);
if (seedBytes.length !== 32) {
throw new Error(`Server seed must be 32 bytes (64 hex characters), got ${seedBytes.length}.`);
}
const key = await crypto.subtle.importKey(
'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const message = new TextEncoder().encode(`${clientSeed}:${nonce}:0`);
return bytesToHex(new Uint8Array(await crypto.subtle.sign('HMAC', key, message)));
}3. Entropia z round seed
Round seed jest kluczem drugiego HMAC-SHA256. Każde wywołanie podpisuje 8-bajtowy licznik (little-endian, zaczynający się od 0) i daje blok 32 bajtów; licznik zwiększa się o jeden przy każdym bloku. Odczyty pobierają bajty z bieżącego bloku w kolejności. Gdy odczyt potrzebuje więcej bajtów niż pozostało w bloku, reszta bloku jest pomijana i generowany jest kolejny blok.
class GameEntropy {
constructor(seedBytes) {
if (seedBytes.length !== 32) throw new Error('Seed must be exactly 32 bytes (64 hex characters).');
this.buffer = new Uint8Array(32);
this.offset = 32;
this.cursor = 0;
this.key = crypto.subtle.importKey(
'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
}
async next(count) {
if (count < 1 || count > 32) throw new RangeError('count must be between 1 and 32');
if (this.offset + count > 32) {
const message = new ArrayBuffer(8);
const view = new DataView(message);
view.setUint32(0, this.cursor >>> 0, true);
view.setUint32(4, Math.floor(this.cursor / 0x100000000) >>> 0, true);
const hash = await crypto.subtle.sign('HMAC', await this.key, message);
this.buffer.set(new Uint8Array(hash));
this.cursor++;
this.offset = 0;
}
const slice = this.buffer.slice(this.offset, this.offset + count);
this.offset += count;
return slice;
}
}4. Bezstronne liczby
Wyniki są losowane jako liczby całkowite. readUint32 odczytuje 4 bajty jako bezznakową liczbę 32-bitową (little-endian). unbiasedUint(entropy, n) zwraca liczbę od 0 do n − 1, z których każda ma taką samą szansę wystąpienia. Gdy n jest potęgą dwójki, bierze resztę z dzielenia bezpośrednio. W przeciwnym razie akceptuje tylko wartości poniżej największej wielokrotności n, która mieści się w 32 bitach, a jeśli wylosowana wartość jest równa lub większa, odczytuje ją ponownie, aby żaden wynik nie był faworyzowany.
async function readUint32(entropy) {
const bytes = await entropy.next(4);
return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true);
}
async function unbiasedUint(entropy, range) {
if (range <= 0) throw new Error('Range must be positive');
if (range === 1) return 0;
if ((range & (range - 1)) === 0) {
return (await readUint32(entropy)) % range;
}
const maxValue = 0xffffffff;
const threshold = maxValue - (maxValue % range) - 1;
for (;;) {
const raw = await readUint32(entropy);
if (raw <= threshold) return raw % range;
}
}5. Pomocne funkcje
function hexToBytes(hex) {
if (typeof hex !== 'string') throw new TypeError('Hex value must be a string.');
const clean = hex.trim().replace(/^0x/i, '');
if (clean.length % 2 !== 0) throw new Error('Hex value must have an even length.');
if (!/^[0-9a-fA-F]*$/.test(clean)) throw new Error('Hex value contains non-hex characters.');
const bytes = new Uint8Array(clean.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
function toArrayBuffer(bytes) {
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}6. Weryfikacja zakładu
- Otwórz zakład z historii i skopiuj swój client seed, nonce oraz zahashowany server seed.
- Zmień parę seedów, aby odsłonić server seed i sprawdź, czy jego hash SHA-256 zgadza się z hashem, który widziałeś przed postawieniem zakładu.
- Uruchom
deriveGameSeedz użyciem server seed, client seed i nonce, aby uzyskać round seed. - Utwórz
GameEntropyz round seed, zastosuj konwersję gry (patrz Konwersje) i porównaj wynik ze swoim zakładem.
