Bot System

This guide covers the internals of the poker bot system — how bots are created, managed, how they make decisions, and how to add new strategy types. It is intended for developers extending or integrating the bot functionality.

Architecture Overview

BotScheduler              — periodic table analysis, adds/removes bots
  └── BotManager          — manages a pool of BotPlayer instances
        └── BotPlayer     — a single bot: joins table, acts on turn
              ├── ParameterizedStrategy  — admin-configured strategies
              ├── Decision Engine        — modular strategy + hand analysis
              └── BotDelayManager       — realistic timing simulation

Bot Identity Pool         — pool of personas (name/avatar) assigned per session
Bot Strategy Profiles     — admin-configurable parameter sets stored in DB
Bot Session Service       — tracks per-session state (P/L, identity, table)
Bot Persistence Service   — syncs bot state to DB across restarts

Source files:

FilePurpose
src/game/bot/bot-player.tsCore bot actor class
src/game/bot/bot-manager/BotManager pool management
src/game/bot/bot-policy.tsbotsMayPlay() and refreshBotPolicy(): gate bot play on the bots feature toggle and FEATURE_BOTS
src/game/bot/bot-scheduler.tsAutomated table-fill scheduler
src/game/bot/decision-engine.tsModular hand analysis + strategies
src/game/bot/parameterized-strategy.tsAdmin-configurable strategy
src/game/bot/delay-config.tsRealistic action timing
src/game/bot/strategies/Variant-specific strategy wrappers
src/services/bot-identity.service.tsIdentity pool assignment
src/services/bot-strategy-profile.service.tsDB-backed strategy profiles
src/services/bot-scheduler-config.service.tsScheduler settings
src/services/bot-session.service.tsPer-session lifecycle
src/services/bot-persistence.service.tsDB persistence

BotPlayer Lifecycle

1. Creation

import { createBotPlayer, BotStrategy } from './bot-player.js';

const bot = createBotPlayer({
  id: 'bot_abc123',          // Must match a user record in DB
  name: 'Ace McGee',
  buyIn: 1000,
  strategy: BotStrategy.TIGHT_AGGRESSIVE,
  minActionDelay: 500,
  maxActionDelay: 3000,
  delayProfile: DelayProfile.NORMAL,
});

Factory shortcuts exist: createTightBot(), createLooseBot(), createCallingStationBot(), createRandomBot().

2. Joining a table

const success = bot.joinTable(table, seatIndex);
// Internally calls table.join(bot.id, seatIndex, bot.getBuyIn(), { isBot: true, ... })

3. Acting on turn

The game engine calls bot.onActionRequired(tableState) when it is the bot’s turn. The bot:

  1. Builds a BotGameContext from tableState.
  2. Calls makeDecision(context) immediately (before the delay).
  3. Schedules executePrecomputedAction(decision) after a calculated delay.

Pre-computing the decision before the delay allows near-timeout bluff timing to work correctly.

4. Session identity

A bot can be assigned a temporary persona from the identity pool:

bot.setSessionIdentity('John Smith', 'https://cdn.example.com/avatar.jpg');
// All table-visible data uses the session identity
bot.clearSessionIdentity(); // Reverts to real name/avatar

5. Leaving a table

const result = bot.leaveTable();          // Only if not mid-hand
const result = bot.leaveTable(true);      // Force-leave (folds hand first)

Strategy System

Built-in strategies (legacy enum-based)

EnumBehaviour
TIGHT_PASSIVEOnly strong hands, rarely raises.
TIGHT_AGGRESSIVEDefault. Fewer hands but bets/raises aggressively.
LOOSE_AGGRESSIVEPlays most hands, bluffs with weak holdings.
LOOSE_PASSIVECalling station. Rarely folds, rarely raises.
RANDOMRandom actions for testing.
HEADS_UPAuto-applied when only 2 players. Very loose with bluffing.

Auto heads-up switch: when a table drops to 2 players, a bot on a legacy enum strategy temporarily switches to HEADS_UP. The original strategy is stored in originalStrategy and restored when more players join. Bots using a strategy profile do not switch.

Parameterized strategy (admin-configurable)

ParameterizedStrategy derives all thresholds from 6 tunable parameters, so admins can create new strategy profiles without code changes:

interface StrategyParameters {
  tightness: number;         // 0 (loose) to 1 (tight)
  aggression: number;        // 0 (passive) to 1 (aggressive)
  betSizingMultiplier: number; // 0.1 to 3.0 — scales pot-fraction bets
  bluffFrequency: number;    // 0 to 1
  positionAwareness: number; // 0 to 1 — tightens range in early position
  preflopRangeWidth: number; // 0 (premiums only) to 1 (any two cards)
}

When strategyParameters is set on a BotPlayer, makeDecision() routes to makeParameterizedDecision() instead of the inline strategy methods.

Decision engine (modular)

BotDecisionEngine separates hand analysis from strategy:

