Розробка гри Mines на блокчейні

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

Розробка блокчейн-гри Mines

Mines (або казино-варіант Minesweeper) — гра на полі N×N, де приховані мини та безпечні комірки. Гравець відкриває комірки одну за одною, кожна безпечна комірка збільшує множник. Можна забрати виграш у будь-який момент або продовжити ризикувати — поки не потрапиш на міну. Елемент вибору робить гру значно більш захопливою ніж Dice.

Математика Mines

Стандартне поле: 5×5 = 25 комірок. Нехай mineCount = 5 (20% шанс міни на кожній відкритій комірці).

Вірогідність безпечно відкрити k комірок:

P(k безпечних) = ∏(i=0 to k-1) [(25 - mines - i) / (25 - i)]

З 5 минами, відкрити 1 комірку безпечно: (25-5)/25 = 80%. Відкрити 2 поспіль: 80% × (19/24) = 63.3%. Відкрити 5 поспіль: ~33%.

Множник при k відкритих комірок = 1 / P(k) × (1 - houseEdge).

Це створює експоненціально зростаючий риск/награду — саме те, що робить Mines психологічно захоплюючим.

Smart Contract: Паттерн Reveal

Ключова складність Mines на блокчейні: не можна зберігати позиції мін on-chain до завершення гри (гравець їх побачить). Розв'язання: commit-reveal.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BlockchainMines is VRFConsumerBaseV2Plus {
    struct Game {
        address player;
        uint8 fieldSize;        // 5 для 5x5
        uint8 mineCount;
        uint256 betAmount;
        uint256 currentMultiplier; // в basis points
        uint8 openedCells;
        uint256 vrfSeed;        // від VRF, зберігається зашифровано до кешау
        bytes32 minesSeedHash;  // hash(vrfSeed) — публічно
        GameStatus status;
        bool[25] openedCellMap; // які комірки відкриті
    }
    
    enum GameStatus { WAITING_VRF, ACTIVE, CASHED_OUT, BUSTED }
    
    mapping(uint256 => Game) public games;
    mapping(address => uint256) public activeGame;
    
    // Таблиця множників: [fieldSize][mineCount][openedCells] → множник
    // Передрахована офлайн, завантажена при деплойменті
    mapping(uint8 => mapping(uint8 => mapping(uint8 => uint256))) public multiplierTable;
    
    function startGame(uint8 mineCount) external payable returns (uint256 gameId) {
        require(activeGame[msg.sender] == 0, "Game already active");
        require(mineCount >= 1 && mineCount <= 24, "Invalid mine count");
        require(msg.value >= MIN_BET, "Bet too low");
        
        gameId = ++gameCounter;
        
        games[gameId] = Game({
            player: msg.sender,
            fieldSize: 5,
            mineCount: mineCount,
            betAmount: msg.value,
            currentMultiplier: 10000,
            openedCells: 0,
            vrfSeed: 0,
            minesSeedHash: 0,
            status: GameStatus.WAITING_VRF,
            openedCellMap: [false, false, /*...*/ false],
        });
        
        activeGame[msg.sender] = gameId;
        
        // Запрашиваємо VRF для seed розташування мін
        uint256 vrfRequestId = _requestVRF();
        vrfToGame[vrfRequestId] = gameId;
    }
    
    function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) 
        internal override 
    {
        uint256 gameId = vrfToGame[requestId];
        Game storage game = games[gameId];
        
        // Зберігаємо seed — не розкриваємо позиції мін гравцю
        // Шифруємо через xor з secret key (можна розкрити після гри)
        game.vrfSeed = randomWords[0]; // в production — шифрувати
        game.minesSeedHash = keccak256(abi.encodePacked(randomWords[0]));
        game.status = GameStatus.ACTIVE;
        
        emit GameStarted(gameId, game.minesSeedHash);
    }
    
    function openCell(uint256 gameId, uint8 cellIndex) external {
        Game storage game = games[gameId];
        require(game.player == msg.sender, "Not your game");
        require(game.status == GameStatus.ACTIVE, "Game not active");
        require(cellIndex < 25, "Invalid cell");
        require(!game.openedCellMap[cellIndex], "Already opened");
        
        game.openedCellMap[cellIndex] = true;
        
        // Визначаємо чи в цій комірці міна
        bool isMine = _isMine(game.vrfSeed, game.mineCount, cellIndex, game.fieldSize);
        
        if (isMine) {
            game.status = GameStatus.BUSTED;
            activeGame[msg.sender] = 0;
            
            // Розкриваємо всі мини
            uint8[] memory minePositions = _getMinePositions(game.vrfSeed, game.mineCount);
            emit GameBusted(gameId, cellIndex, minePositions);
        } else {
            game.openedCells++;
            game.currentMultiplier = multiplierTable[game.fieldSize][game.mineCount][game.openedCells];
            
            emit CellOpened(gameId, cellIndex, game.currentMultiplier);
        }
    }
    
    function cashout(uint256 gameId) external {
        Game storage game = games[gameId];
        require(game.player == msg.sender, "Not your game");
        require(game.status == GameStatus.ACTIVE, "Game not active");
        require(game.openedCells > 0, "No cells opened");
        
        game.status = GameStatus.CASHED_OUT;
        activeGame[msg.sender] = 0;
        
        uint256 payout = (game.betAmount * game.currentMultiplier) / 10000;
        payable(msg.sender).transfer(payout);
        
        emit GameCashedOut(gameId, game.openedCells, game.currentMultiplier, payout);
    }
    
    // Визначення позицій мін з seed
    function _getMinePositions(uint256 seed, uint8 mineCount) 
        internal pure returns (uint8[] memory positions) 
    {
        positions = new uint8[](mineCount);
        bool[25] memory placed;
        uint256 minesPlaced = 0;
        uint256 i = 0;
        
        while (minesPlaced < mineCount) {
            uint8 pos = uint8(uint256(keccak256(abi.encodePacked(seed, i))) % 25);
            if (!placed[pos]) {
                placed[pos] = true;
                positions[minesPlaced] = pos;
                minesPlaced++;
            }
            i++;
        }
    }
    
    function _isMine(
        uint256 seed,
        uint8 mineCount,
        uint8 cellIndex,
        uint8 fieldSize
    ) internal pure returns (bool) {
        uint8[] memory minePositions = _getMinePositions(seed, mineCount);
        for (uint i = 0; i < minePositions.length; i++) {
            if (minePositions[i] == cellIndex) return true;
        }
        return false;
    }
}

Таблиця множників

Множники передраховуються математично й завантажуються у контракт при деплойменті. Приклад для поля 5×5 з 3 мінами:

Відкритих Множник (1% edge)
1 1.14x
2 1.32x
3 1.56x
5 2.22x
10 6.60x
15 27.3x
22 990x

Анімація та UX

Mines вимагає хорошого візуального зворотного зв'язку:

  • Вибух при попаданні на міну
  • Поступове свічення/посилення при успішних комірках
  • Нарощення напруги у звуковому дизайні
  • Миттєво видима кнопка Cashout

Розробка Mines: смарт-контракт + VRF + фронтенд — 4-5 тижнів. Математично коректна таблиця множників та animated UI включені.