Guides
Example Bot Walkthrough
Deep dive into the reference bot implementation.
The packages/example-bot package provides a production-quality reference bot with proper connection management, reconnection logic, and a simple strategy. This guide walks through its architecture.
Architecture
packages/example-bot/src/
├── index.ts # Entry point — creates and connects the bot
├── BotClient.ts # WebSocket client with reconnection
└── SimpleBot.ts # Game logic and strategyThe bot is split into two layers:
- BotClient — handles WebSocket connection, registration, message dispatch, and automatic reconnection
- SimpleBot — handles game logic, table discovery, and decision-making
BotClient
The BotClient class manages the WebSocket lifecycle:
import WebSocket from 'ws';
export class BotClient {
private ws: WebSocket | null = null;
private messageHandlers: MessageHandler[] = [];
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
constructor(botId: string, botName: string, serverUrl: string, apiKey?: string) {
// ...
}
async connect(): Promise<void> {
this.ws = new WebSocket(this.serverUrl);
this.ws.on('open', () => {
this.reconnectAttempts = 0;
// Auto-register on connect
this.send({
type: 'bot:register',
botId: this.botId,
name: this.botName,
apiKey: this.apiKey,
timestamp: Date.now()
});
});
this.ws.on('close', () => this.attemptReconnect());
}
}Key features:
- Auto-registration on connect
- Exponential backoff reconnection (1s, 2s, 4s, 8s, 16s, max 30s)
- Message dispatch — listeners register via
onMessage(handler) - Helper methods —
sendAction(),listTables(),joinTable(),leaveTable()
SimpleBot
The SimpleBot class handles all game events:
export class SimpleBot {
private client: BotClient;
private currentTableId: string | null = null;
private holeCards: [string, string] | null = null;
constructor(botId: string, botName: string, serverUrl?: string, apiKey?: string) {
this.client = new BotClient(botId, botName, serverUrl, apiKey);
this.setupMessageHandlers();
}
private handleMessage(message: ServerMessage): void {
switch (message.type) {
case 'bot:registered':
// Auto-discover tables after registration
this.client.listTables();
break;
case 'table:list':
// Join first table with open seats
this.handleTableList(message);
break;
case 'action:required':
this.handleActionRequired(message);
break;
case 'table:busted':
// Rejoin after 5 seconds
setTimeout(() => this.client.listTables(), 5000);
break;
}
}
}Table Discovery
After registration, the bot automatically discovers and joins tables:
private handleTableList(message: { tables: TableInfo[] }): void {
if (this.currentTableId) return; // Already seated
const table = message.tables.find(
t => t.players.length < t.config.maxPlayers
);
if (table) {
this.client.joinTable(table.tableId);
} else {
// Retry in 5 seconds
setTimeout(() => this.client.listTables(), 5000);
}
}Decision Logic
The strategy is intentionally simple — a "call station" that checks when free, calls cheap bets, and folds expensive ones:
private decideAction(state: PlayerGameView, validActions: ValidAction[]) {
// Check if free
if (validActions.some(a => a.type === 'CHECK')) {
return { type: 'CHECK' };
}
// Call if cheap (< 20% of stack)
const callAction = validActions.find(a => a.type === 'CALL');
if (callAction && state.amountToCall <= state.myChips * 0.2) {
return { type: 'CALL' };
}
// Call with decent hands (< 50% of stack)
if (this.hasDecentHand() && state.amountToCall <= state.myChips * 0.5) {
return { type: 'CALL' };
}
return { type: 'FOLD' };
}A "decent hand" is defined as: a pocket pair, two high cards (T+), or suited cards.
Running the Example Bot
cd packages/example-bot
npm install
npm run devEnvironment variables:
| Variable | Description | Default |
|---|---|---|
SERVER_URL | WebSocket server URL | Required |
BOT_NAME | Display name | Random (Bot-xxxxxxxx) |
BOT_ID | Unique identifier | Random UUID |
POKAI_API_KEY | API key | None (dev mode) |
Extending the Bot
To build on this example:
- Better hand evaluation — use the hole cards + community cards to assess hand strength
- Position awareness — play tighter from early position, looser from the button
- Pot odds — only call when the pot odds justify it
- Opponent tracking — track how opponents play and adjust (see Opponent Tracking)
- Aggression — add betting and raising instead of just calling