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:
@@ -24,6 +24,21 @@ MAX_TOOL_ROUNDS = 5 # cap tool-call iterations per turn
|
||||
TOOL_BACKENDS = {"cloud"}
|
||||
|
||||
|
||||
def _mode_state_note(mode: modes.Mode | None) -> str | None:
|
||||
"""Dynamic, per-turn state for the active mode. Currently: surface Alligator
|
||||
Blood while it's engaged on the live session, so she stays in that register."""
|
||||
if not mode or mode.key != modes.CASH.key:
|
||||
return None
|
||||
from lyra import poker # local import: keep the core/domain coupling at call time
|
||||
if poker.alligator_active():
|
||||
return (
|
||||
"🐊 ALLIGATOR BLOOD is ON for this session. Coach Brian in that register: "
|
||||
"hang around, refuse to die, don't force miracles, make opponents beat him "
|
||||
"correctly. Tough, patient, steady — no heroics, no spew, no quitting."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_switch_mode(session_id: str, tool_name: str) -> None:
|
||||
"""Keep the chat framing aligned with the live data: opening a poker session
|
||||
auto-flips this chat into Cash mode (so the next turn gets the cash card + the
|
||||
@@ -80,6 +95,13 @@ def build_messages(session_id: str, user_msg: str,
|
||||
if mode and mode.card:
|
||||
messages.append({"role": "system", "content": mode.card})
|
||||
|
||||
# Live ritual state (e.g. Alligator Blood ON) — dynamic, so it rides alongside
|
||||
# the static card and keeps her in-register for the whole stretch, not just the
|
||||
# turn she flipped it.
|
||||
state_note = _mode_state_note(mode)
|
||||
if state_note:
|
||||
messages.append({"role": "system", "content": state_note})
|
||||
|
||||
# When she is: current time + the gap since Brian last spoke (she has no clock).
|
||||
messages.append(_now_note())
|
||||
|
||||
|
||||
+21
-2
@@ -38,10 +38,11 @@ _LOOKUPS = ("player_profile", "get_villain_file", "running_stats")
|
||||
# Always-available core tools (her own agency: journaling/notes).
|
||||
_BASE = ("journal_write", "note")
|
||||
|
||||
# The full live cash-game toolset.
|
||||
# The full live cash-game toolset (incl. Brian's mental-game rituals).
|
||||
_CASH_TOOLS = _BASE + _LOOKUPS + (
|
||||
"start_session", "add_buyin", "log_stack", "log_hand", "record_hand",
|
||||
"add_read", "analyze_spot", "session_stats", "end_session", "generate_recap",
|
||||
"scar_note", "confidence_bank", "alligator_blood", "reset_ritual",
|
||||
)
|
||||
|
||||
# Talk mode also gets start_session as the *entry point*: opening a session from a
|
||||
@@ -68,7 +69,25 @@ him. Strategy and mental game get the real Lyra, not a clipped confirmation. Nev
|
||||
Stacks and money are in dollars. For ANY equity / who's-ahead / outs / what-a-card-does \
|
||||
question, call analyze_spot and report its numbers — never eyeball board math. Keep the \
|
||||
session current as the night goes; you can pull session_stats or a player's profile whenever \
|
||||
it helps. When he's ready to leave, end_session, and write the recap if he wants it."""
|
||||
it helps. When he's ready to leave, end_session, and write the recap if he wants it.
|
||||
|
||||
BRIAN'S RITUALS — his mental-game system. Run them, don't just reference them:
|
||||
• SCAR NOTE (scar_note) — a painful, instructive mistake to study. Log it when he punts, \
|
||||
gets over-attached, or leaks — and classify it honestly: punt (his error), cooler \
|
||||
(unavoidable), or standard (right play, bad result). That punt-vs-cooler line matters to him; \
|
||||
don't soften a punt into a cooler, and don't call a cooler a punt.
|
||||
• CONFIDENCE BANK (confidence_bank) — good PROCESS regardless of result: a disciplined fold, \
|
||||
clean value, catching a leak mid-hand, holding the line. Bank it when he earns it, ESPECIALLY \
|
||||
when the result didn't reward the good decision. This is how he stays steady.
|
||||
• ALLIGATOR BLOOD (alligator_blood) — his adversity state: hang around, refuse to die, don't \
|
||||
force miracles, make them beat you correctly. Turn it ON when he calls for it; SUGGEST it when \
|
||||
he's card-dead, short, stuck, or grinding a downswing. While it's on, coach him in that \
|
||||
register — tough, patient, no heroics — not bored or loose.
|
||||
• RESET (reset_ritual) — a circuit-breaker after a loss or tilt spike: a clean mental restart, \
|
||||
treat the rest of the night as a new session. Walk him through it when he's chasing or steaming, \
|
||||
then log it.
|
||||
These are the heart of the job. Use his language, hold the honest line, and let the rituals do \
|
||||
the work mentioning them naturally — never invent a scar or a confidence-bank entry that didn't happen."""
|
||||
|
||||
|
||||
TALK = Mode(
|
||||
|
||||
+102
@@ -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", {}),
|
||||
|
||||
@@ -117,6 +117,51 @@ def _log_stack(args: dict, ctx: dict) -> str:
|
||||
return f"Stack ${amount:g} logged" + (f" (net {net:+.0f})." if net is not None else ".")
|
||||
|
||||
|
||||
def _scar_note(args: dict, ctx: dict) -> str:
|
||||
content = (args.get("content") or "").strip()
|
||||
if not content:
|
||||
return "Nothing to log — give me the scar."
|
||||
cls = (args.get("classification") or "").strip().lower() or None
|
||||
if cls and cls not in ("punt", "cooler", "standard"):
|
||||
cls = None
|
||||
try:
|
||||
poker.log_ritual("scar", content=content, classification=cls,
|
||||
hand_id=args.get("hand_id"))
|
||||
except ValueError:
|
||||
return "No live session — start one and I'll keep the scar notes."
|
||||
return f"Scar note logged{f' ({cls})' if cls else ''}."
|
||||
|
||||
|
||||
def _confidence_bank(args: dict, ctx: dict) -> str:
|
||||
content = (args.get("content") or "").strip()
|
||||
if not content:
|
||||
return "Nothing to bank — tell me the good process."
|
||||
try:
|
||||
poker.log_ritual("confidence", content=content, hand_id=args.get("hand_id"))
|
||||
except ValueError:
|
||||
return "No live session — start one and I'll run the confidence bank."
|
||||
return "Banked. 💰"
|
||||
|
||||
|
||||
def _alligator_blood(args: dict, ctx: dict) -> str:
|
||||
on = bool(args.get("on", True))
|
||||
try:
|
||||
poker.set_alligator(on)
|
||||
except ValueError:
|
||||
return "No live session to set that on."
|
||||
return ("🐊 Alligator Blood ON — hang around, refuse to die, no forced miracles."
|
||||
if on else "Alligator Blood off. Back to standard register.")
|
||||
|
||||
|
||||
def _reset_ritual(args: dict, ctx: dict) -> str:
|
||||
content = (args.get("content") or "").strip() or None
|
||||
try:
|
||||
poker.log_ritual("reset", content=content)
|
||||
except ValueError:
|
||||
return "No live session to reset."
|
||||
return "Reset logged. Clean slate — this is a new session in your head."
|
||||
|
||||
|
||||
def _log_hand(args: dict, ctx: dict) -> str:
|
||||
fields = {k: args.get(k) for k in poker._HAND_FIELDS if args.get(k) not in (None, "")}
|
||||
hid = poker.log_hand(**fields)
|
||||
@@ -288,6 +333,39 @@ TOOLS.update({
|
||||
"Tracks his stack over time and his live net while he's still sitting.",
|
||||
{"amount": {**_N, "description": "Current total chip stack, in dollars"}},
|
||||
["amount"])},
|
||||
"scar_note": {"handler": _scar_note, "spec": _f(
|
||||
"scar_note",
|
||||
"Log a SCAR NOTE — a painful or instructive mistake to study later. Use when "
|
||||
"Brian punts, gets too attached, or makes a leak — or when he flags one. "
|
||||
"Classify honestly: 'punt' (his error), 'cooler' (unavoidable), or 'standard' "
|
||||
"(correct play, bad result). The punt-vs-cooler distinction matters to him.",
|
||||
{"content": {**_S, "description": "What happened and the lesson, in Brian's terms"},
|
||||
"classification": {**_S, "description": "punt | cooler | standard"},
|
||||
"hand_id": {**_N, "description": "Linked hand id, if this scar is a logged hand"}},
|
||||
["content"])},
|
||||
"confidence_bank": {"handler": _confidence_bank, "spec": _f(
|
||||
"confidence_bank",
|
||||
"Log a CONFIDENCE BANK entry — good PROCESS regardless of result: a disciplined "
|
||||
"laydown, clean value bet, catching a leak in real time, sticking to the plan. "
|
||||
"Bank it when he does something right, especially when the result didn't reward it.",
|
||||
{"content": {**_S, "description": "The disciplined / good-process play to bank"},
|
||||
"hand_id": {**_N, "description": "Linked hand id, if applicable"}},
|
||||
["content"])},
|
||||
"alligator_blood": {"handler": _alligator_blood, "spec": _f(
|
||||
"alligator_blood",
|
||||
"Toggle ALLIGATOR BLOOD mode — Brian's adversity state: hang around, refuse to "
|
||||
"die, don't force miracles, make opponents beat him correctly. Turn it ON when he "
|
||||
"invokes it, or SUGGEST it (then turn on if he agrees) when he's card-dead, short, "
|
||||
"stuck, or grinding through a downswing. Turn OFF on reset or when he's back in rhythm.",
|
||||
{"on": {"type": "boolean", "description": "true to engage, false to stand down"}},
|
||||
[])},
|
||||
"reset_ritual": {"handler": _reset_ritual, "spec": _f(
|
||||
"reset_ritual",
|
||||
"Log a RESET — a deliberate mental circuit-breaker after a loss or tilt spike, "
|
||||
"treating the rest of the night as a fresh start (the stats stay continuous). "
|
||||
"Use when he resets, or when you've talked him through one.",
|
||||
{"content": {**_S, "description": "Optional note on what prompted the reset"}},
|
||||
[])},
|
||||
"log_hand": {"handler": _log_hand, "spec": _f(
|
||||
"log_hand",
|
||||
"Log a hand in the live session. All fields optional — capture whatever Brian gives you, even terse.",
|
||||
|
||||
@@ -61,6 +61,23 @@
|
||||
.tag { font-size: .7rem; color: var(--mid); border: 1px solid var(--border); border-radius: 999px; padding: 1px 7px; }
|
||||
.villain b { color: var(--text); } .villain .cat { color: var(--mid); font-size: .78rem; }
|
||||
.note-meta { color: var(--fade); font-size: .72rem; }
|
||||
|
||||
/* Rituals */
|
||||
.gator {
|
||||
display: flex; align-items: center; gap: 12px; background: #1a2e10;
|
||||
border: 1px solid #3c6b1e; border-radius: 14px; padding: 14px 16px; margin-bottom: 14px;
|
||||
}
|
||||
.gator .ico { font-size: 1.7rem; }
|
||||
.gator b { color: #b6e88a; } .gator .sub { color: #8fbf6a; font-size: .82rem; }
|
||||
.scar-cls {
|
||||
font-size: .68rem; text-transform: uppercase; letter-spacing: .4px; border-radius: 999px;
|
||||
padding: 1px 7px; border: 1px solid var(--border); margin-left: 6px;
|
||||
}
|
||||
.scar-cls.punt { color: var(--low); border-color: var(--low); }
|
||||
.scar-cls.cooler { color: var(--mid); border-color: var(--mid); }
|
||||
.scar-cls.standard { color: var(--fade); }
|
||||
.card.scar { border-color: #4a2222; } .card.scar .label { color: #d98a8a; }
|
||||
.card.conf { border-color: #234a23; } .card.conf .label { color: var(--good); }
|
||||
.empty { color: var(--fade); font-size: .92rem; }
|
||||
.err { color: var(--low); text-align: center; padding: 30px; }
|
||||
.big-empty { text-align: center; padding: 50px 20px; color: var(--fade); }
|
||||
@@ -139,11 +156,20 @@
|
||||
const villains = data.villains || [];
|
||||
const notes = data.notes || [];
|
||||
const stats = data.stats || {};
|
||||
const rituals = data.rituals || {};
|
||||
const scars = rituals.scars || [];
|
||||
const confidence = rituals.confidence || [];
|
||||
const resets = rituals.resets || [];
|
||||
|
||||
const title = [s.stakes, s.game].filter(Boolean).join(' ') || 'Session';
|
||||
const tagBits = Object.entries(stats.tags || {}).map(([k,v]) => `${k}×${v}`).join(' · ');
|
||||
|
||||
root.innerHTML = `
|
||||
${rituals.alligator ? `<div class="gator">
|
||||
<span class="ico">🐊</span>
|
||||
<div><b>Alligator Blood</b><div class="sub">refuse to die · no forced miracles · make them beat you correctly</div></div>
|
||||
</div>` : ''}
|
||||
|
||||
<div class="card">
|
||||
<div class="sess-top">
|
||||
<span class="sess-title">${esc(title)}</span>
|
||||
@@ -154,6 +180,7 @@
|
||||
<span class="chip">in <b>${money(s.buy_in_total)}</b></span>
|
||||
<span class="chip">${esc(s.format || 'cash')}</span>
|
||||
<span class="chip"><b>${hands.length}</b> hands</span>
|
||||
${resets.length ? `<span class="chip">🔄 <b>${resets.length}</b> reset${resets.length>1?'s':''}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -180,6 +207,23 @@
|
||||
: '<p class="empty">No hands logged yet.</p>'}
|
||||
</div>
|
||||
|
||||
<div class="card conf">
|
||||
<p class="label">💰 Confidence Bank</p>
|
||||
${confidence.length ? `<ul class="rows">${confidence.slice().reverse().map(c => `
|
||||
<li>${esc(c.content)}${c.hand_id ? ` · <a class="hand" style="display:inline" href="/hand/${c.hand_id}">hand</a>` : ''}
|
||||
<div class="note-meta">${ago(c.at)}</div></li>`).join('')}</ul>`
|
||||
: '<p class="empty">Nothing banked yet — disciplined plays land here.</p>'}
|
||||
</div>
|
||||
|
||||
<div class="card scar">
|
||||
<p class="label">🩹 Scar Notes</p>
|
||||
${scars.length ? `<ul class="rows">${scars.slice().reverse().map(sc => `
|
||||
<li>${esc(sc.content)}${sc.classification ? `<span class="scar-cls ${esc(sc.classification)}">${esc(sc.classification)}</span>` : ''}
|
||||
${sc.hand_id ? ` · <a class="hand" style="display:inline" href="/hand/${sc.hand_id}">hand</a>` : ''}
|
||||
<div class="note-meta">${ago(sc.at)}</div></li>`).join('')}</ul>`
|
||||
: '<p class="empty">No scars logged — mistakes to study land here.</p>'}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="label">Villains seen</p>
|
||||
${villains.length ? `<ul class="rows">${villains.map(v => `
|
||||
|
||||
Reference in New Issue
Block a user