Разработка Keno на блокчейне: VRF, смарт-контракты, gas-оптимизация

Разработка игры Keno на блокчейне Keno — лотерейная игра: игрок выбирает числа из диапазона 1–80, система случайно выбирает 20 чисел, выигрыш зависит от количества совпадений. Простая механика, но реализация на блокчейне требует решения нескольких нетривиальных задач: verifiable randomness для вы

Направления блокчейн-разработки

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1441
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    998
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1267
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    713
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    1003

Разработка игры 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; } } 

Как проверить результат игры?

  1. Получить VRF request и response из on-chain событий.
  2. Применить drawNumbers(vrfResult) — получить те же 20 чисел.
  3. Убедиться, что 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, получите консультацию — оценим ваш проект за один день.