High-performance single-table No-Limit Texas Hold'em poker engine for Node.js
npm install @leoni4/poker-tableHigh-performance single-table No-Limit Texas Hold'em poker engine for Node.js.
A TypeScript-based poker engine that simulates a complete No-Limit Texas Hold'em table. It handles all game mechanics including:
- Player management: Seating, removing, and rebuy operations
- Hand flow: Blind posting, card dealing, betting rounds, and showdown
- Betting logic: All standard actions (fold, check, call, bet, raise, all-in)
- Pot calculation: Main pots and side pots for all-in scenarios
- Hand evaluation: Automatic determination of winners at showdown
- Result-based error handling: No exceptions, all operations return Result
``bash`
npm install @leoni4/poker-table
Here's a complete example showing a typical hand:
`typescript
import {
HoldemTable,
createDefaultTableConfig,
createPlayerId,
chips,
TablePhase,
isOk,
} from '@leoni4/poker-table';
// Create a table with default configuration
const config = createDefaultTableConfig();
const table = new HoldemTable(config);
// Seat players with initial buy-ins
const alice = createPlayerId('alice');
const bob = createPlayerId('bob');
table.seatPlayer(alice, chips(1000));
table.seatPlayer(bob, chips(1000));
// Start a hand
const startResult = table.startHand();
if (!isOk(startResult)) {
console.error('Failed to start hand:', startResult.error.message);
process.exit(1);
}
let state = startResult.value;
console.log(Hand ${state.handId} started in ${state.phase} phase);Community cards: ${state.communityCards.length}
console.log();
// Main game loop: process actions until hand completes
while (state.phase !== TablePhase.Showdown) {
// Check if there's a player to act
if (!state.currentPlayerId) {
console.log('No current player, hand might be complete');
break;
}
console.log(Current player to act: ${state.currentPlayerId});Phase: ${state.phase}
console.log();
// Decide on an action (this would be your bot/AI or user input)
// For this example, we'll just call or check
const player = state.players.find((p) => p.id === state.currentPlayerId);
if (!player) break;
const amountToCall = state.players.reduce(
(max, p) => (p.committed > max ? p.committed : max),
0n
);
const needsToCall = amountToCall - player.committed;
let action;
if (needsToCall > 0n) {
action = { type: 'CALL' as const };
} else {
action = { type: 'CHECK' as const };
}
// Apply the action
const actionResult = table.applyAction(state.currentPlayerId, action);
if (!isOk(actionResult)) {
console.error('Action failed:', actionResult.error.message);
break;
}
state = actionResult.value;
}
// Hand complete
console.log(Hand finished in ${state.phase} phase);Final community cards: ${state.communityCards.length}
console.log();`
console.log('Final pot distribution:', state.pots);
#### Using Default Configuration
`typescript
import { HoldemTable, createDefaultTableConfig } from '@leoni4/poker-table';
const config = createDefaultTableConfig();
// Default: 2-10 players, 1/2 blinds
const table = new HoldemTable(config);
`
#### Custom Configuration
`typescript
import { HoldemTable, chips } from '@leoni4/poker-table';
const config = {
minPlayers: 2,
maxPlayers: 6, // 6-max table
smallBlind: chips(5),
bigBlind: chips(10),
ante: chips(1), // Optional ante
straddle: chips(20), // Optional straddle (2x BB)
rake: {
// Optional rake
percentage: 0.05, // 5%
cap: chips(10), // Max rake per hand
},
rngSeed: 42, // Optional seed for deterministic behavior (testing)
};
const table = new HoldemTable(config);
`
#### Rebuy Options
`typescript`
const table = new HoldemTable(config, {
minRebuy: chips(100),
maxRebuy: chips(5000),
allowDuringHand: false, // Only allow rebuys between hands
});
`typescript
import { createPlayerId, chips, isOk } from '@leoni4/poker-table';
const playerId = createPlayerId('player-123');
const result = table.seatPlayer(playerId, chips(1000));
if (isOk(result)) {
console.log('Player seated successfully');
const state = result.value;
console.log(Total players: ${state.players.length});`
} else {
console.error('Failed to seat player:', result.error.message);
}
`typescript
const result = table.startHand();
if (isOk(result)) {
const state = result.value;
console.log(Hand #${state.handId} started);Dealer seat: ${state.dealerSeat}
console.log();Current phase: ${state.phase}
console.log();Next to act: ${state.currentPlayerId}
console.log();`
} else {
console.error('Cannot start hand:', result.error.message);
// Common reasons: insufficient players, hand already in progress
}
All actions return a Result that must be checked:
`typescript
import { PlayerAction } from '@leoni4/poker-table';
// Fold
const foldAction: PlayerAction = { type: 'FOLD' };
const result = table.applyAction(playerId, foldAction);
// Check (only valid when no bet to call)
const checkAction: PlayerAction = { type: 'CHECK' };
// Call
const callAction: PlayerAction = { type: 'CALL' };
// Bet (only valid when no current bet)
const betAction: PlayerAction = {
type: 'BET',
amount: chips(50),
};
// Raise (only valid when there is a bet to raise)
const raiseAction: PlayerAction = {
type: 'RAISE',
amount: chips(100), // Raise size (not total)
};
// All-in
const allInAction: PlayerAction = { type: 'ALL_IN' };
`
The getState() method returns a complete snapshot of the table:
`typescript
const state = table.getState();
// Check current phase
console.log(Phase: ${state.phase});
// Values: 'idle', 'preflop', 'flop', 'turn', 'river', 'showdown'
// Check whose turn it is
if (state.currentPlayerId) {
console.log(Waiting for: ${state.currentPlayerId});
}
// View community cards
console.log(
Board: ${state.communityCards.map((c) => c.toString()).join(' ')}
);
// View players
for (const player of state.players) {
console.log(${player.id}: ${player.stack} chips, status: ${player.status}); Committed: ${player.committed}
console.log(); Cards: ${player.holeCards.cards.map((c) => c.toString()).join(' ')}
if (player.holeCards.cards) {
console.log(
);
}
}
// View pots
for (const pot of state.pots) {
console.log(Pot: ${pot.total} chips); Eligible: ${pot.participants.join(', ')}
console.log();`
}
Here's a robust game loop for processing a complete hand:
`typescript
import { TablePhase, isOk } from '@leoni4/poker-table';
// Start hand
const startResult = table.startHand();
if (!isOk(startResult)) {
console.error('Cannot start hand:', startResult.error.message);
return;
}
let state = startResult.value;
// Process actions until hand completes
const maxActions = 100; // Safety limit
let actionCount = 0;
while (state.phase !== TablePhase.Showdown && actionCount < maxActions) {
// Check if we're waiting for a player action
if (!state.currentPlayerId) {
console.log('Hand complete (no current player)');
break;
}
// Get the action from your bot/AI/user interface
const action = decideAction(state, state.currentPlayerId);
// Apply the action
const result = table.applyAction(state.currentPlayerId, action);
if (!isOk(result)) {
console.error('Action failed:', result.error.message);
break;
}
state = result.value;
actionCount++;
}
console.log(Hand finished after ${actionCount} actions);Final phase: ${state.phase}
console.log();
// Example bot logic
function decideAction(state, playerId) {
// Your decision logic here
// For example, always call or check
const player = state.players.find((p) => p.id === playerId);
const currentBet = Math.max(...state.players.map((p) => p.committed));
const needsToCall = currentBet - player.committed;
if (needsToCall > 0n) {
return { type: 'CALL' };
} else {
return { type: 'CHECK' };
}
}
`
Configuration for the poker table:
`typescript`
interface TableConfig {
minPlayers: number; // Minimum players to start (typically 2)
maxPlayers: number; // Maximum players allowed (typically 2-10)
smallBlind: ChipAmount; // Small blind amount
bigBlind: ChipAmount; // Big blind amount
ante?: ChipAmount; // Optional ante per player
straddle?: ChipAmount; // Optional straddle amount
rake?: RakeConfig; // Optional rake configuration
rngSeed?: number; // Optional RNG seed for testing
}
Complete snapshot of the table:
`typescript
interface TableState {
phase: TablePhase; // Current game phase
handId: number; // Current hand number
dealerSeat?: number; // Dealer button position
players: PlayerState[]; // All players at table
communityCards: Card[]; // Board cards
pots: PotState[]; // All pots (main + side pots)
currentPlayerId?: PlayerId; // Player to act next
}
enum TablePhase {
Idle = 'idle',
Preflop = 'preflop',
Flop = 'flop',
Turn = 'turn',
River = 'river',
Showdown = 'showdown',
}
`
Information about a single player:
`typescript
interface PlayerState {
id: PlayerId; // Unique identifier
seat: number; // Seat position (0-based)
stack: ChipAmount; // Current chip stack
committed: ChipAmount; // Chips in pot this hand
status: PlayerStatus; // Current status
holeCards: HoleCards; // Private cards
}
enum PlayerStatus {
Active = 'active', // Can act
Folded = 'folded', // Has folded
AllIn = 'all-in', // Is all-in
SittingOut = 'sitting-out', // Not in hand
}
`
Actions a player can take:
`typescript
interface PlayerAction {
type: PlayerActionType;
amount?: ChipAmount; // Required for BET, RAISE
}
type PlayerActionType = 'FOLD' | 'CHECK' | 'CALL' | 'BET' | 'RAISE' | 'ALL_IN';
`
`typescript
// Branded type for chip amounts (uses bigint)
type ChipAmount = bigint & { readonly __brand: 'ChipAmount' };
// Create chip amounts
const amount = chips(100); // 100 chips
// Branded type for player IDs
type PlayerId = string & { readonly __brand: 'PlayerId' };
// Create player IDs
const id = createPlayerId('player-1');
// Result type for error handling
type Result
// Check results
if (isOk(result)) {
// result.value is available
} else {
// result.error is available
}
`
All operations return a Result type - never throw exceptions:
`typescript
const result = table.seatPlayer(playerId, chips(1000));
if (isOk(result)) {
// Success path
const state = result.value;
console.log('Player seated:', state.players.length);
} else {
// Error path
const error = result.error;
console.error(Error [${error.code}]: ${error.message});`
}
Common error codes:
- INVALID_STATE - Operation not valid in current statePLAYER_NOT_FOUND
- - Player ID not found at tableNOT_PLAYER_TURN
- - Player tried to act out of turnINVALID_ACTION
- - Action not allowed (e.g., CHECK when bet exists)INSUFFICIENT_STACK
- - Player doesn't have enough chipsINVALID_BET_AMOUNT
- - Bet/raise amount invalid
Track what happened during hands:
`typescript
// Get current hand history (hand in progress)
const currentHistory = table.getCurrentHandHistory();
// Get last completed hand
const lastHistory = table.getLastHandHistory();
if (lastHistory) {
console.log(Hand #${lastHistory.handId});Started: ${lastHistory.startTime}
console.log();Events: ${lastHistory.events.length}
console.log();
// Process events
for (const event of lastHistory.events) {
switch (event.type) {
case 'HAND_STARTED':
console.log('Hand started');
break;
case 'BLINDS_POSTED':
console.log(Blinds posted by ${event.postingPlayers.join(', ')});${event.playerId} ${event.action.type}
break;
case 'ACTION_TAKEN':
console.log();`
break;
// ... other event types
}
}
}
The library includes comprehensive test coverage to ensure reliability:
- Unit tests: Core components tested in isolation
- Integration tests: Complete hand flows and scenarios
- Edge case tests: Boundary conditions and error cases
- Type safety: Full TypeScript coverage with strict mode
`bashRun all tests
npm test
$3
`bash
Type checking
npm run typecheckLinting
npm run lintBuild
npm run build
`Development
$3
`
poker-table/
├── src/
│ ├── index.ts # Main entry point
│ ├── holdem-table.ts # Public API
│ ├── core/ # Core types (Card, Money, Result, etc.)
│ ├── table/ # Table management
│ ├── betting/ # Betting logic and actions
│ ├── deck/ # Deck shuffling and dealing
│ ├── hand-eval/ # Hand evaluation and comparison
│ ├── pot/ # Pot calculation (main + side pots)
│ ├── rng/ # Random number generation
│ └── history/ # Hand history tracking
├── tests/ # Test files (mirrors src/ structure)
├── dist/ # Build output
└── README.md # This file
`$3
1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Ensure all tests pass:
npm test`MIT
For issues, questions, or contributions, please visit the project repository.