const engine = createDecisionEngine('TAG'); // TAG | TP | LAG | LP | RANDOM
const analysis: HandAnalysis = engine.analyzeHand(context);
const decision: BotDecision  = engine.decide(context);
// BotDecision includes { action, amount, confidence, reasoning }

Implementing a custom strategy

import { type IDecisionStrategy, type GameContext, type HandAnalysis } from './decision-engine.js';

class MyCustomStrategy implements IDecisionStrategy {
  readonly name = 'My Custom';

  decide(context: GameContext, analysis: HandAnalysis): BotDecision {
    // Use analysis.strength, analysis.draws, analysis.preflopCategory, etc.
    if (analysis.strength > 0.7) {
      return {
        action: 'bet',
        amount: Math.floor(context.potSize * 0.75),
        confidence: 0.8,
        reasoning: 'Value bet with strong hand',
      };
    }
    return { action: 'check', amount: 0, confidence: 0.6, reasoning: 'Check weak hand' };
  }
}

// Register with an engine
const engine = new BotDecisionEngine({ defaultStrategy: new MyCustomStrategy() });

Variant-specific strategies

getStrategyForVariant(gameType, personality) returns variant-aware strategies. RANDOM always returns the plain random strategy:

Game TypeStrategy
OMAHA / OMAHA_5 / OMAHA_HI_LOsrc/game/bot/strategies/omaha-strategy.ts (for example OmahaTightAggressiveStrategy) — wider preflop ranges, nut-seeking, strong draws
SHORT_DECKsrc/game/bot/strategies/short-deck-strategy.ts (for example ShortDeckTightAggressiveStrategy) — more aggressive, flush > full house, higher pair values
TEXAS_HOLDEM / othersStandard strategies

The hand analyzer is also variant-aware via getAnalyzerForVariant(gameType). Omaha uses OmahaPostflopAnalyzer which enforces the 2+3 rule (exactly 2 hole cards + 3 board cards) and detects wrap straight draws.

Hand Strength Evaluation

BotPlayer.evaluateHandStrength(context) returns a float 0–1:

  • Preflop: looks up hand quality (pairs, high cards, suitedness, connectivity).
    • 2 hole cards → evaluateHoldemPreflopStrength().
    • 4 hole cards → evaluateOmahaPreflopStrength() (values double-suitedness and rundowns).
    • 3–7 hole cards → evaluateStudPreflopStrength() (trips/pairs/flush draws).
  • Postflop: evaluates the best 5-card hand from all available cards using evaluate5CardHand() / evaluate7CardHand().

Hand rank → strength mapping:

Hand RankStrength
High Card0.10
One Pair0.30
Two Pair0.50
Three of a Kind0.60
Straight0.70
Flush0.75
Full House0.85
Four of a Kind0.95
Straight Flush0.98
Royal Flush1.00

A small kicker bonus (rank / 14 * 0.05) is added on top.

Short-handed adjustment

At tables with fewer than 6 players, a shortHandedBonus() is added to effective strength so bots don’t over-fold in short-handed and heads-up situations:

PlayersBonus
6+0
5+0.04
4+0.08
3+0.13
2+0.18

Action Timing (Delay System)

BotDelayManager in delay-config.ts produces realistic delays so bots don’t act instantly.

Delay profiles

ProfileRangeDescription
INSTANT0No delay, for testing.
FAST100–500 msQuick testing.
NORMAL500–3000 msDefault realistic speed.
SLOW1000–5000 msDeliberate player style.
ADAPTIVE300–4000 msDelay varies strongly with decision complexity.
CUSTOM500–3000 msBase values for custom settings.

Decision complexity → delay

assessDecisionComplexity(context) categorises decisions, checked in this order. Adaptive profiles scale their delay with a per-profile multiplier:

ComplexityTrigger
CRITICALPot > 50% of stack
COMPLEXFacing a bet on the river, or strength 0.4–0.6
TRIVIALNothing to call
EASYStrength < 0.3 or > 0.8
MODERATEDefault

Bluff near-timeout

When a decision is classified as a bluff (betting/raising with strength < 0.35), there is a 15% chance the bot will wait until near the action timeout before acting — mimicking the human "tank-and-bluff" behaviour.

Emotional state

After winning or losing a large pot (> 20 big blinds), the bot’s delay profile shifts:

  • Win → CONFIDENT (slightly faster, smoother timing).
  • Loss → TILTED (faster, more erratic timing).

The state decays to NEUTRAL after 8 hands.

Bot-only table pacing

When all opponents at a table are bots (detected by id.startsWith('bot_')), a minimum delay of 3–8 seconds is enforced so bot-vs-bot tables don’t play at unrealistic speeds.

Bot Manager

BotManager maintains a pool of BotPlayer instances. The global instance is initialized once at server startup via initializeGlobalBotManager().

// Add a bot to a table (seat and buy-in are optional)
const result = await botManager.addBotToTable(table, {
  seatIndex: 3,
  strategyProfileId: 'profile_id',
});
// { success, botId, seatIndex, error }

