Every roll from bot, online and friend games, against what perfectly fair dice would do — and how to check a single match yourself.
Before the first die of a match is thrown, the server draws a secret 32-byte seed and publishes its fingerprint — SHA-256(seed) — to both players. Every roll of that match is then derived from the seed, in order. When the match ends the seed itself is published.
So the dice were fixed before anyone saw the board, and afterwards anyone can recompute them. If we had changed a single roll, the seed would no longer match the fingerprint we showed you at the start.
Die number i of a match is HMAC-SHA256(seed, "<gameId>:<i>"), read byte by byte until one lands below 252. That cut-off matters: 252 is 6 × 42, so each face gets exactly 42 of the accepted byte values. Folding all 256 values with a plain % 6 would hand four faces 43 and two only 42 — which is precisely the bias this Lab caught in September.
/**
* The die at position `index` of a match: rejection sampling over HMAC-SHA256(seed,
* "<gameId>:<index>"). Pure — the same (seed, gameId, index) always gives the same face.
*/
export function derivedDie(seedHex: string, gameId: string, index: number): number {
const key = hexToBytes(seedHex);
for (let extra = 0; extra < 64; extra++) {
const label = extra === 0 ? `${gameId}:${index}` : `${gameId}:${index}:${extra}`;
const digest = hmacSha256(key, utf8(label));
for (const byte of digest) {
const face = faceFromByte(byte);
if (face !== null) return face;
}
}export function faceFromByte(byte: number): number | null {
return byte < REJECT_AT ? (byte % 6) + 1 : null;
}The same function the server rolls with and your browser re-rolls with above.