From 974ee33f71fe9c191b25b1b99ecbbe2731dbb905 Mon Sep 17 00:00:00 2001 From: serversdown Date: Fri, 19 Jun 2026 06:24:28 +0000 Subject: [PATCH] feat: live mental-game rituals in Cash mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 10 ++++ README.md | 4 ++ lyra/chat.py | 22 ++++++++ lyra/modes.py | 23 +++++++- lyra/poker.py | 102 +++++++++++++++++++++++++++++++++++ lyra/tools.py | 78 +++++++++++++++++++++++++++ lyra/web/static/session.html | 44 +++++++++++++++ tests/test_modes.py | 59 ++++++++++++++++++++ 8 files changed, 340 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc6860..7190d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,16 @@ register based on what she's actually doing at the table. - Mode persists per chat session (new `mode` column); Cash mode forces the cloud backend, since tools only fire there. +### Mental-game rituals +- Brian's own rituals are now first-class, live tools (not just post-hoc recap + sections): **Scar Notes** (with the punt / cooler / standard distinction), + **Confidence Bank** (good process, banked regardless of result), **Alligator + Blood** mode (an invokable adversity state โ€” she'll suggest it when he's + card-dead/short/stuck, and her coaching register shifts while it's on), and + **Reset** (a tilt circuit-breaker; mental marker, stats stay continuous). +- Rituals show on the HUD (๐ŸŠ banner, Confidence Bank + Scar Notes panels) and feed + the recap's Scar Notes / Confidence Bank sections with what actually happened. + ### Session HUD - **Live HUD** at `/session` (bottom-nav tab on mobile, header link on desktop) โ€” polls every 5s: header (venue/stakes/elapsed/live net), stack with diff --git a/README.md b/README.md index 5a323a1..47e2808 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ Talk to her during a session; she drives tools behind the scenes: - **Session tracking** โ€” `start_session`, `add_buyin`, `end_session` โ†’ net, hours, $/hr. - **Stack tracking** โ€” `log_stack` records your stack as the night goes โ†’ live net while you're still sitting, and a stack-over-time sparkline on the HUD. +- **Mental-game rituals** โ€” your own system, run live: **Scar Notes** (punt / cooler + / standard), **Confidence Bank** (good process, banked regardless of result), + **Alligator Blood** mode (adversity register she'll suggest when you're card-dead / + stuck), and **Reset** (tilt circuit-breaker). They surface on the HUD and ground the recap. - **Hand histories** โ€” vomit rough shorthand ("AKs btn, 3bet, flop A72โ€ฆ"), she reconstructs a structured, **replayable** hand (unknown cards = `x`, never invented). - **Villain file** โ€” named opponents auto-build persistent dossiers; basic stats diff --git a/lyra/chat.py b/lyra/chat.py index fab17c0..b54a897 100644 --- a/lyra/chat.py +++ b/lyra/chat.py @@ -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()) diff --git a/lyra/modes.py b/lyra/modes.py index 939ba13..7162950 100644 --- a/lyra/modes.py +++ b/lyra/modes.py @@ -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( diff --git a/lyra/poker.py b/lyra/poker.py index 5cba305..8c2a9a1 100644 --- a/lyra/poker.py +++ b/lyra/poker.py @@ -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", {}), diff --git a/lyra/tools.py b/lyra/tools.py index 245c427..fd6946d 100644 --- a/lyra/tools.py +++ b/lyra/tools.py @@ -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.", diff --git a/lyra/web/static/session.html b/lyra/web/static/session.html index f047ed6..e46d4a5 100644 --- a/lyra/web/static/session.html +++ b/lyra/web/static/session.html @@ -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 ? `
+ ๐ŸŠ +
Alligator Blood
refuse to die ยท no forced miracles ยท make them beat you correctly
+
` : ''} +
${esc(title)} @@ -154,6 +180,7 @@ in ${money(s.buy_in_total)} ${esc(s.format || 'cash')} ${hands.length} hands + ${resets.length ? `๐Ÿ”„ ${resets.length} reset${resets.length>1?'s':''}` : ''}
@@ -180,6 +207,23 @@ : '

No hands logged yet.

'} +
+

๐Ÿ’ฐ Confidence Bank

+ ${confidence.length ? `
    ${confidence.slice().reverse().map(c => ` +
  • ${esc(c.content)}${c.hand_id ? ` ยท hand` : ''} +
    ${ago(c.at)}
  • `).join('')}
` + : '

Nothing banked yet โ€” disciplined plays land here.

'} +
+ +
+

๐Ÿฉน Scar Notes

+ ${scars.length ? `
    ${scars.slice().reverse().map(sc => ` +
  • ${esc(sc.content)}${sc.classification ? `${esc(sc.classification)}` : ''} + ${sc.hand_id ? ` ยท hand` : ''} +
    ${ago(sc.at)}
  • `).join('')}
` + : '

No scars logged โ€” mistakes to study land here.

'} +
+

Villains seen

${villains.length ? `
    ${villains.map(v => ` diff --git a/tests/test_modes.py b/tests/test_modes.py index 9249e5a..b52e809 100644 --- a/tests/test_modes.py +++ b/tests/test_modes.py @@ -106,3 +106,62 @@ def test_log_stack_tool_handler(lyra): assert "450" in out and "150" in out # confirms stack + live net # graceful when there's no number assert "number" in tools.dispatch("log_stack", {}, {}).lower() + + +# --- mental-game rituals --- + +def test_ritual_tools_in_cash_only(lyra): + _, _, modes, tools = lyra + cash = _names(tools.specs(modes.CASH.tools)) + talk = _names(tools.specs(modes.TALK.tools)) + rituals = {"scar_note", "confidence_bank", "alligator_blood", "reset_ritual"} + assert rituals <= cash + assert not (rituals & talk) + + +def test_scar_and_confidence_capture(lyra): + _, poker, _, tools = lyra + poker.start_session(stakes="2/5", buy_in=500) + tools.dispatch("scar_note", {"content": "punted bottom set", "classification": "punt"}, {}) + tools.dispatch("scar_note", {"content": "ran KK into AA", "classification": "cooler"}, {}) + tools.dispatch("confidence_bank", {"content": "disciplined river fold"}, {}) + + scars = poker.list_rituals(kinds=("scar",)) + assert len(scars) == 2 + assert {s["classification"] for s in scars} == {"punt", "cooler"} + conf = poker.list_rituals(kinds=("confidence",)) + assert len(conf) == 1 and "fold" in conf[0]["content"] + # bogus classification is dropped, not stored + tools.dispatch("scar_note", {"content": "x", "classification": "nonsense"}, {}) + assert poker.list_rituals(kinds=("scar",))[-1]["classification"] is None + + +def test_alligator_toggle_and_state(lyra): + _, poker, _, tools = lyra + poker.start_session(stakes="2/5", buy_in=500) + assert poker.alligator_active() is False + tools.dispatch("alligator_blood", {"on": True}, {}) + assert poker.alligator_active() is True + tools.dispatch("alligator_blood", {"on": False}, {}) + assert poker.alligator_active() is False # latest toggle wins + + +def test_rituals_in_hud(lyra): + _, poker, _, tools = lyra + poker.start_session(stakes="2/5", buy_in=500) + tools.dispatch("scar_note", {"content": "overplayed top pair"}, {}) + tools.dispatch("confidence_bank", {"content": "good value bet"}, {}) + tools.dispatch("reset_ritual", {"content": "lost a flip"}, {}) + tools.dispatch("alligator_blood", {"on": True}, {}) + + r = poker.hud()["rituals"] + assert r["alligator"] is True + assert len(r["scars"]) == 1 and len(r["confidence"]) == 1 and len(r["resets"]) == 1 + + +def test_rituals_require_live_session(lyra): + _, poker, _, tools = lyra + # tools degrade gracefully (no exception) when nothing is open + assert "no live session" in tools.dispatch("scar_note", {"content": "x"}, {}).lower() + with pytest.raises(ValueError): + poker.log_ritual("scar", content="x")