Skip to content

Latest commit

Β 

History

History
779 lines (578 loc) Β· 21.6 KB

File metadata and controls

779 lines (578 loc) Β· 21.6 KB

πŸƒ @pokertools/engine

Enterprise-grade Texas Hold'em poker game engine

npm version License: MIT TypeScript Tests

A production-ready poker game engine featuring immutable state management, chip conservation auditing, side pot calculation, rake handling, tournament support, and comprehensive rule enforcement.


πŸ“‘ Table of Contents


✨ Features

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          ENGINE FEATURES                                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  🎰 Complete Texas Hold'em Implementation                                   β”‚
β”‚  ♻️  Immutable State Machine (Redux-style)                                  β”‚
β”‚  πŸ’° Chip Conservation Auditing                                              β”‚
β”‚  🏦 Side Pot Calculation                                                    β”‚
β”‚  πŸ“Š Rake Support (% + cap + noFlopNoDrop)                                   β”‚
β”‚  πŸ† Tournament Mode (blind structure)                                       β”‚
β”‚  πŸ‘€ View Masking (anti-cheat)                                               β”‚
β”‚  πŸ’Ύ Snapshot Serialization                                                  β”‚
β”‚  ↩️  Undo Support                                                           β”‚
β”‚  πŸ“œ Hand History Export (JSON, PokerStars)                                  β”‚
β”‚  🌐 Browser Support (Web Crypto RNG)                                        β”‚
β”‚  πŸ”’ Type-safe Error Handling                                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ“¦ Installation

npm install @pokertools/engine
yarn add @pokertools/engine
pnpm add @pokertools/engine

πŸš€ Quick Start

import { PokerEngine, ActionType } from "@pokertools/engine";

// Create a cash game table
const engine = new PokerEngine({
  smallBlind: 1,
  bigBlind: 2,
  maxPlayers: 6,
});

// Seat players
engine.sit(0, "alice", "Alice", 200);
engine.sit(1, "bob", "Bob", 200);

// Deal a hand
engine.deal();

// Get current state
console.log(engine.state.street); // "PREFLOP"
console.log(engine.state.actionTo); // 0 (Alice's turn)

// Execute actions
engine.act({ type: ActionType.CALL, playerId: "alice" });
engine.act({ type: ActionType.CHECK, playerId: "bob" });

// Get player view (masked for opponents)
const aliceView = engine.view("alice");

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           ARCHITECTURE                                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚        PokerEngine           β”‚
                    β”‚   (Stateful API Wrapper)     β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
                                   β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚        gameReducer           β”‚
                    β”‚   f(state, action) => state  β”‚
                    β”‚     (Pure Function)          β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                         β”‚                         β”‚
         β–Ό                         β–Ό                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    Actions      β”‚     β”‚     Rules       β”‚     β”‚    Utilities    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β€’ betting.ts    β”‚     β”‚ β€’ actionOrder   β”‚     β”‚ β€’ viewMasking   β”‚
β”‚ β€’ dealing.ts    β”‚     β”‚ β€’ blinds        β”‚     β”‚ β€’ serialization β”‚
β”‚ β€’ management.ts β”‚     β”‚ β€’ headsUp       β”‚     β”‚ β€’ invariants    β”‚
β”‚ β€’ showdown.ts   β”‚     β”‚ β€’ showdown      β”‚     β”‚ β€’ rake          β”‚
β”‚ β€’ special.ts    β”‚     β”‚ β€’ sidePots      β”‚     β”‚ β€’ deck          β”‚
β”‚ β€’ tournament.ts β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚ β€’ cardUtils     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

State Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ PREFLOP β”‚ ──▢  β”‚  FLOP   β”‚ ──▢  β”‚  TURN   β”‚ ──▢  β”‚  RIVER  β”‚ ──▢  β”‚ SHOWDOWN β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚                                                                    β”‚
     β”‚                    All but one fold                                β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              (Award pot)

πŸ“– API Reference

PokerEngine Class

Constructor

import { PokerEngine } from "@pokertools/engine";

const engine = new PokerEngine(config: TableConfig, timeProvider?: () => number);

TableConfig Options:

