Inspiration
Heads-up Short Deck 5-card PLO has huge equity swings, no public solver, and a scoring rule that only rewards statistically significant wins. We wanted a bot that never loses to strong opponents and still wins clearly against weaker ones, all inside a 1000-deal match with no memory between halves.
What it does
Our bot plays every match in three layers:
- Core engine (exploitative equity bot): a precomputed preflop equity table for every 5-card starting hand, Monte Carlo equity postflop against a range weighted by the opponent's betting, and pot-odds based fold / call / raise decisions.
- Showdown trust: every showdown tells us whether the opponent's raises were really strong. The bot learns this per opponent and per bet size, and narrows or widens the range it assumes. On top of that it tracks fold rates per street, bets into opponents that fold too much, check-raises frequent stabbers, and stops bluffing calling stations.
- Opponent identification: within the first few dozen hands it tests the opponent's actions against known strategy profiles. When it recognizes an equilibrium-style (MCCFR blueprint) opponent or a specific playing style, it switches to a precomputed best response for that opponent. Any mismatch sends it straight back to the core engine.
- Safety valve: if no profile matches and the bot is clearly losing after 300 deals, it switches to a balanced blueprint strategy for the rest of the half, turning a likely loss into a draw.
How we built it
Python on the official participant bundle (FastAPI server, official judge engine), numpy only. Best responses were computed offline with Monte Carlo CFR on a 256-thread server. Every candidate was tested with the official judge in isolated 1-CPU containers: thousands of duplicate matches against baseline bots, in-house opponents, strong self-trained blueprint bots, and adversarial "attacker" bots tuned specifically to beat us. A version was promoted only if it improved expected tournament points and never added a new loss. Time is paced on CPU time: the worst half uses about 3.3 of the 8 seconds, with 0 errors.
Challenges we ran into
- Equilibrium-style bots: our exploitative engine lost to them, and hand-tuned fixes stayed within noise. The breakthrough was computing exact best responses instead of tweaking thresholds.
- Statistical scoring: a match is only a win if the 95% confidence interval clears zero, so many "wins" were really draws. We optimized for expected points (win 1, draw 0.5), not raw chips.
- Self-play lies: ideas that won against our own sparring bots often lost to real opponents, so every decision was made on real-match results.
- The judge counts HTTP overhead against the time bank, so we moved to CPU-time pacing with sample reduction.
Accomplishments that we're proud of
- No losses against any opponent in our final test field, including two strong equilibrium-style bots we used to lose to.
- 0 illegal actions in about 190,000 fuzzed decisions, 0 errors in extreme matches.
- A modular bot where every new module is proven to change nothing unless its detector fires (tens of thousands of identical decisions checked).
What we learned
Measure against the real field, not your own sparring partners. Knowing an algorithm is not the same as knowing an opponent: two bots trained with the same method can need completely different counters. And in a significance-scored tournament, not losing matters as much as winning big.
What's next for solo squad
Real-time subgame solving within the time budget, finer card abstractions for our own blueprint, and faster online identification of unknown equilibrium bots.