Solidify hand histories into one versioned shape that gets stored, replayed, and exported — the foundation the tap recorder will emit into and RTO consumes. - normalize_structured(): single guarantee of the contract shape — canonical cards (unicode/10/case -> RankSuit tokens, unknown 'Ax'/'x' preserved), hero synced into players[] (RTO finds hero via pos==hero_pos), schema_version stamp, and a completeness summary so consumers skip suit-dependent math on partial hands. Idempotent; runs on store AND read (legacy rows conform on the way out). - list_recent_hands: has_structured flag so the export/RTO knows which hands have a replayable body worth fetching. - docs/HAND_HISTORY.md: the shared contract both repos cite (schema, conventions, ownership rule, one-way HTTP coupling, transport endpoints). - replaces the narrow _normalize_parsed (unicode-only) everywhere. Card format chosen: lists of 2-char tokens (unambiguous, matches what Lyra already stores + the viewer reads). Unknowns kept + flagged rather than dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.0 KiB
Hand-history contract (Lyra → RTO)
The canonical structured shape for a poker hand. Lyra owns hands — it produces this shape (LLM parser today; the tap recorder natively, going forward), stores it, replays it in the viewer, and exports it. RTO consumes it over HTTP and never reaches into Lyra.
Ownership rule: whoever owns the data owns the tools that produce it. Lyra owns the hand DB, the viewer, and the copilot loop, so hand capture lives here. RTO is a pure engine.
Coupling: one arrow, Lyra → RTO, HTTP only. RTO is a standalone service (solve / exploit / estimate); Lyra POSTs to it when it wants analysis. No shared package, no shared DB, no shared UI components. If RTO is down, Lyra skips analysis and nothing breaks.
Schema (schema_version: 1)
{
"schema_version": 1,
"game": "NLH", // NLH | PLO | ...
"stakes": "1/3", // or null
"hero_pos": "BTN", // one of POSITIONS
"hero_cards": ["Ah", "Kh"], // convenience mirror of the hero's players[].cards
"players": [ // every player in the hand, incl. hero
{"pos": "BTN", "stack": 300, "name": "Hero", "cards": ["Ah","Kh"], "hero": true},
{"pos": "BB", "stack": 250, "name": "Sal", "cards": null} // cards: null unless shown
],
"actions": [ // one flat chronological list across all streets
{"street": "preflop", "pos": "BTN", "action": "raise", "amount": 15},
{"street": "flop", "board": ["7d","2c","5h"]}, // a street begins with its board reveal
{"street": "flop", "pos": "BB", "action": "check"}
],
"board": ["7d","2c","5h"], // full final board, 0–5 cards
"result": {"pot": 40, "hero_net": 25, "summary": "one line"},
"completeness": {"cards": true, "board": true, "actions": true}
}
Conventions (load-bearing)
- Cards are lists of 2-char tokens,
RankSuit: rank in23456789TJQKA(ten =T), suit inc d h s(lowercase). E.g.["As","5d","2c"]. RTO maps each token viapokercore.parse_card. (Chosen over space-joined strings: unambiguous, no re-splitting, and it's what Lyra already stores + what the viewer reads.) - Unknown cards are kept, not dropped:
"Ax"= known rank / unknown suit,"x"= fully unknown card. The LLM parser emits these when Brian didn't state suits. The tap recorder won't — it captures complete cards by construction — so"x"is an import/parser-only concern. completenesstells a consumer what's safe to use:cards/boardaretrueonly when every relevant card is fully specified (no"x"). RTO usesfalse-card hands for positions/frequencies/pairs and skips suit-dependent math (flushes).- Hero appears in
players[]with"hero": trueand is findable viapos == hero_pos.hero_cardsis a mirror for the viewer;players[].cardsis the source of truth. - Positions:
UTG UTG1 UTG2 MP LJ HJ CO BTN SB BB. - Actions:
post fold check call bet raise allin.amountis a plain number (no$), null for non-sized actions (fold/check). Street boards appear as{street, board}entries. - Streets:
preflop flop turn river.
lyra/poker.py:normalize_structured() is the single function that guarantees this shape.
It runs on store and on read, and is idempotent.
Transport (HTTP, Lyra serves on :7078)
GET /hands/data?limit=N→{ "hands": [ {id, position, hole_cards, board, result, tag, at, lesson, venue, stakes, has_structured}, ... ] }— flat list for browsing. Usehas_structuredto pick which hands have a replayable body worth fetching.GET /hand/{id}/data→ the full hand row;structuredis the object above (ornullfor a flat quick-log that hasn't been reconstructed).
RTO's "Lyra bridge" (its docs/estimator-design.md, Phase B) walks structured.actions
to classify each villain decision into checked_to / facing_bet / facing_raise, and
uses shown cards + that street's board for board-relative categories. Everything that
walk needs is in the schema above.