npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pokertools/engine

v1.0.16

Published

Enterprise-grade Texas Hold'em poker engine

Downloads

1,781

Readme

🃏 @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