Option Type Default Description
smallBlind number required Small blind amount
bigBlind number required Big blind amount
ante number 0 Ante per player
maxPlayers number 9 Maximum seats (2-10)
blindStructure BlindLevel[] - Tournament blind levels
timeBankSeconds number 30 Time bank per player
rakePercent number 0 Rake percentage (0-100)
rakeCap number - Maximum rake per pot
noFlopNoDrop boolean true No rake if hand ends preflop
randomProvider () => number getSecureRandom() RNG function (Node.js crypto; never Math.random)
validateIntegrity boolean true Enable chip auditing
isClient boolean false Client/optimistic mode

Core Methods

sit(seat, id, name, stack)

Add a player to the table.

engine.sit(0, "user123", "Alice", 1000);
stand(id)

Remove a player from the table.

engine.stand("user123");
deal()

Deal a new hand.

engine.deal();
act(action)

Execute a game action.

// Fold
engine.act({ type: ActionType.FOLD, playerId: "user123" });

// Check
engine.act({ type: ActionType.CHECK, playerId: "user123" });

// Call
engine.act({ type: ActionType.CALL, playerId: "user123" });

// Bet (opening bet)
engine.act({ type: ActionType.BET, playerId: "user123", amount: 100 });

// Raise
engine.act({ type: ActionType.RAISE, playerId: "user123", amount: 200 });

// Show cards at showdown
engine.act({ type: ActionType.SHOW, playerId: "user123", cardIndices: [0, 1] });

// Muck cards at showdown
engine.act({ type: ActionType.MUCK, playerId: "user123" });

// Activate time bank
engine.act({ type: ActionType.TIME_BANK, playerId: "user123" });

State Access

state (getter)

Get full unmasked game state.

const state = engine.state;
console.log(state.street); // "PREFLOP" | "FLOP" | "TURN" | "RIVER" | "SHOWDOWN"
console.log(state.actionTo); // Seat number of current actor
console.log(state.board); // Community cards ["As", "Kd", "Qh"]
console.log(state.pots); // Array of pot objects
console.log(state.winners); // null or Winner[] after showdown
view(playerId?, version?)

Get player-specific view with opponent cards masked.

// Player view (sees own cards)
const aliceView = engine.view("alice");

// Spectator view (all hole cards hidden)
const spectatorView = engine.view();

// With version number for sync
const versioned = engine.view("alice", 42);

Validation

validate(action)

Check if action is valid without executing.

const result = engine.validate({
  type: ActionType.BET,
  playerId: "alice",
  amount: 100,
});

if (result.valid) {
  // Action can be executed
} else {
  console.log(result.error); // "Cannot bet, there's already a bet to call"
  console.log(result.code); // "CANNOT_BET"
}

Serialization

snapshot (getter)

Get serializable snapshot.

const snapshot = engine.snapshot;
localStorage.setItem("game", JSON.stringify(snapshot));
PokerEngine.restore(snapshot)

Restore from snapshot.

const saved = JSON.parse(localStorage.getItem("game")!);
const engine = PokerEngine.restore(saved);

Event Handling

on(callback)

Subscribe to state changes.

const unsubscribe = engine.on((action, oldState, newState) => {
  console.log(`Action: ${action.type}`);
  console.log(`Street: ${oldState.street} -> ${newState.street}`);
});

// Later: unsubscribe
unsubscribe();

Undo

undo()

Undo last action.

const success = engine.undo();
if (success) {
  console.log("Action undone");
}

Tournament

nextBlindLevel()

Advance to next blind level.

engine.nextBlindLevel();
console.log(engine.state.blindLevel); // 1
console.log(engine.state.bigBlind); // Updated

Hand History

history(options?)

Export hand history in various formats.

// JSON format (default)
const json = engine.history();

// PokerStars format
const ps = engine.history({ format: "pokerstars" });

// Compact JSON
const compact = engine.history({ format: "compact" });
getHandHistory()

Get structured history object.

const history = engine.getHandHistory();
console.log(history.handId);
console.log(history.winners);
console.log(history.streets);

Optimistic Updates

optimisticAct(action)

Preview action result without modifying state.

const preview = engine.optimisticAct({
  type: ActionType.BET,
  playerId: "alice",
  amount: 100,
});

