Preflop Pack v1
The preflop serving contract — node_path grammar, 169-class truth, and the boundaries bridge.
The preflop half of the product is served under a sibling contract —
ollin-study-api/schema/preflop.v1.ts (JSON Schema mirror alongside),
mirrored client-side as src/lib/api/preflop-contract.ts. One pack per stack
depth carries the complete 8-max decision tree plus the heads-up
boundary bridge.
Source: the CEO's HRC solves — 8-max MTT with antes, ChipEV, ten
symmetric depths. Extraction: solver-lab/data_plane/extract_preflop_v0.py,
which walks the HRC node trees with the same conventions as the validated
converter (99/99 vs reference at 30bb).
Conventions
- All chip amounts are bb unless the field name says
_chips; chip→bb conversion divides bysettings.bb_chips(HRCblinds[0]), chip-exact, ≤ 3 decimals. - Seats are the 8-max labels in table order:
UTG, UTG1, LJ, HJ, CO, BTN, SB, BB— indices 0–7, matching the HRC export's player indices (BTN=5, SB=6, BB=7). - Hands are the 169 canonical classes (
"AA","AKs","AKo", …).
169 is the source of truth here
HRC preflop output is per-class, so — unlike postflop NodeDocument v1, where per-combo data is primary — the 169 grid is the preflop source of truth. There is no per-combo preflop data to fall back to.
- HRC raise convention (verified against 116k all-in nodes): a raise
amount is the raise-to total including the player's blind, excluding
ante.
size_bbon raise/allin actions keeps that convention in bb.
node_path grammar
-
The root node is
"root". Every other path is"root-"followed by theactions[].idof each edge taken, joined by"-":root-fold-fold-fold-fold-r2.3-fold-fold-call -
-is the separator because raise ids contain.(decimal bb). -
A child's path is always
`${parent.node_path}-${action.id}`; action ids are unique within a node, so paths are unique. -
Action ids:
fold|call|r<size_bb>|allin, wherer<size_bb>is the raise-to total in bb with trailing zeros trimmed (r2.3,r25), andallinis a raise-to for the actor's full stack.
The app's ?line= URL parameter is this grammar minus the root- prefix.
PreflopNodeV1 — one decision node
| Field | Meaning |
|---|---|
node_path / node_id | Path per the grammar; the HRC node id for debugging and family slugs. |
seat_to_act | Seat label. |
actions | In HRC order; strategy indices align with this order. Each action carries kind, optional size_bb (raise-to for raises; incremental for calls), is_all_in, is_check (the BB option — HRC "X" is kind: "call", size_bb: 0), and next. |
strategy169 | Per-class action frequencies, aligned with actions, summing to ~1 (tolerance ~1e-4). Always all 169 keys — classes with zero arriving weight still carry the solver's strategy so the UI can render a full chart. |
range169 | Arriving (reach) weight per class in [0,1] for the seat to act. Sparse — classes with weight ≤ 1e-6 are omitted. |
pot_bb / facing_bb | Pot (antes + blinds included) and the incremental bb to call (0 = check option). |
stacks_bb | Remaining stack per seat, length 8, SEAT_ORDER order. |
active_seats / players_remaining | Seats not yet folded, and their count. |
next — what an action leads to
next.type | Meaning |
|---|---|
node | A child decision node exists in the pack (next.node_path). |
fold_end | Everyone else folded — hand ends, pot awarded. |
flop_multiway | Closing call/check → flop with 3+ players (no postflop coverage). |
flop_hu | Closing call/check → heads-up flop. next.boundary indexes into the pack's boundaries array — the preflop→postflop bridge. |
showdown_allin | Closing call of an all-in (0 behind) — the board runs out with no decisions. HU cases still appear in boundaries so the UI can label the leaf. |
PreflopBoundaryV1 — the bridge
One entry per action that closes preflop betting with exactly two players:
{ "node_path": "root-fold-fold-fold-fold-fold-r2-fold", "action_id": "call",
"kind": "flop_hu", "line": "srp_btn_bb", "oop": "BB", "ip": "BTN",
"pot_bb": 5.46, "eff_bb": 7.88,
"family_id": "10bb_srp_btn_bb_p7v5_n21",
"coverage": "served", "solved_boards": 184 }| Field | Meaning |
|---|---|
node_path + action_id | The decision node and the closing action. |
kind | flop_hu (a flop is played) or showdown_allin (runout). |
line | Line label from the validated converter: srp_btn_bb, srp_sb_bb, srp, limp, 3bet_call, 4bet_call, 5bet_call, hu_line. |
oop / ip | Postflop seating — OOP is BB first, else SB, else the earlier seat. |
pot_bb / eff_bb | Pot after the closing call (antes included); effective stack behind (0 = all-in). |
family_id | The arriving-library slug {depth}bb_{line}_p{oopSeat}v{ipSeat}_n{nodeId} when the boundary is one of the 242 converter-emitted spots; null when unmapped. |
coverage | served / solved / library / none — see coverage labels. |
solved_boards | Solved flop count for the family, when known. |
Settings and meta
PreflopSettingsV1 pins the regime as product metadata: format: "MTT",
metric: "ChipEV", table_size: 8, bb_chips (chips per bb, 100 in the
current exports), sb_bb: 0.5, ante_bb (0.12bb per player, MTT regular
ante), stack_bb (= the pack's depth), solver: "HRC".
PreflopPackMetaV1: campaign, stamp, tier ("preflop-v0"),
generator, generated_at, and source_files (the HRC blobs in the Azure
preflop-raw container).
Response shapes
The pack itself is stored as
serving/{campaign}/{stamp}/preflop-v0/packs/{depth}bb.json (sha256 in the
tier manifest). The API slices it into four responses:
| Response | Contents |
|---|---|
PreflopCatalogV1 — GET /v1/preflop | Depths with node_count, boundary_count, covered_boundaries (coverage served+solved), and per-depth settings. |
PreflopDepthResponseV1 — GET /v1/preflop/{depth} | Settings, root node, and the compact index (path / seat / n_actions per node) — not the full nodes map. |
PreflopNodeResponseV1 — GET /v1/preflop/{depth}/node?path=… | One node, plus the boundary documents its actions reference (keyed by boundary index) so leaves render without a second request. |
PreflopBoundariesResponseV1 — GET /v1/preflop/{depth}/boundaries | All boundaries with coverage_counts. |
Wire examples for each: API reference — preflop endpoints.