Blockchain keno 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 keno game development
Medium
~3-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 Keno Game Development

Keno is a lottery game: player selects numbers from range (usually 1–80), 20 numbers drawn randomly, winnings depend on matches. Simple mechanics, but blockchain implementation requires solving non-trivial tasks: verifiable randomness for drawing 20 numbers, gas-efficient match checking, correct payout table.

Unlike Crash, Keno is game with fixed outcome before cashout: numbers drawn, result immediately known. This simplifies some aspects but requires special attention to RNG quality.

RNG: Selecting 20 Unique Numbers from 80

Main technical task: from single VRF random seed get 20 unique numbers in range 1–80. Naive approach (rand % 80 repeat 20 times) creates collisions — one number can fall twice.

Fisher-Yates Shuffle for On-Chain Keno

function drawNumbers(uint256 seed) public pure returns (uint8[20] memory drawn) {
    // Initialize array 1..80
    uint8[80] memory pool;
    for (uint8 i = 0; i < 80; i++) {
        pool[i] = i + 1;
    }
    
    // Fisher-Yates: shuffle first 20 positions
    for (uint8 i = 0; i < 20; i++) {
        // Get pseudo-random index from seed
        uint256 j = uint256(keccak256(abi.encodePacked(seed, i))) % (80 - i);
        
        // Swap pool[i] and pool[i + j]
        uint8 temp = pool[i];
        pool[i] = pool[i + j];
        pool[i + j] = temp;
        
        drawn[i] = pool[i];
    }
}

Fisher-Yates guarantees unique numbers without reject-sampling. For on-chain execution: 20 iterations × keccak256 ≈ 80,000–100,000 gas. On Arbitrum ~$0.01. Acceptable.

Alternative — bitmap approach:

function drawNumbersBitmap(uint256 seed) public pure returns (uint8[20] memory drawn) {
    uint256 bitmap = 0; // each bit = number 1-80
    uint8 count = 0;
    uint256 nonce = 0;
    
    while (count < 20) {
        uint8 num = uint8(uint256(keccak256(abi.encodePacked(seed, nonce))) % 80) + 1;
        nonce++;
        
        if (bitmap & (1 << (num - 1)) == 0) {
            bitmap |= (1 << (num - 1));
            drawn[count] = num;
            count++;
        }
    }
}

Bitmap approach simpler, but reject-sampling may require more keccak256 calls in worst case (collisions more frequent selecting last numbers). Fisher-Yates preferable for predictable gas.

Match Checking: Gas-Efficient Implementation

Player selected M numbers (1–10), need to count matches with 20 drawn numbers.

Naive approach: nested loops O(M×20) — acceptable for small M.

Bitmap approach for efficiency:

function countMatches(
    uint8[] memory playerPicks,  // player's numbers
    uint8[20] memory drawnNumbers
) public pure returns (uint8 matches) {
    // Build bitmap of drawn numbers
    uint256 drawnBitmap = 0;
    for (uint8 i = 0; i < 20; i++) {
        drawnBitmap |= (1 << (drawnNumbers[i] - 1));
    }
    
    // Check player's picks against bitmap
    for (uint8 i = 0; i < playerPicks.length; i++) {
        if (drawnBitmap & (1 << (playerPicks[i] - 1)) != 0) {
            matches++;
        }
    }
}

Bitwise operations faster than nested loops. For typical 1–10 picks: ≈ 3,000–5,000 additional gas.

Payout Table

Keno payouts — most important economic part. Need to balance house edge (usually 20–35% in Keno) with different pick counts.

// Payouts as multiplier of bet (basis points: 100 = 1x)
// [picks][matches] → multiplier
uint256[11][11] public payoutTable;

constructor() {
    // 1 pick
    payoutTable[1][0] = 0;
    payoutTable[1][1] = 360; // 3.6x

    // 3 picks
    payoutTable[3][0] = 0;
    payoutTable[3][1] = 0;
    payoutTable[3][2] = 200;  // 2x
    payoutTable[3][3] = 4600; // 46x

    // 5 picks
    payoutTable[5][0] = 0;
    payoutTable[5][1] = 0;
    payoutTable[5][2] = 0;
    payoutTable[5][3] = 300;   // 3x
    payoutTable[5][4] = 1200;  // 12x
    payoutTable[5][5] = 50000; // 500x

    // 10 picks  
    payoutTable[10][0] = 0;
    payoutTable[10][5] = 200;    // 2x
    payoutTable[10][6] = 1800;   // 18x
    payoutTable[10][7] = 17000;  // 170x
    payoutTable[10][8] = 100000; // 1000x
    payoutTable[10][9] = 250000; // 2500x
    payoutTable[10][10] = 1000000; // 10000x (jackpot)
}

House edge verified mathematically: for each picks variant calculate 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 should be ≈ 0.70–0.80 (70–80% RTP, 20–30% house edge)

Full Game Cycle 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");
        
        // Validate picks (1-80, unique)
        _validatePicks(picks);
        
        uint256 roundId = nextRoundId++;
        rounds[roundId] = KenoRound({
            player: msg.sender,
            betAmount: msg.value,
            playerPicks: picks,
            drawnNumbers: [uint8(0),...], // filled in callback
            matchCount: 0,
            payout: 0,
            status: RoundStatus.PENDING,
            vrfRequestId: 0
        });
        
        // Request VRF
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: s_keyHash,
                subId: s_subscriptionId,
                requestConfirmations: 1,
                callbackGasLimit: 300_000, // with reserve for 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];
        
        // Draw 20 numbers
        round.drawnNumbers = drawNumbers(randomWords[0]);
        
        // Count matches
        round.matchCount = countMatches(round.playerPicks, round.drawnNumbers);
        
        // Calculate payout
        uint256 multiplier = payoutTable[round.playerPicks.length][round.matchCount];
        round.payout = round.betAmount * multiplier / 100;
        round.status = RoundStatus.COMPLETED;
        
        // Pay winner
        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
        );
    }
}

Multi-Player Keno: Shared Draw

For casino-style with multiple players in one draw round:

Shared draw significantly reduces gas per player.

Stack and Timeline

Chain: Polygon PoS or Arbitrum for low fees. Chainlink VRF V2 Plus. Solidity + Foundry. Frontend: React + wagmi. WebSocket for real-time draw animation.

Phase Timeline
Contracts (single player, VRF, payout table) 3–4 weeks
Multi-player shared draw 2 weeks
Frontend + draw animation 2–3 weeks
Bankroll + admin panel 1–2 weeks
Audit + testnet 3–4 weeks

MVP (single player Keno): 5–7 weeks. Full platform with multi-player draw: 9–12 weeks.