postflop

Spade plate

Convergence is measured, never asserted.

postflop is a study tool for postflop decisions in heads-up no-limit hold'em: what to bet on a given board, which hands should do it, and what it costs you to get it wrong. Underneath is an open-source Rust Discounted CFR engine with a full best-response exploitability calculator, a native CLI, WebAssembly bindings, and a browser workbench. Every exploitability and EV figure it prints comes straight out of the best-response calculator.

MIT licensed  ·  Rust engine  ·  tournament ICM
Solves right in your browser, nothing to install.

Action ink  /  inside a cell

Bet Check Fold

Suit ink  /  only on a card face

Four inks, two jobs, never in the same rectangle. Inside a strategy cell an ink means an action. On a card face it means a suit. The check band carries a 45° overprint hatch so the mix survives red-green colour blindness and a greyscale print.

solver solve --config spot.toml
OOP strategy Qs Jh 2h 8c · turn · 165 combos

Heart plate

launch.mp41920 × 1080captions on by default

Seventy-seven
seconds.

The whole argument, filmed: why a solver that grades its own homework can be confidently wrong, and what it looks like to measure convergence instead. Product footage is a real screen recording of the workbench.

What it covers

The best-response calculator that measures exploitability separately from the solve. Walking the tree: per-hand strategies, EVs, blockers, every runout. The same Rust engine running in the browser as WebAssembly. Node locking. Then the source.

Runtime
1:17
Captions
English, on by default
Footage
Real workbench capture
Narration
Text-to-speech

Club plate

Start here.

Three ways in, depending on how much of this you have seen before. Nothing needs installing to take the first one.

Take the tour

The workbench loads a solved turn spot the moment it opens, so there is nothing to set up. A guided tour then walks every panel in order: the strategy grid, the drill-down behind a single cell, the EV and regret views, blocker scores, the runout selector, the trainer, and how to solve a spot of your own.

Or open the workbench and find your own way around.

Five terms first

Spot
One postflop situation, fully specified: the board, what each player can be holding, how deep the stacks are, and how much is already in the middle.
Range
Not one hand but every hand a player could have here, each with a weight. A solver reasons about all of them at once, which is why its answer is a mix rather than a single move.
Exploitability
How much a perfect opponent could win against this strategy, printed as a percentage of the pot. Zero would be unbeatable. postflop measures it with a separate best-response calculator rather than asserting the solve has converged, because a solver grading its own homework can be confidently wrong.
Blocker
A card in your hand is a card the opponent cannot have. Holding the ace of hearts removes every one of their hands that needed it, which is why the same bluff is worth more with some cards than others.
Node lock
Freezing one decision to a fixed strategy and solving the rest of the tree around it. It is how you ask what beats a player who never bluffs this river, instead of what beats a perfect one.

If you know PioSOLVER or GTO Wizard

The 13×13 range grid, the EV and regret views, and node locking all do what you already expect them to do. The naming and the notation carry over: ranges parse in PioSOLVER format, weighted combos included.

Three things are different, and they are the reasons to look. Exploitability is not a claim the solver makes about itself: a best-response calculator, separate from the solve, measures it and reports it in chips and as a percentage of the pot at every interval. The whole thing is MIT-licensed source, so the algorithm, the tests, and the numbers on this page can all be read and re-run. And one solution file round-trips: solve a large tree with the CLI on a real machine, open that same JSON in a browser anywhere, export it back out identically.

Give it a spot. Walk the whole tree.

Board, both ranges, stacks, pot, bet sizings, in a small TOML file. The engine computes an approximate Nash equilibrium with a measured exploitability bound, then lets you inspect everything: per-hand strategies, per-hand EVs, action frequencies, and every runout.

The postflop inspector: stat band, line rail, twin 13 by 13 strategy and EV grids, per-combo breakdown, opponent range and blocker panels, all in one frame
The inspector. 13×13 rank grid, action frequencies weighted by live combo reach.

Discounted CFR, full vector traversal

Brown & Sandholm's DCFR with alternating updates (α=1.5, β=0, γ=2, all configurable), a γ-weighted average strategy, and an optional CFR+-style regret floor. Every hand combo is traversed on every iteration. No sampling in the solve path. Why it matters at the table: the mix you see is computed for every single combo, not smeared across whichever hands happened to get sampled.

Exact card removal everywhere

