From 978cc0d662da2b287e9391043f71136ce4e58200 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sat, 11 Jul 2026 00:11:42 +0000 Subject: [PATCH] feat(poker): auto-fill hero's stack from the last stack log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a hand doesn't state hero's stack, default it to current_stack() (his last logged stack) — the system already knows it from the stack log even when he doesn't restate it every hand. record_hand._fill_hero_stack sets the hero player's stack and marks stack_inferred=True (honest about stated vs inferred); a stack given in the hand text always wins, and observed hands get nothing. 3 tests; suite 226 green. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyra/poker.py | 29 ++++++++++++++++++++++++++- tests/test_hand_dedup.py | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/lyra/poker.py b/lyra/poker.py index d7c8fb6..04a7491 100644 --- a/lyra/poker.py +++ b/lyra/poker.py @@ -931,15 +931,42 @@ def _recent_duplicate_hand(parsed: dict, session_id: int | None, window_sec: int return int(row["id"]) if row else None +def _fill_hero_stack(parsed: dict, session_id: int | None) -> dict: + """Default hero's starting stack to the last logged stack (current_stack) when the hand + didn't state one — the system already knows his stack from the stack log even when he + doesn't restate it every hand. Only fills a genuinely missing value; a stack he gave in + the hand text always wins. Marks the hero player stack_inferred so it's honest about it.""" + if not isinstance(parsed, dict) or parsed.get("hero_involved", True) is False: + return parsed + hero_pos = parsed.get("hero_pos") + if not hero_pos: + return parsed + players = parsed.setdefault("players", []) + hero = next((pl for pl in players if pl.get("hero") or pl.get("pos") == hero_pos), None) + if hero and hero.get("stack") not in (None, 0): + return parsed # he stated a stack — never override it + stack = current_stack(session_id) + if stack is None: + return parsed # nothing logged yet to borrow + if hero is None: + hero = {"pos": hero_pos} + players.append(hero) + hero["stack"] = stack + hero["stack_inferred"] = True + return parsed + + def record_hand(shorthand: str, session_id: int | None = None, stakes: str | None = None, tag: str | None = None, lesson: str | None = None, backend: str | None = None) -> dict: """Parse shorthand -> structured hand -> store. Returns {id, parsed} (id None on parse fail). Idempotent: if this exact hand was just logged for the session (double turn execution), - returns the existing one instead of inserting a duplicate.""" + returns the existing one instead of inserting a duplicate. Hero's stack is auto-filled + from the last stack log when he didn't restate it.""" parsed = parse_hand(shorthand, stakes=stakes, backend=backend) if not parsed: return {"id": None, "parsed": None} + parsed = _fill_hero_stack(parsed, session_id) dup = _recent_duplicate_hand(parsed, session_id) if dup is not None: return {"id": dup, "parsed": parsed, "linked": 0, "deduped": True} diff --git a/tests/test_hand_dedup.py b/tests/test_hand_dedup.py index 3e339bd..70d5027 100644 --- a/tests/test_hand_dedup.py +++ b/tests/test_hand_dedup.py @@ -65,3 +65,45 @@ def test_parse_prompt_records_straddles(): p = pk._HAND_PARSE_PROMPT.lower() assert "straddle" in p and "button straddle" in p assert "acts last preflop" in p or "act last preflop" in p + + +# --- hero stack auto-fill from the last logged stack ---------------------- + +def test_hero_stack_filled_from_last_stack_log(poker, monkeypatch): + sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=400) + poker.log_stack(275) # his last reported stack + monkeypatch.setattr(poker, "parse_hand", + lambda *a, **k: {"game": "NLH", "hero_involved": True, + "hero_pos": "CO", "hero_cards": ["As", "Ks"], + "board": ["2c"], "players": [], "actions": [], + "result": {"hero_net": 50}}) + out = poker.record_hand("AKs in the CO, i raise, flop 2c...") + stored = poker.get_hand(out["id"])["structured"] + hero = next(pl for pl in stored["players"] if pl.get("hero")) + assert hero["stack"] == 275 and hero.get("stack_inferred") is True + + +def test_stated_stack_is_never_overridden(poker, monkeypatch): + sid = poker.start_session(venue="Meadows", buy_in=400) + poker.log_stack(275) + monkeypatch.setattr(poker, "parse_hand", + lambda *a, **k: {"game": "NLH", "hero_involved": True, + "hero_pos": "BTN", "hero_cards": ["Qh", "Qd"], + "players": [{"pos": "BTN", "stack": 500}], + "board": [], "actions": [], "result": {}}) + out = poker.record_hand("500 deep on the btn with QQ") + hero = next(pl for pl in poker.get_hand(out["id"])["structured"]["players"] + if pl.get("pos") == "BTN") + assert hero["stack"] == 500 and not hero.get("stack_inferred") + + +def test_observed_hand_gets_no_hero_stack(poker, monkeypatch): + poker.start_session(venue="Meadows", buy_in=400) + poker.log_stack(275) + monkeypatch.setattr(poker, "parse_hand", + lambda *a, **k: {"game": "NLH", "hero_involved": False, + "hero_pos": None, "hero_cards": [], + "players": [{"pos": "CO", "cards": ["Kx", "Kx"]}], + "board": [], "actions": [], "result": {}}) + out = poker.record_hand("the CO stacked off KK vs the nit") + assert all(not pl.get("stack_inferred") for pl in poker.get_hand(out["id"])["structured"]["players"])