feat: auto-accumulating villain dossiers + player lookup (poker B)

Named players in recorded hands now auto-enrich a persistent dossier, and stats
emerge once the sample is big enough — laying groundwork for A.

- poker: player_observations table (per named player per hand: vpip/pfr/saw_flop/
  showed/cards/summary); record_hand auto-links named players via link_hand_players;
  player_profile(name) returns dossier + reads + shown hands, with inferred
  VPIP/PFR/WTSD gated behind MIN_STATS_SAMPLE (12) so thin samples don't lie;
  list_players()
- player_profile tool ("what do I know about X"); thin files return a blunt
  "don't generalize" directive
- persona: she MUST call player_profile before discussing an opponent and answer
  only from it — fixes observed confabulation (she invented a whole read from one
  hand / from memory). Verified: now reports only the real logged hand.
- tests: observation linking, profile, stat-emergence at sample threshold

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 04:33:16 +00:00
parent 6a911423a2
commit c7d2279f8d
4 changed files with 185 additions and 1 deletions
+38
View File
@@ -173,6 +173,37 @@ def _generate_recap(args: dict, ctx: dict) -> str:
f"at /recap/{out['id']}")
def _player_profile(args: dict, ctx: dict) -> str:
prof = poker.player_profile(args.get("name") or "")
if not prof:
return f"No file on {args.get('name')} yet."
p = prof["player"]
L = [p["name"] + (f" ({p['venue']})" if p.get("venue") else "")
+ (f" [{p['category']}]" if p.get("category") else "")]
thin = not (p.get("tendencies") or p.get("adjustment")) and not prof.get("stats")
if thin:
L.append("⚠ THIN FILE — no standing read on record. Report only the observed "
"hand(s) below and tell Brian you've barely seen him. Do NOT generalize a style.")
if p.get("description"):
L.append(p["description"])
if p.get("tendencies"):
L.append(f"Tendencies: {p['tendencies']}")
if p.get("adjustment"):
L.append(f"Exploit: {p['adjustment']}")
s = prof.get("stats")
if s:
L.append(f"Stats ({s['hands']} hands): VPIP {s['vpip_pct']}% · PFR {s['pfr_pct']}% · WTSD {s['wtsd_pct']}%")
elif prof.get("small_sample"):
L.append(prof["small_sample"])
if prof.get("showdowns"):
L.append("Shown down: " + ", ".join(prof["showdowns"][:6]))
if prof.get("reads"):
L.append("Notes: " + " | ".join(prof["reads"][:4]))
if prof.get("recent"):
L.append("Recent hands: " + " | ".join(prof["recent"][:4]))
return "\n".join(L)
def _villain_file(args: dict, ctx: dict) -> str:
vs = poker.get_villain_file(name=args.get("name"), venue=args.get("venue"))
if not vs:
@@ -271,6 +302,13 @@ TOOLS.update({
"data + this conversation. Use when he asks for the recap/writeup, usually "
"after ending a session.",
{}, [])},
"player_profile": {"handler": _player_profile, "spec": _f(
"player_profile",
"Look up everything known about one opponent — dossier, reads, hands "
"they've shown down, and (once enough hands are logged) inferred stats "
"like VPIP/PFR. Use when Brian asks what's known about a player.",
{"name": {**_S, "description": "Player name to look up"}},
["name"])},
"get_villain_file": {"handler": _villain_file, "spec": _f(
"get_villain_file",
"Pull saved opponent dossiers (the villain file). Filter by name or venue, e.g. before sitting down.",