Files
project-lyra/docs/HAND_HISTORY.md
T
serversdown 66dd880f93 feat: canonical structured-hand contract (Lyra->RTO transport)
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>
2026-06-26 22:36:11 +00:00

4.0 KiB
Raw Blame History

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, 05 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 in 23456789TJQKA (ten = T), suit in c d h s (lowercase). E.g. ["As","5d","2c"]. RTO maps each token via pokercore.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.
  • completeness tells a consumer what's safe to use: cards/board are true only when every relevant card is fully specified (no "x"). RTO uses false-card hands for positions/frequencies/pairs and skips suit-dependent math (flushes).
  • Hero appears in players[] with "hero": true and is findable via pos == hero_pos. hero_cards is a mirror for the viewer; players[].cards is the source of truth.
  • Positions: UTG UTG1 UTG2 MP LJ HJ CO BTN SB BB.
  • Actions: post fold check call bet raise allin. amount is 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. Use has_structured to pick which hands have a replayable body worth fetching.
  • GET /hand/{id}/data → the full hand row; structured is the object above (or null for 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.