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

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

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

Tower (башня) — азартная игра с нарастающим риском: игрок поднимается по уровням, на каждом уровне выбирает одну из нескольких ячеек, одна из которых — «мина». Чем выше уровень — тем больше множитель выигрыша. В любой момент можно «кешаут» и забрать накопленный выигрыш. Структурно похожа на Mines, но с прогрессивным ростом ставок.

Блокчейн Tower интересна тем, что требует честного рандома на каждом уровне отдельно, при этом игрок не должен знать заранее расположение мины на следующем уровне.

Архитектура честного рандома

Ключевая проблема: где располагается мина на каждом уровне? On-chain данные публичны — если хранить расположение мины в storage, игрок может прочитать до хода.

Подход 1: Commit-Reveal per level

Оператор/оракул генерирует хэш seed для каждого уровня до начала игры, публикует hash on-chain, раскрывает seed только когда игрок сделал выбор на этом уровне:

struct TowerGame {
    address player;
    uint256 bet;
    uint8 currentLevel;    // текущий уровень (0 = начало)
    uint8 maxLevels;       // высота башни
    uint256 currentMultiplier; // x1000 для точности
    bytes32 serverSeedHash;    // хэш seed от сервера
    bool active;
}

Проблема: требует backend-а, который честно играет (не может подставить мину post-hoc). Решение — публикация hash до начала игры. Если сервер раскрывает seed несовпадающий с hash — нарушение верифицируемо.

Подход 2: Chainlink VRF per game

Запросить один большой random в начале игры, детерминированно выводить расположение мины на каждом уровне:

mapping(uint256 => TowerGame) public games; // requestId → game

function startTower(uint8 levels, uint8 cellsPerLevel) external payable {
    require(msg.value >= MIN_BET);
    require(levels >= 3 && levels <= 10);
    require(cellsPerLevel >= 2 && cellsPerLevel <= 5);
    
    uint256 requestId = s_vrfCoordinator.requestRandomWords(
        VRFV2PlusClient.RandomWordsRequest({
            keyHash: KEY_HASH,
            subId: subscriptionId,
            requestConfirmations: 3,
            callbackGasLimit: 200000,
            numWords: 1,
            extraArgs: VRFV2PlusClient._argsToBytes(
                VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
            )
        })
    );
    
    games[requestId] = TowerGame({
        player: msg.sender,
        bet: msg.value,
        currentLevel: 0,
        maxLevels: levels,
        currentMultiplier: 1000, // x1.0
        gameSeed: 0, // заполнится в fulfillment
        active: false
    });
}

function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
    TowerGame storage game = games[requestId];
    game.gameSeed = randomWords[0];
    game.active = true;
    emit TowerReady(requestId, game.player);
}

// Получить позицию мины для уровня (только когда уже сделан ход!)
function _getMinePosition(uint256 requestId, uint8 level, uint8 cellsPerLevel) private view returns (uint8) {
    return uint8(uint256(keccak256(abi.encodePacked(
        games[requestId].gameSeed,
        level
    ))) % cellsPerLevel);
}

_getMinePosition — private view. Технически читаемо если знаешь gameSeed. Но gameSeed хранится в storage... и снова публично.

Решение: скрытие seed через хэш

Хранить только keccak256(gameSeed) в events, а сам seed — только как параметр в транзакции, не в storage. Это не идеально, но поднимает планку для мошенничества: нужно мониторить pending transactions.

Практическое решение для production: гибрид — Chainlink VRF для нечитаемого seed + сохранение только хэша seed публично. Позиция мины раскрывается через событие только после хода игрока и не хранится в storage до хода.

Мультипликаторы и математика

Каждый уровень башни с n ячейками и одной миной: вероятность безопасного выбора = (n-1)/n. Математически честный множитель после k уровней:

multiplier(k) = product_{i=1}^{k} (n_i / (n_i - 1))

Для башни 5 уровней, 3 ячейки: каждый уровень ×(3/2) = ×1.5. После 5 уровней: 1.5^5 ≈ 7.59x. House edge добавляется через коэффициент:

// Multiplier таблица (x1000, 3 ячейки, house edge 2%)
uint256[10] public multipliers3Cells = [
    0,      // уровень 0
    1470,   // x1.47 (честный 1.5 * 0.98)
    2161,   // x2.16
    3177,   // x3.18
    4670,   // x4.67
    6865,   // x6.87
    10092,  // x10.09
    14835,  // x14.84
    21807,  // x21.81
    32056   // x32.06
];
function selectCell(uint256 gameId, uint8 cellIndex) external {
    TowerGame storage game = games[gameId];
    require(game.player == msg.sender && game.active);
    require(cellIndex < cellsPerLevel);
    
    uint8 minePosition = _getMinePosition(gameId, game.currentLevel, cellsPerLevel);
    
    if (cellIndex == minePosition) {
        // Попал в мину — потеря ставки
        game.active = false;
        emit GameLost(gameId, msg.sender, game.currentLevel, minePosition);
        // ETH остаётся в контракте (bankroll)
    } else {
        // Прошёл уровень — обновить множитель
        game.currentMultiplier = multipliers[game.currentLevel + 1];
        game.currentLevel++;
        
        if (game.currentLevel == game.maxLevels) {
            // Прошёл всю башню — автоматический кешаут
            _payout(game);
        } else {
            emit LevelCleared(gameId, game.currentLevel, game.currentMultiplier);
        }
    }
}

function cashout(uint256 gameId) external {
    TowerGame storage game = games[gameId];
    require(game.player == msg.sender && game.active && game.currentLevel > 0);
    _payout(game);
}

function _payout(TowerGame storage game) private {
    uint256 payout = game.bet * game.currentMultiplier / 1000;
    game.active = false;
    (bool success, ) = game.player.call{value: payout}("");
    require(success, "Transfer failed");
    emit GameWon(msg.sender, payout, game.currentLevel);
}

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

Tower игра визуально простая, но UX важен: анимация подъёма, пульсация множителя, кнопка кешаут должна быть всегда доступна.

Асинхронный флоу: VRF fulfillment ждём через polling события TowerReady. После — каждый ход — мгновенная on-chain транзакция (нет дополнительного VRF).

// wagmi hook для ожидания старта игры
const { data: gameReadyEvent } = useWatchContractEvent({
  address: TOWER_ADDRESS,
  abi: TOWER_ABI,
  eventName: 'TowerReady',
  args: { player: address },
  onLogs: (logs) => {
    const gameId = logs[0].args.requestId
    setActiveGameId(gameId)
    setGameState('playing')
  }
})

Progressive disclosure множителя: показывать анимацию роста множителя при каждом успешном уровне — это ключевой момент удержания. Текущий возможный выигрыш должен быть виден крупно, в реалтайм.

Bankroll и лимиты

Максимальный выигрыш ограничен bankroll-ом. Проверка до принятия ставки:

function maxWinForBet(uint256 bet) public view returns (uint256) {
    return bet * multipliers[maxLevels] / 1000;
}

modifier bankrollSufficient(uint256 bet) {
    require(address(this).balance >= maxWinForBet(bet) + bet, "Insufficient bankroll");
    _;
}

Стек и сроки

Компонент Технология
Контракт Solidity + Chainlink VRF
Тесты Foundry + VRF mock
Frontend React + wagmi
Сеть Arbitrum / Polygon

Базовая Tower игра (смарт-контракт, тесты, UI): 3-4 недели. С расширенными визуальными эффектами, статистикой, leaderboard: 6-8 недель. Аудит смарт-контракта обязателен — контракт управляет игровым банкроллом.