Доказуемая честность
Проверьте сами каждую раздачу, каждый бросок и каждую рассадку.
Наши настольные игры — это игры мастерства: сид определяет только тасовку, кубики и то, кто где сидит. Каждый ход после этого — ваш. Прежде чем что-либо раздать, сервер фиксирует секретный сид, публикуя его хеш SHA-256. К нему подмешивается ваш собственный клиентский сид, поэтому сервер не может выбрать раздачу, увидев его. Когда сид раскрыт, любой может пересчитать каждую карту, каждый кубик и каждую плитку.
Как это работает
- Фиксация: сервер выбирает секретный сид (32 случайных байта) и показывает вам его хеш SHA-256 до первой карты или первого броска.
- Ваш сид: ваш браузер добавляет клиентский сид (случайный или сохранённый вами). Он отправляется только после того, как показан хеш.
- Генерация: каждое случайное число — это HMAC-SHA256(сид сервера, сообщение со всеми клиентскими сидами и счётчиком). Его первые 52 бита образуют одно число.
- Раскрытие: после игры сид сервера публикуется. Его хеш SHA-256 должен совпадать с фиксацией, а пересчёт должен дать в точности те раздачи, что были сыграны.
От чисел к картам, кубикам и местам
52-битное число превращается в целое число меньше n выборкой с отклонением: число из верхнего неполного блока диапазона 2^52 пропускается и берётся следующий счётчик, поэтому все результаты в точности равновероятны. Тасовка выполняется по Fisher–Yates сверху: для i от последней позиции вниз до 1 выбирается j меньше i + 1, и два элемента меняются местами.
Игра за игрой
- Нарды: первый бросок и оба кубика каждого хода — это int(6) + 1, в порядке ходов.
- Шашки и Xiangqi: ни кубиков, ни карт. Сид определяет, кто ходит первым (порядок мест).
- Tarneeb, Trix и Бридж: одна тасовка Fisher–Yates колоды из 52 карт на раздачу; место i получает карты с 13i по 13i + 12.
- Baloot: одна тасовка колоды из 32 карт на партию; остаток колоды тасуется заново для второй раздачи.
- Маджонг: каждая взятая плитка — это int(осталось плиток) из стены в её фиксированном порядке, то есть тасовка по одной плитке.
- Покер: каждый стол работает в сид-сессии. Перед каждой раздачей стол фиксирует колоду: корень Merkle по её 52 позициям, где каждая позиция — это карта и её собственная соль. Покидая стол, вы получаете свои карманные карты, общие карты и их доказательства Merkle, чтобы сверить их с этой фиксацией; карты других игроков не раскрываются. Полная колода и сид публикуются при смене сессии: каждые 24 часа, после 500 раздач, когда стол пустеет или когда сервер перезапускается.
- Шахматы: ваше соединение держит зафиксированный сид ещё до того, как вы начнёте поиск, бросите вызов или начнёте партию с ботом. Цвета определяются сидами и клиентскими сидами обоих игроков.
Матчи, сыгранные до этой схемы (match_seed_v1), остаются проверяемыми: верификатор выбирает схему по записи.
Проверить
Вставьте запись матча (её заполняет кнопка «Проверить» в ваших играх). Каждая раздача и каждый бросок воспроизводятся по правилам самой игры и сравниваются с тем, что было сыграно.
Введите сиды матча, чтобы увидеть, что они раздают в начале.
Введите сиды раздачи, чтобы пересчитать всю её колоду, или вставьте доказательства карт, полученные при уходе из-за стола, чтобы проверить свои карты и общие карты.
Введите сиды обеих сторон, чтобы пересчитать, кто играет белыми.
Сторона A (автор поиска или вызова)
Сторона B
Проверьте сами
// 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
Верификатор на этой странице работает в вашем браузере и не обращается к серверу. Скачайте его, чтобы запускать офлайн: verifier.js