Inspiration

Heads Up asks for a bot that plays thousands of hands of an unusual game, Heads-Up Short Deck 5-card Pot-Limit Omaha, under tight limits: 8 seconds of thinking per 1,000-hand half, 1 CPU, no network, and a forfeit after five errors. We wanted a bot that is never the one that breaks: fast, legal on every move, and still able to adapt to whoever it is playing.

What it does

XploreBot (Team Xplore) decides every action from four parts:

  • Preflop in O(1). Equity against a random hand is precomputed for all 376,992 five-card holdings (one byte each, 377 KB) and looked up by a combinatorial index.
  • Monte Carlo postflop, throttled by a time governor. Each simulation is sized from the remaining time bank, so the bot never runs out of clock. Measured thinking time is about 1 to 2 seconds per half, against an 8-second budget.
  • Range-aware equity. Opponent hands are sampled from a range that narrows each time the opponent raises.
  • An in-match opponent model. It tracks how often the opponent raises and how often it folds to a raise, then bluffs more against over-folders, stops bluffing against calling stations, and calls down lighter against maniacs.

A legality guard wraps everything: any exception or illegal output falls back to a safe, legal action.

How we built it

Pure Python 3.12 on the organizers' template. lab/gen_preflop.py builds the preflop table (19,998 suit classes, seeded, about 5 minutes on 20 cores). We wrote an in-process re-implementation of the engine for fast experiments and checked it against the organizers' engine on deterministic tests. Policy thresholds live in params.json and were tuned with a seeded cross-entropy search over duplicate-deal self-play against a 12-opponent gauntlet: the four supplied baselines plus our own bot in eight styles (tight-aggressive, loose-aggressive, nit, loose-passive, maniac, bluffer and two earlier parameter sets). Fitness blends the mean result with the mean of the three worst matchups, so the search cannot win by only beating weak bots.

Results

Local matches, 200 duplicate deals per opponent, 95% intervals (shipped parameters):

Opponent bb / deal 95% CI
Random +6.88 [+4.05, +9.71]
Always half-pot +9.92 [+7.01, +12.83]
Always pot +7.22 [+3.42, +11.03]
Calling station +9.12 [+6.94, +11.29]

Zero errors in every match. On fresh seeds the tuned parameters beat the hand-set defaults by +1.41 bb/deal over 1,500 deals (95% CI [+0.83, +1.99]), and lifted the worst-three gauntlet average from -0.96 to +0.31.

Challenges we ran into

  • A first tuning run against the four baselines alone gave no real gain (a chop over 1,000 deals), because the baselines are too easy to separate good settings from bad. Building the self-play gauntlet fixed that.
  • Running enough Monte Carlo samples inside a 2 to 3 ms per-decision budget meant measuring sample cost at startup and spending more of the bank on bigger pots.

What we learned

Duplicate deals and confidence intervals matter more than raw win totals: with only a few hundred deals, real differences between parameter sets are hard to tell from noise. Robust beats clever when one error can cost a match.

What's next

Richer opponent features (showdown strength against betting lines) and a small regret-based solver for river spots.

Built during TAMU Datathon 2026 with AI coding assistants (Claude). Everything above is reproducible from the repository.

Built With

Share this project:

Updates

Submission history