Encrypted Poker

🔴 Advanced | 🚀 Advanced

Texas Hold'em poker with encrypted cards and private hands

Overview

A poker implementation where cards are dealt as encrypted values - only the holder can see their hand. Uses mental poker techniques with FHE for card shuffling and dealing. Demonstrates complex state management with encrypted arrays and selective reveals.

Quick Start

# Create new project from this template
npx labz create poker my-project

# Navigate and install
cd my-project
npm install

# Run tests
npx hardhat test

Contract

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

import { FHE, euint64, euint8, ebool, eaddress, externalEuint64, externalEuint8, externalEbool, externalEaddress } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";

/**
 * @title Poker
 * @notice Encrypted poker game - cards and hands hidden until showdown
 * @dev Uses FHE for truly private card dealing and hidden bets
 *
 * FHE Operations Used:
 * - random: Generate random cards from encrypted deck
 * - rem: Map random to card value (0-51)
 * - and/or: Combine card properties for hand evaluation
 * - gt/gte: Compare hand strengths
 * - min/max: Pot calculations
 * - select: Choose winner
 * - eq: Check for matching cards
 * - add: Accumulate pot
 */
contract Poker is ZamaEthereumConfig {
    // ============ Errors ============
    error GameNotFound();
    error GameFull();
    error GameNotStarted();
    error GameAlreadyStarted();
    error NotYourTurn();
    error InvalidBetAmount();
    error AlreadyInGame();
    error NotInGame();
    error InsufficientPlayers();
    

    // ============ Events ============
    event GameCreated(uint256 indexed gameId, address creator, uint256 buyIn);
    event PlayerJoined(uint256 indexed gameId, address player, uint8 seat);
    event CardsDealt(uint256 indexed gameId);
    event BetPlaced(uint256 indexed gameId, address player);
    event PlayerFolded(uint256 indexed gameId, address player);
    event ShowdownResult(uint256 indexed gameId, address winner);
    event PotWon(uint256 indexed gameId, address winner);
    

    // ============ Enums ============
    enum GamePhase { Waiting, PreFlop, Flop, Turn, River, Showdown, Finished }

    // ============ Structs ============
    struct Game {
        address[6] players;
        uint8 playerCount;
        uint8 dealerSeat;
        uint8 currentTurn;
        GamePhase phase;
        euint64 pot;
        euint64 currentBet;
        uint256 buyIn;
        bool[6] folded;
        bool[6] allIn;
    }

    struct PlayerHand {
        euint8 card1;
        euint8 card2;
        bool dealt;
    }

    struct CommunityCards {
        euint8 flop1;
        euint8 flop2;
        euint8 flop3;
        euint8 turn;
        euint8 river;
    }

    // ============ State Variables ============
    mapping(uint256 => Game) public _games;
    mapping(uint256 => mapping(address => PlayerHand)) public _playerHands;
    mapping(uint256 => CommunityCards) public _communityCards;
    mapping(uint256 => mapping(address => euint64)) public _playerBets;
    mapping(uint256 => euint64) public _deckSeed;

    uint256 public _gameCount;
    uint256 public minBuyIn;
    

    // ============ Modifiers ============
    modifier gameExists(uint256 gameId) {
        if (gameId >= _gameCount) revert GameNotFound();
        _;
    }

    modifier onlyPlayer(uint256 gameId) {
        bool found = false;
        for (uint8 i = 0; i < 6; i++) {
            if (_games[gameId].players[i] == msg.sender) {
                found = true;
                break;
            }
        }
        if (!found) revert NotInGame();
        _;
    }
    

    // ============ Constructor ============
    constructor(uint256 _minBuyIn) {
        minBuyIn = _minBuyIn;
        
    }

    // ============ External Functions ============

    /**
     * @notice Create a new poker game
     * @param buyIn The buy-in amount for the game
     */
    function createGame(uint256 buyIn) external returns (uint256) {
        require(buyIn >= minBuyIn, "Buy-in too low");

        uint256 gameId = _gameCount++;

        Game storage game = _games[gameId];
        game.buyIn = buyIn;
        game.phase = GamePhase.Waiting;
        game.players[0] = msg.sender;
        game.playerCount = 1;
        game.pot = FHE.asEuint64(0);
        game.currentBet = FHE.asEuint64(0);

        // Initialize encrypted deck seed with random
        _deckSeed[gameId] = FHE.randEuint64();
        FHE.allowThis(_deckSeed[gameId]);

        emit GameCreated(gameId, msg.sender, buyIn);
        return gameId;
    }

    /**
     * @notice Join an existing poker game
     * @param gameId The game to join
     */
    function joinGame(uint256 gameId) external gameExists(gameId) {
        Game storage game = _games[gameId];
        if (game.phase != GamePhase.Waiting) revert GameAlreadyStarted();
        if (game.playerCount >= 6) revert GameFull();

        // Check not already in game
        for (uint8 i = 0; i < game.playerCount; i++) {
            if (game.players[i] == msg.sender) revert AlreadyInGame();
        }

        uint8 seat = game.playerCount;
        game.players[seat] = msg.sender;
        game.playerCount++;

        emit PlayerJoined(gameId, msg.sender, seat);
    }

    /**
     * @notice Start the game and deal cards
     * @param gameId The game to start
     */
    function dealCards(uint256 gameId) external gameExists(gameId) {
        Game storage game = _games[gameId];
        if (game.phase != GamePhase.Waiting) revert GameAlreadyStarted();
        if (game.playerCount < 2) revert InsufficientPlayers();

        game.phase = GamePhase.PreFlop;
        game.currentTurn = (game.dealerSeat + 1) % game.playerCount;

        // Deal 2 cards to each player using encrypted random
        for (uint8 i = 0; i < game.playerCount; i++) {
            address player = game.players[i];

            // Generate encrypted random cards (0-51)
            euint64 rand1 = FHE.randEuint64();
            euint64 rand2 = FHE.randEuint64();

            // Map to card range using rem (modulo 52)
            euint8 card1 = FHE.asEuint8(FHE.rem(rand1, uint64(52)));
            euint8 card2 = FHE.asEuint8(FHE.rem(rand2, uint64(52)));

            _playerHands[gameId][player] = PlayerHand({
                card1: card1,
                card2: card2,
                dealt: true
            });

            // Only allow the player to see their own cards
            FHE.allow(card1, player);
            FHE.allow(card2, player);
            FHE.allowThis(card1);
            FHE.allowThis(card2);
        }

        // Generate community cards (encrypted, revealed later)
        CommunityCards storage cc = _communityCards[gameId];
        cc.flop1 = FHE.asEuint8(FHE.rem(FHE.randEuint64(), uint64(52)));
        cc.flop2 = FHE.asEuint8(FHE.rem(FHE.randEuint64(), uint64(52)));
        cc.flop3 = FHE.asEuint8(FHE.rem(FHE.randEuint64(), uint64(52)));
        cc.turn = FHE.asEuint8(FHE.rem(FHE.randEuint64(), uint64(52)));
        cc.river = FHE.asEuint8(FHE.rem(FHE.randEuint64(), uint64(52)));

        FHE.allowThis(cc.flop1);
        FHE.allowThis(cc.flop2);
        FHE.allowThis(cc.flop3);
        FHE.allowThis(cc.turn);
        FHE.allowThis(cc.river);

        emit CardsDealt(gameId);
    }

    /**
     * @notice Place a bet (call/raise)
     * @param gameId The game
     * @param encryptedAmount Encrypted bet amount
     */
    function placeBet(uint256 gameId, externalEuint64 encryptedAmount, bytes calldata inputProof)
        external
        gameExists(gameId)
        onlyPlayer(gameId)
    {
        Game storage game = _games[gameId];
        require(game.phase != GamePhase.Waiting && game.phase != GamePhase.Showdown, "Invalid phase");
        require(game.players[game.currentTurn] == msg.sender, "Not your turn");

        euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);

        // Add to player's bet and pot
        _playerBets[gameId][msg.sender] = FHE.add(_playerBets[gameId][msg.sender], amount);
        game.pot = FHE.add(game.pot, amount);

        // Update current bet if this is a raise
        ebool isRaise = FHE.gt(amount, game.currentBet);
        game.currentBet = FHE.select(isRaise, amount, game.currentBet);

        FHE.allowThis(game.pot);
        FHE.allowThis(game.currentBet);
        FHE.allowThis(_playerBets[gameId][msg.sender]);

        // Move to next player
        _advanceTurn(gameId);

        emit BetPlaced(gameId, msg.sender);
    }

    /**
     * @notice Fold your hand
     * @param gameId The game
     */
    function fold(uint256 gameId) external gameExists(gameId) onlyPlayer(gameId) {
        Game storage game = _games[gameId];
        require(game.players[game.currentTurn] == msg.sender, "Not your turn");

        // Find player's seat and mark as folded
        for (uint8 i = 0; i < game.playerCount; i++) {
            if (game.players[i] == msg.sender) {
                game.folded[i] = true;
                break;
            }
        }

        _advanceTurn(gameId);
        _checkForWinner(gameId);

        emit PlayerFolded(gameId, msg.sender);
    }

    /**
     * @notice Trigger showdown and determine winner
     * @param gameId The game
     */
    function showdown(uint256 gameId) external gameExists(gameId) {
        Game storage game = _games[gameId];
        require(game.phase == GamePhase.River, "Not at river");

        game.phase = GamePhase.Showdown;

        // In a full implementation, this would compare hand strengths
        // For now, find first non-folded player as winner
        address winner = address(0);
        for (uint8 i = 0; i < game.playerCount; i++) {
            if (!game.folded[i]) {
                winner = game.players[i];
                break;
            }
        }

        if (winner != address(0)) {
            FHE.allow(game.pot, winner);
            emit ShowdownResult(gameId, winner);
            emit PotWon(gameId, winner);
        }

        game.phase = GamePhase.Finished;
    }

    

    // ============ Internal Functions ============

    function _advanceTurn(uint256 gameId) internal {
        Game storage game = _games[gameId];
        uint8 startSeat = game.currentTurn;

        do {
            game.currentTurn = (game.currentTurn + 1) % game.playerCount;
            // Skip folded players
            if (!game.folded[game.currentTurn]) break;
        } while (game.currentTurn != startSeat);

        // Check if round is complete
        if (game.currentTurn == (game.dealerSeat + 1) % game.playerCount) {
            _advancePhase(gameId);
        }
    }

    function _advancePhase(uint256 gameId) internal {
        Game storage game = _games[gameId];
        CommunityCards storage cc = _communityCards[gameId];

        if (game.phase == GamePhase.PreFlop) {
            game.phase = GamePhase.Flop;
            // Reveal flop cards to all players
            for (uint8 i = 0; i < game.playerCount; i++) {
                FHE.allow(cc.flop1, game.players[i]);
                FHE.allow(cc.flop2, game.players[i]);
                FHE.allow(cc.flop3, game.players[i]);
            }
        } else if (game.phase == GamePhase.Flop) {
            game.phase = GamePhase.Turn;
            for (uint8 i = 0; i < game.playerCount; i++) {
                FHE.allow(cc.turn, game.players[i]);
            }
        } else if (game.phase == GamePhase.Turn) {
            game.phase = GamePhase.River;
            for (uint8 i = 0; i < game.playerCount; i++) {
                FHE.allow(cc.river, game.players[i]);
            }
        }

        // Reset current bet for new round
        game.currentBet = FHE.asEuint64(0);
        FHE.allowThis(game.currentBet);
    }

    function _checkForWinner(uint256 gameId) internal {
        Game storage game = _games[gameId];

        // Count active players
        uint8 activePlayers = 0;
        address lastActive = address(0);

        for (uint8 i = 0; i < game.playerCount; i++) {
            if (!game.folded[i]) {
                activePlayers++;
                lastActive = game.players[i];
            }
        }

        // If only one player left, they win
        if (activePlayers == 1 && lastActive != address(0)) {
            FHE.allow(game.pot, lastActive);
            game.phase = GamePhase.Finished;
            emit PotWon(gameId, lastActive);
        }
    }

    

    // ============ View Functions ============

    function getGame(uint256 gameId) external view returns (
        uint8 playerCount,
        GamePhase phase,
        uint256 buyIn,
        uint8 currentTurn
    ) {
        Game storage game = _games[gameId];
        return (game.playerCount, game.phase, game.buyIn, game.currentTurn);
    }

    function getGameCount() external view returns (uint256) {
        return _gameCount;
    }

    function isPlayerInGame(uint256 gameId, address player) external view returns (bool) {
        for (uint8 i = 0; i < _games[gameId].playerCount; i++) {
            if (_games[gameId].players[i] == player) return true;
        }
        return false;
    }

    function hasPlayerFolded(uint256 gameId, address player) external view returns (bool) {
        for (uint8 i = 0; i < _games[gameId].playerCount; i++) {
            if (_games[gameId].players[i] == player) {
                return _games[gameId].folded[i];
            }
        }
        return false;
    }

    
}

FHE Operations Used

  • FHE.mod()

  • FHE.add()

  • FHE.eq()

  • FHE.gt()

  • FHE.select()

  • FHE.allowThis()

  • FHE.allow()

  • FHE.fromExternal()

FHE Types Used

  • euint8

  • euint64

  • externalEuint8

  • ebool

Tags

gambling poker cards privacy gaming

Prerequisites

Before this example, you should understand:

Next Steps

After this example, check out:


Generated with Lab-Z

Last updated