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

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

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

PvP (Player vs Player) blockchain игры — один из наиболее технически сложных сегментов Web3 gaming. В отличие от казино, где игрок против house, в PvP нужно обеспечить честность между двумя игроками, которые не доверяют друг другу и оба хотят выиграть. Плюс реальные деньги на кону — каждый будет искать эксплойты.

Ключевые технические проблемы PvP

Commitment схемы для скрытых действий

В карточных играх, стратегиях, файтингах игрок не должен видеть ход противника до своего хода. На блокчейне все данные публичны — как скрыть карты?

Commit-reveal паттерн:

contract PvPGame {
    struct GameState {
        address player1;
        address player2;
        bytes32 p1CommitHash;  // hash(action + secret)
        bytes32 p2CommitHash;
        uint8 p1Action;        // раскрывается после commit обоих
        uint8 p2Action;
        Phase phase;
        uint256 commitDeadline;
        uint256 revealDeadline;
    }
    
    enum Phase { WAITING, COMMIT, REVEAL, RESOLVED }
    
    // Phase 1: оба игрока отправляют hash(action + secret)
    function commitAction(uint256 gameId, bytes32 commitHash) external {
        GameState storage game = games[gameId];
        require(game.phase == Phase.COMMIT, "Not commit phase");
        require(block.timestamp <= game.commitDeadline, "Commit deadline passed");
        
        if (msg.sender == game.player1) {
            game.p1CommitHash = commitHash;
        } else if (msg.sender == game.player2) {
            game.p2CommitHash = commitHash;
        } else revert("Not a player");
        
        // Если оба сделали commit — переход к reveal
        if (game.p1CommitHash != bytes32(0) && game.p2CommitHash != bytes32(0)) {
            game.phase = Phase.REVEAL;
            game.revealDeadline = block.timestamp + REVEAL_WINDOW;
        }
    }
    
    // Phase 2: раскрываем реальные действия
    function revealAction(uint256 gameId, uint8 action, bytes32 secret) external {
        GameState storage game = games[gameId];
        require(game.phase == Phase.REVEAL, "Not reveal phase");
        
        bytes32 expectedHash = keccak256(abi.encodePacked(action, secret));
        
        if (msg.sender == game.player1) {
            require(game.p1CommitHash == expectedHash, "Hash mismatch");
            game.p1Action = action;
        } else if (msg.sender == game.player2) {
            require(game.p2CommitHash == expectedHash, "Hash mismatch");
            game.p2Action = action;
        }
        
        // Если оба раскрыли — resolve
        if (game.p1Action != 0 && game.p2Action != 0) {
            _resolveGame(gameId);
        }
    }
    
    // Если игрок не reveal в срок — forfeit
    function claimTimeout(uint256 gameId) external {
        GameState storage game = games[gameId];
        require(game.phase == Phase.REVEAL, "Not reveal phase");
        require(block.timestamp > game.revealDeadline, "Deadline not passed");
        
        // Игрок который не reveal — проигрывает
        address winner;
        if (game.p1Action == 0 && game.p2Action != 0) {
            winner = game.player2;
        } else if (game.p2Action == 0 && game.p1Action != 0) {
            winner = game.player1;
        } else {
            // Оба не revealed — возврат ставок
            _refundBothPlayers(gameId);
            return;
        }
        
        _payWinner(gameId, winner);
    }
}

Скрытые карты / приватные данные

Для карточных игр (Poker, Hearthstone-like) нужно скрыть карты игрока от соперника. Варианты:

Mental poker (cryptographic card dealing): классический алгоритм на основе commutative encryption. Сложен в реализации, but fully trustless.

Trusted server (hybrid): сервер знает карты, но не может манипулировать (ключи разделены, commit-reveal). Большинство blockchain card games используют этот подход.

ZK proofs: игрок доказывает что его карта в допустимом диапазоне, не раскрывая её. Технически сложно, latency при proof generation.

Matchmaking и рейтинговая система

class EloMatchmaker {
  async findMatch(playerId: string): Promise<Match | null> {
    const player = await db.getPlayer(playerId);
    const searchRange = this.getSearchRange(player.waitTime);
    
    // Ищем оппонента в диапазоне рейтинга
    const candidates = await db.findAvailablePlayers({
      minRating: player.rating - searchRange,
      maxRating: player.rating + searchRange,
      excludeId: playerId,
    });
    
    if (candidates.length === 0) return null;
    
    // Выбираем наиболее близкий рейтинг
    const opponent = candidates.reduce((best, c) =>
      Math.abs(c.rating - player.rating) < Math.abs(best.rating - player.rating) ? c : best
    );
    
    return this.createMatch(player, opponent);
  }
  
  calculateNewRatings(
    winner: Player,
    loser: Player
  ): { winnerNew: number; loserNew: number } {
    const K = 32; // K-factor
    const expectedWin = 1 / (1 + Math.pow(10, (loser.rating - winner.rating) / 400));
    
    return {
      winnerNew: Math.round(winner.rating + K * (1 - expectedWin)),
      loserNew: Math.round(loser.rating + K * (0 - (1 - expectedWin))),
    };
  }
}

State channels для real-time PvP

On-chain каждый ход дорого и медленно. State channels решают это: открываем channel (on-chain deposit), играем off-chain, закрываем channel (on-chain settlement).

