Blockchain wheel of fortune 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 wheel of fortune 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 Wheel of Fortune Game Development

Wheel of Fortune is one of the simplest gambling mechanisms: a player bets, the wheel spins, a sector lands, payout calculated by coefficient. The essence comes down to one question: who generates the random number and can it be trusted? Without fair randomization, blockchain wheel of fortune is just a beautiful interface wrapping fraud.

This makes Chainlink VRF (Verifiable Random Function) a central technical element. Not optional — mandatory. Any other solution is either predictable or subject to operator manipulation.

Why Standard Randomness Sources Don't Work

block.timestamp, block.prevrandao — controlled by validators. A miner can choose not to include a transaction if they see an unfavorable outcome (grinding attack). For high stakes, this is a direct exploitation vector.

On-chain hash of future block — same problem. A malicious operator sees the hash, can cancel reveal if outcome is unfavorable.

Off-chain oracle without proof — complete trust in operator. User cannot verify that the number wasn't chosen retroactively.

Chainlink VRF v2.5 — the only production-ready solution: random number generated with cryptographic proof, verified on-chain. Operator physically cannot manipulate the result.

Chainlink VRF Integration

Subscription-based approach

VRF v2.5 works via subscription: create subscription, fund with LINK tokens, contract makes requests through subscription ID.

import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";

contract WheelOfFortune is VRFConsumerBaseV2Plus {
    // Chainlink VRF parameters (Ethereum mainnet)
    bytes32 constant KEY_HASH = 0x9fe0eebf5e446e3c998ec9bb19951541aee00bb90ea201ae456421a2ded86805;
    uint256 immutable subscriptionId;
    uint32 constant CALLBACK_GAS_LIMIT = 100_000;
    uint16 constant REQUEST_CONFIRMATIONS = 3;

    struct Spin {
        address player;
        uint256 betAmount;
        uint8 wheelType;       // 0=standard, 1=premium (different sector sets)
        uint256 requestId;
        bool fulfilled;
    }

    mapping(uint256 => Spin) public spins;       // requestId => Spin
    mapping(address => uint256) public pendingSpins; // player => requestId

    event SpinRequested(address indexed player, uint256 indexed requestId, uint256 betAmount);
    event SpinResult(address indexed player, uint256 indexed requestId, uint8 sector, uint256 payout);

    function spin(uint8 wheelType) external payable {
        require(msg.value >= MIN_BET && msg.value <= MAX_BET, "Invalid bet");
        require(pendingSpins[msg.sender] == 0, "Spin pending");

        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: KEY_HASH,
                subId: subscriptionId,
                requestConfirmations: REQUEST_CONFIRMATIONS,
                callbackGasLimit: CALLBACK_GAS_LIMIT,
                numWords: 1,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );

        spins[requestId] = Spin({
            player: msg.sender,
            betAmount: msg.value,
            wheelType: wheelType,
            requestId: requestId,
            fulfilled: false
        });
        pendingSpins[msg.sender] = requestId;

        emit SpinRequested(msg.sender, requestId, msg.value);
    }

    function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
        internal override {
        Spin storage s = spins[requestId];
        require(!s.fulfilled, "Already fulfilled");
        s.fulfilled = true;
        delete pendingSpins[s.player];

        // Determine wheel sector
        uint8 sector = _getSector(randomWords[0], s.wheelType);
        uint256 payout = _calculatePayout(s.betAmount, sector);

        // Payout
        if (payout > 0) {
            payable(s.player).transfer(payout);
        }

        emit SpinResult(s.player, requestId, sector, payout);
    }
}

Wheel sector design

Sectors determine house edge and excitement. Important: final RTP (Return to Player) must be explicitly stated and verifiable on-chain.

struct Sector {
    string name;
    uint16 weight;       // out of 10000 (basis points)
    uint16 multiplier;   // multiplier x100 (200 = 2x, 500 = 5x, 0 = lose)
}

// Standard wheel — sum of weight = 10000
Sector[] standardWheel = [
    Sector("2x",   4000, 200),    // 40% chance, 2x
    Sector("3x",   2000, 300),    // 20% chance, 3x
    Sector("5x",   1500, 500),    // 15% chance, 5x
    Sector("10x",  800,  1000),   // 8% chance, 10x
    Sector("20x",  300,  2000),   // 3% chance, 20x
    Sector("50x",  100,  5000),   // 1% chance, 50x
    Sector("MISS", 1300, 0),      // 13% chance, loss
];

