Rust · heads-up no-limit hold'em · Tave La Rocque · 2026

holdem-cfr

A counterfactual regret minimization solver for heads-up no-limit Texas hold'em, built from scratch in Rust. It covers the same pieces as Libratus and Pluribus at laptop scale: an abstracted blueprint trained by Monte Carlo CFR, an exact range-vs-range postflop solver, and a measurement stack that reports how exploitable the result is.

Every layer is checked against something it cannot fake: closed-form equilibria, published game values, brute-force enumeration, or an independent implementation.

share raised share limped faded = folded
The trained blueprint's first decision on the button, 100 bb deep: the share of each starting hand it raises, limps or folds. Suited hands above the diagonal. Read from the 924M-iteration strategy the web table plays.
0evaluator mismatches across all 133,784,560 seven-card hands
4.9×10⁻⁶Leduc exploitability with DCFR after 10,000 iterations
31 sflop solve of a 293,390-node tree to 0.33% of pot, on a MacBook Air
1.55 BMCCFR iterations trained on a laptop for the 13,704-node blueprint
~9,600lines of Rust, 95 Rust tests plus property tests, CI on every push
How it was built

Ten milestones, each with a gate it had to pass

Each milestone shipped with a walkthrough PDF and only counted as done when its gate passed in CI.

MilestoneWhat it addsGate
M1Generic Game trait, Kuhn poker, vanilla CFR, exact best responsegame value −1/18; strategy inside the analytic equilibrium family
M2Leduc hold'em, CFR+, Linear CFR, Discounted CFR936 information sets; value −0.0856 matches the literature; CFR+ 280× better than CFR at 10k iterations
M3Perfect-hash 5/6/7-card evaluator (colex rank hashing, direct flush table, 125 KB)matches a brute-force reference on every 7-card hand; ~62M hands/s
M4No-limit range-vs-range river and turn solver: vectorized DCFR, O(n) showdowns with card removalpolarized river matches the closed form P/(P+B); agrees with the scalar engine; catches 5 injected bugs
M5Flop solving: rayon parallelism with safe split_at_mut, exact suit isomorphism, memory budgetparallel results bit-identical to serial; isomorphic solve equals the full solve
M6.1Perfect hand-isomorphism indexer and full no-limit rulesflop indexer proven a bijection exhaustively; 169 / 1.29M / 13.96M / 123.16M classes
M6.2Exact hand-strength tables for every class; E[HS²] percentile buckets (169/50/50/50)preflop equities match published values (AA 85.20%, 72o 34.58%)
M6.3Abstract no-limit blueprint (13,704 nodes) trained by multithreaded external-sampling MCCFRMCCFR converges on Leduc against the exact engine; checkpoints round-trip
M6.4Duplicate-match arena; f64 accumulators; blueprint exported to a browser tableself-play scores exactly 0; browser bucketing bit-identical to Rust on 660 spots
M6.5Local best response: exploitability lower bound with exact Bayesian range trackingequities match brute force; ranges equal Bayes' rule to 10⁻¹²
Results

From exact solutions to measured approximations

Small games are solved exactly and verified with a full best response. The full game is too large for that, so the blueprint is measured with local best response, which finds a lower bound on how much a counter-strategy can win.

Leduc hold'em: four CFR variants

Leduc exploitability by iteration for four CFR variants 10-6 10-5 10-4 10-3 10-2 10-1 100 101 1 10 100 1,000 10,000 iterations exploitability (chips/hand) CFR Linear CFR CFR+ DCFR
Exact exploitability after each iteration, log scale. CFR+ and DCFR reach 6.7×10⁻⁶ and 4.9×10⁻⁶ chips per hand at 10,000 iterations; vanilla CFR reaches 1.9×10⁻³.

Full game: how exploitable is each checkpoint?

Local best response winnings against four checkpoints, with 95% intervals 0 400 800 1,200 1,600 LBR winnings, mbb/hand (lower is better) 34M 6 min, f32 +1,143 408M f32 +1,207 924M f32 · on the table +1,100 1,552M f64, average reset +1,389 folding every hand loses 750
What local best response wins against four blueprint checkpoints, 50,000 duplicate hands each, with 95% intervals. Exploitability flattened after the first few minutes of training, which points at the card abstraction, not training time.
What the measurements caught

Three findings the tests forced

A cross-check that missed bugs

The postflop solver was checked against an independent scalar implementation, and deliberately injected bugs were used to test the check itself. It first caught only one of three: the test ranges had no tied hands and no shared combos, so those code paths never ran. Adding them made all five injected bugs fail the test.

Float precision past 2²⁴

A checkpoint trained 12× longer lost to a shorter one by a statistically clear margin. The cause was 32-bit accumulators: past about 8 million, adding a probability like 0.3 rounds to nothing, so 3.4% of the strategy table had silently stopped averaging. Switching to 64-bit fixed it at no speed cost.

Head-to-head stops ranking

Matches between checkpoints became intransitive: A beat B, B tied C, C beat A. Near equilibrium, a match measures how leaks line up, not quality. Local best response ranked them instead, and showed that more training was no longer the bottleneck.

Limits, stated plainly: the blueprint plays an abstracted game with a fixed bet menu and no real-time search, and local best response gives a lower bound, not the true exploitability. Real-time re-solving of the turn and river with the exact solver is the next milestone.