Comprovadamente justo

Verifique por si mesmo cada distribuição, cada lançamento e cada atribuição de lugares.

Os nossos jogos de mesa são jogos de perícia: a semente só decide o baralhar, os dados e quem se senta onde. Cada jogada a seguir é sua. Antes de se distribuir o que quer que seja, o servidor compromete-se com uma semente secreta publicando o seu hash SHA-256. A sua própria semente de cliente é misturada, para que o servidor não possa escolher uma distribuição depois de a ver. Assim que a semente é revelada, qualquer pessoa pode recalcular cada carta, cada dado e cada peça.

Os seus jogos e a sua semente de cliente

Como funciona

  1. Compromisso: o servidor escolhe uma semente secreta (32 bytes aleatórios) e mostra-lhe o seu hash SHA-256 antes da primeira carta ou do primeiro dado.
  2. A sua semente: o seu navegador acrescenta uma semente de cliente (aleatória ou a que guardou). Só é enviada depois de o hash ser mostrado.
  3. Extração: cada número aleatório é HMAC-SHA256(semente do servidor, uma mensagem com todas as sementes de cliente e um contador). Os seus primeiros 52 bits formam um número.
  4. Revelação: depois do jogo, a semente do servidor é publicada. O seu hash SHA-256 tem de ser igual ao compromisso, e o recálculo tem de dar exatamente as distribuições que foram jogadas.

Dos números às cartas, dados e lugares

Um número de 52 bits torna-se um inteiro menor que n por amostragem por rejeição: um número que cai no bloco parcial superior do intervalo 2^52 é descartado e usa-se o contador seguinte, pelo que cada resultado é exatamente equiprovável. Um baralhar é Fisher–Yates a partir do topo: para i da última posição até 1, escolhe-se j menor que i + 1 e trocam-se os dois.

Jogo a jogo

  • Gamão: o lançamento de abertura e os dois dados de cada jogada são int(6) + 1, pela ordem de jogo.
  • Damas e Xiangqi: sem dados e sem cartas. A semente determina quem joga primeiro (a ordem dos lugares).
  • Tarneeb, Trix e Bridge: um baralhar Fisher–Yates do baralho de 52 cartas por distribuição; o lugar i recebe as cartas 13i a 13i + 12.
  • Baloot: um baralhar do baralho de 32 cartas por mão; o resto do baralho é baralhado de novo para a segunda distribuição.
  • Mahjong: cada peça tirada é int(peças restantes) da muralha na sua ordem fixa, um baralhar feito peça a peça.
  • Póquer: cada mesa funciona com uma sessão de semente. Antes de cada distribuição, a mesa compromete-se com o baralho: uma raiz de Merkle sobre as suas 52 posições, sendo cada posição uma carta mais o seu próprio sal. Quando sai da mesa, recebe as suas próprias cartas, as cartas comunitárias e as respetivas provas de Merkle, para as poder verificar face a esse compromisso; as cartas dos outros jogadores não são reveladas. O baralho completo e a semente são publicados quando a sessão roda: a cada 24 horas, após 500 mãos, quando a mesa fica vazia ou quando o servidor reinicia.
  • Xadrez: a sua ligação detém uma semente comprometida antes de procurar um jogo, lançar um desafio ou começar um jogo contra um bot. As cores resultam das sementes e das sementes de cliente de ambos os jogadores.

Os jogos disputados antes deste esquema (match_seed_v1) continuam verificáveis: o verificador escolhe o esquema a partir do registo.

Verificar

Cole um registo de jogo (o botão Verificar nos seus jogos preenche-o). Cada distribuição e cada lançamento são repetidos com as regras do próprio jogo e comparados com o que foi jogado.

Verifique por si mesmo

// Node.js 18+ — check a table-game match yourself (scheme match_seed_v2)
const { createHash, createHmac } = require('crypto');
const sha256 = (s) => createHash('sha256').update(s).digest('hex');
const word = (key, msg) => BigInt('0x' + createHmac('sha256', key).update(msg).digest('hex').slice(0, 13));

console.log(sha256(serverSeed) === commitment);            // 1. the reveal matches the commitment

let k = 0;                                                  // 2. the draw stream: one counter per match
const seeds = clientSeeds.map((s, i) => i + '=' + s).join(',');
const next = () => word(serverSeed, 'pf2:' + game + ':' + matchId + ':' + seeds + ':' + (k++));
const SPAN = 1n << 52n;
function int(n) {                                           // 3. whole number below n, no modulo bias
  const N = BigInt(n), limit = SPAN - (SPAN % N);
  for (;;) { const w = next(); if (w < limit) return Number(w % N); }
}
const die = () => int(6) + 1;
function shuffle(a) {                                       // 4. Fisher–Yates from the top
  a = a.slice();
  for (let i = a.length - 1; i > 0; i--) { const j = int(i + 1); [a[i], a[j]] = [a[j], a[i]]; }
  return a;
}
// seat order (pairings): shuffle([0, 1]) first, then the game's opening deal / roll
// Poker (scheme poker_hmac_v1): hand n of seed session S at table T
const handValue = createHmac('sha256', serverSeed).update('pk1:hand:' + T + ':' + S + ':' + n).digest('hex');
console.log(sha256(handValue) === handCommitment);         // stored before the deal
// deck = shuffle([0..51]); key = handValue; message = pk1:T:S:n:{seat}={seed},…:{k}; seats ↑
// rank = id % 13 → 23456789TJQKA; suit = Math.floor(id / 13) → c d h s
// deck commitment (before the deal): salt_i = HMAC(handValue, 'pk1:salt:' + i); leaf_i = sha256('pk1:leaf:' + i + ':' + card_i + ':' + salt_i)
// 64 leaves (52..63 = sha256('pk1:pad')), parent = sha256('pk1:node:' + left + right) → root; on leave you get your leaves + paths
// Chess colour (scheme chess_colour_v1): A = owner of the seek / challenge, B = the other side (a bot adds no seed)
const m = (k) => 'cc1:' + commitA + ':' + (commitB || '-') + ':0=' + clientA + ',1=' + clientB + ':' + k;
// w = word(seedA, m(k)) XOR word(seedB, m(k)); int(2) over w as above → 0 means A plays White

O verificador desta página corre no seu navegador e não faz nenhuma chamada ao servidor. Descarregue-o para o executar offline: verifier.js