// RTP = sum(weight * multiplier / 100) / 10000
// = (4000*2 + 2000*3 + 1500*5 + 800*10 + 300*20 + 100*50 + 1300*0) / 10000
// = (8000 + 6000 + 7500 + 8000 + 6000 + 5000 + 0) / 10000 = 40500/10000 = 4.05...
// Correct: RTP = sum(weight/10000 * multiplier/100)
// = 0.4*2 + 0.2*3 + 0.15*5 + 0.08*10 + 0.03*20 + 0.01*50 = 0.8+0.6+0.75+0.8+0.6+0.5 = 4.05
// House edge = 1 - RTP = ... need normalization to multiplier → bet
// RTP = 0.4*2 + 0.2*3 + 0.15*5 + 0.08*10 + 0.03*20 + 0.01*50 + 0.13*0 = 4.05?
// Here multiplier = outgoing / bet. RTP as bet fraction = same numbers.
// For house edge < 1.0: adjust weights for desired RTP (typically 90-97%)

function _getSector(uint256 randomWord, uint8 wheelType) internal view returns (uint8) {
    Sector[] storage wheel = wheelType == 0 ? standardWheel : premiumWheel;
    uint256 position = randomWord % 10000;
    uint256 cumulative = 0;

    for (uint8 i = 0; i < wheel.length; i++) {
        cumulative += wheel[i].weight;
        if (position < cumulative) return i;
    }
    return uint8(wheel.length - 1);
}

House Bankroll and Liquidity

Contract must maintain balance to pay maximum possible payout. Minimum bankroll = MAX_BET × max_multiplier. At 50x multiplier and MAX_BET 1 ETH — minimum 50 ETH reserve.

Liquidity provider model. Users deposit ETH into pool as LP, earn share of house edge profits. Solves bankroll problem and creates yield-bearing product:

mapping(address => uint256) public lpShares;
uint256 public totalShares;
uint256 public houseBalance;

function addLiquidity() external payable {
    uint256 shares = totalShares == 0
        ? msg.value
        : (msg.value * totalShares) / houseBalance;

    lpShares[msg.sender] += shares;
    totalShares += shares;
    houseBalance += msg.value;
}

function removeLiquidity(uint256 shares) external {
    require(lpShares[msg.sender] >= shares, "Insufficient shares");
    uint256 amount = (shares * houseBalance) / totalShares;

    // Check sufficient liquidity remains after withdrawal
    require(houseBalance - amount >= MIN_BANKROLL, "Insufficient bankroll");

    lpShares[msg.sender] -= shares;
    totalShares -= shares;
    houseBalance -= amount;
    payable(msg.sender).transfer(amount);
}

Frontend: Animation and UX

Wheel visual animation must be deterministic from VRF result — not random on frontend. Important for fairness perception: result already determined on-chain, animation only visualizes it.

// After receiving SpinResult event
function animateWheel(sector: number, totalSectors: number, onComplete: () => void) {
    const sectorAngle = 360 / totalSectors
    const targetAngle = 360 * 5 + sector * sectorAngle // 5 full rotations + target sector

    wheelElement.style.transition = 'transform 4s cubic-bezier(0.17, 0.67, 0.12, 0.99)'
    wheelElement.style.transform = `rotate(${targetAngle}deg)`

    setTimeout(onComplete, 4000)
}

Waiting for VRF response. VRF takes 3-5 blocks (~36-60 seconds on Ethereum). User sees spinning animation + timer. On L2 (Arbitrum, Base) — faster, 1-3 blocks. Polygon — even faster.

For instant feel: show "spinning" animation immediately, wait for VRF response, play final spin revealing result.

NFT Boosts and Game Mechanics

Spin boost NFT. NFTs grant additional multiplier on winnings (+10%), extra spin once per 24h, access to premium wheel with higher multipliers. Creates secondary market and token sink.

Jackpot mechanic. Small percentage of each bet (1-2%) goes to jackpot pool. Landing special "JACKPOT" sector (very small weight, 0.1%) takes entire pool. Psychologically attractive mechanism.

Daily bonus spin. Free spin once per 24 hours with limited max payout. Increases retention without significant house impact.

Stack and Infrastructure

Component Technology
Smart contracts Solidity + Foundry + OpenZeppelin
VRF Chainlink VRF v2.5
Frontend React + wagmi + viem
Animation Framer Motion / GSAP
Events monitoring viem watchContractEvent
NFT ERC-721 (boosts) + ERC-1155 (cosmetics)
Deploy Arbitrum / Base (low gas, fast blocks)

Development Process

Game design (3-5 days). Sector design, RTP and house edge calculation, LP model, NFT mechanics. Math verification before coding.

Smart contracts (2-3 weeks). VRF integration, wheel logic, LP mechanism, NFT contracts. Foundry tests with mocked VRF coordinator.

Frontend (2-3 weeks). Wheel visualization, animations, VRF waiting, wallet integration, LP dashboard.

Audit. VRF integration and LP mechanism — mandatory audit. Special attention: can operator change sectors without timelock, correct bankroll checks.

Basic version without LP and NFT — 4-5 weeks. Full with LP pool, jackpot, NFT system — 8-10 weeks.