Source: https://oliviercouthaud.com/poker · Status: Prototype · 2026 · Tags: Python, PyTorch, Deep CFR, FastAPI # Poker Bot A no-limit hold'em bot that plays one openly declared seat at friends' home games. ## What I wanted I wanted a poker bot I could bring to my friends' game nights and have them play against. A home game is messy. People join and leave, rebuy and change the blinds halfway through, and stacks run from short to deep. I wanted a bot that copes with a table that keeps changing and runs on a laptop. The bot sits in one seat and everyone at the table knows it's a bot. It isn't there to help a hidden player. I started it in August 2026, and finished with a 17-hour push to have something usable for a game night. It has since played a couple of home games. The hard part turned out to be feeding the live game to the bot while a turn clock is ticking down. With real cards it can't see the table, so a person, in practice me, types the hands and actions into a phone and the bot suggests its move. That works, though it takes many taps per hand. So I also built an online table that friends join from their own phones, with the bot in a labelled seat. The program already has the game state there, so nothing needs typing. ## How it works Everything sits on a hand engine I wrote in Python for two to nine players. It handles blinds, min-raise tracking, the short all-in rule (a small all-in raise doesn't reopen the betting), side pots, uncalled-bet refunds and odd-chip awards. The live table rebuilds the engine from scratch after every typed update, so the engine stays the one place that knows the pots and what's legal. The strategy is trained with Deep CFR, a 2019 research method. Traversals of the game tree are sampled, neural networks predict the regrets, and a final policy network learns the average strategy. The network embeds the cards (hole, flop, turn, river) and combines them with betting features. I trained on rented GPUs from Vast.ai, and training resumes where it stopped after a preemption. **What is Deep CFR?** CFR is short for counterfactual regret minimisation. The program plays against itself, and after each pass it asks, for every decision it made, how much better some other action would have done. That gap is the regret, and it shifts toward the actions it regrets not having taken. Averaged over many passes, the strategy gets close to unexploitable in two-player games. Deep CFR swaps the giant table of regrets, which is far too big to store for poker, for neural networks that estimate them. Figure: Diagram. Training and evaluation. Training runs on rented GPUs; the evaluation and the console run on my laptop. Three choices mattered. The action menu is fold, call, five bet sizes (0.33, 0.5, 0.75, 1 and 1.5 times the pot) and all-in. Training hands randomise the table size and the stack depth (roughly 20 to 200 big blinds), so one network, the generalist, covers a range of tables. And the average strategy is weighted by iteration squared, so the near-random early iterations stop dragging it down. The online table is a small FastAPI server on the same engine. Each phone opens a WebSocket, picks a name and a seat, and gets a private view with only its own hole cards. The server starts a hand when two people are seated, plays the bot on its turn, and checks or folds for a player who runs out the turn clock. The generalist is the default bot there. With real cards, the operator enters the bot's cards, each opponent action and each board card, then taps the suggested action. The console seats two to six players. Between hands I can add or remove players, rebuy and change the blinds, and the bot stays loaded. Click through one decision below. ### One decision at the table - **Operator.** That's me, with a phone. When a hand is dealt I type the bot's two cards. After that I type each opponent's action and each board card as it happens. - **LiveHand.** Nothing in the console keeps its own running count of the pot. After every update it rebuilds the whole hand from the start in the hand engine, so the pot, the stacks and what's legal always come from one place. - **BlueprintBot.** The trained network looks at the cards and the betting so far and gives a probability for each of its eight moves. Moves that aren't legal right now are zeroed out, and the suggestion comes from what's left. - **Search, optional.** When only two players are left it can spend several seconds, around nine, re-solving the exact spot instead of trusting the network as it is. It does nothing with three or more players, and it stays off unless I switch it on. - **Suggested move.** The console shows the pick, for example a raise to some number of chips. I round it to real chips, make that move at the table, tap to confirm, and go back to typing. - **Hand log.** When a hand finishes, it's appended to a log with one line per hand: the cards, the actions and the payouts. **What is a blueprint, and what does search add?** A blueprint is a strategy worked out in advance for the whole game, so it has to be rough about any single spot. Search starts from the exact situation in front of the bot and works that one spot out in more detail, then falls back on the blueprint for everything after. The strongest poker bots in the research literature, Libratus and Pluribus, use that pairing. Mine does it too, but only heads-up. [Image: Three large phone buttons: Fold in red, Call 40 in blue, Raise in green] Figure: The online table's action buttons, from a test hand. Everyone joins from their own phone and sees only their own cards. The console for typing in a real game is plainer. ## Results Everything here comes from simulations, not from live play. The matches are scored in duplicate and measured in big blinds per 100 hands. Pick a bot and an opponent below. The same numbers for the two current bots are in the table underneath. **What are duplicate scoring and bb/100?** A big blind (bb) is the smallest forced bet, so bb/100 is how many of them the bot wins per hundred hands. Duplicate scoring plays each deal twice with the seats swapped, once with the bot holding each side of the cards, so luck of the draw cancels out and a smaller sample gives a fairer comparison. A positive number means the bot is ahead. ### Win rate by opponent, in bb per 100 hands **Current bots against scripted opponents, in bb per 100 hands. Simulations, rough. A nit plays only premium hands.** | Bot | Opponent | Result | |---|---|---| | Heads-up specialist | Random | +110 | | Heads-up specialist | Calling station | +147 | | Heads-up specialist | Tight nit, break-even | +2.9 | | Generalist, at several table sizes | Random | +200 to +800 | Measured against an exact best-response calculation, the heads-up bot is exploitable by about 330 to 346 mbb per game at 20 big blinds. Roughly, a strong bot sits under 100 and a fully solved one in single digits. **What does exploitable mean here?** Imagine an opponent who knows exactly how the bot plays and answers every decision perfectly. Exploitability is how much that opponent wins per game, in milli-big-blinds, where 1,000 mbb is one big blind. A perfect strategy scores zero. The calculation is exact, with no luck in it, but it only works in a smaller version of the game. That smaller version is heads-up with one preflop betting round, where the later streets are replaced by all-in equity. So the number is exact and repeatable, but it isn't full poker, and I have no equivalent for deep stacks or for more players. The generalist's results are noisier. It is roughly break-even to slightly negative against a disciplined nit, and its edge is over loose and passive players. I expect that is closer to a home game than a disciplined nit is, but a couple of games is not a test. The console seats two to six players, and my results stop at six. ## What I ran into **The first generalist over-folded.** It folded jacks 92% of the time on the heads-up button and aces about 19%. To tell a bug from undertraining I compared the policy network with the advantage network. Weighting the average toward later iterations, and a bigger, longer run, reduced the problem without removing it. Both current bots still over-fold the button, more as stacks get deeper. **Preflop fold rates from spot checks, heads-up. bb is big blinds of stack.** | Bot | Hand | Folds | Where | |---|---|---|---| | First generalist | Jacks | 92% | Button | | First generalist | Aces | about 19% | Button | | Heads-up specialist | Aces | about 18% | 100 bb | | Heads-up specialist | Kings | about 87% | 100 bb | | Generalist (current) | Aces | 0% | 100 bb | The heads-up specialist was trained at 20 to 60 big blinds, which is why it does not hold up at 100. The generalist is the default, and the specialist is for short-stacked heads-up only. **The first generalist against Slumbot.** Slumbot is the standard heads-up benchmark. Over 2,000 hands that version scored -81.7 bb/100, with a 95% interval of -115 to -58. It beat call-stations, maniacs and random players clearly, and scored negative against the nit at six-handed. I have retrained since, and the Slumbot match is yet to be re-run. Search looked better against it in a short sample, but heads-up variance is far too high to put a number on that. **Search helps in some spots.** It passes its sanity checks, for example it never folds the nuts, and on the first generalist it fixed the worst single spots: jacks went from folding 92% of the time to 0%. On the current bots it doesn't lower worst-case exploitability, because its ceiling is the quality of the blueprint underneath it. **The original blueprint:** my original blueprint (about 500 MB) no longer loads with the current feature encoding, so every default now points at the current generalist. The console takes more time when the bot stays in a hand, because one person types every action. A hand the bot folds takes about 5 taps and one it plays deep takes 15 to 20. I haven't timed it at a real table. That, with a clock running, is the main reason I built the online table. **The screen-reading helper.** I also started a helper that reads an online club app from phone screenshots and shows the bot's move. I did not take it far, because by then I had built the online table. ## Where it stands Everything above is against scripted opponents, and the Slumbot match is yet to be re-run on the current bots. A couple of home games is not a measurement. Search covers heads-up only; for three or more players it is not built yet. I planned an optional layer that adapts to each opponent, which is also not built yet. There's also a model that imitates human play, trained on a public set of hand histories: about 199,000 hands, of which 163,565 were usable, giving 1.8 million decisions. I have no results from it yet. A stronger bot would need weeks of GPU time, a finer action menu and proper deep-stack subgame solving. - **Stack**: Python, PyTorch, Deep CFR, FastAPI, WebSockets, a mobile web console and a multiplayer phone table - **Training**: Rented GPUs (Vast.ai); resumable - **Status**: Prototype - **Code**: Not public [← All projects](https://oliviercouthaud.com/projects)