A few months ago Guillermo Rauch built a chess game where AI models played against each other. I thought it was a cool demo watching models compete head-to-head in a game with real strategy. But it got me thinking: chess is a perfect information game. Every model sees the same board. What happens when you throw LLMs into a game built on incomplete information, deception, and reading your opponents?
So I built a full Texas Hold’em poker game where you sit at a table with Claude Sonnet 4.5, Grok 4, Gemini Flash 2.5, and GPT-5, and they all play real poker against you and each other.
You can play it at ai-poker-delta.vercel.app.
The Naive Version Was Terrible
The first version was simple: send the game state to an LLM, ask it what to do, parse the response. And the models were awful. They’d fold pocket aces to a min-raise. They’d check the nuts on the river. They’d call massive bets with nothing because the text “pot odds” appeared somewhere in their reasoning. They played like someone who’d read a poker book once and remembered none of it.
Getting from “technically functional” to “actually competent” was the real engineering challenge, and it happened across several layers.
Layer 1: Don’t Ask the Model to Do Math
The single biggest improvement came from a simple realization: LLMs are bad at poker math but great at poker reasoning. Asking a model to count outs, calculate pot odds, and estimate equity in its head leads to confidently wrong numbers. But if you give it accurate numbers, it makes surprisingly good strategic decisions with them.
So the system pre-computes everything before the model ever sees the prompt. A Monte Carlo simulation runs 500 iterations to estimate equity. A draw detection system counts outs for flush draws (9 outs), open-ended straights (8 outs), gutshots (4 outs), and combo draws. A board texture analyzer classifies the board across multiple dimensions — wetness, connectivity, suitedness, whether straights or flushes are possible.
All of this gets formatted into a structured analytical breakdown that the model receives as context:
## HAND ANALYSIS
Your hand: A♠ K♠
Made hand: High Card
Hand strength: 85% absolute, 72% relative
Category: MONSTER
## DRAW ANALYSIS
Total outs: 15
*** COMBO DRAW - Very strong drawing hand! ***
## EQUITY & MATH
Equity vs 3 opponent(s): 47%
Draw equity (Rule of 4): ~60%
The difference was immediate and dramatic. With pre-computed math, the models stopped making mathematically indefensible plays. They’d correctly fold draws without odds and correctly call with them. The reasoning in their responses went from vaguely hand-wavy to citing specific numbers from the context.
Layer 2: The Paired Board Problem
One of the subtlest bugs in early playtesting: the AI would consistently overvalue hands on paired boards. If the board showed K♠ K♣ 7♦ 3♥ 2♠ and the model held A♠ Q♣, it would reason “I have a pair of Kings with an Ace kicker” and bet aggressively. But everyone at the table has a pair of Kings. The Ace kicker matters, but the hand isn’t nearly as strong as the model thinks.
The fix was a relative strength calculation that penalizes hands on paired boards when you don’t hold the paired card:
if (pairedValue && !handValues.includes(pairedValue)) {
if (rank <= 3) {
relativeStrength *= 0.6 // Significant penalty
}
}
This concept — the gap between absolute hand strength and relative hand strength — turned out to be critical. The prompt now reports both numbers, and the model gets explicit warnings like “Paired board - full house possible” in its vulnerabilities section. Double-paired boards get an even harsher penalty (0.4x multiplier) because you basically need trips or a full house to win.
Layer 3: Fixing the Passivity Problem
Even with good math, every model defaulted to playing extremely passive poker. Check, check, check, call, fold. They’d limp into pots, never bluff, and only bet with the absolute nuts. This is bad poker — it’s exploitable and it’s boring to play against.
The fix was a two-part aggression system.
First, the prompt itself includes an explicit aggression policy with calibrated frequencies:
## AGGRESSION POLICY (IMPORTANT)
- Baseline pure bluff frequency in good spots: ~18%.
- Semi-bluff frequency with 8+ outs or combo draws: ~28%.
- Increase bluffing vs tight-passive / high fold-to-cbet opponents.
- In position on dry boards, favor pressure (bet/raise) over passive checks.
These aren’t static numbers. The bluff frequencies dynamically adjust based on opponent tendencies at the table. If opponents fold to c-bets frequently, the baseline jumps up. Against calling stations, it drops.
Second — and this was necessary because models still occasionally ignored the aggression guidelines — there’s a post-processing layer that injects pressure into good bluffing spots:
const bluffOpportunity =
validActions.includes('raise') &&
(toCall === 0 || toCall <= Math.round(gameState.pot * 0.2)) &&
(
(equity >= 0.2 && equity <= 0.55) ||
draws.totalOuts >= 6 ||
(boardTexture.wetness === 'dry' && position.includes('LATE'))
)
if (bluffOpportunity && (finalAction === 'check' || finalAction === 'call') && Math.random() < pressureFreq) {
finalAction = 'raise'
}
This catches situations where the model had a legitimate bluffing spot — in position, on a dry board, with some equity — but chose to play passively anyway. The frequency is capped and weighted by how exploitable the opponents are. It made the AI feel like it was actually playing poker instead of just reacting to cards.
Layer 4: Preflop Guardrails
The most embarrassing AI mistake is folding premium hands to small raises preflop. It doesn’t happen often, but when GPT-5 folds pocket Queens to a 2BB raise, it breaks the illusion completely.
The preflop policy system classifies every starting hand into tiers — premium (AA, KK, QQ, AKs), strong (JJ, TT, AQs), playable (suited connectors, medium pairs), weak (everything else) — and then enforces minimum standards:
const shouldDefend =
handTier === 'premium' ||
(handTier === 'strong' && cheapPrice) ||
(handTier === 'playable' && veryCheapPrice)
Premium hands never fold to small raises. They get upgraded from passive calls to 3-bets. Strong hands defend at cheap prices. The policy also catches the opposite mistake: the model limping with Aces when it should be raising for value.
This layer acts purely as a safety net. The model makes its decision, and then the policy says “are you sure about that?” for the most egregious lines. It fires maybe 5% of the time, but those 5% of hands are the ones that would make players close the tab.
Layer 5: Opponent Modeling That Learns
This is probably the most technically interesting piece. Each AI player maintains a statistical profile of every other player at the table, updated incrementally across hands:
interface OpponentStats {
vpip: number // Voluntarily put money in pot %
pfr: number // Pre-flop raise %
cBetPercent: number // Continuation bet %
foldToCBet: number // Fold to c-bet %
aggressionFactor: number // (bets + raises) / calls
wtsd: number // Went to showdown %
}
These are real HUD stats that any serious poker player would recognize. The system classifies opponents into archetypes — tight-passive (nit), tight-aggressive (TAG), loose-passive (calling station), loose-aggressive (LAG) — and generates specific exploit suggestions that get injected into each model’s prompt:
GPT-5: TIGHT-PASSIVE (12 hands, FvCBet 70%) | Bluff frequently, respect their aggression
Claude: TIGHT-AGGRESSIVE (15 hands, AF 2.8) | Play solid, avoid marginal spots
The tricky engineering problem was preventing double-counted actions. The AI endpoint gets called multiple times per betting round, and the same actions can appear in the game state across calls. Each phase of each hand has its own cursor:
const phaseCursorKey = `${observerKey}::${handKey}::${gameState.phase}`
const startCursor = processedRoundActionCursors.get(phaseCursorKey) ?? 0
const incrementalActions = gameState.roundActions.slice(startCursor)
It’s in-memory tracking so it won’t survive a cold start, but across a warm session the AI genuinely adapts. After 10-15 hands, you can watch Claude start targeting GPT-5 with more bluffs because it noticed GPT-5 folds too much. That emergent behavior wasn’t programmed — it comes from feeding real data to models that are good at reasoning about it.
Layer 6: The Equity Safety Net
One more post-processing rule that made a big difference: if the model wants to fold, but its equity clearly exceeds the pot odds needed to call, the system overrides the fold:
if (finalAction === 'fold' && equity >= (potOdds / 100) + 0.08) {
finalAction = 'call'
}
The 8% buffer prevents marginal overrides, but it catches the cases where the model is throwing away a +EV call. Models sometimes fold with 40% equity facing a half-pot bet (only needs ~25% to call). This rule alone probably improved the AI’s win rate by a meaningful margin.
What I Learned
LLMs are better strategists than tacticians. Don’t ask them to do the math. Do the math yourself, then let them reason about what to do with it. This principle probably applies way beyond poker.
Post-processing > better prompts for edge cases. You can write the most detailed prompt in the world and the model will still occasionally fold Aces. A thin policy layer that catches 5% of decisions improved perceived quality more than any prompt iteration.
Opponent modeling is where it gets fun. Static AI is boring after five hands. AI that adapts to how you play is compelling after fifty. The emergent bluffing and counter-adjustment behavior that comes from feeding real stats to models is the most entertaining part of the whole project.
Poker is deceptively hard to implement correctly. Side pots alone took longer than the entire AI integration. Dealer button rotation, blind posting, all-in runout logic, the wheel straight — there’s a reason commercial poker software costs what it does.

