feat: live mental-game rituals in Cash mode

Brian's own rituals (mined from his logs) become first-class, live tools instead
of post-hoc recap sections:

- Scar Note — instructive mistakes with the punt/cooler/standard distinction.
- Confidence Bank — good process, banked regardless of result.
- Alligator Blood — invokable adversity state; she suggests it when he's
  card-dead/short/stuck, and her coaching register shifts while it's on (live
  state injected into context per-turn via chat._mode_state_note).
- Reset — tilt circuit-breaker; mental marker only, stats stay continuous.

poker_rituals table + log_ritual/list_rituals/set_alligator/alligator_active;
4 tools added to the Cash toolset and taught in the mode card; HUD gains a 🐊
banner + Confidence Bank + Scar Notes panels; recap grounded via _rituals_block.
tests/test_modes.py +5 ritual tests; 41 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 06:24:28 +00:00
parent dfb6425395
commit 974ee33f71
8 changed files with 340 additions and 2 deletions
+102
View File
@@ -108,6 +108,20 @@ CREATE TABLE IF NOT EXISTS poker_stack_log (
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_stacklog_session ON poker_stack_log(session_id);
-- Mental-game rituals Brian developed (scar notes, confidence bank, alligator
-- blood, reset). Session-scoped events: capture entries (scar/confidence/reset)
-- carry text; the alligator state is the latest alligator_on/alligator_off event.
CREATE TABLE IF NOT EXISTS poker_rituals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
kind TEXT NOT NULL, -- scar | confidence | reset | alligator_on | alligator_off
content TEXT,
classification TEXT, -- scar only: punt | cooler | standard
hand_id INTEGER,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_rituals_session ON poker_rituals(session_id);
"""
# Below this many observed hands, don't surface % stats (too small a sample).
@@ -275,6 +289,62 @@ def stack_state(session_id: int | None = None) -> dict:
}
# --- mental-game rituals (scar notes / confidence bank / alligator blood / reset) ---
RITUAL_CAPTURE = ("scar", "confidence", "reset")
def log_ritual(kind: str, content: str | None = None, classification: str | None = None,
hand_id: int | None = None, session_id: int | None = None) -> int:
"""Record a ritual event (a scar note, confidence-bank entry, reset, or an
alligator on/off toggle) against a session. Returns the row id."""
sid = _resolve(session_id)
if sid is None:
raise ValueError("no live session")
conn = _c()
with conn:
cur = conn.execute(
"INSERT INTO poker_rituals (session_id, kind, content, classification, hand_id, created_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
(sid, kind, content, classification, hand_id, _now()),
)
return int(cur.lastrowid)
def list_rituals(session_id: int | None = None,
kinds: tuple[str, ...] | None = None) -> list[dict]:
"""Ritual events for a session, oldest first; optionally filtered by kind."""
sid = _resolve(session_id)
if sid is None:
return []
sql = "SELECT * FROM poker_rituals WHERE session_id = ?"
params: list = [sid]
if kinds:
sql += " AND kind IN (%s)" % ",".join("?" * len(kinds))
params += list(kinds)
sql += " ORDER BY id"
return [dict(r) for r in _c().execute(sql, params).fetchall()]
def set_alligator(on: bool, session_id: int | None = None) -> bool:
"""Toggle Alligator Blood mode for the session. Returns the new state."""
log_ritual("alligator_on" if on else "alligator_off", session_id=session_id)
return bool(on)
def alligator_active(session_id: int | None = None) -> bool:
"""Whether Alligator Blood mode is currently ON (latest toggle wins)."""
sid = _resolve(session_id)
if sid is None:
return False
r = _c().execute(
"SELECT kind FROM poker_rituals WHERE session_id = ? "
"AND kind IN ('alligator_on', 'alligator_off') ORDER BY id DESC LIMIT 1",
(sid,),
).fetchone()
return bool(r and r["kind"] == "alligator_on")
def end_session(cash_out: float, mood: str | None = None,
session_id: int | None = None) -> dict:
"""Close a session: record cashout, compute net + hours. Returns the row."""
@@ -552,6 +622,21 @@ def _hand_line(h: dict) -> str:
return " | ".join(str(b) for b in bits if b)
_RITUAL_LABEL = {"scar": "Scar Note", "confidence": "Confidence Bank",
"reset": "Reset", "alligator_on": "Alligator Blood ON",
"alligator_off": "Alligator Blood OFF"}
def _rituals_block(rituals: list[dict]) -> str:
lines = []
for r in rituals:
label = _RITUAL_LABEL.get(r["kind"], r["kind"])
cls = f" [{r['classification']}]" if r.get("classification") else ""
body = f": {r['content']}" if r.get("content") else ""
lines.append(f"- {label}{cls}{body}")
return "\n".join(lines)
def generate_recap(session_id: int | None = None, backend: str | None = None) -> dict | None:
"""Generate Brian's .md recap from a session's structured data + conversation, store it."""
backend = backend or "cloud"
@@ -563,6 +648,7 @@ def generate_recap(session_id: int | None = None, backend: str | None = None) ->
reads = [dict(r) for r in _c().execute(
"SELECT seat, note FROM player_reads WHERE session_id = ?", (sid,)).fetchall()]
stats = session_stats(sid)
rituals = list_rituals(sid)
convo = ""
if s.get("chat_session_id"):
@@ -579,6 +665,9 @@ def generate_recap(session_id: int | None = None, backend: str | None = None) ->
f"{stats.get('per_hour')}/hr | hands logged: {stats.get('hands_logged')} | tags: {stats.get('tags')}\n\n"
"HANDS:\n" + ("\n".join("- " + _hand_line(h) for h in hands) or "(none logged)") + "\n\n"
"READS:\n" + ("\n".join(f"- seat {r.get('seat')}: {r['note']}" for r in reads) or "(none)") + "\n\n"
"RITUALS (use these for the Scar Notes / Confidence Bank / Mental Game sections — "
"they are what actually happened, not to be invented):\n"
+ (_rituals_block(rituals) or "(none logged)") + "\n\n"
"CONVERSATION DURING SESSION:\n" + (convo or "(none captured)")
)
md = llm.complete(
@@ -868,6 +957,13 @@ def hud(session_id: int | None = None) -> dict | None:
# Context: how Brian runs at these stakes overall (closed sessions).
ctx = running_stats(stakes=s.get("stakes")) if s.get("stakes") else {}
rituals = list_rituals(sid)
by_kind = lambda k: [ # noqa: E731
{"content": r["content"], "classification": r["classification"],
"hand_id": r["hand_id"], "at": r["created_at"]}
for r in rituals if r["kind"] == k
]
return {
"session": {
"id": sid, "venue": s.get("venue"), "stakes": s.get("stakes"),
@@ -882,6 +978,12 @@ def hud(session_id: int | None = None) -> dict | None:
"hands": hands,
"villains": _session_villains(sid),
"notes": notes,
"rituals": {
"alligator": alligator_active(sid),
"scars": by_kind("scar"),
"confidence": by_kind("confidence"),
"resets": by_kind("reset"),
},
"stats": {
"hands_logged": stats.get("hands_logged", 0),
"tags": stats.get("tags", {}),