donksolver
A tabular Monte Carlo CFR solver for no-limit hold'em. It solves an abstract game of up to six players on CPU, freezes the average strategy into a versioned bundle, and serves actions for real game states through a thread-safe batch server.
pip install donksolver
The core is C++17 behind a plain C ABI, loaded from Python with ctypes. Because it is not a CPython extension, one wheel per OS/CPU serves every Python version (free-threaded builds included): Linux x86_64/aarch64 (glibc and musl), macOS x86_64/arm64, Windows AMD64/ARM64. Elsewhere pip builds the sdist, which needs CMake and a C++17 compiler.
What it is not. It is not a Nash certificate for 6-max: with more than two players CFR converges to a coarse correlated equilibrium at best. Exact exploitability is available for the validation games (Kuhn, Leduc) only.
Quick start
donksolver abstraction --buckets 50,50,50 --threads 2 --out abs.dab # about a minute
donksolver size --preset 6max-lite --stack-bb 100 --abstraction abs.dab
donksolver solve --preset 6max-lite --stack-bb 100 --abstraction abs.dab \
--iterations 50000000 --threads 6 --out runs/6max-lite_100bb # resumable (state.bin)
donksolver bundle --abstraction abs.dab --out runs/bundle.json runs/*/policy.dpol
donksolver toy --game leduc --players 2 --iterations 1000000 # exact exploitability
python -m donksolver ... is the same command line.
From Python:
from donksolver import Abstraction, Solver, Solve, Bundle, preset, toy
s = Solver(toy("leduc", 2))
s.run(10**6, threads=2)
print(s.exploitability()) # {'ev': ..., 'gains': ..., 'nashconv': ...}
Abstraction.build((50, 50, 50), threads=2).save("abs.dab")
s = Solver.nlhe(preset("6max-lite", stack_bb=100), "abs.dab", Solve())
s.solve("runs/6max-lite_100bb", iterations=50_000_000, threads=6)
b = Bundle("runs/bundle.json")
out = b.act(stacks, blinds, hole, board, histories, uniforms) # kind/chips/prob/status per row
Card abstraction
Preflop is always the 169 isomorphic classes. Postflop there are two kinds, chosen when the abstraction file is built; a solve reads the bucket counts from the file.
percentile (default). One global ranking per street: percentile buckets of E[HS^2] on the flop and turn and of HS on the river (Johanson et al. 2013). The board is not part of the key.
nested. Buckets are relative to the board and split by potential:
donksolver abstraction --kind nested --buckets 12,12,24 --sub 4 --boards none --out nested.dab
e1is the equity:E_river[HS]on the turn,E_turn[E_river[HS]]on the flop.e2is the potential:E_river[HS^2]on the turn,E_turn[(E_river[HS])^2]on the flop. Two holdings with the same equity differ ine2when one of them gets there by drawing.- On each flop and each turn, holdings fall into N equal-width bins of
e1between the weakest and strongest holding on that board, and each bin is split into--subequal-width bins ofe2. The river uses N equal-width bins of HS. --boardsadds a board class to the key, per street (none,suits, ortexture; one value orflop,turn,river).suitsis the flush structure (3 flop, 5 turn, 4 river classes).texturemultiplies it by the counts of cards seven-or-higher and jack-or-higher (30 / 75 / 84 classes).
Buckets per street are classes x N x sub (river: classes x N), so
12,12,24 with --sub 4 and no board classes is 48 / 48 / 24, and the same
with --boards texture is 1,440 / 3,600 / 2,016. Nothing is sampled, so the
file is a deterministic function of its parameters. It builds in about 30 s on
4 threads.
Multiway postflop
postflop_max_players is the number of players that may see a flop before the
tree ends in a check-down leaf. The 6max-lite and 6max presets set it to 2
to keep the tables small. Set it to the number of players (preset 6max-mw, or
--postflop-max-players 6) and every flop is played out with the multiway
menus (mw_menus, mw_max_raises); nothing then comes back no_node for
being multiway. The cost is memory:
| 6max-lite menus, 50/50/50 buckets | 2 | 3 | 4 | 6 players postflop |
|---|---|---|---|---|
| 40bb, tables (double) | 0.4 GB | 0.9 GB | 1.9 GB | 3.7 GB |
| 100bb, tables (double) | 1.5 GB | 3.6 GB | 8.1 GB | 17.9 GB |
precision=float halves these.
Serving real states
A bundle holds one card abstraction and one policy per stack class. For each
row Bundle.act:
- picks the class nearest the actor's effective stack (log scale);
- replays the real action history through that class's tree, mapping each real raise to a neighbouring abstract size with the pseudo-harmonic mapping (Ganzfried & Sandholm, 2013). The mapping's coin is a hash of what the acting seat knows, so a state is always read the same way;
- buckets the actor's cards, samples the strategy row with the caller's uniform, and converts the abstract action back to chips.
Every row reports a status: ok, offtree (translated, still served),
no_node (the tree has no node here, such as a multiway flop in a tree cut
by postflop_max_players),
desync, bad_input. Rows at no_node and above carry no usable action; the
caller decides what plays them.
donksolver.engine.Player does the packing for hands played on
pokerfast's FastHand
(pip install donksolver[pokerfast]):
from pokerfast import FastHand
from donksolver.engine import Player
player = Player("runs/bundle.json")
hand = FastHand()
actions, chips, probs, status = player.act([hand])
It reads the public state and the acting seat's own hole cards, nothing else.
Layout
native/include/donk/ header-only C++17 core
cards.hpp card ids, 7-card evaluator, RNG
betting.hpp no-limit betting rules, side pots, rake
tree.hpp abstract trees: no-limit menu builder, fixed-limit builder
abstraction.hpp 169 preflop classes; percentile or nested (equity x potential) buckets
games.hpp Kuhn / Leduc (validation) / NLHE games; range deal sampler
solver.hpp MCCFR: es / cs traversal, linear | linear_avg | vanilla weighting
policy.hpp frozen policy files, stack-class bundles, translation, batch act
native/src/capi.cpp the C ABI (one shared library, no Python headers)
native/CMakeLists.txt builds donksolver_core.{dll,so,dylib}
src/donksolver/ Python package (ctypes): Solver, Abstraction, Bundle, CLI
engine.py adapter: pokerfast FastHand -> action server
tests/ audits (each with a negative control); pytest or run_all.py
Algorithms
| piece | options | notes |
|---|---|---|
| tree | per-street, per-raise-level menus of pot-after-call fractions; kappa_bet drops bets that commit most of the stack; all-in by rule (always, threshold, never) |
preflop 0.6 at the open is a 2.5bb raise |
| deals | uniform for a blueprint; a rejection sampler for weighted ranges | whole-deal restart on a card collision, audited against the enumerated law |
| traversal | algo=es external sampling (default), algo=cs chance sampling with pruning at sigma <= 1e-7 |
ES makes a 6-max iteration affordable |
| weighting | linear (regrets and average weighted by t; Linear CFR, Brown & Sandholm 2019), default; linear_avg (uniform regrets, t-weighted average); vanilla |
linear_avg has no proven bound and measured about 3.6x worse on Leduc |
| average | avg_mode=weighted (own reach, opponents full width) or sampled (own action sampled, as in Pluribus), each run with probability p_avg per iteration |
both are consistent, audited |
| doneness | root EV per chunk with a standard error, one seat per deal | iterations per node is logged, but it is a budget and not a regret bound |
| cards | 169 preflop classes; percentile buckets, or per-board equity bins split by potential, optionally keyed by a board class | imperfect recall |
| threads | shared tables, striped spinlocks on writes; hogwild=1 drops them |
The betting rules are pokerkit's cash-game no-limit semantics, checked step by
step against pokerfast's engine: every raise re-queues the other live players,
short all-ins accumulate toward reopening the betting, and the minimum raise is
capped by the effective stack. Blinds default to 100/200. Side pots merge on
winner sets and odd chips go to the lowest seat. The default rake is 5% of the
pot with a cap by stake and players dealt, no flop no drop (rake.py); set
Game.rake_pct = 0 for none.
Building from source
git clone https://github.com/wesboyt/donksolver
cd donksolver
pip install ".[test]"
DONKSOLVER_TEST_INSTALLED=1 pytest
Or build the library by hand and point the package at it:
cmake -S native -B native/build
cmake --build native/build --config Release
export DONKSOLVER_LIB=$PWD/native/build/donksolver_core.so # Release/donksolver_core.dll with MSVC
PYTHONPATH=src pytest
AVX2 is opt-in (-DDONK_AVX2=ON) and off in every published wheel. A paired,
interleaved A/B on a 6-max-lite solve measured a 1.025x mean ratio, which is
noise (pairs 1.07/0.99/0.94/1.10), and MSVC 14.43 /arch:AVX2 miscompiled a
masked loop in the abstraction builder (see docs/RESULTS.md). An AVX2 build
must re-run tests/audit_abstraction.py.
Audits
pytest, or python tests/run_all.py. Every check that could pass vacuously
has a negative control: a deliberately broken variant that must be detected.
The card abstraction and two small solved policies are built on first use and
cached under artifacts/ (about three minutes).
| audit | what | control |
|---|---|---|
| audit_eval | evaluator order == textbook best-of-21 and == pokerfast's eval7 | straight/flush swap |
| audit_payoff | replay == pokerfast FastHand at every step; settlement + rake == FastHand + pokerfast.rake | rake preflop; exact splits |
| audit_deal_law | range sampler law, chi-square vs enumeration | seat-only redraw |
| audit_abstraction | river HS exact; suit isomorphism lossless; bucket mass; buckets predict strength | hole-only suit permutation |
| audit_nested | nested e1/e2 == brute force; bucket == the nested rule; board classes; suit isomorphism; draws sit in higher sub-bins | equal-mass bins; sub-bins cut by equity; class ignoring suits; hole-only permutation |
| audit_cs_regret | chance-sampled regret is unbiased, Leduc 2p/3p, exact SE | no opponent reach |
| audit_average | sampled average -> exact linear average | opponent-weighted average |
| audit_convergence | Kuhn value -1/18; Leduc LCFR O(1/sqrt T); linear_avg not better; 3p gains fall |
current strategy (no average) |
| audit_translation | on-tree round trip; pseudo-harmonic frequency; legality in FastHand | linear mapping |
| audit_serving | served frequencies == table; mixed-seat hands; throughput | permuted table |
| audit_multiway | three-way flop/turn/river nodes exist, are solved and are served with no no_node |
tree cut at two players |
Measured
docs/RESULTS.md has every number and its conditions. Headlines, on 2 threads
of a laptop CPU:
- abstraction build in about 30 s, byte-reproducible;
- 6-max-lite 40bb ES solve at about 28k iterations/s;
- serving at 250k rows/s;
- Leduc 2p LCFR NashConv of about 0.02 at 1e6 iterations.
Limitations and known biases
- Stack classes are symmetric. A state with unequal stacks is served by the class nearest the actor's effective stack, and real sizes are translated by pot fraction. The cost of that translation is not measured.
- Multiway postflop costs memory. The
6max-liteand6maxpresets cut the tree when more than two players see a flop: the leaf is a check-down, which biases preflop values toward position-less equity, and those states come backno_nodein serving.6max-mwplays every flop out, at about ten times the table size. No multiway blueprint has been solved and measured yet; the audit covers a three-player game. - The abstraction has imperfect recall in both kinds: each street's bucket is looked up independently of the previous street's.
- Nested buckets are equal-width, not equal-mass. Some buckets hold few holdings and converge slowly. Which kind plays better at a given table size has not been measured.
- No NLHE best response. For hold'em, doneness is root-EV stability only.
precision=floathalves table memory but loses increments once a row sees about 1e7 visits under linear weighting. The default is double.- A policy is tied to its abstraction's hash. A rebuilt abstraction with different parameters invalidates the bundles made with the old one.
License
MIT. See LICENSE.
Metadata
Release files for donksolver 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| donksolver-0.1.0.tar.gz | 84.3 kB | With provenance | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| donksolver-0.1.0-py3-none-win_arm64.whl | Python 3 | none | Windows ARM64 | Details |
| donksolver-0.1.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| donksolver-0.1.0-py3-none-musllinux_1_2_x86_64.whl | Python 3 | none | Linux musl 1.2+ x86-64 | Details |
| donksolver-0.1.0-py3-none-musllinux_1_2_aarch64.whl | Python 3 | none | Linux musl 1.2+ ARM64 | Details |
| donksolver-0.1.0-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64, Linux glibc 2.24+ x86-64 | Details |
| donksolver-0.1.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64, Linux glibc 2.24+ ARM64 | Details |
| donksolver-0.1.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| donksolver-0.1.0-py3-none-macosx_10_13_x86_64.whl | Python 3 | none | macOS 10.13+ x86-64 | Details |
Total release size: 3.9 MB
Release files / donksolver-0.1.0.tar.gz
| Download URL | donksolver-0.1.0.tar.gz |
|---|---|
| Size | 84.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
78edaa593248f8773be63fea5cfaeaed3606a1d6c5d51e3e7160c5f5b5d796c4
|
|
BLAKE2b-256 checksum How to use checksums |
aabe9d092b04b218f3268dc6dbf01f65268148b9b7c3d35543e56da157ec13e2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-win_arm64.whl
| Download URL | donksolver-0.1.0-py3-none-win_arm64.whl |
|---|---|
| Size | 260.6 kB |
| Tags | Python 3 Windows ARM64 |
|
SHA-256 checksum How to use checksums |
883cb2808804459b64fe5fa0620f1e79ed90146855e725b9356a9535939e7cb8
|
|
BLAKE2b-256 checksum How to use checksums |
7235e4a24dbe91a53f0f597531830b64e750979b1d0e3b2e307ad6240b4a541d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-win_amd64.whl
| Download URL | donksolver-0.1.0-py3-none-win_amd64.whl |
|---|---|
| Size | 291.3 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
039dc12cbd4e39a2019c94afdffd3240712d6b00521216cf5447715deeb85b47
|
|
BLAKE2b-256 checksum How to use checksums |
c9f1869346523b8744a4a99ecec41797167b0b5214bd0de231ccbfc6f19cbfb6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-musllinux_1_2_x86_64.whl
| Download URL | donksolver-0.1.0-py3-none-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | Linux musl 1.2+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
e33290f988e951b6cdbacceba889278f4819ab5785b070080f62fba60f4d803d
|
|
BLAKE2b-256 checksum How to use checksums |
c67b92a1f8096fe7527e479819cc6be060559729ac85f215f9660bf0b86394fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-musllinux_1_2_aarch64.whl
| Download URL | donksolver-0.1.0-py3-none-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Linux musl 1.2+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
8b7f7cde55384103f1443c930cabdd088546e95e28203d12f3568ca5597ade0d
|
|
BLAKE2b-256 checksum How to use checksums |
ae9d4c8c857986b674680f15208c9368fd44e37c9449993ab34792f2e7a0874e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
| Download URL | donksolver-0.1.0-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl |
|---|---|
| Size | 228.0 kB |
| Tags | Linux glibc 2.24+ x86-64 Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
098e47aef2c3cc6072bc7b671a795579579c7d7851152bd6e3a8217bbaafae56
|
|
BLAKE2b-256 checksum How to use checksums |
742c35a6e50ecaaf62ca077a93c6b57aa9d04bbf9cb80f9ba6039bad75f8a2e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl
| Download URL | donksolver-0.1.0-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl |
|---|---|
| Size | 218.4 kB |
| Tags | Linux glibc 2.24+ ARM64 Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
9ad162e8746e86f5589bc39d169fe1e8a2745416a1879328c6d70e25e38ec115
|
|
BLAKE2b-256 checksum How to use checksums |
3b1a627c3b89dc7202b2bdbc974ac2683b902a35ac604a9ffe4b8d8c6ea088c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-macosx_11_0_arm64.whl
| Download URL | donksolver-0.1.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 161.1 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
b95840844fa94645e9f8da26c4b95a0a8aa1768321be4289e00a0fd6e1491735
|
|
BLAKE2b-256 checksum How to use checksums |
32debb51e17c4ad00e45e5817d1319285e2038920b86a049633a5ea16dac920d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / donksolver-0.1.0-py3-none-macosx_10_13_x86_64.whl
| Download URL | donksolver-0.1.0-py3-none-macosx_10_13_x86_64.whl |
|---|---|
| Size | 160.9 kB |
| Tags | Python 3 macOS 10.13+ x86-64 |
|
SHA-256 checksum How to use checksums |
99645a6f565944662c5708f9ec14102fb90369c84223d4c33775a6b6e3b694eb
|
|
BLAKE2b-256 checksum How to use checksums |
222f7c035cf4ba640c72b7bc4d7c7643b798f514ddc25d0fc196024b24fbd7aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logSecurity for donksolver 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 9 files in this release have been uploaded with provenance metadata.
Attestation for donksolver-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.