Разработка игры Wheel of Fortune на блокчейне

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка игры Wheel of Fortune на блокчейне
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1258
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1170
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1092
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830

Разработка игры Wheel of Fortune на блокчейне

Колесо фортуны — один из простейших gambling механизмов: игрок ставит, рулетка крутится, выпадает сектор, выплата по коэффициенту. Вся суть в одном вопросе: кто генерирует случайное число и можно ли этому доверять? Без честной рандомизации blockchain wheel of fortune — просто красивый интерфейс вокруг мошенничества.

Это делает Chainlink VRF (Verifiable Random Function) центральным техническим элементом. Не опциональным — обязательным. Любое другое решение либо предсказуемо, либо поддаётся манипуляции оператором.

Почему стандартные источники рандома не работают

block.timestamp, block.prevrandao — управляются валидаторами. Майнер может выбрать не включать транзакцию, если видит невыгодный outcome (grinding attack). Для высоких ставок это прямой вектор эксплуатации.

On-chain хэш будущего блока — аналогичная проблема. Мошеннический оператор видит хэш, может отменить reveal если результат невыгоден.

Off-chain оракул без proof — полное доверие оператору. Пользователь не может верифицировать, что число не было выбрано постфактум.

Chainlink VRF v2.5 — единственное production-ready решение: случайное число генерируется с cryptographic proof, который верифицируется on-chain. Оператор физически не может манипулировать результатом.

Chainlink VRF интеграция

Subscription-based подход

VRF v2.5 работает через subscription: создаёшь подписку, пополняешь LINK токенами, контракт делает запросы через 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 параметры (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 (разные наборы секторов)
        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];

        // Определяем сектор колеса
        uint8 sector = _getSector(randomWords[0], s.wheelType);
        uint256 payout = _calculatePayout(s.betAmount, sector);

        // Выплата
        if (payout > 0) {
            payable(s.player).transfer(payout);
        }

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

Дизайн секторов колеса

Сектора определяют house edge и excitement. Важно: итоговый RTP (Return to Player) должен быть явно заявлен и верифицируем on-chain.

struct Sector {
    string name;
    uint16 weight;       // из 10000 (basis points)
    uint16 multiplier;   // множитель x100 (200 = 2x, 500 = 5x, 0 = lose)
}

// Стандартное колесо — сумма weight = 10000
Sector[] standardWheel = [
    Sector("2x",   4000, 200),    // 40% шанс, 2x
    Sector("3x",   2000, 300),    // 20% шанс, 3x
    Sector("5x",   1500, 500),    // 15% шанс, 5x
    Sector("10x",  800,  1000),   // 8% шанс, 10x
    Sector("20x",  300,  2000),   // 3% шанс, 20x
    Sector("50x",  100,  5000),   // 1% шанс, 50x
    Sector("MISS", 1300, 0),      // 13% шанс, проигрыш
];

// 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... 
// Нет, правильно: 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 = ... нужно нормировать 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? 
// Здесь multiplier — это outgoing / bet. RTP как доля ставки = те же числа.
// Для house edge нужно <1.0: подбираем веса под желаемый RTP (обычно 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 и liquidity

Контракт должен иметь баланс для выплаты максимального возможного выигрыша. Минимальный банкролл = MAX_BET × max_multiplier. При 50x multiplier и MAX_BET 1 ETH — минимум 50 ETH резерва.

Liquidity provider модель. Пользователи вносят ETH в пул как LP, получают долю house edge прибыли. Это решает проблему bankroll и создаёт yield-bearing продукт:

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;

    // Проверяем что достаточно ликвидности после вывода
    require(houseBalance - amount >= MIN_BANKROLL, "Insufficient bankroll");

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

Frontend: анимация и UX

Визуальная анимация колеса должна быть детерминированной от результата VRF — не рандомной на frontend. Это важно для честности восприятия: результат уже определён on-chain, анимация только визуализирует его.

// После получения SpinResult события
function animateWheel(sector: number, totalSectors: number, onComplete: () => void) {
    const sectorAngle = 360 / totalSectors
    const targetAngle = 360 * 5 + sector * sectorAngle // 5 полных оборотов + целевой сектор

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

    setTimeout(onComplete, 4000)
}

Ожидание VRF ответа. VRF занимает 3-5 блоков (~36-60 секунд на Ethereum). Пользователь видит spinning animation + таймер. На L2 (Arbitrum, Base) — быстрее, 1-3 блока. Polygon — ещё быстрее.

Для мгновенного feel: показываем «крутим» анимацию сразу, ждём VRF response, проигрываем финальный spin с раскрытием результата.

NFT бусты и игровые механики

Spin boost NFT. NFT дают дополнительные multiplier на выигрыш (+10%), extra spin раз в 24 часа, доступ к premium колесу с более высокими multiplier-ами. Создаёт вторичный рынок и sink для токенов.

Jackpot механика. Малый процент каждой ставки (1-2%) идёт в jackpot pool. Выпадение специального сектора «JACKPOT» (очень малый вес, 0.1%) забирает весь пул. Это психологически привлекательный механизм.

Daily bonus spin. Бесплатный spin раз в 24 часа с ограниченным max payout. Повышает retention без значимого влияния на house balance.

Стек и инфраструктура

Компонент Технология
Smart contracts Solidity + Foundry + OpenZeppelin
VRF Chainlink VRF v2.5
Frontend React + wagmi + viem
Анимация Framer Motion / GSAP
Events мониторинг viem watchContractEvent
NFT ERC-721 (бусты) + ERC-1155 (cosmetics)
Deploy Arbitrum / Base (низкий gas, быстрые блоки)

Процесс разработки

Game design (3-5 дней). Дизайн секторов, расчёт RTP и house edge, LP модель, NFT механики. Верификация математики перед кодированием.

Smart contracts (2-3 недели). VRF интеграция, wheel логика, LP механизм, NFT контракты. Foundry тесты с мокнутым VRF coordinator.

Frontend (2-3 недели). Визуализация колеса, анимации, ожидание VRF, wallet интеграция, LP dashboard.

Аудит. VRF интеграция и LP механизм — обязательный аудит. Особое внимание: может ли оператор изменить sectors без timelock, корректность bankroll проверок.

Базовая версия без LP и NFT — 4-5 недель. Полная с LP pool, jackpot, NFT системой — 8-10 недель.