// preview contains new state
// engine.state is unchanged
reconcile(serverState)

Merge server state into client engine.

// After receiving state from server
engine.reconcile(serverState);

πŸ’° Money Handling

Chip Conservation

The engine enforces strict chip conservation:

βˆ‘(player.stack) + βˆ‘(pot.amount) + βˆ‘(currentBets) + rake = constant

Any violation throws CriticalStateError.

Side Pots

Automatic side pot calculation for all-in scenarios:

// Example: 3 players with different stacks
// Alice: 100 (all-in)
// Bob: 300 (all-in)
// Charlie: 500 (active)

// Results in:
// Main Pot: 300 (100 Γ— 3) - Alice, Bob, Charlie eligible
// Side Pot: 400 (200 Γ— 2) - Bob, Charlie eligible
// Uncalled: 200 - returned to Charlie

Rake

const engine = new PokerEngine({
  smallBlind: 1,
  bigBlind: 2,
  rakePercent: 5, // 5% rake
  rakeCap: 10, // Max $10 per pot
  noFlopNoDrop: true, // No rake if ends preflop
});

πŸ† Tournament Mode

const tournament = new PokerEngine({
  smallBlind: 25,
  bigBlind: 50,
  ante: 5,
  maxPlayers: 9,
  blindStructure: [
    { smallBlind: 25, bigBlind: 50, ante: 5 },
    { smallBlind: 50, bigBlind: 100, ante: 10 },
    { smallBlind: 75, bigBlind: 150, ante: 15 },
    { smallBlind: 100, bigBlind: 200, ante: 25 },
    { smallBlind: 150, bigBlind: 300, ante: 50 },
  ],
});

// Seat players with their starting stacks
tournament.sit(0, "alice", "Alice", 10000);
tournament.sit(1, "bob", "Bob", 10000);

// Advance blinds (e.g., on timer)
tournament.nextBlindLevel();

Tournament-specific rules:

  • Sitting-out players must post blinds/antes
  • Dead button rule for empty seats
  • No rake

🌐 Browser Usage

import { createBrowserEngine } from "@pokertools/engine/browser";

// Uses Web Crypto API for secure RNG
const engine = createBrowserEngine({
  smallBlind: 1,
  bigBlind: 2,
});

❌ Error Handling

Error Types

Error Description
IllegalActionError Invalid game action (send to client)
CriticalStateError Engine invariant violated (should never happen)
ConfigError Invalid configuration

Error Codes

import { ErrorCodes } from "@pokertools/engine";

try {
  engine.act({ type: ActionType.CHECK, playerId: "alice" });
} catch (err) {
  if (err instanceof IllegalActionError) {
    switch (err.code) {
      case ErrorCodes.NOT_YOUR_TURN:
        showMessage("Wait for your turn");
        break;
      case ErrorCodes.CANNOT_CHECK:
        showMessage("You must call or fold");
        break;
      case ErrorCodes.BET_TOO_SMALL:
        showMessage(`Minimum bet is ${err.context.minBet}`);
        break;
    }
  }
}

Available Error Codes:

Category Codes
Player PLAYER_NOT_FOUND, NOT_YOUR_TURN, PLAYER_NOT_ACTIVE, NOT_SEATED, NO_CHIPS
Betting CANNOT_CHECK, NOTHING_TO_CALL, BET_TOO_SMALL, CANNOT_RAISE, CANNOT_RERAISE, RAISE_TOO_SMALL
Deal CANNOT_DEAL, NOT_ENOUGH_PLAYERS
Seat INVALID_SEAT, SEAT_OCCUPIED, INVALID_STACK
Validation INVALID_ACTION, INVALID_AMOUNT, INVALID_TIMESTAMP

🎴 Action Types

import { ActionType } from "@pokertools/engine";

enum ActionType {
  // Management
  SIT = "SIT",
  STAND = "STAND",
  ADD_CHIPS = "ADD_CHIPS",
  RESERVE_SEAT = "RESERVE_SEAT",

  // Dealing
  DEAL = "DEAL",

  // Betting
  FOLD = "FOLD",
  CHECK = "CHECK",
  CALL = "CALL",
  BET = "BET",
  RAISE = "RAISE",

