Releases: amaster97/poker_solver
Release list
v1.10.0 — Postflop optimization milestone + Premium-A blueprint
v1.8.3 — Engine + UI: full-tree preflop RvR, BR-walk cache, chained orchestrator, true Nash default
poker-solver v1.8.3 — Full-tree preflop RvR + BR-walk caching + chained orchestrator + UI True Nash default (major engine + UI release)
Status: DRAFT (compile-since-v1.8.2). Compiled 2026-05-28 from the
post-v1.8.2 main branch; refreshed 2026-05-27 after the PR
#139/#122/#121/#126/#20 merge wave.
Baseline commit on origin/main: 16c92e6 (v1.8.2 tag, 2026-05-28).
Compilation tip: 30cbd9f (PR #20 cross-platform CI matrix, last of
the 2026-05-27 merge wave).
Release date: TBD (user-gated; release notes are user-facing).
Tag: v1.8.3 (to be created at ship time).
Final tag SHA will be set at git tag time.
Hold for user review. This is a draft anticipating the next ship. The
contents reflect what has landed on main after 16c92e6. The four
previously-held major PRs ([#139][pr139] BR-walk caching, [#122][pr122]
full-tree preflop RvR engine, [#121][pr121] chained orchestrator Phase A,
[#126][pr126] UI True Nash default toggle) plus [#20][pr20] cross-platform
CI matrix have all merged and are promoted to first-class entries below.
Headline
v1.8.3 — Full-tree preflop RvR engine + BR-walk caching + chained
orchestrator + UI True Nash default — major engine + UI release.
Six user-visible items land in this release:
- CLI true-Nash default for range queries.
poker-solver riverand
poker-solver subgamenow dispatch multi-combo villain ranges through
solve_range_vs_range_nash(vector-form CFR, joint imperfect-info Nash)
by default. Sample wall: 3-combo villain, river, 50 iters: ~0.46 s
default vs ~1.70 s legacy. See "CLI" §1. - UI True Nash RvR default flip (PR [#126][pr126]). The GUI
range-vs-range run panel now defaultssolver_mode = "true_nash". The
user-visible story is now "CLI and GUI default to true Nash for
range queries". See "UI" §6. - Full-tree preflop RvR engine — Phase A (PR [#122][pr122]). First
landing of the full-tree preflop range-vs-range engine. Unblocks W2.1
(Sarah preflop chart) at the engine layer. See "Engine" §7. - Chained preflop orchestrator — Phase A (PR [#121][pr121]). Single-pass
- lazy + Route A chained orchestrator infrastructure that drives the
full-tree preflop engine. Unblocks W2.1 at the orchestration layer. See
"Engine" §8.
- lazy + Route A chained orchestrator infrastructure that drives the
- BR-walk terminal-leaf caching (PR [#139][pr139]). Applies PR #114's
TerminalCachepattern to the best-response walk inexploit.rs.
cargo test -p cfr_core --lib --release: 58 passed in 2.83 s (was
~110 s pre-cache). Unblocks W2.3 (Sarah deep-stack turn RvR) at the
exploitability-compute layer. See "Engine perf" §9. - Sarah W2.4 (batch-solve) PARTIAL → PASS. Post-PR-#133 retest hits
3/3 fixture rows in 2.01 s wall (149× safety margin). See "Persona
milestones" §2.
Persona delta (pre-retest baseline, post-PR #140): PASS 14 / PARTIAL 2
/ BLOCKED 1 / FAIL 0. Expected target post-empirical retest (with
PR #122 unblocking W2.1 and PR #139 unblocking W2.3): PASS 16 / PARTIAL 1
/ BLOCKED 0 / FAIL 0 — pending empirical confirmation at production scale
per memory rule feedback_post_ship_persona_retest.md.
The release also folds in the v1.8.2 post-ship audit (PR [#138][pr138];
8 checks, 6 PASS / 2 INFORMATIONAL / 0 release-blockers), the held-PR
backlog snapshot (PR [#137][pr137]), the 2026-05-28 current-state persona
snapshot (PR [#135][pr135]), and infra hardening via cross-platform CI
matrix (PR [#20][pr20]).
Highlights
1. CLI — range queries default to true Nash; --legacy-blueprint for opt-in fast mode (PR [#136][pr136])
Merge SHA: 5a6dd08.
Refactors _run_subgame_solve (the shared core for the river and
subgame subcommands) to dispatch multi-combo villain ranges through
range_aggregator.solve_range_vs_range_nash (vector-form CFR — true joint
imperfect-info Nash) instead of the per-combo blueprint-shape loop.
Why now. Post-PR-#114 TerminalCache (~213× river speedup), the
true-Nash path is competitive with — often faster than — the loop, and it
is mathematically correct (joint range-vs-range solve, not per-combo 1v1
averages). The default flip is the user-visible payoff of the v1.8.2
TerminalCache work.
Dispatch table:
| Input | Default behavior | --legacy-blueprint opt-in |
|---|---|---|
| Single-combo villain range | Diagnostic fixed-hand path (solve_hunl_postflop, legacy action_N positional keys) |
(no effect — single-combo never used the loop) |
| Multi-combo villain range | solve_range_vs_range_nash (true Nash; engine action labels check/bet_75/fold/...) |
Per-combo blueprint loop (backward-compat opt-in) |
Presentation modes (--walk-tree/--node/`--format=json |
csv`) | Per-combo loop unconditionally (tree-walk formatters require a full SolveResult per villain combo) |
Bench (3-combo villain QcQh,JcJh,JcJd, river, 50 iters):
| Mode | Wall time |
|---|---|
| Default (true Nash) | ~0.46 s (Rust solve ~15 ms + Python overhead) |
--legacy-blueprint |
~1.70 s |
Test plan PASS:
- All existing CLI tests pass: 7
test_cli_subgame.py+ 7
test_cli_subcommands.py+ 7test_cli_walk_tree.py(incl.
backward-compat structural assertion). - 9 new tests in
tests/test_cli_range_default_true_nash.pycovering
bothriverandsubgamepaths.
Migration impact: existing scripts that compared against the
per-combo blueprint output will see engine action labels
(check/bet_75/fold/...) instead of the legacy action_N positional
keys. Pin the legacy behavior with --legacy-blueprint to retain the
pre-v1.8.3 output. See "Migration / breaking changes" below.
2. Persona — Sarah W2.4 PARTIAL → PASS via PR #133 batch-solve Rust backend (PR [#140][pr140])
Merge SHA: 14b09b0.
PR [#135][pr135] landed the 2026-05-28 persona snapshot but did not verify
W2.4 against the new batch-solve --backend rust CLI surface that
PR #133 added (PR #133 itself shipped pre-v1.8.2). PR #140 amends the
snapshot with an empirical retest.
W2.4 fixture: 3-row river CSV (scripts_retest/w2_4_test_spots.csv,
iter=100) — the same fixture used to declare W2.4 PARTIAL pre-PR-#133.
Result: poker-solver batch-solve --backend rust --input <fixture>
completes 3/3 OK in 2.01 s wall (per-row 0.58–0.68 s). Sarah's 5-min
session gate hit with 149× safety margin.
Reclassification: W2.4 PARTIAL → PASS.
Bottom-line persona counts (pre-retest baseline, snapshot at HEAD
14b09b0):
| Verdict | Pre-PR-#140 | Post-PR-#140 | Delta |
|---|---|---|---|
| PASS | 13 | 14 | +1 |
| PARTIAL | 3 | 2 | −1 |
| BLOCKED | 1 | 1 | = |
| FAIL | 0 | 0 | = |
Sarah: 1/5 PASS → 2/5 PASS (W2.4 closed; W2.1, W2.2 remain PARTIAL;
W2.3 BLOCKED at this snapshot).
Expected target post-empirical retest (PR #122 + #121 + #139 merge wave):
PASS 16 / PARTIAL 1 / BLOCKED 0 / FAIL 0 — W2.1 PARTIAL → PASS (PR
#122 + #121) and W2.3 BLOCKED → PASS (PR #139), pending empirical
confirmation at production scale per memory rule
feedback_post_ship_persona_retest.md. The empirical retest is in flight.
Build caveat called out in the per-workflow row. The PR #140
verification rebuilds the _rust.so from current source via
maturin develop --release. The shipped v1.8.0 .dmg wheel (May 23
build, pre-PR #16) hard-fails on the W2.4 fixture with
index out of bounds: the len is 65 but the index is 70 at
dcfr_vector.rs:651 — the asymmetric-range hand-count panic that
PR #16 already fixed at the source layer. The next .dmg rebuild needs
to ship a source-current .so so the end-user W2.4 path mirrors the
buildable-from-source PASS state recorded here.
Verification artifact:
/tmp/persona_retests/w2_4_post133_rust_rebuilt.json. PR #133's own test
suite (tests/test_cli_batch_solve_rust.py) also passes end-to-end
(4/4) on the rebuilt .so.
3. Persona — current-state snapshot 2026-05-28 (PR [#135][pr135])
Merge SHA: e6df209.
Empirical snapshot taken at main HEAD 261fb7e after multiple
persona-affecting PRs landed on 2026-05-27 (PR #125 W1.5 return_ev,
PR #128 W4.2 amendment, PR #129 off-path annotation, PR #130 W3.5
amendment, PR #114 TerminalCache, PR #94 + PR #120 earlier retests).
Net delta from the prior snapshot (post-PR #128 + #130):
- PASS: 12 → 13 (+1: W1.5 via PR #125)
- PARTIAL: 4 → 3 (−1: W1.5 moved up)
- BLOCKED: 1 → 1 (=)
- FAIL: 0 → 0 (=)
Single reclassification on 2026-05-28 (pre-PR #140): W1.5 Marcus
push/fold sanity (76s @ 10 BB) PARTIAL → PASS via PR #125 (return_ev=True
keyword shipped; the Type C-NICE structural blocker is closed). Empirical
measurement:
get_pushfold_strategy(10, 'sb_jam', '76s', return_ev=True) -> {'strategy': 1.0, 'ev_bb': -0.207}
(0.16 s wall, well under Marcus 30s budget).
All other expected effects (PR #128 W4.2, PR #130 W3.5) were already
absorbed into the prior baseline. PR #129 off-path annotation surface
verified on SolveResult (8/8 tests pass); does not flip a persona
because dependent workflows were already PASS. PR #114 vector-RvR perf
gain alone was not enough to unblock W2.3 turn fixture (>20 min) — W2.3
retest deferred to the post-PR #139 BR-walk caching retest now folded
into this release (see "Engine perf §9" and "Persona table" below).
No unexpected reclassifications, no regressions.
4. Held PRs snapshot (PR [#137][pr137])
Merge SHA: c4843d5.
Captures a 2026-05-28 snapshot of the four major HELD PRs awaiting user
merge decision, with per-PR scope, lines/files, test coverage, hold
rationale, merge recommendation, and downstream blockers:
- PR [#121][pr121] — chained orchestrator Phase A.
- PR [#122][pr122] — full-tree preflop RvR engine (Phase A).
- PR [#126][pr126] — True Nash UI default flip.
- PR [#20][pr20] — cross-platform CI matrix.
All four were HELD per the project memory rule at PR #137 compilation
time...
v1.8.2 — CLI walk-tree + TerminalCache 213× + DCFR alpha-guard
poker-solver v1.8.2 — TerminalCache 213× river win + CLI walk-tree + DCFR safety guards
Status: DRAFT (compile-since-v1.8.0). Compiled 2026-05-27 from the
post-v1.8.0 main branch. Skips the v1.8.1 tag — the v1.8.1 candidate
patches (#91 / #95 / #96) landed on main directly during the
post-v1.8.0 doc-hygiene wave and are folded into v1.8.2 rather than
shipping under a separate tag, mirroring the v1.7.x → v1.8.0 fold-in
decision (per docs/v1_8_1_release_decision_2026-05-27.md).
Baseline commit on origin/main: 8a9c8d2 (v1.8.0 tag, 2026-05-27).
Compilation tip: 27e6b1d (PR #130, 2026-05-28 01:58 UTC).
Release date: TBD (user-gated; release notes are user-facing).
Tag: v1.8.2 (to be created at ship time; v1.8.1 skipped per
fold-in decision above).
Final tag SHA will be set at git tag time.
Headline
v1.8.2 — TerminalCache 213× river RvR speedup + CLI walk-tree/drill-down + DCFR α-guard hard-fail.
Three things land together in this release:
- TerminalCache: 213× speedup on vector-form river RvR. Profiling
identifiedevaluate_7calls in the O(N²) per-pair terminal loop as
~100% of inner-kernel time. The newTerminalCacheprecomputes
per-player hand-strength vectors at each Showdown leaf and constant
chip-flow payoffs at each Fold leaf, once per solve. Result on a
1081-hand × 30-decision-node full-tree river fixture: 28.62 s/iter
→ 0.134 s/iter (~213×). The bit-equality unit test
(cached_matches_uncached_terminal_value) confirms zero strategy
drift vs the uncached path. See "Engine perf" §1. - CLI walk-tree + per-class drill-down + JSON/CSV output. The
poker-solver riverCLI grows three composable presentation knobs
(--walk-tree,--node,--format) that let users navigate the
full decision tree instead of only the first-decision aggregate. The
newsubgame --street flop|turn|rivercommand generalizes the same
surface across all postflop streets with byte-identical backward
compatibility for the legacyriverinvocation. See "CLI" §3. - DCFR α-guard: hard-fail on α ≤ 0, warn on 0 < α < 0.5. Closed a
silent non-Nash footgun whereα=0produced flat exploitability on
Kuhn (8.6e-2vs the correct~1.27e-3atα=1.5) by halving
positive regrets every iteration. Production defaultα=1.5was
never affected; the guard prevents reasonable-looking experimental
configs from silently passing. See "Engine quality" §2.
The release also folds in the v1.8.1 candidate doc/test-hygiene wave
(#91 Brown dump literals, #95 .dmg build absolute path, #96 release
notes broken refs + naming collision + silent-skip hazard), the
EV(action) invariance gauntlet (#98) as a Nash-invariant cross-solver
sanity check that complements strict-σ parity at deep-cap multiplicity
manifolds, and four ship-script hazard checks (#119) encoding the
post-v1.8.0 burst's release-process lessons. Marcus W1.5 (pushfold
return_ev=True), Priya W4.2 (spec amendment), and Daniel W3.5
(range-setup-mode spec amendment) all move into PASS in the persona
table. PR [#129][pr129] also adds reach_probability +
off_path_keys fields to SolveResult so downstream consumers can
filter phantom 5% infosets (closes user-issue #47).
Highlights
1. TerminalCache: 213× speedup on vector-form river RvR (PR [#114][pr114])
Merge SHA: 036a101.
A per-phase profile of dcfr_vector::traverse on a 1326-hand × 30-decision-node
full-tree river fixture (crates/cfr_core/benches/rvr_profile.rs,
new in this PR) pinpointed terminal_value_vector as ~100% of inner-kernel
time. Root cause: the O(N²) per-pair loop at every terminal leaf called
evaluate_7 afresh on every iteration even though the board is fixed
across the entire solve.
Fix: TerminalCache (new in crates/cfr_core/src/dcfr_vector.rs)
precomputes:
- Per-player
Strengthvectors at each Showdown leaf, once per
solve. - Constant chip-flow payoffs at each Fold leaf, once per solve.
terminal_value_vector_cached reads from the cache instead of calling
evaluate_7 per-pair-per-iter. The old uncached function is retained
as the parity reference and exercised by a new bit-exact Rust unit test
(cached_matches_uncached_terminal_value).
Bench numbers (full-tree river, 1081 hands × 30 decision nodes,
M4 Pro arm64):
| Variant | s / iter | Notes |
|---|---|---|
| Baseline (uncached) | 28.62 | Matches HIGH-2's ~26 s/iter framing |
| Cached | 0.134 | Cache build amortized over iters |
| Speedup | ~213× | Well past the 2× HIGH-2 target |
After caching, terminal_value_vector still dominates (~99% of cached
inner-kernel time) but in absolute terms it's now 0.134 s/iter from
28.6 s/iter. All other phases (compute_strategy, update_regret_sum,
update_strategy_sum, etc.) sum to <0.001 s/iter — no remaining
low-hanging optimization at this fixture size.
Bit-exactness gates:
- New Rust unit test
cached_matches_uncached_terminal_valueasserts
bit-identical output between cached + uncached paths across all
terminal leaves of a 1081-hand fixture. PASSES. - All 57
cfr_corelibrary unit tests pass (cargo test --lib). - All 37 RvR Python differential / aggregator / Nash tests pass
(tests/test_range_vs_range_rust_diff.py,
tests/test_range_vs_range_aggregator.py,
tests/test_range_vs_range_nash.py).
Tooling shipped alongside:
crates/cfr_core/benches/rvr_profile.rs: per-phaseInstant-based
bench harness. Reports total wall, per-iter wall, and (with
--features profile_rvr) a cost breakdown across 11 named phases.CFR_VECTOR_NO_TERMINAL_CACHE=1env knob routes through the uncached
path for baseline comparison; production has this unset.profile_rvrfeature flag is opt-in; without it the profiling macros
expand to no-ops (zero overhead on the production hot path).
Why caching instead of wider SIMD? The original HIGH-2 framing
assumed the bottleneck was inner-f64 vector ops missing wider SIMD. The
profile refuted that: the kernel was bottlenecked by repeated O(N²)
evaluate_7 calls on a constant board, not by f64 arithmetic. NEON
SIMD on the post-cache hot path remains a clean follow-up but the ~200×
speedup from caching alone takes river RvR wall-clock from minutes to
seconds at typical iteration counts — sufficient to unblock the
downstream UI True-Nash work (PR #126, HOLD) and the W2.3 perf-budget
retest (still BLOCKED on best-response walk caching, separate follow-on
wall).
Full bench report: see PR #114 description.
2. DCFR α-guard: hard-fail α ≤ 0, warn α < 0.5 (PR [#113][pr113])
Merge SHA: 3ef9b76. Implements Option B from the merged
proposal in PR [#99][pr99] (c534bf0).
Closes the HIGH-1 silent non-Nash bug from
docs/perpetual_qa_findings_2026-05-27.md. The DCFR positive-regret
discount kernel computed pos_scale = t^α / (t^α + 1), which at α=0
collapses to 1/2 for all t, halving positive regrets every
iteration regardless of t. On Kuhn at 10k iter, α=0 stays at
exploitability ~8.6e-2 vs ~1.27e-3 at the production default α=1.5
(50× worse). Game value lands at −0.093 BB instead of the correct
~−0.056 BB. Max strategy-cell diff vs α=1.5 @ 10k iter: 0.411.
Behavior in v1.8.2:
| α value | Behavior |
|---|---|
| α ≤ 0 | HARD-FAIL (PyValueError / Rust panic) at solver init. |
| Non-finite (NaN/±∞) | HARD-FAIL at solver init. |
| 0 < α < 0.5 | WARN — UserWarning (Python) / eprintln! (Rust). |
| α ≥ 0.5 | Silent OK. Includes production α=1.5 and paper-range α=2.0. |
The warn band reflects that Brown & Sandholm 2019 validates α=3/2
only; values in (0, 0.5) are below the paper's analyzed range but
not provably broken.
Surface area covered (12 entry points per the proposal):
- Rust: Single
cfr_core::dcfr::validate_alphahelper called from
4 DCFR ctors (DCFRSolver,PreflopDcfr,VectorDCFR,HUNLDcfr).
The 5 PyO3 entries (solve_kuhn,solve_leduc,solve_hunl_postflop,
solve_hunl_preflop,solve_range_vs_range_rust) already wrap
invocations incatch_unwind, so panics surface asPyValueError. - Python:
poker_solver.dcfr._validate_alphacalled from
DCFRSolver.__init__,solver._solve_rust,
range_aggregator.solve_range_vs_range_nash.
Test plan PASS:
crates/cfr_core/tests/test_dcfr_alpha_guard.rs— 7 Rust cases (α=0,
<0, NaN, ±inf, warn-band 0.3, prod 1.5, paper-range 2.0). All PASS.tests/test_dcfr_alpha_guard.py— 14 Python cases including the
user-requestedtest_alpha_zero_hard_fails/test_alpha_small_warns
/test_alpha_default_silenton_rust.solve_kuhn+_solve_rust.
All PASS.- Regression:
test_dcfr_core.py+test_dcfr_diff.py+
test_kuhn_dcfr.py+test_leduc_dcfr.py— 20/20 PASS. Existing
α=2.0 sensitivity probes remain silent. - Rust lib:
cargo test -p cfr_core --release --lib— 56/56 PASS.
Migration impact: any downstream call passing α ≤ 0 will now raise
instead of silently producing wrong strategies. See "Migration /
breaking changes" below.
3. EV(action) invariance gauntlet (PR [#98][pr98])
Merge SHA: 3cc5eba.
A new test-only deliverable
(tests/test_ev_invariance_gauntlet.py, ~530 LOC) implementing the
Nash-invariance EV(action) gauntlet on the canonical K72 + A83 deep-cap
multiplicity fixtures. Per Brown 2019 Thm 2 / von Neumann minimax,
Q_p(I, a) is unique across all Nash of a 2-player constant-sum game
even when strategy probabilities are not (Nash multiplicity at
indifference manifolds). This compares EV-of-action at depth=0 (true
root) between Brown's solver and o...
v1.8.0 — Cross-platform SIMD + .dmg fork-bomb fix
poker-solver v1.8.0 — Cross-platform SIMD portability + terminal-utility convention purge + .dmg fork-bomb fix
Status: DRAFT (post-purge framed) — Phases 1-4 + AVX2 all merged to
main as of 77e751c (PR #32, 2026-05-26). The terminal-utility
convention purge lands as PR 78 (37e5be1)
and is the engine correctness fix in v1.8.0 — see "Engine fixes" §5
and "Migration / breaking changes" below. Engine + parity fixes from
the v1.7.1 bundle and v1.7.2 (.dmg fork-bomb fix + CI hardening) are
folded into v1.8.0 per docs/v1_6_1_ship_hold_review_2026-05-26.md
and docs/v1_7_1_tag_decision_2026-05-26.md. The v1.6.1 ship hold is
LIFTED under the canonical convention (see "Known issues remaining"
A83 entry). All substantive placeholders have been substituted; final
ship step is tag + GitHub release.
Release date: 2026-05-27
Tag: v1.8.0 (to be created at ship time)
Baseline commit on origin/main: eb74fb3 (PR #60, 2026-05-26),
plus the terminal-utility convention purge at 37e5be1
(PR 78).
Final tag SHA will be set at git tag time.
Headline
v1.8.0 — Cross-platform SIMD portability + terminal-utility convention purge + .dmg fork-bomb fix.
Three things land together in this release:
- Cross-platform SIMD complete (portability win). The Discounted
CFR solver's hot inner loops are now hand-vectorized across NEON
(Apple Silicon), AVX2 + SSE2 (x86_64, runtime-detected), with an
automatic scalar fallback for anything else. This is a portability
win, not a speedup — measured wall-clock on Apple Silicon
(M4 Pro, aarch64) is within noise (~1.0×) because LLVM's-O3
autovectorizer already covers the small-slice case. The headline
value is closing the ~3-month gap between Apple Silicon and x86_64
coverage with a stable hand-written floor that doesn't depend on the
compiler's heuristics. - Terminal-utility convention purge (engine correctness fix). The
prior "rust" terminal-utility convention treatedinitial_contributions
as recoverable, which produced a per-action regret bias of 12-50pp
versus the reference Brown solver at deep cap. v1.8.0 adopts the
single canonical real-poker convention (winner collects the full pot
including dead money from prior streets). See "Engine fixes" and
"Migration / breaking changes" for the formula, a one-line numeric
example, and the rebaseline policy. Pre-v1.8.0 solver outputs are
NOT comparable to v1.8.0 outputs. .dmgfork-bomb fix (CRITICAL). The v1.6.0.dmghad an
uncontrolled-spawn bug on Finder launch
(multiprocessing.freeze_support()missing from the PyInstaller
entry point). v1.6.0.dmghas been pulled from its GitHub Release;
v1.8.0 ships the repackaged build.
SIMD vector kernels (cross-platform): Explicit NEON/AVX2/SSE2
intrinsics replace the previous scalar inner loops in dcfr_vector.rs.
Bit-identical SIMD-vs-scalar output on the same convention; measured
wall-clock impact on Apple Silicon (M4 Pro, aarch64) is within noise
(~1.0×) because LLVM's autovectorizer at -O3 already covers the
small-slice case (action_count = 2-5 per decision row). Primary value
is portability (x86_64 with explicit AVX2 dispatch, runtime-detected)
and a stable hand-written floor that doesn't depend on the compiler's
heuristics. x86_64 wall-clock measurement is pending (no AVX2 hardware
in the bench fleet at time of write). Full benchmark report:
docs/v1_8_simd_perf_benchmark_2026-05-26.md.
Highlights
1. Cross-platform SIMD vector kernels (Phase 1-4)
Four hot loops in crates/cfr_core/src/dcfr_vector.rs are now
vectorized, each behind a single safe public API with runtime ISA
detection. Bit-identical output verified by an end-to-end cross-backend
smoke test that runs 1000 iterations of a Kuhn poker solve on both the
scalar and SIMD paths and asserts equality.
| Phase | Kernel | PR | Merge SHA |
|---|---|---|---|
| 1 | discount_regrets + discount_strategy_sum |
[#23][pr23] | 485aa8c |
| 1+ | AVX2 runtime-detect path (x86_64) | [#35][pr35] | db8d646 |
| 2 | update_regret_sum |
[#41][pr41] | 8073bcc |
| 3 | update_strategy_sum |
[#33][pr33] | a712950 |
| 4 | compute_strategy |
[#32][pr32] | 77e751c |
Per-backend coverage:
- NEON for
target_arch = "aarch64"(Apple Silicon, ARM servers) —
128-bit, 2-lane f64. - AVX2 + SSE2 for
target_arch = "x86_64", with
is_x86_feature_detected!("avx2")runtime dispatch:- AVX2 path: 256-bit, 4-lane f64 (Haswell+, ~2013 and later).
- SSE2 baseline: 128-bit, 2-lane f64 (any x86_64 CPU).
- Scalar fallback for any other architecture and for the epilogue
(the 0-3 trailing elements that don't fit a SIMD lane).
Why this matters: the previous v1.7.x line was already NEON-optimized
for Apple Silicon, but x86_64 users (Intel Macs, Windows PCs, Linux
desktops, CI runners) got the scalar fallback. That's a ~3-month gap
between Apple Silicon and x86_64 performance, now closed (at the
architecture level — the empirical x86_64 wall-clock measurement
remains pending). The cross-platform spec
(docs/pr_proposals/v1_8_cross_platform_simd_spec.md) drives the
architecture. Sarah persona W2.3 remains pending the post-v1.8 retest
(turn-fixture, agent in flight 2026-05-26); the original "unblocked
on M-series" projection was tied to a 4-8× SIMD speedup that did not
materialize on M4 Pro arm64 (see "Persona test status" below).
2. .dmg fork-bomb fix (CRITICAL)
The v1.6.0 .dmg had a critical fork-bomb on Finder launch.
Double-clicking Poker Solver.app on macOS spawned the app's
multiprocessing workers recursively because
multiprocessing.freeze_support() was missing from the PyInstaller
entry point. Each spawned child re-launched the parent process,
exponentially — freezing the user's Mac.
Resolution:
- PR #42 ([
728206e][pr42]) adds themultiprocessing.freeze_support()
guard to the PyInstaller entry point, eliminating the fork-bomb on
the v1.8.0 build. - The v1.6.0
.dmgasset has been retroactively pulled from its
GitHub Release. The v1.6.0 release page now carries a critical
warning pointing users at the v1.8.0 repackaged build (or the
from-source install) instead. - A user-facing warning was also added to the CHANGELOG retroactive
amendment for v1.6.0.
If you previously downloaded the v1.6.0 .dmg, delete the
Poker Solver.app bundle and switch to the v1.8.0 .dmg when this
release publishes. If you ran the v1.6.0 .dmg and your Mac froze on
launch, no persistent damage was done — the workers stopped when the
parent process was killed.
Full RCA: docs/dmg_spawn_loop_rca_2026-05-26.md.
3. Lint / format / deps green-up (PR #43)
[PR #43][pr43] (cfc6bc5) is a non-functional cleanup pass that brings
the main-branch lint gates back to green after the v1.7.x churn:
cargo clippy --all-targets --release— clean.ruff check .— clean (lint).ruff format --check .— clean (format).blackis removed in favor
ofruff format; the two formatters had diverged on a handful of
multi-line argument lists.rich>=13.0added as an explicit runtime dependency. It was
implicitly required bypoker-solver parity(Brown reference parity
CLI subcommand) viarich.console.Consoleimports, but the install
wouldn't fail until users actually invoked the subcommand.
No public API or algorithm changes.
4. Documentation accuracy (PRs #44, #45)
Two doc-only PRs land alongside the SIMD bundle:
- [PR #44][pr44] (
a6b89f7) fixed three executable code bugs in
README.mdandUSAGE.mdexamples:Rangeparser example used the wrong constructor signature.- A
solve_river_nash()example was missing theiterations=
keyword. - A hero/board overlap example assumed a card slot that the API
rejects.
Plus a stale claim about "no CLI for parity comparison" was scrubbed
(thepoker-solver paritysubcommand has shipped since v1.7.0).
- [PR #45][pr45] (
dbfc8d0) cleaned up downstream doc drift:USAGE.mdheader version stamp brought to v1.7.x.CHANGELOG.md.dmgretroactive amendment for v1.6.0 (see
Highlight 2).docs/dmg_install_guide.mdbanner pointing to the v1.8.0 repackaged
build.- Supersede banners on a handful of stale planning docs.
5. Engine + parity-wrapper fixes carried from the v1.7.1 bundle
v1.8.0 inherits a 10-PR bundle of engine and parity-wrapper fixes that
landed piecewise on main between v1.7.0 and v1.8.0. No formal
v1.7.1 tag was created (per docs/v1_7_1_tag_decision_2026-05-26.md);
the fixes are folded into this release. The v1.6.1 ship hold has been
lifted per docs/v1_6_1_ship_hold_review_2026-05-26.md.
Engine correctness:
-
PR 78 (
37e5be1) — Terminal-utility
convention purge (canonical real-poker convention). Prior versions
used a "rust" terminal-utility formula that treated
initial_contributionsas recoverable by the player who folded; this
produced a per-action regret bias that diverged 12-50pp from the
reference Brown solver at deep cap (PR #93 ablation,
docs/a83_terminal_utility_ablation_results_2026-05-26.md). v1.8.0
deletes that path and ships a single canonicalutility()function.Canonical formula (per
feedback_brown_convention_adopt.md):winner_utility = pot_total - contrib_subgame_winner = base_pot + contrib_subgame_loser (in BB) loser_utility = -contrib_subgame_loser (in BB) tie_each = pot_total/2 - contrib_subgame_player (in BB)where
contrib_subgame[i] = state.contributions[i] - cfg.initial_contributions[i]
and `pot_total = cfg.initial_pot + contri...
v1.7.0: aggregator->vector wiring + CLI subcommands
What's New
solve_range_vs_range_nash direct API (PR 43)
Joint range Nash equilibrium routed through the Rust vector-form CFR. Distinct from the aggregator pattern's per-combo blueprint approach; 12 tests including W3.5-style monotone polarization validation.
CLI subcommands (PR 39)
poker-solver pushfold- preflop push/fold chart lookuppoker-solver river- river-only solvepoker-solver parity- Brown reference parity comparison
Status notes
- v1.6.1 engine bundle: HELD pending acceptance gate redefinition (deep-cap Brown apples-to-apples reveals architectural divergence in payoff convention)
- PR 44 .dmg packaging fix: verified on disk; ready for Gate 5 attachment
Test results
- Rust lib: 50/50 passing
- Python nash wrapper: 12/12 passing
- CLI subcommands: 6/6 passing (1 env-skipped)
Post-release validation findings (2026-05-23 late)
Persona acceptance retesting after this release surfaced an API semantics nuance that users should understand. Initial retests suggested a wrapper bug; a subsequent diff-test investigation confirmed there is no bug -- but the result clarifies how solve_range_vs_range_nash interprets range inputs.
Class-label inputs vs. combo-level inputs
solve_range_vs_range_nash accepts ranges as class labels (e.g., AA, KK, AKs, AKo). Internally, the wrapper expands each class to its full combo set: AA -> 6 combos, KK -> 6, AKs -> 4, AKo -> 12, etc. A 15-class symmetric input expands to ~79 combos per player.
The class-expanded range can have a Nash equilibrium that differs meaningfully from a hand-curated combo subset of the same hand classes. For example, on a monotone Ts8s6s4c2d-ish board:
- A hand-curated 15-combo input (one specific combo per class) yields AA pure-check
- The full class-expansion (79 combos) yields AA mixing toward small bets -- because in the larger range, AA dominates more of villain's range AND its specific suit combos (AhAd, AhAc, etc.) block villain's AKs/AKo bluff candidates
Both are valid Nash equilibria for their respective inputs. Neither is a bug.
Workflow guidance
- For population-level frequency reads on production ranges: use
solve_range_vs_range(aggregator path, faster, designed for blueprint-style analysis) - For tight per-combo pot-odds bluff-catch decisions: use
solve_range_vs_range_nashwith hand-curated combo lists (use 4-char labels likeAhKh,JhJdinstead of class labels likeAKs,JJ) - For Nash on full class-expanded ranges: use class labels as designed; understand that the answer reflects the class-expanded range's Nash, not a subset's
Performance scope
Nash path scales linearly in iterations but super-linearly in street depth and class count. Viable envelopes (Sarah's <=5 min budget):
- River: any practical size
- Turn: <=4 classes x <=200 iter, OR <=8 classes x <=100 iter
- Flop: NOT interactive on Nash path; use the aggregator (
solve_range_vs_range, ~1 s/flop solve)
See USAGE.md section 5.6 for code examples and full guidance.
What v1.7.0 ships
- True joint Nash for class-expanded ranges via
solve_range_vs_range_nash[shipped] - Distinct from aggregator's per-combo blueprint pattern [shipped]
- 12 tests in
tests/test_range_vs_range_nash.pyvalidating correctness on small fixtures [shipped] - CLI subcommands:
pushfold,river,parity[shipped]
See CHANGELOG.md for full details.
Post-publication notes (2026-05-26)
Three days after v1.7.0 shipped, the release roadmap was restructured. Readers landing here from search engines or release lists should know:
-
v1.6.1 hold has been LIFTED. The hold listed above ("v1.6.1 engine bundle: HELD pending acceptance gate redefinition") was lifted on 2026-05-26 per the ship review. See
docs/v1_6_1_ship_hold_review_2026-05-26.md. -
A83 deep-cap divergence cause corrected. The status note above attributed the divergence to "architectural divergence in payoff convention." The actual cause is Nash multiplicity at deep-cap indifference manifolds (empirically confirmed via three independent DCFR-math audits + a Track A bench). It is a design difference vs. Brown/Pluribus, not a bug. See
docs/a83_nash_multiplicity_confirmed_2026-05-26.md. -
Next release boundary is v1.8.0. v1.6.1 and v1.7.1 will not ship as separate tagged releases; their fixes shipped piecewise on
mainand are folded into v1.8.0. Seedocs/v1_7_1_tag_decision_2026-05-26.md.
The solve_range_vs_range_nash API-semantics note above remains accurate.
v1.6.0: GUI Gate 2 (range editor, RvR, node-locking, asymmetric, slider tiers)
⚠️ CRITICAL — v1.6.0.dmgis BROKEN. Do NOT launch from Finder.The
.appbundle has a multiprocessing fork-bomb: launching from
Finder causes uncontrolled process spawning that can freeze your Mac.
Root cause is a missingmultiprocessing.freeze_support()call in
scripts/pyinstaller_entry.py. The patch is in flight on
PR #42; a
repackaged.dmgwill ship in v1.7.2.Until v1.7.2, install from source instead:
git clone https://github.com/amaster97/poker_solver.git cd poker_solver pip install -e .Full RCA:
docs/dmg_spawn_loop_rca_2026-05-26.mdon the
pr-78-dmg-freeze-support-fixbranch.
v1.6.0 — GUI Gate 2
User-facing surfaces for v1.3.0+ engine features that were previously library-only.
New in this release
- Range editor: per-hand weight editing (drag/paste/preset ranges); state round-trips through solver
- Range-vs-range UI: in the GUI, not just the library
- Node-locking controls: freeze a node's strategy and re-solve against it (exploitative analysis primitive)
- Asymmetric contributions input: per-player pot contribution (for non-standard stack/contribution scenarios)
- Exploitability slider: Draft (1% pot) - Standard (0.5%) - Tight (0.25%) - Library (0.1%) - tier numeric defaults measured against the real solver
Install
git clone https://github.com/amaster97/poker_solver.git
cd poker_solver
pip install -e .See README.md for full quick-start and library usage.
macOS installer
Poker-Solver-1.6.0-arm64.dmg attached below. Apple silicon (arm64) only. Adhoc-signed (no Apple Developer enrollment) — on first launch, right-click → Open, or System Settings → Privacy & Security → Open Anyway.
For users who prefer source install, pip install -e . is supported and recommended for non-arm64 Macs.
Known issues
- .dmg installer: CRITICAL — DO NOT LAUNCH (see warning at top of these notes). Source install via
pip install -e .is the only safe path until v1.7.2 ships a repackaged .dmg. The earlier "experimental" framing here understated the danger. - v1.5.0 Brown apples-to-apples acceptance test: currently fails at deep-cap facing-raise spots. Per docs/aggregator_vs_true_nash_explainer.md, three independent code reviews verified the vector-form CFR is structurally faithful to Brown's reference; an empirical divergence at deep-cap A83 spots is under investigation (root cause identified: a non-zero-sum vs zero-sum payoff convention divergence; documentation + tolerance widening in v1.6.1 candidate).
- Range fractional-frequency: set-membership only in current Range class. Refactor scoped for a future release.
What's next
- v1.6.1: engine bundle (Python delegate + paired cap-guard + tolerance docs)
- v1.7.0: aggregator->vector wiring (
solve_range_vs_range_nashAPI) + CLI subcommands
v1.5.1: Test rigor + docs honesty (engine fixes deferred to v1.5.2)
v1.5.1 — Test rigor + docs honesty (PATCH)
Headline: A small, no-engine-touch patch release combining new equity test-helper scaffolding, rigorous memory-profiler oracles, and an honest framing correction on a long-standing performance claim. Engine fixes (per-action divergence diagnosis, Python delegate, off-by-one fixes) are deferred to v1.5.2.
What changed
-
Equity test-helper (PR 37). New
tests/_equity_helpers.pyexposesequity_of,equity_vs_range, andassert_equity_closebuilt on top ofpoker_solver.equity.equity(). Re-exported viatests/conftest.py. Lets persona / acceptance tests assert exact equity values inline. NO source-code change. -
Memory-profiler test rigor (PR 36). 4 new tests upgrade the return-non-empty-without-crashing baseline to genuine external oracles: a closed-form 3-action x 4-infoset toy where the profiler output matches byte-for-byte; a real-config closed-form calibration on a small river solve; a golden-file check; and a structure-invariant check on per-street partitioning. NO source-code change.
-
PR 7 docs honesty (PR 32). The "<10s/spot on a typical dev box" comment in
tests/test_river_diff_self_sanity.py:42was aspirational (PR 7 / v0.5.1) and never empirically validated. The canonical parity test takes >660s on the Python tier due to chance-enum-at-root (1.6M hole-card combos per iter) — seedocs/river_parity_timeout_investigation_2026-05-23.md. Comment reframed; no assertion or test-behavior change.
Honest framing
- PATCH bump. NO
poker_solver/source change. NOcrates/cfr_core/source change. NO public API change. NO behavior change. - v1.5.0
_rust.cpython-313-darwin.sois reused byte-identically; users on v1.5.0 do not need to rebuild Rust for v1.5.1. - v1.5.0 acceptance-test status is unchanged. This release does NOT address the per-action divergence observed in the v1.5.0 Brown apples-to-apples acceptance test. The engine bundle that WILL address it (PR 33 Python delegate + PR 34 P0 off-by-one fix + PR 35 canonicalization) is deferred to v1.5.2 pending the divergence diagnosis.
- Smoke regression:
test_equity_helpers.py,test_memory_profiler.py,test_range.py,test_dcfr_diff.py,test_exploit_diff.py,test_range_vs_range_aggregator.py,test_node_locking.py— 91 passed, 5 skipped.
v1.5.0: True Nash range-vs-range
True Nash range-vs-range via Brown's vector-form CFR
v1.5.0 closes the documented architectural gap in v1.4.x: the range_aggregator.solve_range_vs_range path was an Option B approximation (chance-enum-at-root with averaged-strategy aggregation), which is structurally non-Nash for true range-vs-range play. v1.5.0 ships a Rust port of Brown's vector-form CFR (trainer.cpp:138-209, MIT-licensed) that walks the betting tree once per iteration with per-infoset hand_count x action_count regret + strategy_sum tables — the canonical CFR architecture for solving public-game-tree range-vs-range Nash equilibria.
Acceptance test caveat (RESOLVED in v1.6.1)
The v1.5.0 release initially shipped with the Brown apples-to-apples acceptance test in a FAILING state. The failure was traced to two compound TEST bugs (not solver bugs):
-
Action-ordering mismatch: Brown's binary emits actions in order
[c, f, r_low, r_med, r_high]; our Rust solver emits[f, c, r_low, r_med, A]. The test compared position-by-position, so position 0 lined up Brown's CALL with our FOLD. The cell that looked like "Brown=0% call, Rust=99% call" was actually "Brown=0% call, Rust=99% FOLD" — both engines agreeing on FOLD with a labeling mismatch. -
Range-to-player-slot misassignment: Brown's P0 acts first; our P1 acts first. The test passed ranges in the wrong slots, so the two engines weren't even solving the same game.
After fixing both test bugs (PR 40 in v1.6.1), the acceptance test passes; residual divergence is ~10% of cells with max ~0.10 magnitude — characteristic Nash polytope sizing-mix non-uniqueness (legit), not bug territory.
The PR 23 vector-form CFR algorithm itself was algorithmically correct from the start. Brown's cpp/trainer.cpp:138-209 line-by-line match was confirmed by audit. v1.5.0's solve_range_vs_range_rust entry point delivers genuine vector-form Nash; the empirical confirmation just had to wait for the test plumbing to be debugged.
The previously reported DCFR "100x slowdown" against v1.4.x was a measurement artifact (apples-to-oranges configuration), not a real regression. Scalar-path performance is byte-identical to v1.4.0.
Added
- New Rust entry point
_rust.solve_range_vs_range_rust(config_json, iters, alpha, beta, gamma, p0_holes=None, p1_holes=None) -> dict. Output dict matches the Python tier's shape (average_strategykeyed by<hole>|<board>|<street>|<history>), plusdecision_node_count,iterations,wallclock_seconds,hand_count_per_player,memory_profile,backend = "rust_vector". Game::hand_count()trait method (default1) for backward-compatible opt-in. All existing scalar paths (DCFRSolver<G>,hunl_solver.rs,preflop.rs) remain byte-identical to v1.4.0.- Per-street memory profiler (
VectorMemoryProfile) surfaced in the PyO3 dict'smemory_profilefield withtotal_bytes,infoset_count,bytes_by_street,infoset_count_by_street. - Brown apples-to-apples acceptance test (
tests/test_v1_5_brown_apples_to_apples.py, opt-in via-m parity_noambrown) — runs Brown'sriver_solver_optimizedreference binary against the new Rust vector-form CFR ondry_K72_rainbowanddry_A83_rainbow, comparing average strategies at matching histories. Now passes on both spots after the v1.6.1 test-plumbing fix (PR 40). - Differential tests (
tests/test_range_vs_range_rust_diff.py) — 4 active tests covering exploitability-under-restricted-game vs Pythondcfr.py, structural smoke checks, and end-to-end binding chain verification. Both Python and Rust achieve <= 0.05 BB exploitability on the Case A spot.
Performance
- ~72x faster than the Python aggregator on a medium 10x10 RvR river spot (measured; honest single-machine comparison on macOS arm64).
Unchanged
- All existing scalar diff tests (Kuhn / Leduc / fixed-combo river / exploit / node-locking) remain green; 40 differential tests confirm v1.4.0 byte-identical behavior on the scalar paths.
- Public API surface for
solve_hunl_postflop/solve_hunl_preflop/solve()is unchanged. v1.4.3 input-validation guards (PR 31) remain in force.
Caveats and v1.5.x roadmap
- Postflop only in v1.5.0 — preflop RvR deferred to v1.5.1 (16 GB memory edge at full-1326 hand vector without suit-iso reduction).
- Terminal-leaf O(N^2) blocker check is the next perf candidate (SIMD kernels expected to deliver 4-8x speedup based on PR 8 NEON-on-scalar experience).
- EMD hand-bucketing in the vector dimension deferred to v1.5.1.
range_aggregator.solve_range_vs_rangeis not yet wired to the new Rust tier (Q3 default for v1.5.0 keeps it internal-only); user-facing surfacing deferred to a later minor.
License
The Rust port follows Brown's MIT-licensed reference implementation with attribution in source comments.
v1.4.3: Validation hardening + Range.diff + docs refresh
v1.4.3 — Validation hardening + Range.diff + docs refresh (PATCH)
A bundled patch release combining an input-validation hardening pass on HUNLConfig, a small set-difference utility on Range, and a documentation refresh covering v1.4.x capabilities, CLI ergonomics gaps, and two-tier architecture honesty.
HUNLConfig input-validation hardening
HUNLConfig.__post_init__ now raises TypeError/ValueError early on malformed inputs (type-level guards on contribution tuple shape, scalar types for starting_stack/big_blind/small_blind/ante/initial_pot, bet-size-fraction containers, and bool rejected where int is expected). Loud failure at the Python/Rust boundary instead of silent corruption propagating to the Rust backend. Builds on v1.4.1's validation foundation (which caught negative and over-stack contributions). 28 new test cases in tests/test_hunl_config_validation.py cover the expanded guard surface.
Range.diff() set-difference utility
New Range.diff(other) method returns a new Range of combos in self not in other — directional, non-mutating set-difference. Equivalent to frequency-aware max(self.freq - other.freq, 0) under the current Range invariant (all stored frequencies are 1.0). Useful for computing range complements without rebuilding combo lists by hand. 8 new test cases in tests/test_range.py (22/22 passing) cover empty/self/superset/disjoint diffs, directionality, partial overlap, and non-mutation. Fractional-frequency exemplars remain pending the Range refactor.
USAGE.md + DEVELOPER.md refresh
USAGE.md updated for v1.4.x capabilities (node-locking, asymmetric contributions, the new Range.diff() utility), known CLI ergonomics gaps (pushfold subcommand, exploit-target positional convention, batch-solve CSV quoting), and observed performance cliffs (range-vs-range walk vs. Rust port). DEVELOPER.md adds two-tier (Python solve_hunl_postflop chance-enum-at-root vs. Rust solve_range_vs_range_rust vector-form CFR) honesty framing, action-abstraction notes (ActionAbstractionConfig defaults; library round-trip behavior including exploitability_history truncation), and operational guidance for contributors.
Honest framing
PATCH bump on the v1.4.x line: additive utility + input-validation hardening + docs refresh. No public API breakage, no behavior change to existing call sites, no Rust source changes. The v1.4.2 compiled binding is byte-identical for this release — users on v1.4.2 do not need to rebuild Rust to pick up v1.4.3. Smoke regression on test_range.py (22 tests) and test_hunl_config_validation.py (28 tests) all green. No new persona workflow structurally unblocked beyond v1.4.1; PR 27 enables the W2.2 categorical-leak-slice workflow at the set-membership level (utility-level convenience).
v1.4.2: Docs honesty + test marker
Type: PATCH (no API or behavior changes)
Fixes:
-
Docs honesty in
range_aggregator.py: Thehero_playerparameter docstring and the module-level docstring both described P0 as making the "first postflop decision," which is backwards. In HUNL, slot 0 is the SB seat (button) — first to act PREFLOP, last to act POSTFLOP. Slot 1 is the BB seat — last to act PREFLOP, first to act POSTFLOP. Both docstrings now state the position semantics correctly and include a note on the BB-defending workflow (hero_player=1ANDhero_range=bb_rangeso BB cards land in the BB seat). -
Slow-test marker:
tests/test_river_parity_vs_brown.py::test_river_parity_vs_brownnow carries@pytest.mark.slowso it is excluded from the default CI suite (which runs-m 'not slow') and only executes when developers explicitly opt in. Formalizes the opt-in posture that was already implicit in CI config.
Honest framing:
No API or behavior changes. Docs say what the code has always done; the slow-test marker formalizes existing opt-in posture. Safe drop-in upgrade from v1.4.1.
Smoke tests: 31/31 passed (test_dcfr_diff, test_exploit_diff, test_range_vs_range_aggregator).