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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Разработка игры Crash на блокчейне
Средний
~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

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

Crash — азартная игра, в которой множитель растёт от 1x вверх и в случайный момент «крашится». Игрок должен успеть вывести ставку до краша. Чем дольше ждёшь — тем выше потенциальный выигрыш, тем выше риск потерять всё.

Ключевая особенность blockchain Crash в отличие от традиционного online casino: результат раунда должен быть provably fair. Игрок не доверяет серверу — он может математически проверить, что множитель краша не был определён после его ставки. Это достигается через commitment scheme + Chainlink VRF или RANDAO.

Архитектура провабли-фэйр Crash

Commitment + Reveal схема (без оракула)

Классическая схема: operator заранее публикует hash следующего seed, потом раскрывает seed после завершения ставок.

contract CrashGame {
    struct Round {
        bytes32 seedHash;     // hash(seed) — публикуется до ставок
        bytes32 seed;         // раскрывается после завершения ставок
        uint64 crashPoint;    // результат (в basis points: 150 = 1.5x)
        uint256 totalBets;
        uint256 startTime;
        RoundStatus status;
    }
    
    enum RoundStatus { ACCEPTING_BETS, IN_PROGRESS, CRASHED, CASHOUT_PHASE }
    
    // Operator публикует hash следующего seed заблаговременно
    function commitNextRound(bytes32 seedHash) external onlyOperator {
        require(rounds[nextRoundId].status == RoundStatus.CRASHED, "Previous not finished");
        rounds[nextRoundId + 1].seedHash = seedHash;
    }
    
    // После окончания фазы ставок — раскрываем seed
    function revealAndStart(uint256 roundId, bytes32 seed) external onlyOperator {
        Round storage round = rounds[roundId];
        require(round.status == RoundStatus.ACCEPTING_BETS, "Wrong status");
        require(keccak256(abi.encodePacked(seed)) == round.seedHash, "Seed mismatch");
        
        round.seed = seed;
        round.crashPoint = _calculateCrashPoint(seed, roundId);
        round.status = RoundStatus.IN_PROGRESS;
        round.startTime = uint64(block.timestamp);
        
        emit RoundStarted(roundId, round.crashPoint); // crashPoint скрыт от frontend до краша
    }
}

Проблема commitment схемы: оператор знает seed заранее и может отказаться раскрывать невыгодный (griefing, хотя это его loss). Решение — VRF.

Chainlink VRF V2 Plus: trustless random

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

contract CrashGame is VRFConsumerBaseV2Plus {
    uint256 private immutable s_subscriptionId;
    bytes32 private immutable s_keyHash;
    
    mapping(uint256 => uint256) public roundToVrfRequest; // roundId → VRF requestId
    mapping(uint256 => uint256) public vrfRequestToRound; // VRF requestId → roundId
    
    // Запрашиваем random в конце фазы ставок
    function closeAndRequestRandom(uint256 roundId) external onlyOperator {
        Round storage round = rounds[roundId];
        require(round.status == RoundStatus.ACCEPTING_BETS, "Wrong status");
        
        round.status = RoundStatus.IN_PROGRESS;
        
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: s_keyHash,
                subId: s_subscriptionId,
                requestConfirmations: 1, // быстро для UX
                callbackGasLimit: 100_000,
                numWords: 1,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );
        
        roundToVrfRequest[roundId] = requestId;
        vrfRequestToRound[requestId] = roundId;
    }
    
    // Callback от Chainlink: получаем random, вычисляем crash point
    function fulfillRandomWords(
        uint256 requestId,
        uint256[] calldata randomWords
    ) internal override {
        uint256 roundId = vrfRequestToRound[requestId];
        Round storage round = rounds[roundId];
        
        uint256 rand = randomWords[0];
        round.crashPoint = _calculateCrashPoint(rand);
        round.seed = bytes32(rand); // для верификации
        
        // Запускаем таймер раунда
        emit RoundActive(roundId, round.startTime = uint64(block.timestamp));
    }
}

Формула crash point

Математика crash point должна давать правильное математическое ожидание (house edge).

// Целевое распределение: P(crash >= X) = 1/X * (1 - house_edge)
// House edge = 1% → P(crash >= X) = 0.99/X
// Минимальный crash = 1.00x (house забирает 1% от раундов с очень ранним крашем)

function _calculateCrashPoint(uint256 rand) internal pure returns (uint64) {
    // Нормализуем rand в [0, 1) с точностью 1e9
    uint256 h = rand % 1_000_000_000;
    
    // P(crash >= X) = 0.99/X → X = 0.99/(rand/1e9) = 990_000_000/h
    // Если h < 10_000_000 (1%) → instant crash (1.00x) — house edge
    if (h < 10_000_000) return 100; // 1.00x в basis points * 100
    
    // X в basis points (100 = 1.00x, 150 = 1.50x, 200 = 2.00x)
    uint256 crashPoint = 990_000_000 * 100 / h;
    
    // Минимум 100 (1.00x), максимум cap 100000 (1000x) для защиты от overflow
    if (crashPoint < 100) return 100;
    if (crashPoint > 100_000) return 100_000;
    
    return uint64(crashPoint);
}

Верификация: любой игрок может взять VRF randomWords[0] из on-chain данных и воспроизвести эту формулу — получит тот же crash point.

Ставки и cashout механика

struct Bet {
    address player;
    uint256 amount;
    uint64 autoCashoutAt; // 0 = manual, >0 = авто вывод при достижении множителя
    bool cashedOut;
    uint64 cashoutMultiplier; // фактический множитель при выводе
}

mapping(uint256 => mapping(address => Bet)) public bets; // roundId → player → bet

