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 testContract
// 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
euint8euint64externalEuint8ebool
Tags
gambling poker cards privacy gaming
Related Examples
Prerequisites
Before this example, you should understand:
Next Steps
After this example, check out:
Generated with Lab-Z
Last updated
