feat: deterministic equity/board-reading tool (math via tools, not LLM)
Lyra was hallucinating poker facts — phantom flushes, missed straights, wrong equity, only correcting when spoon-fed. Board reading + equity are combinatorial facts an LLM can't do reliably; this is exactly the "math via deterministic tools, never the LLM" principle. - lyra/equity.py: treys-backed analyze(hero, villain, board) -> made hands, who's ahead, EXACT equity (enumerated), and outs (one to come). Handles 'Jx' unknown suits (assigned rainbow to avoid phantom flushes); rejects 'x'/dupes. - analyze_spot tool wired into chat; persona MANDATES it for any equity/board/ who's-ahead/outs question — never eyeballed. - tests on the real JJ-vs-65 hand: flop 78.7%, turn villain straight + hero 6.8% with outs "9s 9h 9c" (correctly excludes 9d, which makes villain a flush). Verified live: she now calls the tool and reports exact numbers, no hallucinated flush. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-1
@@ -10,8 +10,9 @@ the same way once we build that side.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from lyra import logbus, memory, poker
|
||||
from lyra import equity, logbus, memory, poker
|
||||
|
||||
|
||||
def _journal_write(args: dict, ctx: dict) -> str:
|
||||
@@ -173,6 +174,31 @@ def _generate_recap(args: dict, ctx: dict) -> str:
|
||||
f"at /recap/{out['id']}")
|
||||
|
||||
|
||||
def _analyze_spot(args: dict, ctx: dict) -> str:
|
||||
def cards(s):
|
||||
return [c for c in re.split(r"[\s,]+", (s or "").strip()) if c]
|
||||
try:
|
||||
r = equity.analyze(cards(args.get("hero")), cards(args.get("villain")),
|
||||
cards(args.get("board")))
|
||||
except equity.EquityError as e:
|
||||
return f"(can't compute equity: {e})"
|
||||
except Exception as e: # never let a bad spot kill the turn
|
||||
return f"(equity error: {e})"
|
||||
street = {0: "preflop", 3: "flop", 4: "turn", 5: "river"}.get(len(r["board"]), "")
|
||||
L = [f"Board: {' '.join(r['board']) or '(preflop)'}" + (f" — {street}" if street else "")]
|
||||
if "hero_hand" in r:
|
||||
L.append(f"You ({' '.join(r['hero'])}): {r['hero_hand']}")
|
||||
L.append(f"Villain ({' '.join(r['villain'])}): {r['villain_hand']}")
|
||||
L.append(f"Currently ahead: {r['ahead']}")
|
||||
tie = f" / tie {r['tie_equity']}%" if r.get("tie_equity") else ""
|
||||
L.append(f"EQUITY (exact): you {r['hero_equity']}% / villain {r['villain_equity']}%{tie}")
|
||||
o = r.get("hero_outs")
|
||||
if o:
|
||||
L.append(f"Your outs (one card to come): {o['count']}"
|
||||
+ (f" — {' '.join(o['cards'])}" if o["count"] else " — drawing dead"))
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def _player_profile(args: dict, ctx: dict) -> str:
|
||||
prof = poker.player_profile(args.get("name") or "")
|
||||
if not prof:
|
||||
@@ -302,6 +328,15 @@ TOOLS.update({
|
||||
"data + this conversation. Use when he asks for the recap/writeup, usually "
|
||||
"after ending a session.",
|
||||
{}, [])},
|
||||
"analyze_spot": {"handler": _analyze_spot, "spec": _f(
|
||||
"analyze_spot",
|
||||
"Compute EXACT poker equity, what each hand makes, who's ahead, and outs "
|
||||
"for a hero-vs-villain spot. ALWAYS use this for any equity / board-reading "
|
||||
"/ 'am I ahead' / outs question — never compute it yourself.",
|
||||
{"hero": {**_S, "description": "Hero's hole cards, rank+suit letters, e.g. 'Jh Js' (use 'Jx' if a suit is unknown)"},
|
||||
"villain": {**_S, "description": "Villain's hole cards, e.g. '6d 5d'"},
|
||||
"board": {**_S, "description": "Board cards so far, e.g. '8c 7d Ts' (flop) or '8c 7d Ts 4d' (turn); omit for preflop"}},
|
||||
["hero", "villain"])},
|
||||
"player_profile": {"handler": _player_profile, "spec": _f(
|
||||
"player_profile",
|
||||
"Look up everything known about one opponent — dossier, reads, hands "
|
||||
|
||||
Reference in New Issue
Block a user