66dd880f93
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>
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
"""The canonical structured-hand contract (docs/HAND_HISTORY.md): normalize + export.
|
|
|
|
normalize_structured() is the single guarantee that every stored / replayed / exported
|
|
hand has the versioned shape RTO consumes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def poker(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
|
|
from lyra import llm
|
|
monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts])
|
|
import lyra.memory as memory
|
|
importlib.reload(memory)
|
|
import lyra.poker as poker
|
|
importlib.reload(poker)
|
|
return poker
|
|
|
|
|
|
def _full_hand():
|
|
return {
|
|
"game": "NLH", "stakes": "1/3", "hero_pos": "BTN",
|
|
"hero_cards": ["ah", "kh"],
|
|
"players": [
|
|
{"pos": "BTN", "stack": 300, "name": "Hero"},
|
|
{"pos": "BB", "stack": 250, "name": "Sal", "cards": ["qs", "qd"]},
|
|
],
|
|
"actions": [
|
|
{"street": "preflop", "pos": "BTN", "action": "raise", "amount": 15},
|
|
{"street": "flop", "board": ["7♦", "2♣", "5♥"]},
|
|
{"street": "flop", "pos": "BB", "action": "check"},
|
|
],
|
|
"board": ["7♦", "2♣", "5♥"],
|
|
"result": {"pot": 40, "hero_net": 25, "summary": "won at showdown"},
|
|
}
|
|
|
|
|
|
def test_stamps_version(poker):
|
|
out = poker.normalize_structured({"hero_pos": "CO"})
|
|
assert out["schema_version"] == poker.HAND_SCHEMA_VERSION
|
|
|
|
|
|
def test_card_normalization(poker):
|
|
out = poker.normalize_structured(_full_hand())
|
|
assert out["hero_cards"] == ["Ah", "Kh"] # lowercased input -> canonical
|
|
assert out["board"] == ["7d", "2c", "5h"] # unicode suits -> letters
|
|
assert out["actions"][1]["board"] == ["7d", "2c", "5h"]
|
|
# ten + suit symbol together
|
|
assert poker.normalize_structured({"board": ["10♠"]})["board"] == ["Ts"]
|
|
|
|
|
|
def test_unknown_cards_preserved(poker):
|
|
out = poker.normalize_structured({"hero_cards": ["Ax", "x"], "board": ["Ax", "4x", "x"]})
|
|
assert out["hero_cards"] == ["Ax", "x"] # placeholders kept, not dropped
|
|
assert out["completeness"]["cards"] is False
|
|
assert out["completeness"]["board"] is False
|
|
|
|
|
|
def test_hero_synced_into_players(poker):
|
|
out = poker.normalize_structured(_full_hand())
|
|
hero = next(p for p in out["players"] if p["pos"] == "BTN")
|
|
assert hero["hero"] is True
|
|
assert hero["cards"] == ["Ah", "Kh"] # mirrored from hero_cards
|
|
assert sum(1 for p in out["players"] if p.get("hero")) == 1
|
|
|
|
|
|
def test_hero_inserted_when_missing_from_players(poker):
|
|
out = poker.normalize_structured({"hero_pos": "SB", "hero_cards": ["As", "Ad"], "players": []})
|
|
assert out["players"] == [{"pos": "SB", "hero": True, "cards": ["As", "Ad"]}]
|
|
|
|
|
|
def test_completeness_full_hand(poker):
|
|
c = poker.normalize_structured(_full_hand())["completeness"]
|
|
assert c == {"cards": True, "board": True, "actions": True}
|
|
|
|
|
|
def test_idempotent(poker):
|
|
once = poker.normalize_structured(_full_hand())
|
|
twice = poker.normalize_structured(once)
|
|
assert once == twice
|
|
|
|
|
|
def test_store_and_get_roundtrip_is_normalized(poker):
|
|
sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=400)
|
|
hid = poker.store_hand_history(_full_hand(), session_id=sid, tag="well_played")
|
|
got = poker.get_hand(hid)["structured"]
|
|
assert got["schema_version"] == poker.HAND_SCHEMA_VERSION
|
|
assert got["board"] == ["7d", "2c", "5h"]
|
|
assert got["completeness"]["cards"] is True
|
|
|
|
|
|
def test_list_recent_hands_flags_structured(poker):
|
|
sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=400)
|
|
structured_id = poker.store_hand_history(_full_hand(), session_id=sid)
|
|
flat_id = poker.log_hand(session_id=sid, position="CO", hole_cards="Jc Jd")
|
|
rows = {r["id"]: r for r in poker.list_recent_hands()}
|
|
assert rows[structured_id]["has_structured"] is True
|
|
assert rows[flat_id]["has_structured"] is False
|