Fold and showdown evaluation run O(N+M) sweeps with per-card weight sums and inclusion-exclusion; ties are handled as distinct rank groups. Blocked combos are filtered out of the working vectors, never zero-weighted: 1176 live combos on a flop, 1128 on the turn, 1081 on the river. Why it matters at the table: the two cards in your hand change what the opponent can hold, so a hand that blocks their value gets its own answer instead of being averaged in with a hand that blocks nothing.

Deterministic parallelism

The traversal fans out across chance-node runouts with rayon using a parallel-map, sequential-reduce design. Solved strategies are bit-identical for every thread count: 1 thread and 24 threads produce the same file. Why it matters at the table: a frequency you quote from a solve is checkable, because the same spot on someone else's machine returns the same answer rather than a nearby one.

Memory-conscious by construction

Flat arena game tree with no pointer chasing, chance tables shared across betting lines by board, and an optional i16 storage mode with per-node scale factors that roughly halves peak memory and documents its quantization floor. Why it matters at the table: it is the difference between a tree you can actually solve on the laptop in front of you and one you have to go and rent a machine for.

Suit isomorphism, exhaustively verified

Suits only matter relative to one another, so the 22,100 distinct flops collapse to 1,755 canonical classes. That mapping is not spot-checked: it is verified over every one of the 22,100. Why it matters at the table: Ah 7h 2c and Ad 7d 2c are the same problem, and the isomorphism map that says so is verified exhaustively, not spot-checked.

The spot file carries the whole rulebook

Rake as a percent with an optional chip cap, off by default. A raise cap, counted per street. An all-in threshold, so any sizing close enough to a shove collapses into the shove instead of cluttering the tree with a near-shove. All of it sits in the same TOML as the board. Why it matters at the table: the tree you solve can be the game you actually sit in, rake included, rather than a rake-free version of it.

Diamond plate

Nothing on this
page is estimated.

All numbers below were measured on a 24-logical-core Windows machine using the benchmark harnesses committed in engine/examples/. Run them yourself.

7-card hand evaluation, 10M-hand pool, best of 3 passes

engine/examples/eval_bench.rs10M-hand pool, 3 passes

77,000,000evals / sec

Full flop solve: 830k-node tree, 305 vs 196 combos, 1 thread → 24 threads

engine/examples/solve_flop.rsmilestone4.toml · 24 threads

2524 → 370ms / iter
6.8×

Peak memory on that same spot (f32 storage; ~0.52× with i16)

engine/examples/solve_flop.rs--report-memory

1513megabytes

Turn spot, 1,881 nodes, solved to 0.12% of pot

engine/examples/solve_turn.rssample-turn.toml

0.2seconds

Toy river spot: 20k iterations plus 200 best-response reports

engine/examples/solve_river.rsbr_interval = 100

32milliseconds

Reproduce cargo run -p engine --release --example solve_flop -- engine/examples/configs/milestone4.toml 400

The workbench runs in your browser.

The same engine, compiled to WebAssembly. It opens with a solved spot already on screen: the Qs Jh 2h 8c turn, BTN vs BB single-raised, 100bb. That is one of two bundled sample solutions, alongside a Ks 7d 2c 8h 3d river polarisation drill, and a guided tour walks every panel from there. Load a solution produced by the CLI instead, or solve small spots directly on the page.

Drill into any combo

Click a cell and see every combo behind it: per-combo weights, the full action mix, and per-hand EVs in zero-sum chips. The IP range panel alongside shows reach-weighted density at the same node.

Combo drill-down: the KTo cell expanded into its twelve combos, each with weight, a stacked strategy bar under the 45 degree check hatch, per-action EVs, and its EV in chipsCombo drill-down. One cell opened into its combos: weight, mix, per-action EV.

Paint ranges by hand or by weight

An interactive range editor with paintable grids, a weight brush, a top-X% slider, and preset spots. Ranges accept standard and PioSOLVER notation, weighted combos included, over all 1326 combos. The detail frame under the editor is the weight brush at work: a hand can sit at any fraction of a full combo, so a range that plays 22 half the time is written as exactly that, and the notation carries the weight back out with it.

The range editor: two paintable 13 by 13 grids with per-hand weights, a weight brush, and a top-percent slider A detail of the same grid showing weighted combos: cells partly filled to the fraction of the time that hand is in the range, the weight brush set to 50 percent, and the range string reading 22 plus colon 0.5 Range editor. Two paintable grids, and the weight brush at 50 per cent alongside.

Watch it converge, live

