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:
| Style | VPIP | PFR | How 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:
| Stat | Minimum Hands for Reliability |
|---|---|
| VPIP | 30+ |
| PFR | 30+ |
| Aggression Factor | 50+ |
| Player style classification | 50+ |
Until you have enough data, default to a solid TAG strategy.