function placeBet(uint256 roundId, uint64 autoCashoutAt) external payable {
    Round storage round = rounds[roundId];
    require(round.status == RoundStatus.ACCEPTING_BETS, "Not accepting bets");
    require(msg.value >= MIN_BET && msg.value <= MAX_BET, "Invalid amount");
    require(bets[roundId][msg.sender].amount == 0, "Already bet");
    
    bets[roundId][msg.sender] = Bet({
        player: msg.sender,
        amount: msg.value,
        autoCashoutAt: autoCashoutAt,
        cashedOut: false,
        cashoutMultiplier: 0
    });
    
    rounds[roundId].totalBets += msg.value;
    emit BetPlaced(roundId, msg.sender, msg.value, autoCashoutAt);
}

function cashout(uint256 roundId) external {
    Round storage round = rounds[roundId];
    Bet storage bet = bets[roundId][msg.sender];
    
    require(round.status == RoundStatus.IN_PROGRESS, "Round not active");
    require(bet.amount > 0 && !bet.cashedOut, "No active bet");
    
    // Вычисляем текущий множитель по времени
    uint64 currentMultiplier = _getCurrentMultiplier(round.startTime);
    
    // Нельзя вывести после краша
    require(currentMultiplier <= round.crashPoint, "Round already crashed");
    
    bet.cashedOut = true;
    bet.cashoutMultiplier = currentMultiplier;
    
    uint256 payout = bet.amount * currentMultiplier / 100; // basis points
    payable(msg.sender).transfer(payout);
    
    emit CashedOut(roundId, msg.sender, currentMultiplier, payout);
}

// Множитель растёт по экспоненциальной кривой
function _getCurrentMultiplier(uint64 startTime) public view returns (uint64) {
    uint256 elapsed = block.timestamp - startTime;
    // 1.00x → растёт каждую секунду
    // Формула: multiplier = e^(elapsed * growth_rate) * 100
    // Аппроксимация для gas efficiency:
    uint256 multiplier = 100 + (elapsed * elapsed * 2); // квадратичный рост
    return uint64(multiplier > 100_000 ? 100_000 : multiplier);
}

Важная проблема: manual cashout on-chain имеет latency. Игрок нажимает cashout в UI → транзакция в mempool → включается в блок (10–12 сек на Ethereum). За это время раунд может крашнуться. На L2 (Arbitrum: 250 мс, Solana: 400 мс) это более приемлемо, но всё равно не идеально.

Решение для low-latency: гибридная архитектура. Off-chain cashout: игрок подписывает cashout request → game server сохраняет подписанный timestamp → при settlement on-chain game server доказывает, что игрок запросил cashout до краша. Требует доверия к game server, но с cryptographic accountability.

Off-chain batch settlement

// Game server аккумулирует cashout и batch-settle после раунда
struct CashoutRecord {
    address player;
    uint64 multiplier;
    bytes signature; // игрок подписал {roundId, multiplier, nonce}
}

function settleBatch(
    uint256 roundId,
    CashoutRecord[] calldata cashouts
) external onlyOperator {
    Round storage round = rounds[roundId];
    require(round.status == RoundStatus.CRASHED, "Round not crashed");
    
    for (uint i = 0; i < cashouts.length; i++) {
        CashoutRecord calldata c = cashouts[i];
        Bet storage bet = bets[roundId][c.player];
        
        require(!bet.cashedOut, "Already settled");
        require(c.multiplier <= round.crashPoint, "Invalid multiplier");
        
        // Верифицируем подпись игрока (он согласен с этим cashout)
        _verifyCashoutSignature(roundId, c.player, c.multiplier, c.signature);
        
        bet.cashedOut = true;
        bet.cashoutMultiplier = c.multiplier;
        
        uint256 payout = bet.amount * c.multiplier / 100;
        payable(c.player).transfer(payout);
    }
}

House bankroll управление

Crash — zero-sum игра. House принимает риск выплатить выигрыши. Нужен bankroll management:

contract CrashBankroll {
    uint256 public maxBetPercent = 100; // максимум 1% от bankroll за раунд
    
    function maxAllowedBet() public view returns (uint256) {
        return address(this).balance * maxBetPercent / 10_000;
    }
    
    // При крупных суммах ставок в раунде — ограничиваем дополнительные ставки
    function availableCapacity(uint256 roundId) public view returns (uint256) {
        uint256 maxExposure = address(this).balance * 500 / 10_000; // 5% от bankroll
        uint256 currentExposure = rounds[roundId].totalBets;
        return currentExposure < maxExposure ? maxExposure - currentExposure : 0;
    }
}

Регуляторные аспекты

Blockchain Crash — азартная игра. Регулирование:

  • Мальта (MGA лицензия): самая доступная EU лицензия для crypto gambling, €25k bond
  • Кюрасао: популярная offshore лицензия, быстрее и дешевле ($15k–30k)
  • Великобритания (UKGC): строгие требования, дорого, но доступ к UK рынку

Технические требования регуляторов часто включают: RNG сертификацию (VRF от Chainlink считается acceptable), responsible gambling механики (deposit limits, self-exclusion), transaction monitoring для AML.

Стек

Chain: Arbitrum One или Polygon PoS (низкие fees, быстрые блоки). Контракты: Solidity + Foundry. Chainlink VRF V2 Plus. Backend: Node.js game server с WebSocket для real-time UI. Frontend: React + WebSocket + Three.js для визуализации множителя.

Ориентиры по срокам

MVP (Chainlink VRF, ручной cashout on-chain, базовый UI): 4–6 недель. Production (hybrid cashout, batch settlement, bankroll management, регуляторный compliance): 10–14 недель.