Provably Fair

Редакция от 5 ноября 2026 г.

Во всех играх собственной разработки KOZYR — кейсах, апгрейде, Dice, Limbo, Plinko, Mines, Краше, джекпоте, батлах и коинфлипе — результат заранее зафиксирован и может быть проверен тобой. Мы не можем изменить исход после твоей ставки, а ты не можешь предсказать его до ставки. Слоты сторонних провайдеров работают на их собственных сертифицированных генераторах.

01Как это устроено

Для каждого игрока есть пара сидов:

Результат любой игры получается из HMAC-SHA256(серверный сид, "клиентский сид:nonce:cursor"). Из каждого 32-байтового блока берутся восемь чисел по 4 байта (big-endian). Когда числа заканчиваются, cursor увеличивается на единицу и считается следующий блок.

Чтобы перевести число в диапазон без перекоса, используется отбраковка: числа не меньше floor(2^32 / n) × n отбрасываются, из остальных берётся остаток от деления на n.

02Как проверить свою игру

  1. Запомни хеш серверного сида и nonce, на котором сыграна ставка, — они есть в профиле и на экране результата.
  2. Смени пару сидов в профиле. Сервер покажет старый серверный сид.
  3. Убедись, что SHA-256 старого сида совпадает с хешем, который ты видел до ставки.
  4. Пересчитай результат по формулам ниже — он должен совпасть с тем, что показала игра.

03Игры

Dice

Число от 0 до 9999 (то есть 00.00–99.99) = nextInt(10000). Ставка «меньше» выигрывает, если число меньше шанса, «больше» — если число не меньше 10000 − шанс. Выплата = ставка × 99 / шанс в процентах, преимущество Сервиса — 1%.

Limbo

Из потока берутся два первых числа a и b, и r = a × 2^20 + floor(b / 2^12) — это 52 случайных бита. Результат = max(1.00, floor(99 × 2^52 / (2^52 − r)) / 100). Ставка выигрывает, если результат не меньше выбранной цели; выплата = ставка × цель. Вероятность выигрыша равна 0.99 / цель, преимущество Сервиса — 1%.

Plinko

Для каждого ряда берётся nextInt(2): 0 — шарик уходит влево, 1 — вправо. Номер лунки равен числу отскоков вправо. Выплата = ставка × множитель лунки из таблицы для выбранного числа рядов и уровня риска. Таблицы показаны в игре, отдача каждой — от 98,4 до 98,9%.

Кейсы

Сумма весов всех предметов = W, r = nextInt(W). Предметы перебираются по возрастанию их id; из r вычитается вес каждого, и выпадает предмет, на котором r стал меньше нуля. Шанс предмета = его вес / W, он показан в кейсе. Порядок по id не зависит от цен, поэтому любое прошлое открытие пересчитывается одинаково.

Апгрейд

r = nextInt(1 000 000). Апгрейд удался, если r меньше шанса в миллионных долях. Шанс = стоимость ставки / стоимость цели × (1 − 8%), но не больше 75%.

Mines

Клетки поля пронумерованы от 0 до 24. Колода [0…24] перемешивается алгоритмом Фишера — Йетса: для i от 24 до 1 берётся j = nextInt(i + 1) и клетки i и j меняются местами. Первые m клеток колоды — мины. Выплата после k открытых клеток = ставка × 0.99 × C(25, k) / C(25 − m, k).

Краш

У краша общий раунд для всех игроков и свой серверный сид у каждого раунда. Хеш сида публикуется до начала приёма ставок, сам сид — после падения. Берутся первые 52 бита HMAC-SHA256(сид раунда, "crash") как число r, множитель = max(1.00, floor(99 × 2^52 / (2^52 − r)) / 100). Вероятность дожить до множителя x равна 0.99 / x, поэтому любая цель автовывода в среднем возвращает 99%.

Батлы

У каждого батла свой серверный сид, хеш которого виден с момента создания. Предмет для раунда R и места S выбирается как в кейсах, но поток чисел строится с клиентским сидом battle:ID и nonce R × 4 + S. Ничья решается числом nextInt(число лидеров) из потока с сидом battle:ID:tie и nonce 0.

Джекпот

У каждого раунда свой серверный сид, хеш которого виден с начала раунда. Каждый поставленный цент — один билет; билеты нумеруются с нуля в порядке ставок. Выигрышный билет = nextInt(банк в центах) из потока с клиентским сидом jackpot:ID и nonce 0. Таймер запускается, когда в раунд входит второй игрок, поэтому итоговый банк заранее неизвестен никому. Победитель получает банк за вычетом комиссии 4%.

Коинфлип

У каждой игры свой серверный сид, хеш которого виден с момента создания. Берётся первое число потока с клиентским сидом coinflip:ID и nonce, равным внутреннему ID присоединившегося игрока (0 для бота). Чётное число — «орёл», нечётное — «решка». Кто присоединится, заранее неизвестно, поэтому создатель не может подобрать исход.

04Код для проверки

Этот код на Node.js повторяет расчёты сервера:

const { createHmac, createHash } = require('node:crypto');

function* stream(serverSeed, clientSeed, nonce) {
  for (let cursor = 0; ; cursor++) {
    const block = createHmac('sha256', serverSeed).update(`${clientSeed}:${nonce}:${cursor}`).digest();
    for (let i = 0; i < 32; i += 4) yield block.readUInt32BE(i);
  }
}

function nextInt(s, n) {
  const limit = Math.floor(2 ** 32 / n) * n;
  for (;;) {
    const v = s.next().value;
    if (v < limit) return v % n;
  }
}

const sha256 = (s) => createHash('sha256').update(s).digest('hex');
const dice = (ss, cs, nonce) => nextInt(stream(ss, cs, nonce), 10000);
const upgrade = (ss, cs, nonce) => nextInt(stream(ss, cs, nonce), 1000000);

// weights — веса предметов кейса по возрастанию id
function caseItem(ss, cs, nonce, weights) {
  let r = nextInt(stream(ss, cs, nonce), weights.reduce((a, b) => a + b, 0));
  return weights.findIndex((w) => (r -= w) < 0);
}

function mines(ss, cs, nonce, m) {
  const s = stream(ss, cs, nonce);
  const deck = [...Array(25).keys()];
  for (let i = 24; i > 0; i--) {
    const j = nextInt(s, i + 1);
    [deck[i], deck[j]] = [deck[j], deck[i]];
  }
  return deck.slice(0, m).sort((a, b) => a - b);
}

function crash(roundSeed) {
  const h = createHmac('sha256', roundSeed).update('crash').digest();
  const r = BigInt('0x' + h.subarray(0, 7).toString('hex')) >> 4n;
  const e = 1n << 52n;
  return Math.max(100, Number((99n * e) / (e - r))) / 100;
}

function limbo(ss, cs, nonce) {
  const s = stream(ss, cs, nonce);
  const r = (BigInt(s.next().value) << 20n) | (BigInt(s.next().value) >> 12n);
  const e = 1n << 52n;
  return Math.max(100, Number((99n * e) / (e - r))) / 100;
}

function plinko(ss, cs, nonce, rows) {
  const s = stream(ss, cs, nonce);
  return Array.from({ length: rows }, () => nextInt(s, 2));
}

const jackpot = (ss, roundId, potCents) => nextInt(stream(ss, `jackpot:${roundId}`, 0), potCents);

05Вопросы

Если результат не сходится, напиши в поддержку @kozyrs_bot: укажи игру, время, хеш сида и nonce. Мы разберём каждый случай.