Blockchain blackjack game development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Blockchain blackjack game development
Medium
~5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Blockchain Blackjack Game Development

Blockchain Blackjack is one of the most technically interesting challenges in on-chain gaming. The problem is as follows: cards must be dealt randomly and fairly, yet the player should not see the cards until they are "revealed". In off-chain casinos this is trivial — the server knows all cards and shows them step by step. On-chain, all contract state is public, and blockchain randomness is manipulable.

The solution is a commit-reveal scheme combined with Chainlink VRF, or the mental poker protocol. Let's examine both approaches.

Fair Randomness: Chainlink VRF

Chainlink VRF provides verifiable randomness: a number is generated off-chain with cryptographic proof that is verified on-chain. Neither the player nor the operator can predict the result.

Flow for Blackjack:

  1. Player places a bet, contract requests VRF
  2. VRF fulfillment (via callback) — contract receives random number, generates deck or initial cards
  3. Player decides: hit or stand
  4. If hit — new VRF request for the next card

Problem with "if hit": each VRF request is a delay (1-3 blocks) and additional LINK. For real-time gameplay this is uncomfortable.

Optimization: request entire deck at once

contract Blackjack is VRFConsumerBaseV2Plus {
    struct Game {
        address player;
        uint256 bet;
        uint8[] deck;     // all 52 cards in encrypted order
        uint8 playerIdx;  // current index in deck
        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, // one large number for shuffle
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
        
        games[requestId] = Game({
            player: msg.sender,
            bet: msg.value,
            deck: new uint8[](0),
            playerIdx: 0,
            dealerIdx: 4, // dealer takes cards from position 4
            active: false // becomes true after fulfillment
        });
        playerGame[msg.sender] = requestId;
    }
    
    function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
        Game storage game = games[requestId];
        
        // Fisher-Yates shuffle deterministic from single 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]);
        }
        
        // First 4 cards dealt immediately: player, dealer, player, dealer
        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]); // visible player cards
        // deck[1] and deck[3] - dealer cards, deck[1] hidden until end
    }
}

After fulfillment the deck is shuffled and fixed. Subsequent moves (hit) take cards from the already generated deck — without new VRF requests. Fast and cheap.

Problem: deck visible on-chain

But the entire deck is stored in game.deck — publicly! Technically a player can read all future cards from storage.

Solution: store only the seed, derive cards deterministically only when they are "revealed":

// Don't store deck, only seed
mapping(uint256 => uint256) private gameSeeds;

function getCard(uint256 gameId, uint8 position) private view returns (uint8) {
    // Deterministically compute card from seed and position
    // Card not in storage — cannot read in advance
    return uint8(uint256(keccak256(abi.encodePacked(gameSeeds[gameId], position))) % 52);
}

This doesn't fully solve the problem: technically one can simulate getCard for all positions in the same block. Full solution — Mental Poker protocol with encryption of each card, but that's considerably more complex.

Blackjack Logic On-Chain

Card values: ace = 1 or 11, face cards = 10, others by rank:

function cardValue(uint8 card) internal pure returns (uint8) {
    uint8 rank = card % 13; // 0-12: ace, 2-10, jack, queen, king
    if (rank == 0) return 11; // ace (soft value)
    if (rank >= 10) return 10; // face cards
    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;
    }
    
    // Soft ace becomes hard (1) if bust
    while (score > 21 && aces > 0) {
        score -= 10;
        aces--;
    }
    
    return score;
}

Dealer logic on-chain: dealer takes cards while score < 17, stops at 17+ (including soft 17 depending on rules).

Liquidity Management: Bankroll

The contract must have ETH for payouts. House edge in Blackjack ~0.5% with optimal strategy — this is real margin. But variance is high, sufficient bankroll is needed.

Kelly Criterion for maximum bet: with edge e and bankroll B, maximum bet ≈ B * e / variance. For Blackjack with 0.5% edge and ~1.3 variance — maximum bet ≈ 0.38% of bankroll. In practice: limit maximum bet at 1-2% of bankroll.

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");
    _;
}

Frontend and UX

Blockchain Blackjack requires careful UX for asynchronicity. VRF fulfillment is not instantaneous. Player clicks "Deal" — waits 1-3 blocks for cards to appear.

UI flow:

  1. Bet → startGame() → status "Dealing..." (wait for GameStarted event)
  2. Cards appear → player sees own 2 cards, one dealer card
  3. Hit/Stand → instantaneous, from shuffled deck
  4. Stand → dealer reveals cards → outcome → payout

For event polling: wagmi with useWatchContractEvent or WebSocket connection to node.

Stack

Component Technology
Smart contract Solidity 0.8.x + VRF v2.5
Testing Foundry + VRF mock
Frontend React + wagmi + viem
Network Polygon / Arbitrum (low gas)
Audit Mandatory (gambling + custody of funds)

Audit is critical — the contract holds ETH and pays out winnings. Errors in payout logic or random seed generation — direct loss of funds.

Timeline

Working on-chain Blackjack (smart contract, tests, basic frontend): 4-5 weeks. With complete UI, animations, statistics, and mobile adaptation — 8-10 weeks.