An advisory No-Limit Hold'em engine driven by structured game data — and an honest attempt to find out whether it is any good.
Describe a poker hand as it happens and the engine answers at every decision point with an action, a size, its equity estimate, and the reasoning behind it. It never plays for you.
The engine is the easy half. The interesting half is the measurement — and the headline result of this repository is a negative one.
The engine's long-standing internal win-rate was +75.38 bb/100. No real 6-max player wins at a tenth of that, so either the engine was extraordinary or the measurement was wrong. It was the measurement: every opponent it had ever been tested against was a rule-based archetype built with a deliberate exploitable flaw.
So I built an opponent that is this engine's own strategy with its exploit layer switched off — one that, by construction, has no leak to find — and re-ran everything against four fields of increasing softness:
| Field | Opponents | Value of opponent modelling | 95% CI | Verdict |
|---|---|---|---|---|
| mirror | 5 × our own baseline | −5.00 bb/100 | [−11.24, +1.23] | inconclusive |
| tough | 4 × baseline + 1 weak | +26.15 bb/100 | [+16.78, +35.52] | clears zero |
| soft | mostly exploitable | +42.59 bb/100 | [+23.74, +61.45] | clears zero |
| legacy | the original five | +19.03 bb/100 | [+5.08, +32.99] | clears zero |
Common random numbers, 4 seeds × 16,000 paired hands per arm. Full provenance in reports/data/experiments/.
Against opponents with nothing to exploit, the exploit layer wins nothing. That is the correct answer, and it is the only reason to believe the other three rows. A large positive number on the mirror field would have meant a broken instrument, not an edge.
The absolute win-rates behind those deltas are upper bounds, not claims — the "weak" opponent calls far looser than any real recreational player. That caveat, and several others, are stated in the paper rather than buried.
🏗 Architecture — how the system is put together and why, with diagrams. 📄 The paper — the experimental method, and what the simulations actually show. 📐 The mathematics — every quantity the engine computes, what bounds it, and a worked decision produced by running the engine at build time.
pip install -e ".[dev]"Feed it a hand as a stream of events, one JSON object per line:
python -m pokeradvisor.research stream --text < examples/hand.jsonl[preflop] BB Cal ➜ RAISE to 6.0bb (high)
• AKo: top 5%; vs-open response for BB (solver-approx).
• Top of range vs the open → 3-bet for value.
[flop] BB Cal ➜ BET to 4.3bb (high)
equity ≈ 89.5% pot 6.5bb to call 0.0bb
• Equity ≈ 89.5% vs 1 modeled range(s) on Ac 7d 2s (flop).
• Strong made hand → bet for value/protection.
[flop] BB Cal ➜ RAISE to 7.9bb (high)
equity ≈ 89.5% pot 10.5bb to call 4.0bb
• Equity ≈ 89.5% vs 1 modeled range(s) on Ac 7d 2s (flop).
• Pot odds require 27.6% to continue.
• Effective 93bb, SPR 8.9 → need ≥72% to commit/raise.
• Equity past the commitment line → raise for value & protection.
...
Or in Python, where the feed keeps an opponent model across hands:
from pokeradvisor.research import ResearchFeed
feed = ResearchFeed()
for event in stream: # hand_start / post / deal_hole / action / ...
advice = feed.apply(event) # an Advice at every hero decision, else None
if advice:
print(advice.action, advice.size_bb, advice.rationale)Replay an archived hand history through the identical decision path:
python -m pokeradvisor.research replay hands.txt --hero Cal # PokerStars text
python -m pokeradvisor.research replay hand.phh # the open PHH standardOr serve it over HTTP:
uvicorn pokeradvisor.api.research:app --port 8801
curl -s localhost:8801/api/research/schema | jq .schema_versionInput is a description of the table, not an observation of one. A client reports observable facts — who posted what, who did what, which cards appeared. Position, pot size, whose turn it is, whether a wager is a bet or a raise, and of course what to do are all derived. That single choice is what makes results reproducible by anyone holding a hand history, on hardware I have never touched.
Two properties of the contract carry most of the weight:
- Contradictions are refused, not repaired. Checking into a live bet, or a raise that does not raise, raises an error rather than being normalised into something plausible. Advice computed from a quietly patched-up state looks authoritative and is wrong, which is the failure mode most likely to contaminate a corpus silently.
- Raise sizes are total street contributions. The alternative convention — chips added — differs by exactly the amount already wagered. Confusing the two is the classic way an importer produces a corpus that parses cleanly and means something else, so where a source supplies both, they are cross-checked.
The strategy itself is a position-aware range baseline with a bounded exploit layer on top: range-weighted Monte-Carlo equity against each opponent's modelled range, pot odds and EV, and stack geometry (effective stack, an SPR-dependent commitment threshold, capped implied odds that go to zero on the river). The exploit layer perturbs that baseline from observed opponent statistics and is structurally incapable of overriding it. Below 20 observed hands a profile is not trusted at all.
pokeradvisor/research/ |
the event contract, the reducer, importers, the experiment runner |
pokeradvisor/strategy/ |
ranges, equity, stack geometry, the exploit layer |
pokeradvisor/sim/ |
a 6-max NLHE simulator, opponent archetypes, the CRN harness |
pokeradvisor/players/ |
the opponent model and the statistics reducer |
pokeradvisor/api/ |
the HTTP surface |
The dependency graph is acyclic and nothing below strategy/ knows that research/ or
sim/ exist — docs/architecture.md walks through the package
layering, the reducer's refusal gates, the four runtime surfaces and how an experiment
fans out.
Poker results are dominated by the shuffle, so every comparison runs both arms on identical seeds and differences them per hand. Hands where the arms made the same decisions cancel to exactly zero, so the paired interval resolves effects the absolute one cannot.
The rule is absolute and predates the interesting results: a confidence interval spanning zero is inconclusive — not trusted, not enabled by default. Two levers currently clear zero against the hard field and remain switched off, because four levers tested at 95% against one control is a ~19% chance of a false positive, and one of the two turned out not to be measuring what its name claims. The paper explains both.
python -m pokeradvisor.research.experiment calibration --preset default
python -m pokeradvisor.research.experiment levers --preset full
python reports/paper/build.py # regenerates the paper from the results
python reports/paper/mathematics.py # regenerates the mathematics companionEvery experiment writes JSON carrying its engine version, git commit, host and timestamp,
so a figure in the paper traces back to the commit that produced it. Each (arm, seed) cell
is independently seeded, so results are byte-identical at any worker count — asserted by
a test, not assumed.
The reports are generated, never transcribed. Every win-rate, interval and verdict is read from the experiment JSON at build time, and every threshold, multiplier and cap is imported from the engine that uses it — the opening ranges, the SPR commitment ladder (by calling the function), the implied-odds payoff fractions. A study that has not been run leaves a stated gap instead of a stale number, both documents build byte-reproducibly, and CI fails if either stops building.
pytest -q # 207 tests
./preflight # the merge gate: tests + a win-rate regression checkThe gate runs the unit suite and re-runs a fixed deterministic simulation, failing the build if the win-rate drops more than 1.0 bb/100 against a blessed baseline. That baseline is deliberately kept on the legacy field: its job is detecting drift, and for that a stable, historically comparable number beats a defensible one. It is a canary, not a performance claim.
The suite leans on the refusals as much as the acceptances — roughly half the research-feed tests assert that bad input is rejected, because that is the behaviour the design exists to guarantee.
- The mirror opponent shares our blind spots. It is our own strategy, so any systematic error in the range model or the equity estimate sits on both sides of the table and is invisible in the comparison. A mirror result near zero shows the exploit layer is not manufacturing an edge; it says nothing about whether the baseline is good.
- The weak opponent is a caricature, which is why absolute win-rates are upper bounds.
- Self-play measures the exploit layer, not the whole engine.
- One street at a time — no multi-street bet-tree planning; multiway pots are played conservatively.
- Bounded heuristics, not solver output. No claim of equilibrium is made anywhere.
- No real-player data. The simulated opponents exhibit the behaviours the psychological levers model because they were programmed to. Simulation is structurally incapable of telling us whether real players do.
MIT — see LICENSE.

