Разработка игры Poker на блокчейне
Покер на блокчейне — задача, которая выглядит проще, чем есть на самом деле. Смарт-контракт гарантирует прозрачность и честные выплаты. Но карты нельзя показывать on-chain до вскрытия — это уничтожает игру. Значит нужно скрыть информацию в публичной среде, где всё видно всем. Это задача mental poker, формально описанная ещё в 1979 году Шамиром, Ривестом и Эдлманом. Практическая реализация требует выбора: полноценная криптография (сложно и дорого) или trusted off-chain компонент (проще, но требует доверия).
Фундаментальная проблема: скрытие информации on-chain
В традиционном покере дилер физически раздаёт карты лицом вниз. В публичном блокчейне все данные транзакций видны всем нодам. Если записать карты в state контракта — любой нод читает их напрямую. Если зашифровать — кто держит ключ?
Подход 1: Mental Poker с commit-reveal
Классический криптографический подход:
- Каждый игрок вносит случайный seed для shuffling (commit phase)
- Deck перемешивается детерминированно из комбинации всех seed
- Каждая карта шифруется ключом каждого игрока (layered encryption)
- Карта открывается только когда все игроки раскрыли свою часть ключа
Проблема: при 6 игроках каждая карта зашифрована 6 слоями. Открытие требует 6 on-chain транзакций. При 5 улицах покера — десятки транзакций на руку. Gas и latency неприемлемы для mainnet.
Подход 2: ZK Proof для скрытых карт
ZK-SNARK позволяет доказать «игрок держит карту с нужной ценностью для выплаты» без раскрытия самой карты до showdown. Проекты типа ZK-Holdem (на базе Circom) пробуют этот путь.
Сложность: ZK схемы для покерных правил (full hand evaluation) — нетривиальная задача. Proof generation на мобильном устройстве — медленно (секунды). На desktopе — приемлемо.
Подход 3: Trusted Execution Environment (TEE)
Дилерский сервис запускается в Intel SGX или AWS Nitro Enclave — изолированной среде, где даже оператор сервера не видит данные. Карты раздаются внутри TEE, игроки видят только свои карты через зашифрованный канал. На блокчейн идут только результаты раундов и обязательства (commitments).
Компромисс: доверие к TEE производителю (Intel). Для большинства gaming приложений это приемлемо — это не хуже доверия казино дилеру.
Подход 4: Off-chain game server + on-chain settlement
Наиболее практичный вариант для production: игровой сервер ведёт состояние игры off-chain, игроки подписывают ходы (bet, fold, raise) через state channel, итоговый результат записывается on-chain для выплаты.
Players → [Game Server] → manages hidden cards, game state
↕ signed moves (state channel)
↓ final result + signatures
[Settlement Contract] → pays out winners
State Channel архитектура
State channel — идеальная модель для покера. Игроки открывают channel, вносят депозит, все ходы — это подписанные off-chain сообщения. On-chain транзакция только для открытия и закрытия channel.
contract PokerStateChannel {
struct Channel {
address[6] players;
uint256[6] deposits;
uint256 totalPot;
bytes32 stateHash; // хэш текущего состояния игры
uint256 nonce; // счётчик ходов
ChannelStatus status;
}
struct PlayerMove {
uint8 playerId;
MoveType moveType; // BET, RAISE, CALL, FOLD, CHECK
uint256 amount;
uint256 channelNonce; // должен соответствовать nonce channel
bytes signature; // подпись игрока
}
function openChannel(address[6] calldata players, uint256[6] calldata deposits)
external payable returns (bytes32 channelId) {
// Verify all deposits
uint256 totalDeposit = 0;
for (uint i = 0; i < 6; i++) totalDeposit += deposits[i];
require(msg.value == totalDeposit, "Incorrect deposit");
channelId = keccak256(abi.encode(players, block.timestamp, block.prevrandao));
channels[channelId] = Channel({
players: players,
deposits: deposits,
totalPot: totalDeposit,
stateHash: bytes32(0),
nonce: 0,
status: ChannelStatus.OPEN
});
}
function closeChannel(
bytes32 channelId,
uint256[6] calldata finalBalances,
bytes[6] calldata playerSignatures
) external {
Channel storage ch = channels[channelId];
require(ch.status == ChannelStatus.OPEN, "Channel not open");
// Верифицируем подписи всех игроков на final state
bytes32 finalStateHash = keccak256(abi.encode(channelId, finalBalances, ch.nonce));
for (uint i = 0; i < 6; i++) {
require(
ECDSA.recover(ECDSA.toEthSignedMessageHash(finalStateHash), playerSignatures[i])
== ch.players[i],
"Invalid player signature"
);
}
// Выплачиваем
for (uint i = 0; i < 6; i++) {
if (finalBalances[i] > 0) {
payable(ch.players[i]).transfer(finalBalances[i]);
}
}
ch.status = ChannelStatus.CLOSED;
}
}
Dispute механизм
Что если игровой сервер исчез или пытается смошенничать? State channel должен иметь dispute resolution:
Timeout-based: если игрок не получил ответ в течение N блоков, он может начать dispute, предъявив последнее подписанное состояние. Counter-party должен ответить более новым состоянием. Если не отвечает — timeout player wins.
Forced reveal: в showdown все активные игроки обязаны раскрыть карты on-chain в течение timeout. Если не раскрыл — считается fold. Это защита от стратегии «отключиться при проигрышном showdown».
function initiateDispute(bytes32 channelId, GameState calldata lastKnownState, bytes calldata sig)
external {
Channel storage ch = channels[channelId];
require(ch.players[_getPlayerId(channelId, msg.sender)] == msg.sender, "Not player");
disputes[channelId] = Dispute({
challenger: msg.sender,
challengeState: lastKnownState,
challengeTime: block.timestamp,
resolved: false
});
ch.status = ChannelStatus.DISPUTED;
emit DisputeInitiated(channelId, msg.sender, lastKnownState.nonce);
}
Fairness: верифицируемое тасование
Игровой сервер тасует колоду. Как доказать, что не жульничает, зная карты игроков?
Commit-Reveal shuffling:
- До начала раздачи сервер публикует хэш seed:
commitment = hash(seed + salt) - Игроки вносят свои entropy contributions
- Final deck = shuffle(seed XOR player_entropy_1 XOR ... XOR player_entropy_N)
- После игры сервер раскрывает seed — все могут верифицировать тасование
// Server side
const serverSeed = crypto.randomBytes(32)
const commitment = keccak256(concat([serverSeed, salt]))
await contract.publishCommitment(channelId, commitment)
// After all player entropy received:
const finalSeed = xorAll([serverSeed, ...playerEntropyContributions])
const deck = shuffleDeck(standardDeck, finalSeed) // deterministic Fisher-Yates
// After game:
await contract.revealSeed(channelId, serverSeed, salt)
// Anyone can verify: hash(serverSeed + salt) == commitment
// And: shuffle(standardDeck, serverSeed XOR playerEntropy) == used deck
Game Logic Off-chain
Покерная логика (Texas Hold'em hand evaluation, betting rounds, pot management) — полностью off-chain на сервере. Контракт занимается только: deposit, state commitments, dispute, payout.
// Hand evaluator
import { Hand } from 'pokersolver'
function evaluateHand(holeCards: Card[], communityCards: Card[]): HandResult {
const hand = Hand.solve([...holeCards, ...communityCards].map(c => c.toString()))
return {
rank: hand.rank,
name: hand.name, // 'Royal Flush', 'Full House', etc.
value: hand.value,
cards: hand.cards
}
}
function determineWinner(players: ActivePlayer[], communityCards: Card[]): Winner[] {
const hands = players.map(p => ({
player: p,
hand: Hand.solve([...p.holeCards, ...communityCards].map(c => c.toString()))
}))
const winners = Hand.winners(hands.map(h => h.hand))
return winners.map(w => hands.find(h => h.hand === w)!.player)
}
NFT и игровые активы
Player avatars / profile NFT. Cosmetic NFTs не влияют на gameplay, но дают identity и вторичный рынок. ERC-721 с dynamic metadata (win rate, games played) через tokenURI с on-chain или off-chain данными.
Poker table NFT. Приватный стол как NFT: владелец NFT управляет настройками стола (rake %, blinds structure, invite-only), получает часть rake. Пассивный доход для NFT holders.
Chip sets и card deck skins. Pure cosmetic, но значимо для retention. ERC-1155 для fungible cosmetics.
Токен-экономика и rake
Rake — комиссия с каждого pot, аналог казино. 2-5% от pot — стандарт. В on-chain покере rake идёт в treasury протокола. Распределение:
Pot rake (3%) → 50% burn / buyback game token
→ 30% staking rewards (stakers = liquidity providers)
→ 20% development fund
Rakeback NFT. Игроки с определённым NFT получают частичный возврат rake. Incentive to hold NFT, sink для токена (NFT покупается за токены).
Стек
| Компонент | Технология |
|---|---|
| Smart contracts | Solidity + Foundry |
| State channels | Nitro Protocol / кастомный |
| Game server | Node.js + TypeScript |
| Real-time | WebSocket (Socket.io) |
| Frontend | React + Three.js / Pixi.js для стола |
| Wallet | wagmi + WalletConnect v2 |
| ZK (если выбран) | Circom + snarkjs |
| TEE (если выбран) | AWS Nitro Enclaves |
Процесс разработки
Архитектурное решение (1 неделя). Выбор подхода к скрытию карт: off-chain trusted server, TEE, или ZK. Это определяет всё.
Smart contracts (3-4 недели). State channel контракт, dispute mechanism, NFT контракты, settlement логика.
Game server (3-5 недель). Покерная логика, state management, WebSocket мультиплеер, commit-reveal shuffling, anti-cheat.
Frontend (4-6 недель). 3D или 2D стол, анимации раздачи, betting UI, real-time ходы других игроков, wallet интеграция.
Безопасность и аудит. Аудит state channel контракта и dispute mechanism — критично. Экономическое тестирование rake модели.
MVP с off-chain сервером и базовым state channel — 3-4 месяца. Production с ZK или TEE, полным NFT ecosystem, lobby системой — 6-9 месяцев.







