Engineering
The interesting part was not just the solver. It was keeping a C++ training backend provably identical to the Python reference while making it 74.2× faster, and learning which trust signals were real.
Problem → decision → result: the 74.2× backend
Python trained at ~21.9 it/s, so one 2M-iteration run took more than a day. Profiling put ~60% of the time in hand evaluation and river percentile tables.
Moving only helper functions native was measured and found insufficient — crossing the language boundary per node ate the gains. A real lesson, kept.
Move the ENTIRE hot path to C++17: rules, chance sampling, evaluator, feature tables, infoset encoding, regret matching, and the recursive traversal. Python crosses the pybind11 boundary once per training chunk, GIL released. Infosets are packed 64-bit keys over fixed-size native nodes.
~22 → 837 it/s warm (38.11×, median of 3 trials; historical benchmark). Multi-seed, 2M-iteration training became practical on a laptop, which is what made the uncertainty analysis affordable at all.
A sampling profile of the native trainer found 69.5% of CPU rebuilding river percentile tables. Deduplicating that work, reusing evaluator board counts and a flat open-addressing infoset index gave a further 1.96× in an A/B test between native builds, with an identical strategy digest.
21.9 → 1,625 it/s warm (74.2×, fresh benchmark, median of 3 trials). This is MCCFR training throughput on the release configuration, not end-user analysis latency.

Bit-exact across two languages
Faster isn’t useful if it silently becomes a different algorithm. Both backends consume a shared random tape; the parity suite requires identical trajectories, regret sums and average strategies, bitwise. CI runs 1,000 iterations; a stress run held for 10,000. The evaluator matches on every five-, six- and seven-card hand (133,784,560 seven-card hands), and checkpoint resume is bit-identical (100k continuous equals 10 × 10k).
It caught real bugs: clang’s FMA contraction fused a multiply-add in betting arithmetic and changed results in the last ulp — harmless-looking, but through MCCFR’s sampling it forked entire training trajectories. The build now requires -ffp-contract=off, and a platform-independent fsum dot product replaced BLAS dispatch for the same reason.
What breaks what: the test matrix
| Poker rules | property/fuzz over engine-generated legal states + exhaustive reduced-variant enumeration |
| Hand evaluator | exhaustive 5-, 6- and 7-card parity (all 133,784,560 seven-card hands) |
| Native backend | shared-random-tape bitwise parity, ASan/UBSan clean |
| Checkpoints | train(a)+save+load+train(b) ≡ train(a+b), bitwise |
| Confidence gate | calibrated + held-out against exact-game EV regret |
| Regret semantics | paired-SE and exact-size invariants on every review |
| API | contract tests + fuzzed direct-vs-JSON parity |
| Web | vitest incl. browser-CFR-vs-Python fixture; strict tsc; static build |
| Desktop | sidecar smoke + packaged standalone smoke outside the repo |
| Observer | experimental: limited real PokerNow tuning, no independent held-out validation yet |
1008 tests collected at this commit (counted by pytest when this page’s data was generated).
Code map
| Hold'em engine | poker_alpha/holdem/ |
| CFR / CFR+ / MCCFR | poker_alpha/solvers/ |
| C++ training backend | cpp/ |
| Abstraction + encoders | poker_alpha/abstraction/ |
| Confidence gate | poker_alpha/decision/solver_gate.py |
| Ranges & opponent models | poker_alpha/opponent/ |
| Analysis / study / red team | poker_alpha/analysis/ |
| Agents & tournaments | poker_alpha/agents/ |
| Local API | poker_alpha/api/ |
| This site | apps/web/ (data: scripts/build_web_research_data.py) |
| Desktop overlay | apps/desktop/ |
The local API, in one request
POST http://127.0.0.1:8765/analyze
{"state": {…pokeralpha.observed/v1…}}
→ { "recommended_tag": "HIGHEST FREQUENCY",
"solver_state": "ACCEPTED",
"actions": [{"label": "check", "frequency": 0.83}, …],
"receipt": {"receipt_id": "PA-…", "strategy_artifact_sha256": "…"} }Binds 127.0.0.1 only; warm analysis ~0.1 s; every response can carry a reproducibility receipt.
Evidence & validation ladder Benchmarks Native solver deep dive