Inspiration
Poker has always been one of the most popular games in the world, and we've spent way too many hours watching poker Instagram reels, especially @vegasmatt. It's always fun watching him be completely transparent about his hands, then either win big or lose a lot. Watching those swings made us wonder how a computer would play the same spots.
What it does
On every decision our bot:
- Rates its hand. Before the flop, a fitted model scores its five hole cards in microseconds. After the flop, it simulates hundreds of possible opponent hands and remaining cards to estimate how often it wins.
- Reads the opponent. It weights the opponent's likely hands by how they've been betting, and tracks how often this opponent folds when we bet. Against bots that fold too much it bluffs more, and against bots that never fold it stops bluffing.
- Picks the most profitable action. It compares the expected chips won by folding, calling and each raise size, and plays the best one.
- Never breaks. Every decision is checked against the legal moves, any error falls back to a safe check or fold, and the bot watches its own time bank, switching to a near-instant mode before it can run out.
How we built it
Fast hand evaluation. We turned the official evaluator into a dense lookup table: every 2-card hole pair × every 3-card board triple, \( 630 \times 7140 \) entries stored as int16. An Omaha hand is the best of \( 10 \times 10 = 100 \) combinations, so numpy can score hundreds of opponent hands in a single gather. We cache the table to disk, which cuts startup to under a second.
Postflop: Monte Carlo equity. For each decision we sample opponent hands and runouts from the unseen cards (exact card removal, half credit for ties). Each sampled opponent hand gets weight \( w = s^k \), where \( s \) is how strong it looks on the visible board and \( k \) grows each time the opponent bets or raises.
Choosing an action. We compare one-step expected values:
$$ \text{EV}(\text{call}) = E \cdot r \cdot (P + C) - C $$
$$ \text{EV}(\text{raise}) = f \cdot P + (1 - f) \left( E' \cdot r \cdot P' - A \right) $$ Here \( E \) is our equity, \( r \) discounts equity we won't fully realize (more when out of position), \( P \) is the pot, \( C \) the amount to call, \( A \) the chips we add and \( P' \) the pot if the raise is called. The fold probability \( f \) is learned per opponent, starting from a prior:
$$ \hat f = \frac{\text{folds} + 0.4 \cdot 8}{\text{faced} + 8} $$
This makes our bluffing adaptive: we bluff more against bots that fold too much and stop bluffing against bots that never fold.
Preflop. A 16-feature linear model (pairs, suitedness, straight potentiale to simulated equity, ranks our five hole cards in microseconds.
Robustness. Every function is wrapped in a guard that falls back to checkailures in a half, the bot switches to safe play for the rest of that half. If the strategy module fails to load at all, the server still starts. The bot also tracks its own time spent and steps down to a near-free heuristic before the bank can run out.
Challenges we ran into
- The clock lied to us. The judge charges wall-clock time, but our bot could only measure its own CPU time. When we flooded every core with busy processes, the judge charged 2.3–2.4× what the bot thought it had used. Our first fix, a "contention factor", didn't work. We measured it, saw it, removed it, and moved the safety thresholds earlier instead.
- Startup under load. On a saturated machine, startup took 11 seconds against a 15-second limit. Missing that limit is a forfeit. Caching the lookup tables brought it down to about 4 s under load and 0.8 s idle.
- Tooling gotchas. Windows vs. WSL environments, line-ending churn, and two test runs colliding on the same ports all took time. Our test harness refuses to play against a server it didn't start, which turned a confusing bug into a clear error message.
Accomplishments that we're proud of
We are most proud of attempting to make:
- A bot that adapts to each opponent. It learns how often each opponent folds and adjusts its bluffing automatically.
- Utilizing Claude Code to help us implement our strategy.
What we learned
- Robustness beats brilliance. One unhandled exception path, triggered 5 times, loses everything.
- Variance is the enemy in this format. Matches are decided by a 95% confidence interval. A gimmick like random all-ins can turn a clear win into a chop.
- Measure, don't guess. Stress-testing under real CPU contention found failures that never showed up on an idle laptop.
What's next for OP Heads-Up
Lunch