Разработка игры Keno на блокчейне
Keno — лотерейная игра: игрок выбирает числа из диапазона 1–80, система случайно выбирает 20 чисел, выигрыш зависит от количества совпадений. Простая механика, но реализация на блокчейне требует решения нескольких нетривиальных задач: verifiable randomness для выбора 20 чисел, gas-эффективная проверка совпадений, математически корректная таблица выплат. Мы разрабатываем такие игры под ключ более 5 лет и успешно запустили 20+ проектов на Ethereum, Polygon и Arbitrum.
В отличие от Crash, Keno — игра с фиксированным исходом до cashout: числа вытягиваются, результат сразу известен. Это упрощает архитектуру, но требует особого внимания к качеству RNG и управлению банкроллом. Наша команда помогает клиентам избежать типичных ошибок — например, неверного расчёта house edge или перерасхода газа при розыгрыше. Свяжитесь с нами для бесплатной консультации по архитектуре контракта.
Как обеспечить случайность в Keno?
Основная техническая задача: из одного VRF random seed получить 20 уникальных чисел в диапазоне 1–80. Наивный подход (rand % 80 повторить 20 раз) создаёт коллизии — одно число может выпасть дважды. Мы используем Fisher-Yates shuffle на смарт-контракте:
function drawNumbers(uint256 seed) public pure returns (uint8[20] memory drawn) {
// Инициализируем массив 1..80
uint8[80] memory pool;
for (uint8 i = 0; i < 80; i++) {
pool[i] = i + 1;
}
// Fisher-Yates: тасуем первые 20 позиций
for (uint8 i = 0; i < 20; i++) {
// Получаем псевдо-random индекс из seed
uint256 j = uint256(keccak256(abi.encodePacked(seed, i))) % (80 - i);
// Swap pool[i] и pool[i + j]
uint8 temp = pool[i];
pool[i] = pool[i + j];
pool[i + j] = temp;
drawn[i] = pool[i];
}
}
Fisher-Yates даёт гарантированно уникальные числа без reject-sampling. Для on-chain выполнения: 20 итераций × keccak256 ≈ 80,000–100,000 gas. На Arbitrum это ~$0.01 — приемлемо для большинства сценариев.
Почему Fisher-Yates предпочтительнее bitmap?
Bitmap-подход (запись занятых чисел в битовую маску) проще в реализации, но может потребовать больше вызовов keccak256 при выборе последних чисел из-за коллизий. Fisher-Yates гарантирует фиксированное число итераций — 20. Для предсказуемого газа мы рекомендуем именно его, особенно при массовых розыгрышах.
Проверка совпадений: gas-эффективная реализация
Игрок выбрал M чисел (1–10), нужно подсчитать, сколько совпадает с 20 drawn numbers. Вложенные циклы O(M×20) допустимы для small M, но мы используем bitmap для экономии газа:
function countMatches(
uint8[] memory playerPicks,
uint8[20] memory drawnNumbers
) public pure returns (uint8 matches) {
// Строим bitmap drawn numbers
uint256 drawnBitmap = 0;
for (uint8 i = 0; i < 20; i++) {
drawnBitmap |= (1 << (drawnNumbers[i] - 1));
}
// Проверяем picks игрока против bitmap
for (uint8 i = 0; i < playerPicks.length; i++) {
if (drawnBitmap & (1 << (playerPicks[i] - 1)) != 0) {
matches++;
}
}
}
Битовые операции быстрее nested loops. Для типичных 1–10 picks: ≈ 3,000–5,000 дополнительного gas.
Таблица выплат и house edge
Keno выплаты — самая важная экономическая часть. Нужно балансировать house edge (обычно 20–35% в Keno) при разных количествах picks. Вот фрагмент таблицы множителей для популярных вариантов:
| Количество picks | Совпадения | Множитель (X) |
|---|---|---|
| 1 | 1 | 3.6 |
| 3 | 2 | 2 |
| 3 | 3 | 46 |
| 5 | 3 | 3 |
| 5 | 4 | 12 |
| 5 | 5 | 500 |
| 10 | 5 | 2 |
| 10 | 6 | 18 |
| 10 | 7 | 170 |
| 10 | 8 | 1000 |
| 10 | 9 | 2500 |
| 10 | 10 | 10000 |
House edge верифицируется математически: для каждого варианта picks рассчитывается Expected Value:
EV(5 picks) = Σ P(k matches) × payout(5, k) для k = 0..5
P(k matches) = C(20,k) × C(60, 5-k) / C(80, 5)
EV должен быть ≈ 0.70–0.80 (70–80% RTP, 20–30% house edge)
Например, для 1 pick: P(1 match) = 20/80 = 0.25, EV = 0.25 × 3.6 = 0.9, то есть RTP 90%, house edge 10%.
Полный игровой цикл on-chain
contract KenoGame is VRFConsumerBaseV2Plus {
struct KenoRound {
address player;
uint256 betAmount;
uint8[] playerPicks;
uint8[20] drawnNumbers;
uint8 matchCount;
uint256 payout;
RoundStatus status;
uint256 vrfRequestId;
}
mapping(uint256 => KenoRound) public rounds;
mapping(uint256 => uint256) public vrfToRound;
uint256 public nextRoundId;
function playKeno(uint8[] calldata picks) external payable {
require(picks.length >= 1 && picks.length <= 10, "Invalid picks count");
require(msg.value >= MIN_BET && msg.value <= maxBet(), "Invalid bet");
// Валидируем picks (1-80, уникальные)
_validatePicks(picks);
uint256 roundId = nextRoundId++;
rounds[roundId] = KenoRound({
player: msg.sender,
betAmount: msg.value,
playerPicks: picks,
drawnNumbers: [uint8(0),...], // заполнится в callback
matchCount: 0,
payout: 0,
status: RoundStatus.PENDING,
vrfRequestId: 0
});
// Запрашиваем VRF
uint256 requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: s_keyHash,
subId: s_subscriptionId,
requestConfirmations: 1,
callbackGasLimit: 300_000, // с запасом для drawNumbers
numWords: 1,
extraArgs: ""
})
);
rounds[roundId].vrfRequestId = requestId;
vrfToRound[requestId] = roundId;
emit KenoRoundStarted(roundId, msg.sender, picks, msg.value);
}
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
uint256 roundId = vrfToRound[requestId];
KenoRound storage round = rounds[roundId];
// Вытягиваем 20 чисел
round.drawnNumbers = drawNumbers(randomWords[0]);
// Считаем совпадения
round.matchCount = countMatches(round.playerPicks, round.drawnNumbers);
// Рассчитываем выплату
uint256 multiplier = payoutTable[round.playerPicks.length][round.matchCount];
round.payout = round.betAmount * multiplier / 100;
round.status = RoundStatus.COMPLETED;
// Выплачиваем победителю
if (round.payout > 0) {
require(address(this).balance >= round.payout, "Insufficient bankroll");
payable(round.player).transfer(round.payout);
}
emit KenoResult(
roundId,
round.player,
round.drawnNumbers,
round.matchCount,
round.payout
);
}
}
Что такое shared draw и как он экономит газ?
Для казино-стиля, где несколько игроков участвуют в одном draw раунда, мы реализуем shared draw. Один VRF-запрос на весь раунд делится между всеми участниками, что снижает gas на игрока с ~100,000 до ~15,000.
contract MultiPlayerKeno is VRFConsumerBaseV2Plus {
struct DrawRound {
uint8[20] drawnNumbers;
uint256 drawTime;
bool resolved;
address[] participants;
}
// Раунды каждые N минут
uint256 public roundInterval = 3 minutes;
// Ставки привязываются к будущему draw раунду
struct PlayerBet {
uint8[] picks;
uint256 amount;
uint256 drawRoundId;
}
function getBetsOnNextDraw(address player) external view returns (PlayerBet[] memory) {
uint256 nextDraw = (block.timestamp / roundInterval + 1) * roundInterval;
return pendingBets[nextDraw][player];
}
// Один VRF запрос на весь draw — делится между всеми участниками
// Gas cost per player: ~15,000 gas (vs ~100,000 для single player)
function triggerDraw(uint256 drawRoundId) external {
require(block.timestamp >= drawRoundId, "Too early");
require(!drawRounds[drawRoundId].resolved, "Already drawn");
uint256 requestId = s_vrfCoordinator.requestRandomWords(...);
vrfToDrawRound[requestId] = drawRoundId;
}
}
Как проверить результат игры?
- Получить VRF request и response из on-chain событий.
- Применить drawNumbers(vrfResult) — получить те же 20 чисел.
- Убедиться, что house не манипулировал.
Chainlink публично публикует cryptographic proof каждого VRF ответа — верификация возможна независимо от казино. Этот механизм основан на технологии Chainlink VRF.
Что входит в работу
- Разработка смарт-контракта игры (single-player или multi-player)
- Интеграция Chainlink VRF V2 Plus
- Настройка таблицы выплат с математической верификацией house edge
- Создание frontend-интерфейса (React + wagmi + RainbowKit)
- Deploy на тестнет и mainnet (Polygon, Arbitrum или другая сеть)
- Проведение аудита смарт-контракта (внутренний или сторонний)
- Документация по интеграции и поддержке
Ориентировочные сроки
| Фаза | Срок |
|---|---|
| Контракты (single player, VRF, payout table) | 3–4 нед |
| Multi-player shared draw | 2 нед |
| Frontend + draw animation | 2–3 нед |
| Bankroll + admin panel | 1–2 нед |
| Audit + тестнет | 3–4 нед |
Итого MVP (single player Keno): 5–7 недель. Полная платформа с multi-player draw: 9–12 недель.
Наша команда имеет 5+ лет опыта в блокчейн-разработке и успешно реализовала 20+ игровых смарт-контрактов. Если вы планируете запустить собственную Keno-платформу с гарантированной прозрачностью и низким gas, получите консультацию — оценим ваш проект за один день.