Solves report exploitability at every interval while they run. The curve you watch is the best-response calculator's own output, in chips and as a percentage of the pot, recomputed at every report.

The solve panel's convergence column while a solve runs: tree stats, a 3.3 megabyte memory bill, exploitability at 0.0177 percent of pot, the falling curve, and a report table in which every row is marked measuredSolve panel. Convergence while it runs: the curve and 26 measured reports.

Priced before it runs

The solve panel above builds the tree, prices the solver's memory bill before the solve commits to it, and prints the tree stats it measured. A large spot gets a warning; one that would take the tab down with it gets a hard red gate instead of an optimistic start. The solve itself runs in a Web Worker, so the page keeps scrolling, keeps drawing the curve, and keeps answering while the engine works.

See which hands actually care

Beyond frequencies: colour the grid by the highest-EV action per hand, or by regret, the chips lost taking the worse action, fading out where the solver is indifferent. Per-action EVs sit next to every combo's strategy bar, so a 63/37 mix becomes "these six combos are worth real chips, the rest are free."

The strategy grid beside its regret surface: the 13 by 13 printed in heart-lit red by chips lost versus the best action, sinking back into the plate where the solver is indifferentStrategy and regret. The same node on two surfaces, with the combo's own EVs.

Train against the solve

Self-contained: pick a sample spot or set up any board, ranges and stacks right in the trainer, and it deals the first hand the moment the solve converges: board, pot, and line, but not the strategy. Pick an action and it is graded on the chips it costs, Best through Blunder, with the full solver mix and a d100 randomizer revealed after you answer. Every spot carries its table context: positions, starting stacks, the preflop action, live range widths, and the VPIP/PFR profile each range models. A running session score, a worst-hands-first review list, a "close decisions only" filter, and type an exact hand like AhKd to drill that combo on every deal.

The trainer: a dealt hand on Qs Jh 2h 8c graded against the solve in chips, the solver mix revealed after answering, and a session scorecard with hand historyTrainer. One graded decision, the solver mix revealed, and the session card.

Lock a node, solve the exploit

Freeze any decision node, the strategy on screen or hand-written per-action frequencies in the TOML, and re-solve: the rest of the tree becomes the equilibrium counter-strategy to "villain never bluffs here." Locks travel inside the solution file, the structure guard holds stored strategies to them, and reported exploitability is measured against the locked profile. Locks captured on a different spot are refused, never silently re-applied.

The node locks panel: one pending lock on the root for OOP, with remove and clear-all controlsNode locks. One pending lock on the root, with remove and clear-all.

Tournaments: solve for equity, not chips

Tick score this spot with ICM, enter every remaining stack at the table and the payout ladder, and say which two seats are in the hand. Terminal payoffs switch from chips to exact Malmuth-Harville tournament equity, so the strategy itself moves under ICM pressure. The chipEV solve of the same spot sits beside it with the frequency shift called out, plus your bubble factor and required equity against the other seat. Five payout presets, or paste your own; the same block works from the CLI as --tournament.

Honest limit: under ICM the game is general-sum, so the headline is NashConv, each player's unilateral best-response gain, not exploitability. Zero means neither player gains by deviating alone; it does not promise a minimum result. The hand is still heads-up; the other seats are stacks that price your chips, not players who act.

The workbench stat band on a satellite spot: NashConv 0.0139 percent, root EV in chip-scaled tournament equity, bubble factor 8.627 for OOP against IP needing 89.6 percent equity, and the six-seat five-paid structureStat band. The same spot under ICM: NashConv, seat equity, bubble factor.

Blocker scores

How much holding these two cards moves the opponent's frequencies, and the whole range ranked by it, so the best bluff cards sort to the top.

The blocker panel: how much each card in the selected hand shifts the opponent's action frequencies, and the whole range ranked by blocker effectBlocker panel. How the held cards move villain, and the range ranked by it.

Every runout, coloured by consequence

A 52-card selector re-roots the inspector at any turn or river card, heat-coloured green to red by how each runout shifts hero EV, range-wide or for one selected combo, with arrow-key stepping between sibling cards.

The runout view: a 52-card selector grouped by suit, shaded green to red by hero EV shift, beside a per-runout EV table sorted by consequenceRunout selector. Forty-eight rivers shaded by hero EV, ranked beside the grid.

All four plates  ·  the cycle closes

Validated through four gated milestones.

In order, each with its committed test evidence. A milestone did not open until the previous one passed. One milestone per ink.

Kuhn poker

Passed