contract PvPStateChannel {
    struct Channel {
        address player1;
        address player2;
        uint256 player1Deposit;
        uint256 player2Deposit;
        uint256 channelNonce;  // версия состояния
        bytes32 stateHash;
        bool isOpen;
        uint256 disputeTimeout;
    }
    
    // Открытие channel
    function openChannel(address opponent) external payable returns (bytes32 channelId) {
        channelId = keccak256(abi.encodePacked(msg.sender, opponent, block.timestamp));
        
        channels[channelId] = Channel({
            player1: msg.sender,
            player2: opponent,
            player1Deposit: msg.value,
            player2Deposit: 0,
            channelNonce: 0,
            stateHash: bytes32(0),
            isOpen: true,
            disputeTimeout: 0,
        });
    }
    
    // Закрытие с согласованным финальным состоянием
    function closeChannel(
        bytes32 channelId,
        uint256 nonce,
        uint256 p1Balance,
        uint256 p2Balance,
        bytes calldata sig1,
        bytes calldata sig2
    ) external {
        Channel storage ch = channels[channelId];
        
        bytes32 stateHash = keccak256(abi.encodePacked(channelId, nonce, p1Balance, p2Balance));
        
        // Верифицируем подписи обоих игроков
        require(ECDSA.recover(stateHash, sig1) == ch.player1, "Invalid p1 sig");
        require(ECDSA.recover(stateHash, sig2) == ch.player2, "Invalid p2 sig");
        require(nonce > ch.channelNonce, "Stale state");
        require(p1Balance + p2Balance <= ch.player1Deposit + ch.player2Deposit, "Invalid balances");
        
        ch.isOpen = false;
        
        payable(ch.player1).transfer(p1Balance);
        payable(ch.player2).transfer(p2Balance);
    }
    
    // Если соперник не отвечает — dispute resolution
    function initiateDispute(
        bytes32 channelId,
        uint256 nonce,
        uint256 p1Balance,
        uint256 p2Balance,
        bytes calldata mySignature
    ) external {
        Channel storage ch = channels[channelId];
        require(ch.disputeTimeout == 0 || block.timestamp < ch.disputeTimeout);
        
        // Публикуем последнее известное состояние
        ch.stateHash = keccak256(abi.encodePacked(channelId, nonce, p1Balance, p2Balance));
        ch.channelNonce = nonce;
        ch.disputeTimeout = block.timestamp + DISPUTE_WINDOW;
    }
}

Анти-чит и валидация

Для on-chain PvP вся логика в смарт-контракте — читерство невозможно. Для off-chain + on-chain settlement нужна серверная валидация:

class GameValidator {
  async validateMove(
    gameState: GameState,
    move: Move,
    playerId: string
  ): Promise<ValidationResult> {
    // 1. Проверяем очерёдность хода
    if (gameState.currentTurn !== playerId) {
      return { valid: false, reason: "Not your turn" };
    }
    
    // 2. Проверяем допустимость хода по правилам игры
    const allowedMoves = this.getAllowedMoves(gameState, playerId);
    if (!allowedMoves.includes(move.type)) {
      return { valid: false, reason: "Invalid move" };
    }
    
    // 3. Проверяем что move не был уже сделан (replay protection)
    if (this.moveCache.has(move.id)) {
      return { valid: false, reason: "Duplicate move" };
    }
    
    // 4. Проверяем timestamp (move не старше N секунд)
    if (Date.now() - move.timestamp > MAX_MOVE_AGE_MS) {
      return { valid: false, reason: "Move too old" };
    }
    
    return { valid: true };
  }
}

Tournament система

contract PvPTournament {
    struct Tournament {
        uint256 entryFee;
        uint256 maxPlayers;
        address[] participants;
        TournamentType tournamentType; // SINGLE_ELIMINATION, ROUND_ROBIN, SWISS
        uint256 prizePool;
        TournamentStatus status;
    }
    
    // Prize distribution (в basis points)
    uint256[] public prizeDistribution = [5000, 3000, 1500, 500]; // 50%, 30%, 15%, 5%
    
    function registerForTournament(uint256 tournamentId) external payable {
        Tournament storage t = tournaments[tournamentId];
        require(t.participants.length < t.maxPlayers, "Tournament full");
        require(msg.value == t.entryFee, "Wrong entry fee");
        
        t.participants.push(msg.sender);
        t.prizePool += msg.value;
    }
    
    function distributePrizes(uint256 tournamentId, address[] calldata rankedPlayers) 
        external onlyAdmin 
    {
        Tournament storage t = tournaments[tournamentId];
        
        for (uint i = 0; i < prizeDistribution.length && i < rankedPlayers.length; i++) {
            uint256 prize = (t.prizePool * prizeDistribution[i]) / 10000;
            payable(rankedPlayers[i]).transfer(prize);
        }
    }
}

Стек

Компонент Технология
Smart contracts Solidity + Foundry, State Channels
Randomness Chainlink VRF v2.5
Real-time комм. WebSocket (Socket.io)
Game server Node.js + TypeScript
Matchmaking Redis sorted sets
Frontend React / Unity WebGL
L2 Arbitrum / Starknet (для ZK игр)
Anti-cheat Сервер-side validation + zkProofs

Сроки

  • 1v1 игра (Coinflip, RPS, базовая карточная): 4-6 недель
  • State Channel PvP: +3-4 недели
  • Tournament система: +3-4 недели
  • Сложная карточная/стратегия игра: 3-5 месяцев
  • Security audit: обязателен, +4-6 недель