feat(prompting): harden the poker classifier against real-world phrasings

Probed the heuristic against live-style messages and fixed 6 real gaps:
- action verbs missed -ing forms ("TAG's been limping") → no READ
- the descriptor-subject regex required words between "the" and the noun, so
  "the whale called" missed → now handles bare "the <noun>"
- player departures ("TAG busted", "TAG left") weren't TABLE → added _DEPART
  (gated to non-first-person so "I busted, heading home" isn't a roster op)
- the bare word "stack" made strategy questions ("should I stack off?") classify
  as LOG → LOG now needs a number/result word AND excludes questions
- thin MENTAL lexicon → added coolered/sick/brutal/run bad/etc.

21-case probe (13 former misses + 8 regression guards) all green; folded into the
suite as 5 hardening tests. Full suite 201 green. classify() stays the swappable
seam for an LLM/MI50 upgrade later.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 03:28:10 +00:00
parent 2fd7469033
commit e482ad591c
2 changed files with 59 additions and 13 deletions
+26 -13
View File
@@ -27,13 +27,20 @@ _QUESTION = re.compile(r"\?\s*$|^\s*(?:should|would|could|was|were|is|are|do|did
# Table positions / structural poker terms.
_POS = re.compile(r"\b(?:utg|mp|lj|hj|co|btn|button|hijack|cutoff|sb|bb|straddle|straddled)\b", re.I)
_STREET = re.compile(r"\b(?:preflop|flop|turn|river|board|runout)\b", re.I)
# A poker ACTION a player takes (vs a table-op verb below).
# A poker ACTION a player takes (vs a table-op verb below). Includes -ing forms
# ("TAG's been limping") since those are common in live reads.
_ACTION = re.compile(
r"\b(?:limp(?:ed|s)?|call(?:ed|s)?|rais(?:e|ed|es)|bet(?:s)?|check(?:ed|s)?|"
r"fold(?:ed|s)?|shov(?:e|ed|es)|jam(?:med|s)?|3-?bet(?:s|ted)?|4-?bet(?:s|ted)?|"
r"open(?:ed|s)?|straddl(?:e|ed|es)|stack(?:ed|s)?|flat(?:ted|s)?|donk(?:ed|s)?)\b",
r"\b(?:limp(?:ed|s|ing)?|call(?:ed|s|ing)?|rais(?:e|ed|es|ing)|bet(?:s|ting)?|"
r"check(?:ed|s|ing)?|fold(?:ed|s|ing)?|shov(?:e|ed|es|ing)|jam(?:med|s|ming)?|"
r"3-?bet(?:s|ted|ting)?|4-?bet(?:s|ted|ting)?|open(?:ed|s|ing)?|"
r"straddl(?:e|ed|es|ing)|stack(?:ed|s|ing)?|flat(?:ted|s|ting)?|donk(?:ed|s|ing)?)\b",
re.I,
)
# A player LEAVING the table (departure) — routes to TABLE (unseat) when the actor
# isn't Brian himself.
_DEPART = re.compile(
r"\b(?:busted(?: out)?|left(?: the table)?|took off|racked up|stood up|got up|"
r"is gone|took a walk|quit(?:s|ting)?)\b", re.I)
_FIRST_PERSON = re.compile(r"\b(?:i|i'm|im|i've|my|me|myself|mine)\b", re.I)
# Leading capitalized words that are poker VERBS, not player names (so a hand
# narrated without "I" — "Flopped a set, bet the river" — isn't read as a villain).
@@ -54,13 +61,17 @@ _TABLE = re.compile(
_MENTAL = re.compile(
r"\b(?:tilt(?:ed|ing)?|steam(?:ing|ed)?|on tilt|fried|tired|exhausted|frustrat(?:ed|ing)|"
r"pissed|angry|annoyed|stuck|bored|checked out|in my head|mental|rattled|spewy|"
r"confiden(?:t|ce)|steady|card ?dead|feel like|i feel|losing my mind|going crazy)\b",
r"confiden(?:t|ce)|steady|card ?dead|feel like|i feel|losing my mind|going crazy|"
r"cooler(?:ed)?|sick(?: of)?|brutal|run(?:ning)? (?:so |real |bad)|disgust(?:ed|ing)?|"
r"fed up|hate this|can'?t win|miserable|deflated|demoralized|over it)\b",
re.I,
)
# Bare money/result prose (a fact to log that slipped past the quick-capture box).
# Needs an actual number OR a strong result keyword — the bare word "stack" is too
# eager (it appears in questions like "should I stack off?").
_MONEY = re.compile(
r"(?:\$?\d{2,5})\b|\b(?:stack|down to|up to|out for|cashed|rebought|rebuy|buy ?in|"
r"felted|stuck|booked)\b", re.I,
r"\b\d{2,5}\b|\b(?:down to|up to|out for|cashed|rebought|rebuy|buy ?in|felted|booked)\b",
re.I,
)
# Pure logistics (no cards, no roster action) — a neutral update, not a mood.
_STATUS = re.compile(
@@ -97,8 +108,10 @@ def _read_subject(msg: str, low: str, roster_handles) -> bool:
m = re.match(r"([A-Z][a-zA-Z'.]+)\b", msg)
if m and m.group(1).lower() not in _POKER_VERB_LEAD:
return True
# "the neck-tattoo guy 3bet" — a descriptor subject.
if re.search(r"\bthe .+?\b(?:guy|reg|kid|player|villain|man|woman|lady|fish|whale|nit|lag|maniac)\b", low):
# A descriptor subject: "the neck-tattoo guy 3bet", or a bare "the whale called"
# (zero words between "the" and the noun).
if re.search(r"\bthe [\w\s'-]{0,24}?(?:guy|reg|kid|player|villain|man|woman|lady|"
r"fish|whale|nit|lag|maniac|donk|reg)\b", low):
return True
return False
@@ -118,8 +131,8 @@ def classify(user_msg: str, roster_handles=()) -> str:
# 2) HAND — Brian's hand (first-person card/position/street content).
if _looks_like_hand(low, msg):
return "HAND"
# 3) TABLE — roster ops with a tool action.
if _TABLE.search(low):
# 3) TABLE — roster ops (seat/clear) or another player leaving (departure).
if _TABLE.search(low) or (not first_person and _DEPART.search(low)):
return "TABLE"
# 4) MENTAL — first-person feeling / mental game.
if _MENTAL.search(low):
@@ -127,8 +140,8 @@ def classify(user_msg: str, roster_handles=()) -> str:
# 5) STATUS — pure logistics, no cards, no roster action.
if _STATUS.search(low):
return "STATUS"
# 6) LOG — bare money/result fact.
if _MONEY.search(low):
# 6) LOG — bare money/result fact (a statement, not a strategy question).
if _MONEY.search(low) and not _QUESTION.search(msg):
return "LOG"
# 7) CHAT — open talk / questions.
return "CHAT"
+33
View File
@@ -81,3 +81,36 @@ def test_fragment_for_maps_each_type():
def test_base_is_nonempty_and_names_the_hard_rules():
assert "LOG FIRST" in pp.BASE
assert "descriptor" in pp.BASE and "session_state" in pp.BASE
# --- hardening: real-world phrasings that used to miss ---
def test_hardening_reads_ing_and_bare_descriptor():
assert c("TAG's been limping every pot", roster=("TAG",)) == "READ" # -ing form
assert c("the whale called again") == "READ" # bare "the <noun>"
assert c("saw JD open utg") == "READ"
def test_hardening_player_departures_are_table():
assert c("TAG busted") == "TABLE"
assert c("TAG left the table") == "TABLE"
assert c("new guy just sat down") == "TABLE"
def test_hardening_questions_never_log():
# "stack" appears but it's a strategy question, not a stack update.
assert c("should I stack off top set on that board?") == "CHAT"
assert c("was I good to call there with AK?") == "CHAT"
def test_hardening_mental_lexicon():
assert c("im getting coolered every hand, so sick of this") == "MENTAL"
assert c("this is brutal, run so bad") == "MENTAL"
def test_hardening_log_needs_number_or_result_word():
assert c("down to 220") == "LOG"
assert c("sitting on 450 now") == "LOG"
assert c("rebought for 300") == "LOG"
# first-person departure is Brian, not a roster op → not TABLE
assert c("I busted, heading home") != "TABLE"