Converges to the known analytic equilibrium family: exploitability 0.002% of pot, K-bet equal to 3× J-bet, game value −1/18. A failure here would have meant the solving core itself was wrong, before any poker was involved, on a toy game whose exact answer was already written down.

AKQ half-street game

Passed

Matches the closed-form equilibrium derived from the indifference conditions, including the boundary case B ≥ P where the Nash set is a segment rather than a point. A failure here would have meant the solver could not recover an answer handed to it in closed form, and would have been quietly picking one point where a whole family is correct.

River spot

Passed

Indifference conditions verified against hand algebra for named combos (bluff EV equals check EV, call EV equals fold EV within 1e-3). Bluff ratio 1/3 and minimum defense frequency 1/2 recovered on a blocker-free construction. A failure here would have meant the bluffing and calling frequencies it prints do not actually leave the opponent indifferent, which is the only reason to play them.

Full flop spot

Passed

An 830k-node tree whose exploitability falls monotonically from 16.5% to 0.22% of pot, zero-sum error at 7e-7, bit-identical across 1, 8, and 24-thread pools. A failure here would have meant everything above worked on toys and broke on a real board, which is exactly where a solver is worth having.

Beyond the milestones: the 7-card evaluator is checked against a slow reference on 1,000,000 random hands and exhaustively on all 2,598,960 five-card hands with zero mismatches. Terminal sweeps are property-tested against naive O(N·M) oracles at full 1081-combo width. The suit isomorphism collapse described above is checked the same way, exhaustively rather than by sample. And where the engine approximates, it says so: the CLI refuses the turn-sampling flag until an exact-labeled sampling mode exists. The solver never silently approximates.

Solve your first spot.

A spot is a small TOML file. Requires Rust (stable); Node 20+ and wasm-pack for the web UI.

spot.toml  ·  the whole spot
# spot.toml
board = "Qs Jh 2h"            # 3, 4 or 5 cards: flop, turn or river
oop_range = "22+,ATs+,KTs+,QTs+,JTs,T9s,98s,ATo+,KJo+"
ip_range = "66+,A9s+,KTs+,QTs+,JTs,ATo+,KQo"
effective_stack = 40.0        # chips behind, per player, at the root
starting_pot = 6.0            # chips already in the middle
max_iterations = 600          # hard iteration ceiling
target_exploitability = 0.5  # stop at 0.5% of pot

[sizings.oop.flop]            # per street, per player, bet / raise / donk
bet = { percents = [50.0], allin = false }
[sizings.ip.flop]
bet = { percents = [50.0], allin = false }
raise = { percents = [60.0], allin = false }

1  ·  Build and solve

cargo build --release
solver solve --config spot.toml \
  --report-every 100 --out solution.json

2  ·  Inspect any line

show never re-solves: it rebuilds the deterministic tree and validates the file against it.

solver show --solution solution.json \
  --line "check,bet:50" --combo AhKh

3  ·  Or skip all of that

The easiest local on-ramp: one command builds the Rust engine, the wasm package and the web dependencies, whichever of them are missing, starts the workbench and opens it in your browser. Or install nothing at all and use the hosted workbench.

python launch.py
the rest of the dials  ·  cli flags and toml fields
--storage f32|i16
Half-precision strategy storage. Roughly halves peak memory, and documents its quantization floor rather than hiding it.
--threads
How many cores the solve uses. The file it writes is bit-identical whichever number you pick.
--target-exploitability
Stop as soon as the measured bound falls under this percent of the pot.
--report-every
How often the best-response calculator runs and prints. Every reported figure costs a real best-response pass.
--board, --oop-range, --ip-range, --stack, --pot, --max-iterations, --out
Override the matching TOML field for a single run, without editing the spot file.
[rake] percent, cap
Rake taken at every terminal, as a percent with an optional chip ceiling. Zero by default.
raise_cap
Maximum raises per street, not counting the opening bet.
allin_threshold
Percent at which a sizing close enough to a shove collapses into the shove itself.
alpha, beta, gamma
The Discounted CFR exponents: positive regret, negative regret, strategy averaging. Defaults α=1.5, β=0, γ=2.
regret_floor
CFR+ style: clamp cumulative regret at zero after every update, so a negative regret never has to be climbed back out of. Off by default.
[[locks]] line, player, freqs
Freeze a decision node, addressed as an action line from the root, and solve the rest of the tree around it. Use strategy in place of freqs for a full per-combo distribution.