"""Poker-mode prompting: classify the turn, inject a small per-type contract. Replaces the one big `_CASH_CARD` monolith (which was sent every turn) with a lean always-on BASE + exactly ONE response-shape fragment chosen by `classify`. BASE carries what's true regardless of the message (tool routing, identity rules, rituals, equity); the fragment carries how to *respond* to this specific kind of message. See docs/superpowers/specs/2026-07-01-poker-prompts-design.md. `classify` is a pure function of (message, seated roster handles) — no DB, unit- tested like `perceive.read`. It's the swappable seam: a heuristic today, an LLM/MI50 classifier later behind the same signature. """ from __future__ import annotations import re # --- classifier ----------------------------------------------------------- MSG_TYPES = ("READ", "HAND", "TABLE", "MENTAL", "STATUS", "LOG", "CHAT") # A card like "As", "Td", "9c" (rank+suit). Two+ of these ≈ a described hand. _CARD = re.compile(r"\b(?:10|[2-9TJQKA])[shdc]\b", re.I) # Hand-class shorthand: AKs, QJo, T9s ("s"/"o" = suited/offsuit, not a suit). _HANDCLASS = re.compile(r"\b[2-9TJQKA]{2}[so]\b", re.I) # A question (strategy talk) rather than a hand narration to log. _QUESTION = re.compile(r"\?\s*$|^\s*(?:should|would|could|was|were|is|are|do|did|how|what|why|when|which)\b", re.I) # 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). Includes -ing forms # ("TAG's been limping") since those are common in live reads. _ACTION = re.compile( 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). _POKER_VERB_LEAD = frozenset(( "flopped", "turned", "rivered", "bet", "raised", "called", "folded", "checked", "shoved", "jammed", "limped", "straddled", "opened", "hit", "made", "got", "had", "won", "lost", "stacked", "flatted", "3bet", "4bet", "cold", "min", )) # Roster/table operations — these DO something (seat/clear/unseat). _TABLE = re.compile( r"\b(?:seat the table|seat (?:me |them |him )?|table broke|they broke us|broke the table|" r"got moved|moved tables|moved to (?:a |another )?(?:new )?table|switch(?:ed|ing)? tables|" r"new table|table change|racked up and|busted out|left the table|sat down|new guy in seat)\b", re.I, ) # Feelings / mental game (first-person emotional state). _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|" 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"\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( r"\b(?:waiting for a seat|on the list|seat opened|heading (?:to|out)|grabbing|break|" r"bathroom|food|dinner|lunch|be right back|brb|\d{1,2}[:.]?\d{0,2}\s*(?:am|pm)|" r"o'?clock|almost|about to)\b", re.I, ) def _has_action(low: str) -> bool: return bool(_ACTION.search(low)) def _looks_like_hand(low: str, msg: str) -> bool: """Card content that reads as a described (loggable) hand — not a strategy question.""" if len(_CARD.findall(low)) >= 2 or _HANDCLASS.search(low) or _POS.search(low): return True # A street + action narration ("...bet $40 on the river, he folded") is a hand, # but "should I have folded the river?" is a question → CHAT, not a logged hand. return bool(_STREET.search(low)) and _has_action(low) and not _QUESTION.search(msg) def _read_subject(msg: str, low: str, roster_handles) -> bool: """True if ANOTHER player (not Brian) is the actor — the signal for a READ.""" # A seated handle named in the message is the strongest signal. for h in roster_handles or (): h = (h or "").strip().lower() if h and re.search(rf"\b{re.escape(h)}\b", low): return True # An ALL-CAPS handle (TAG, JD) used as a token — a Bravo-style name. if re.search(r"\b[A-Z]{2,}\b", msg): return True # A leading proper noun that isn't a poker verb ("Jonathan called ..."). 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 # 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 def classify(user_msg: str, roster_handles=()) -> str: """Message type for poker mode. Pure; roster_handles are the seated players (passed in by the caller) so a villain's action resolves as READ, not HAND.""" msg = (user_msg or "").strip() if not msg: return "CHAT" low = msg.lower() first_person = bool(_FIRST_PERSON.search(low)) # 1) READ — another player did a poker action (beats HAND). if _has_action(low) and not first_person and _read_subject(msg, low, roster_handles): return "READ" # 2) HAND — Brian's hand (first-person card/position/street content). if _looks_like_hand(low, msg): return "HAND" # 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): return "MENTAL" # 5) STATUS — pure logistics, no cards, no roster action. if _STATUS.search(low): return "STATUS" # 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" # --- always-on base (poker) ---------------------------------------------- BASE = """You are copiloting Brian's LIVE cash game — at the table with him, a session open. \ Two things are always true: LOG FIRST, then reply. If his message contains anything trackable, call the tool BEFORE you \ answer — every time — and NEVER claim you logged/seated/cleared something without actually \ calling the tool. Routing: his stack → log_stack (pass `note` with the why if he gives one). \ His own hand → record_hand. A VILLAIN's action (someone else did something) → add_read, with \ `name` for a real handle or `descriptor` for an unnamed player. A rebuy → add_buyin. Who's at \ the table → seat_players / unseat_player / clear_table. Catching a name for a player you'd been \ describing → name_villain. Confirmed same/different person → link_villains (never merge on a \ guess). For any equity / who's-ahead / outs question → analyze_spot; never eyeball board math. \ When he asks where he's at (stack, net, gator) → session_state, answer from what it returns. IDENTITY RULES (villains): `name` is a REAL handle only (what he calls a person — "Jonathan", \ "TAG"); a physical description NEVER goes in `name` (it spawns duplicates) — put the look in \ `descriptor`, a few distinctive tags. A handle like "TAG" (initials/all-caps off Bravo) is a \ PERSON, never the tight-aggressive style. If a SCOUTING DESK note is in context with a player's \ history, cite it — don't re-fetch or invent; if unsure two references are the same person, ASK. RITUALS (his mental-game system — run them, don't just mention them): scar_note (a punt/leak to \ study — classify honestly punt vs cooler vs standard), confidence_bank (good process regardless \ of result), alligator_blood (adversity mode — suggest when he's card-dead/stuck), reset_ritual \ (circuit-breaker after tilt). Never invent one that didn't happen. Use `note` for session \ narration — factual beats of the night (table texture, his arc), not your feelings. Money is in \ dollars. Everything you log shows on his live HUD.""" # --- per-type response fragments ----------------------------------------- _F_READ = """MESSAGE TYPE: READ — a villain did something and he wants it on their file. Call \ add_read(name|descriptor, note) FIRST, before replying — this is the log that keeps getting \ missed. Attach to the seated handle if he named one; use `descriptor` if the player's unnamed. \ Confirm in ONE short line ("Noted on TAG — limped A4o SB."). At most one crisp exploit read if \ it's worth it; the log is mandatory, the commentary optional. Do NOT analyze it as Brian's hand.""" _F_HAND = """MESSAGE TYPE: HAND. First: was Brian IN this hand? If he only WATCHED it (no I/me/my \ holding cards — two other players), it's really observed: log the players' actions as reads / \ record it as an observed hand, and do NOT analyze it as his. If it's HIS hand → record_hand first. \ Then read the hand off the RECORDED cards, not by eye: name his made hand by the street it mattered \ (flopped/turned/rivered top pair / set / quads / etc.). At a SHOWDOWN where his and the caller's \ cards are both known, call analyze_spot(hero, villain, full board) to confirm the made hands and \ who won BEFORE you comment — NEVER eyeball a finished board (it also catches impossible cards). Same \ for any close equity / who's-ahead / outs spot. (NLH only) reason about BET INTENT: for each \ meaningful bet, what was it for (value / bluff / protection) and did it work — a fold to a value bet \ = value left behind; a call of a bluff = it failed. Name leaks plainly (owning value, missed value, \ sizing) and give ONE real opinion. If there's genuinely no leak (e.g. he flopped the near-nuts and \ stacked off), SAY so — don't manufacture a takeaway. NO reflexive praise ("nice hand"), NO \ variance-evens-out / resilience / life-lesson filler, NO cross-hand pep talk. If a named villain is \ referenced, use their profile/the scouting note — don't invent a read. PLO/non-NLH: log and replay \ it, offer at most a light read, do NOT attempt NLH-style equity. Prose, not a listicle.""" _F_TABLE = """MESSAGE TYPE: TABLE — roster management. "seat the table: …" → seat_players. A table \ change ("table broke", "I got moved", "switched tables") → clear_table, then wait for the new \ roster. Someone leaves/busts → unseat_player. Do the tool call, confirm ONE line, don't narrate. \ The session and his stack keep going through a table change — only who's seated resets.""" _F_MENTAL = """MESSAGE TYPE: MENTAL — he told you how he's feeling. This is when he needs you most. \ Drop the logging shorthand, full presence, your real voice — talk him down off tilt, hold him \ disciplined through a card-dead stretch, engage the mental game honestly. Suggest a ritual if it \ fits (alligator_blood when he's grinding adversity, reset_ritual after a tilt spike). Never a \ clipped confirmation, never bury him in analysis. Meet him first, then help.""" _F_STATUS = """MESSAGE TYPE: STATUS — pure logistics (time, waiting for a seat, a break). Acknowledge \ in 1–2 sentences, log a stack ONLY if a bare number is present, then stop. No coaching, no \ strategy dump, and do NOT read him as tilted/tired/impatient — a neutral update is not a mood.""" _F_LOG = """MESSAGE TYPE: LOG — a bare fact (stack / result / buyin) not already captured. Log it \ (log_stack / add_buyin), confirm in ONE short line ("$317 logged."), stop. No coaching.""" _F_CHAT = """MESSAGE TYPE: CHAT — open talk or a question that isn't a specific logged fact. Your \ real voice, an actual opinion, no filler sign-offs. If it's a concrete strategy spot with cards, \ engage it for real and call analyze_spot.""" FRAGMENTS = { "READ": _F_READ, "HAND": _F_HAND, "TABLE": _F_TABLE, "MENTAL": _F_MENTAL, "STATUS": _F_STATUS, "LOG": _F_LOG, "CHAT": _F_CHAT, } def fragment_for(msg_type: str | None) -> str: """The response-shape contract for a message type (CHAT is the fallback).""" return FRAGMENTS.get(msg_type or "", FRAGMENTS["CHAT"])