  // Showdown
  SHOW = "SHOW",
  MUCK = "MUCK",

  // Special
  TIMEOUT = "TIMEOUT",
  TIME_BANK = "TIME_BANK",
  UNCALLED_BET_RETURNED = "UNCALLED_BET_RETURNED",

  // Tournament
  NEXT_BLIND_LEVEL = "NEXT_BLIND_LEVEL",
}

πŸ“œ Hand History Export

JSON Format

const history = engine.history({ format: "json" });
{
  "handId": "hand-1734012345678-123456",
  "timestamp": 1734012345678,
  "tableName": "Table 1",
  "gameType": "Cash",
  "stakes": { "smallBlind": 1, "bigBlind": 2, "ante": 0 },
  "buttonSeat": 0,
  "players": [
    { "seat": 0, "name": "Alice", "startingStack": 200, "endingStack": 220 },
    { "seat": 1, "name": "Bob", "startingStack": 200, "endingStack": 180 }
  ],
  "streets": [
    {
      "street": "PREFLOP",
      "board": [],
      "actions": [...]
    }
  ],
  "winners": [
    { "seat": 0, "playerName": "Alice", "amount": 20, "hand": ["As", "Kd"], "handRank": "Two Pair" }
  ],
  "totalPot": 40
}

PokerStars Format

const history = engine.history({ format: "pokerstars" });
PokerStars Hand #hand-1734012345678: Hold'em No Limit ($1/$2 USD)
Table 'Table 1' 6-max Seat #1 is the button
Seat 1: Alice ($200 in chips)
Seat 2: Bob ($200 in chips)
Alice: posts small blind $1
Bob: posts big blind $2
*** HOLE CARDS ***
Dealt to Alice [As Kd]
Alice: calls $1
Bob: checks
*** FLOP *** [Qh Jc Ts]
...

πŸ”§ Utilities

View Masking

import { createPublicView } from "@pokertools/engine";

// Create masked view for specific player
const aliceView = createPublicView(state, "alice");

// Spectator view (all cards hidden)
const spectatorView = createPublicView(state, null);

Chip Auditing

import { calculateTotalChips, auditChipConservation } from "@pokertools/engine";

// Get total chips in game
const total = calculateTotalChips(state);

// Verify chip conservation (throws on failure)
auditChipConservation(state, expectedTotal);

Snapshot

import { createSnapshot, restoreFromSnapshot } from "@pokertools/engine";

// Serialize
const snapshot = createSnapshot(state);
const json = JSON.stringify(snapshot);

// Deserialize
const restored = restoreFromSnapshot(JSON.parse(json));

πŸ§ͺ Testing

The engine includes 362 tests across 38 suites:

Category Files Description
Unit 27 Individual component tests
Integration 4 Full game flow tests
Property 3 Randomized invariant testing
Bug Regression 2 Fixed bug verification
Security 2 Anti-cheat/exploit tests
npm test -w @pokertools/engine

πŸ“Š State Structure

interface GameState {
  // Configuration
  config: TableConfig;
  players: (Player | null)[];
  maxPlayers: number;

  // Hand State
  handNumber: number;
  buttonSeat: number | null;
  deck: number[]; // Card codes (server only)
  board: string[]; // Community cards
  street: Street;

  // Betting
  pots: Pot[];
  currentBets: Map<number, number>;
  minRaise: number;
  lastRaiseAmount: number;
  actionTo: number | null;
  lastAggressorSeat: number | null;

  // Progress
  activePlayers: number[];
  winners: Winner[] | null;
  rakeThisHand: number;

  // Blinds
  smallBlind: number;
  bigBlind: number;
  ante: number;
  blindLevel: number;

  // Time Bank
  timeBanks: Map<number, number>;
  timeBankActiveSeat: number | null;

  // History
  actionHistory: ActionRecord[];
  previousStates: GameState[]; // For undo

  // Chip Conservation
  initialChips?: number; // Baseline for auditing (set at deal)

  // Metadata
  timestamp: number;
  handId: string;
}

πŸ”— Related Packages

Package Description
@pokertools/types Type definitions
@pokertools/evaluator Hand evaluation
@pokertools/api REST/WebSocket API

πŸ“„ License

MIT Β© A.Aurelius