// Take a bot off a table (it stays managed)
botManager.removeBotFromTable(botId);

// Remove a bot from the manager entirely
await botManager.removeBot(botId);

// Get pool stats
const stats = botManager.getStats();
// { totalBots, botsAtTables, botsInPool, tablesWithBots, botsByStrategy }

addBotToTable returns { success: false, error: 'Bots are disabled' } when the bot policy is off. autoRebuyBots(tableId, minStack, rebuyAmount) tops up bots that have fallen below a threshold. State is synced to the database every 30 seconds by startPeriodicSync(30000).

Bot Scheduler

BotScheduler runs a periodic check loop (checkIntervalSeconds, default 30) that:

  1. Loads the global scheduler config from the database.
  2. Iterates all active in-memory tables that are eligible, skipping tables whose template has botsEnabled === false.
  3. For each table, calculates effective config (global defaults merged with per-template overrides).
  4. Counts real players (excludes bots via isBot(id)).
  5. Removes all bots if real players fall below minHumanPlayers (default 1).
  6. Removes one bot per cycle if realPlayers >= autoDepartThreshold (default 6).
  7. Adds a bot if realPlayers < autoJoinThreshold (default 3), the table holds fewer than maxBotsPerTable (default 3) bots, and the time is within active hours.
  8. Ends sessions where the stop-loss or win-limit has been reached.

The scheduler is off by default (enabled defaults to false). When autoFillEnabled is on, tables with a BotTableAutoFill record or a template botTargetPlayers value are managed by auto-fill instead, which adds or removes bots to reach the target.

Identity assignment

When adding a bot, the scheduler calls acquireIdentity() which picks an available persona from the identity pool and assigns it as the bot’s session identity. The identity is released when the bot leaves via releaseIdentity(name, cooldownMinutes).

Cooldown

After a bot leaves a table, it cannot rejoin the same table for cooldownMinutes (default 5), tracked in the in-memory cooldownMap. The released identity also goes on cooldown (cooldownUntil).

Active hours

The scheduler respects activeHoursStart / activeHoursEnd from the global config. If both are null, it is always active. Outside these hours, bots are not added, but removals still run.

Bot Identity Pool

The identity pool (src/services/bot-identity.service.ts, model BotIdentityPool) stores a list of personas (unique name, optional avatar). When a bot session starts:

  1. An available identity is atomically acquired (marked inUse: true).
  2. The bot’s session identity is set so it appears at the table with that name/avatar.
  3. On session end, the identity is released back to the pool.

This prevents two bots from appearing with the same persona simultaneously.

Bot Strategy Profiles (Database-backed)

Admins can create named strategy profiles via the Admin Panel or API. Each BotStrategyProfile row stores the six StrategyParameters values as columns, plus name, displayName and description.

// Loads the profile, applies its parameters to the bot and persists the assignment
await botManager.setStrategyProfile(botId, profileId);

Creating a Custom Strategy (Step by Step)

  1. Implement IDecisionStrategy in a new file under src/game/bot/strategies/:
// src/game/bot/strategies/my-strategy.ts
import { type IDecisionStrategy, type GameContext, type HandAnalysis, type BotDecision } from '../decision-engine.js';

export class MyStrategy implements IDecisionStrategy {
  readonly name = 'My Strategy';

  decide(context: GameContext, analysis: HandAnalysis): BotDecision {
    // Your logic here
    return { action: 'fold', amount: 0, confidence: 0.5 };
  }
}
  1. Register with the decision engine (optional — only needed for createDecisionEngine() factory):

Add a case to getStrategy() and createDecisionEngine() in decision-engine.ts.

  1. Or use a parameterized profile — create a profile on the Strategy Profiles page of the Admin Panel (/admin/bot-strategy-profiles). No code required.
  2. Test your strategy:
import { BotDecisionEngine } from '../decision-engine.js';
import { MyStrategy } from '../strategies/my-strategy.js';

const engine = new BotDecisionEngine({ defaultStrategy: new MyStrategy() });
const decision = engine.decide({
  holeCards: ['AH', 'KD'],
  communityCards: ['TH', 'JD', 'QC'],
  potSize: 500,
  amountToCall: 100,
  stack: 2000,
  bigBlind: 50,
  activePlayers: 3,
  position: 2,
  totalPlayers: 4,
  round: 'flop',
});

Statistics and Persistence

BotPlayer.recordHandPlayed(won, potSize, bigBlind) updates in-memory win/loss counters and triggers emotional state changes for large pots.

bot-persistence.service.ts syncs bot state to the database, so bot continuity survives server restarts. BotManager.startPeriodicSync() drives the sync, and the server starts it with a 30-second interval.

bot-session.service.ts manages session records — each time a bot joins a table, a new session is created. Sessions track entry/exit chips for P/L calculation against stop-loss and win-limit thresholds.

Try the live demo Pricing

Have questions about Poker Engine? We reply fast.