Provably Fair
Редакция от 5 ноября 2026 г.
Во всех играх собственной разработки KOZYR — кейсах, апгрейде, Dice, Limbo, Plinko, Mines, Краше, джекпоте, батлах и коинфлипе — результат заранее зафиксирован и может быть проверен тобой. Мы не можем изменить исход после твоей ставки, а ты не можешь предсказать его до ставки. Слоты сторонних провайдеров работают на их собственных сертифицированных генераторах.
01Как это устроено
Для каждого игрока есть пара сидов:
- серверный сид — случайные 32 байта, которые генерирует сервер. До ставки ты видишь только его хеш
SHA-256; - клиентский сид — строка, которую ты можешь задать сам в профиле;
- nonce — номер ставки на этой паре сидов, начинается с нуля и растёт на единицу после каждой игры.
Результат любой игры получается из HMAC-SHA256(серверный сид, "клиентский сид:nonce:cursor"). Из каждого 32-байтового блока берутся восемь чисел по 4 байта (big-endian). Когда числа заканчиваются, cursor увеличивается на единицу и считается следующий блок.
Чтобы перевести число в диапазон без перекоса, используется отбраковка: числа не меньше floor(2^32 / n) × n отбрасываются, из остальных берётся остаток от деления на n.
02Как проверить свою игру
- Запомни хеш серверного сида и nonce, на котором сыграна ставка, — они есть в профиле и на экране результата.
- Смени пару сидов в профиле. Сервер покажет старый серверный сид.
- Убедись, что
SHA-256старого сида совпадает с хешем, который ты видел до ставки. - Пересчитай результат по формулам ниже — он должен совпасть с тем, что показала игра.
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. Мы разберём каждый случай.