Розробка гри Blackjack на блокчейні

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка гри Blackjack на блокчейні
Середній
~5 днів
Часті запитання

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

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1288
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    902
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1122
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    589
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    859

Розробка блокчейн-гри Blackjack

Блокчейн Blackjack — одна з найтехнічніших задач у on-chain gaming. Проблема полягає в тому, що карти потрібно роздавати випадково і чесно, але гравець не має видити карти до їх «розкриття». В офлайн-казино це тривіально — сервер знає всі карти, показує їх поетапно. On-chain вся стан контракту публічна, а блокчейн-рандом маніпульований.

Розв'язання — commit-reveal схема в поєднанні з Chainlink VRF або mental poker протокол. Розберемо обидва підходи.

Чесний рандом: Chainlink VRF

Chainlink VRF забезпечує верифіковану випадковість: число генерується офлайн з криптографічним доказом, який верифікується on-chain. Ніхто — ні гравець, ні оператор — не можуть передбачити результат.

Потік для Blackjack:

  1. Гравець робить ставку, контракт запитує VRF
  2. VRF виконання (через callback) — контракт отримує випадкове число, генерує коду або перші карти
  3. Гравець вирішує: hit або stand
  4. Якщо hit — новий VRF запит на наступну карту

Проблема з «якщо hit»: кожен VRF запит — це затримка (1-3 блоки) та додатковий LINK. Для реального часу гри це неприйнятно.

Оптимізація: запросити всю колоду одразу

contract Blackjack is VRFConsumerBaseV2Plus {
    struct Game {
        address player;
        uint256 bet;
        uint8[] deck;     // всі 52 карти в зашифрованому порядку
        uint8 playerIdx;  // поточний індекс в колоді
        uint8 dealerIdx;
        bool active;
    }
    
    mapping(uint256 => Game) public games;         // requestId -> game
    mapping(address => uint256) public playerGame; // player -> gameId
    
    function startGame() external payable {
        require(msg.value >= MIN_BET, "Below minimum bet");
        
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: KEY_HASH,
                subId: subscriptionId,
                requestConfirmations: 3,
                callbackGasLimit: 300000,
                numWords: 1, // одне велике число для перетасування
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
        
        games[requestId] = Game({
            player: msg.sender,
            bet: msg.value,
            deck: new uint8[](0),
            playerIdx: 0,
            dealerIdx: 4, // дилер бере карти з позиції 4
            active: false // стає true після виконання
        });
        playerGame[msg.sender] = requestId;
    }
    
    function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
        Game storage game = games[requestId];
        
        // Fisher-Yates перетасування детерміноване з одного seed
        uint8[52] memory deck;
        for (uint8 i = 0; i < 52; i++) deck[i] = i;
        
        uint256 seed = randomWords[0];
        for (uint8 i = 51; i > 0; i--) {
            seed = uint256(keccak256(abi.encodePacked(seed)));
            uint8 j = uint8(seed % (i + 1));
            (deck[i], deck[j]) = (deck[j], deck[i]);
        }
        
        // Перші 4 карти роздані одразу: гравець, дилер, гравець, дилер
        game.deck = new uint8[](52);
        for (uint8 i = 0; i < 52; i++) game.deck[i] = deck[i];
        game.active = true;
        
        emit GameStarted(requestId, game.player, deck[0], deck[2]); // видимі карти гравця
        // deck[1] та deck[3] - карти дилера, deck[1] приховано до кінця
    }
}

Після виконання колода перетасована і зафіксована. Наступні ходи (hit) беруть карти з уже згенерованої колоди — без нових VRF запитів. Швидко і дешево.

Проблема: колода видима on-chain

Але вся колода зберігається в game.deck — публічно! Технічно гравець може прочитати всі майбутні карти зі сховища.

Розв'язання: зберігати тільки seed, карти обчислювати детерміновано тільки коли вони «розкриваються»:

// Не зберігаємо колоду, тільки seed
mapping(uint256 => uint256) private gameSeeds;

function getCard(uint256 gameId, uint8 position) private view returns (uint8) {
    // Детерміновано обчислюємо карту з seed та позиції
    // Карта не у сховищі — не можна прочитати заздалегідь
    return uint8(uint256(keccak256(abi.encodePacked(gameSeeds[gameId], position))) % 52);
}

Це не повністю вирішує проблему: технічно можна симулювати getCard для всіх позицій у тому ж блоці. Повне рішення — Mental Poker протокол з шифруванням кожної карти, але це значно складніше.

Логіка Blackjack On-Chain

Значення карт: туз = 1 або 11, картинки = 10, решта за номіналом:

function cardValue(uint8 card) internal pure returns (uint8) {
    uint8 rank = card % 13; // 0-12: туз, 2-10, валет, дама, король
    if (rank == 0) return 11; // туз (м'яке значення)
    if (rank >= 10) return 10; // картинки
    return rank + 1;
}

function handScore(uint8[] memory cards) internal pure returns (uint8) {
    uint8 score = 0;
    uint8 aces = 0;
    
    for (uint i = 0; i < cards.length; i++) {
        uint8 val = cardValue(cards[i]);
        if (val == 11) aces++;
        score += val;
    }
    
    // М'який туз стає жорстким (1) якщо bust
    while (score > 21 && aces > 0) {
        score -= 10;
        aces--;
    }
    
    return score;
}

Логіка дилера on-chain: дилер бере карти доки score < 17, зупиняється на 17+ (включаючи soft 17 залежно від правил).

Управління ліквідністю: банкролл

Контракт повинен мати ETH для виплат. House edge в Blackjack ~0.5% при оптимальній стратегії — це реальна маржа. Але дисперсія висока, потрібен достатній банкролл.

Kelly Criterion для максимальної ставки: при edge e та банкролі B, максимальна ставка ≈ B * e / variance. Для Blackjack з edge 0.5% та дисперсією ~1.3 — максимальна ставка ≈ 0.38% від банкролу. На практиці: обмежити максимальну ставку на рівні 1-2% банкролу.

uint256 public constant MAX_BET_PERCENT = 200; // 2% = 200/10000

function maxBet() public view returns (uint256) {
    return address(this).balance * MAX_BET_PERCENT / 10000;
}

modifier validBet() {
    require(msg.value >= MIN_BET && msg.value <= maxBet(), "Invalid bet");
    _;
}

Фронтенд та UX

Блокчейн Blackjack вимагає обережного UX для асинхронності. VRF виконання не миттєве. Гравець натиснув "Deal" — очікує 1-3 блоки до появи карт.

Потік в UI:

  1. Ставка → startGame() → статус "Dealing..." (очікуємо подію GameStarted)
  2. Карти з'являються → гравець бачить свої 2 карти, одну карту дилера
  3. Hit/Stand → миттєві, з перетасованої колоди
  4. Stand → дилер розкриває карти → результат → виплата

Для polling подій: wagmi з useWatchContractEvent або WebSocket з'єднання з вузлом.

Стек

Компонент Технологія
Smart contract Solidity 0.8.x + VRF v2.5
Тестування Foundry + VRF mock
Фронтенд React + wagmi + viem
Мережа Polygon / Arbitrum (низька газа)
Аудит Обов'язковий (gambling + управління коштами)

Аудит критично важливий — контракт містить ETH і виплачує виграші. Помилки в логіці виплат або генерації seed — пряма втрата коштів.

Терміни

Робочий on-chain Blackjack (смарт-контракт, тести, базовий фронтенд): 4-5 тижнів. З повним UI, анімаціями, статистикою та мобільною адаптацією — 8-10 тижнів.