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

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

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

HiLo — карточная игра с простой механикой: угадать, будет ли следующая карта выше или ниже текущей. При правильном угадывании умножитель растёт, в любой момент можно "кешаут" и забрать выигрыш. Простота механики делает HiLo идеальным примером для provably fair реализации на блокчейне — вся логика прозрачна и верифицируема.

Контракт: commit-reveal схема

Chainlink VRF для HiLo слишком медленный — игра предполагает быстрые решения. Используем server seed + client seed commit-reveal: казино публикует hash сида заранее, игрок добавляет свой seed, результат детерминирован из комбинации обоих. Ни казино не может знать seed игрока заранее, ни игрок не может знать server seed.

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

contract HiLoGame {
    uint8 constant DECK_SIZE = 52;

    struct Game {
        address player;
        bytes32 serverSeedHash;  // hash сида, опубликованный до игры
        bytes32 clientSeed;       // сид игрока, revealed в конце
        string serverSeed;        // revealed после игры
        uint256 betAmount;
        uint8 currentCard;        // текущая карта (0-51)
        uint8 position;           // позиция в колоде
        uint256 multiplier;       // x100 для точности (200 = 2.0x)
        bool active;
        bool cashed;
    }

    mapping(bytes32 => Game) public games;  // gameId → Game
    uint256 public constant HOUSE_EDGE = 100; // 1% (100/10000)

    event GameStarted(bytes32 indexed gameId, address player, uint8 firstCard);
    event CardRevealed(bytes32 indexed gameId, uint8 card, uint256 multiplier);
    event GameCashed(bytes32 indexed gameId, uint256 payout);
    event GameLost(bytes32 indexed gameId, uint8 card);

    // Казино публикует hash server seed до начала игры
    function startGame(
        bytes32 serverSeedHash,
        bytes32 clientSeed
    ) external payable returns (bytes32 gameId) {
        require(msg.value > 0, "Bet required");

        gameId = keccak256(abi.encodePacked(
            msg.sender, serverSeedHash, clientSeed, block.timestamp
        ));

        // Генерируем первую карту из clientSeed + serverSeedHash
        // (server seed ещё не known, но hash зафиксирован)
        uint8 firstCard = _deriveCard(serverSeedHash, clientSeed, 0);

        games[gameId] = Game({
            player: msg.sender,
            serverSeedHash: serverSeedHash,
            clientSeed: clientSeed,
            serverSeed: "",
            betAmount: msg.value,
            currentCard: firstCard,
            position: 0,
            multiplier: 100, // 1.0x
            active: true,
            cashed: false
        });

        emit GameStarted(gameId, msg.sender, firstCard);
    }

    // Игрок делает выбор: Higher (true) или Lower (false)
    // Казино вызывает reveal следующей карты вместе с частичным server seed
    function revealNextCard(
        bytes32 gameId,
        string calldata serverSeedPartial, // частичный reveal для верификации
        bool guessHigher
    ) external {
        Game storage game = games[gameId];
        require(game.active, "Game not active");
        require(msg.sender == owner() || msg.sender == gameServer, "Unauthorized");

        uint8 nextCard = _deriveCard(
            game.serverSeedHash,
            game.clientSeed,
            game.position + 1
        );

        bool correct;
        if (guessHigher) {
            correct = _cardValue(nextCard) > _cardValue(game.currentCard);
        } else {
            correct = _cardValue(nextCard) < _cardValue(game.currentCard);
        }

        // Ничья (равные карты) — проигрыш
        if (_cardValue(nextCard) == _cardValue(game.currentCard)) {
            correct = false;
        }

        game.position++;
        game.currentCard = nextCard;

        if (!correct) {
            game.active = false;
            emit GameLost(gameId, nextCard);
            return;
        }

        // Обновляем multiplier: вероятность угадать * house edge
        uint256 probability = _calculateProbability(game.currentCard, guessHigher);
        game.multiplier = (game.multiplier * 9900) / probability; // 9900 = 99% (1% house edge)

        emit CardRevealed(gameId, nextCard, game.multiplier);
    }

    // Игрок забирает выигрыш
    function cashout(bytes32 gameId) external {
        Game storage game = games[gameId];
        require(game.active, "Game not active");
        require(msg.sender == game.player, "Not your game");

        game.active = false;
        game.cashed = true;

        uint256 payout = (game.betAmount * game.multiplier) / 100;
        payable(game.player).transfer(payout);

        emit GameCashed(gameId, payout);
    }

    // После окончания игры казино reveals полный server seed
    // Игрок может верифицировать: hash(serverSeed) == serverSeedHash
    function revealServerSeed(bytes32 gameId, string calldata serverSeed) external {
        Game storage game = games[gameId];
        require(!game.active, "Game still active");
        require(
            keccak256(bytes(serverSeed)) == game.serverSeedHash,
            "Invalid server seed"
        );
        game.serverSeed = serverSeed;
    }

    function _deriveCard(
        bytes32 serverSeedHash,
        bytes32 clientSeed,
        uint8 position
    ) internal pure returns (uint8) {
        bytes32 combined = keccak256(abi.encodePacked(serverSeedHash, clientSeed, position));
        return uint8(uint256(combined) % DECK_SIZE);
    }

    function _cardValue(uint8 card) internal pure returns (uint8) {
        return (card % 13) + 1; // 1=Ace, 13=King
    }

    function _calculateProbability(uint8 currentCard, bool higher) internal pure returns (uint256) {
        uint8 value = _cardValue(currentCard);
        uint256 cardsHigher = 13 - value;
        uint256 cardsLower = value - 1;
        // Возвращаем probability * 100 (для точности)
        if (higher) return (cardsHigher * 100 * 100) / 13; // *100 для масштаба multiplier
        return (cardsLower * 100 * 100) / 13;
    }

    receive() external payable {}
    address public gameServer;
    address public owner;
    constructor() { owner = msg.sender; gameServer = msg.sender; }
    modifier onlyOwner() { require(msg.sender == owner); _; }
}

Верифицируемость для игрока

Провабл-фэйр работает так: после игры пользователь берёт revealed serverSeed и вычисляет keccak256(serverSeed) — если совпадает с serverSeedHash, опубликованным до игры, казино не подменяло сид. Затем воспроизводит последовательность карт через _deriveCard — результаты должны совпасть с игрой.

// Клиентская верификация (JavaScript)
import { keccak256, encodePacked } from "viem";

function verifyGame(
  serverSeed: string,
  serverSeedHash: string,
  clientSeed: string,
  cards: number[]
): boolean {
  // Проверяем, что hash совпадает
  const computedHash = keccak256(new TextEncoder().encode(serverSeed));
  if (computedHash !== serverSeedHash) return false;

  // Воспроизводим карты
  for (let i = 0; i < cards.length; i++) {
    const combined = keccak256(
      encodePacked(["bytes32", "bytes32", "uint8"], [serverSeedHash, clientSeed as `0x${string}`, i])
    );
    const card = Number(BigInt(combined) % 52n);
    if (card !== cards[i]) return false;
  }

  return true;
}

Фронтенд

Анимация карт — CSS flip transitions или Pixi.js. Ключевые элементы UI: текущая карта, история последних 5 карт, текущий multiplier, кнопки Higher/Lower, кнопка Cash Out. Multiplier должен обновляться анимированно при каждом правильном угадывании — это core feedback loop игры.

Для быстрого feedback без ожидания on-chain confirmation — используем optimistic UI: показываем результат немедленно на основе данных от game server, подтверждаем on-chain асинхронно. Если on-chain транзакция фейлится — откатываем состояние.