Pokai Docs
Guides

Opponent Tracking

Track and exploit opponent tendencies.

Tracking how opponents play allows your bot to adapt its strategy. Instead of playing the same way against everyone, you can exploit specific weaknesses.

Key Statistics

VPIP (Voluntarily Put $ In Pot)

The percentage of hands where a player voluntarily puts money in the pot (calls or raises preflop, excluding blinds).

VPIP = hands voluntarily played / total hands × 100
  • Low (< 20%): Tight player — only plays good hands
  • Medium (20–35%): Solid, selective
  • High (> 35%): Loose player — plays many hands

PFR (Pre-Flop Raise)

The percentage of hands where a player raises preflop.

PFR = hands raised preflop / total hands × 100
  • Low (< 10%): Passive — rarely raises
  • High (> 20%): Aggressive — raises frequently

Aggression Factor

How aggressive a player is postflop.

AF = (bets + raises) / calls
  • < 1: Passive — calls more than bets
  • 1–2: Moderate
  • > 2: Aggressive — bets and raises frequently

Player Styles

Combine VPIP and PFR to classify players:

StyleVPIPPFRHow to Exploit
Nit (Tight-Passive)< 15%< 10%Steal their blinds, fold to their raises
TAG (Tight-Aggressive)15–25%12–20%Hardest to exploit — play solid poker
Calling Station (Loose-Passive)> 35%< 10%Value bet relentlessly, don't bluff
LAG (Loose-Aggressive)> 30%> 20%Trap with strong hands, let them bluff
Maniac> 50%> 30%Call down with medium hands, let them hang themselves

Implementation

interface OpponentStats {
  handsPlayed: number;
  vpipCount: number;
  pfrCount: number;
  bets: number;
  raises: number;
  calls: number;
  folds: number;
}

class OpponentTracker {
  private stats = new Map<string, OpponentStats>();

  recordAction(playerId: string, action: string, phase: string): void {
    const s = this.getOrCreate(playerId);
    s.handsPlayed++; // Increment per hand, not per action

    if (phase === 'PREFLOP') {
      if (action === 'CALL' || action === 'RAISE' || action === 'BET') {
        s.vpipCount++;
      }
      if (action === 'RAISE') {
        s.pfrCount++;
      }
    }

    if (action === 'BET') s.bets++;
    if (action === 'RAISE') s.raises++;
    if (action === 'CALL') s.calls++;
    if (action === 'FOLD') s.folds++;
  }

  getVPIP(playerId: string): number {
    const s = this.stats.get(playerId);
    if (!s || s.handsPlayed === 0) return 0;
    return (s.vpipCount / s.handsPlayed) * 100;
  }

  getAF(playerId: string): number {
    const s = this.stats.get(playerId);
    if (!s || s.calls === 0) return 0;
    return (s.bets + s.raises) / s.calls;
  }

  getStyle(playerId: string): string {
    const vpip = this.getVPIP(playerId);
    const af = this.getAF(playerId);

    if (vpip < 20 && af < 1) return 'Nit';
    if (vpip < 25 && af >= 1) return 'TAG';
    if (vpip >= 35 && af < 1) return 'Calling Station';
    if (vpip >= 30 && af >= 1.5) return 'LAG';
    return 'Unknown';
  }
}

Using Opponent Data

Adjust your strategy based on the opponent's style:

function adjustStrategy(style: string, baseAction: Action): Action {
  switch (style) {
    case 'Nit':
      // They only raise with monsters — fold to aggression
      if (baseAction.type === 'CALL') return { type: 'FOLD' };
      break;

    case 'Calling Station':
      // They call everything — never bluff, value bet more
      if (baseAction.type === 'CHECK' && handStrength > 0.5) {
        return { type: 'BET', amount: pot * 0.5 };
      }
      break;

    case 'LAG':
      // They bluff a lot — call more, trap with strong hands
      if (baseAction.type === 'FOLD' && handStrength > 0.4) {
        return { type: 'CALL' };
      }
      break;
  }

  return baseAction;
}

Sample Sizes

Statistics are only reliable with enough data:

StatMinimum Hands for Reliability
VPIP30+
PFR30+
Aggression Factor50+
Player style classification50+

Until you have enough data, default to a solid TAG strategy.