Inspiration

Heads-up poker under duplicate scoring is a pure skill test: every deal is played twice with the seats swapped, so card luck cancels and you only score where you play the same cards better than your opponent. That pushed us away from building an unexploitable "solver" bot and toward a bot that reads each opponent quickly and exploits their habits, while staying robust against strong players.

What it does

For every decision, AFK-Adaptive estimates how often its hand wins against the hands this particular opponent is likely to hold, compares that to the price of the bet, and keeps learning throughout the 1,000-hand match.

The core rule is pot odds: call \( c \) into a pot \( P \) when equity \( e \) clears the price plus a margin:

$$ e > \frac{c}{P + c} + \text{margin} $$

  • Preflop: an offline table of all 376,992 five-card starting hands (19,998 classes after suit symmetry), with equity against random, top-50%, top-30% and top-15% ranges.
  • Postflop: Monte Carlo simulation against a soft opponent range: a mixture of the hands consistent with their actions and a learned share of bluffs:

$$ e = (1-\beta)\, e_{\text{top-}q} + \beta\, e_{\text{random}} $$

  • Opponent model: action frequencies with priors, whether their raises actually mean strength (checked at showdowns), their bluff share \( \beta \), and a calibration term that corrects our own systematic errors.
  • Exploits: bluffs opponents who fold too much (a half-pot bluff profits when they fold more than \( \tfrac{1}{3} \) of the time), value-bets thinner against callers, calls down maniacs, defends against over-aggression, and sets check-raise traps against opponents who bet whenever checked to.
  • Robustness: every decision is timed against the 8-second time bank with an adaptive simulation budget, and any error falls back to a legal action, so the bot cannot forfeit.

How we built it

  • Python bot on the provided FastAPI server template.
  • C + OpenMP offline program to compute the preflop equity tables.
  • An in-process duplicate-match simulator and a pool of sparring bots (calling stations, maniacs, nits, equity bots, random) for fast iteration.
  • A strict evaluation rule: every change had to beat the previous version on many duplicate seeds, and again on fresh, unseen seeds, before shipping.

Challenges we ran into

  • Noise. A single 1,000-deal match swings by about ±0.5 bb/deal on luck alone, so every decision needed many seeds and confidence intervals.
  • A subtle modelling bug. Our first range model treated bets on consecutive streets as independent evidence, concluded that aggressive opponents always held monsters, and folded far too often. Logging every decision and comparing predicted equity with actual outcomes exposed it: on the river we predicted 18% equity where the true win rate was 42%.
  • Good ideas that didn't work. EV-based bet sizing, a full Bayesian range model, per-street bluff learning and recency weighting all sounded better, and all lost or tied when measured. We kept only what the data supported.

Accomplishments that we're proud of

  • Clear wins against every simple opponent type, by up to +7 bb/deal.
  • A bot that turned a large deficit against strong adaptive opponents into roughly break-even through calibration and traps.
  • Zero errors across tens of thousands of simulated matches, and a clean tournament build.

What we learned

Calibration beats complexity. A simpler model with honest, data-driven corrections outperformed more sophisticated ones. Evaluation infrastructure (duplicate seeds, held-out checks, confidence intervals) mattered as much as the strategy itself.

What's next

Combining an equilibrium (CFR-style) baseline with our exploitation layer, and per-street opponent modelling.

Built With

Share this project:

Updates

Submission history