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:
@@ -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
|
- Mode persists per chat session (new `mode` column); Cash mode forces the cloud
|
||||||
backend, since tools only fire there.
|
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
|
### Session HUD
|
||||||
- **Live HUD** at `/session` (bottom-nav tab on mobile, header link on desktop) —
|
- **Live HUD** at `/session` (bottom-nav tab on mobile, header link on desktop) —
|
||||||
polls every 5s: header (venue/stakes/elapsed/live net), stack with
|
polls every 5s: header (venue/stakes/elapsed/live net), stack with
|
||||||
|
|||||||
@@ -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.
|
- **Session tracking** — `start_session`, `add_buyin`, `end_session` → net, hours, $/hr.
|
||||||
- **Stack tracking** — `log_stack` records your stack as the night goes → live net
|
- **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.
|
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
|
- **Hand histories** — vomit rough shorthand ("AKs btn, 3bet, flop A72…"), she
|
||||||
reconstructs a structured, **replayable** hand (unknown cards = `x`, never invented).
|
reconstructs a structured, **replayable** hand (unknown cards = `x`, never invented).
|
||||||
- **Villain file** — named opponents auto-build persistent dossiers; basic stats
|
- **Villain file** — named opponents auto-build persistent dossiers; basic stats
|
||||||
|
|||||||
@@ -24,6 +24,21 @@ MAX_TOOL_ROUNDS = 5 # cap tool-call iterations per turn
|
|||||||
TOOL_BACKENDS = {"cloud"}
|
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:
|
def _maybe_switch_mode(session_id: str, tool_name: str) -> None:
|
||||||
"""Keep the chat framing aligned with the live data: opening a poker session
|
"""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
|
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:
|
if mode and mode.card:
|
||||||
messages.append({"role": "system", "content": 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).
|
# When she is: current time + the gap since Brian last spoke (she has no clock).
|
||||||
messages.append(_now_note())
|
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).
|
# Always-available core tools (her own agency: journaling/notes).
|
||||||
_BASE = ("journal_write", "note")
|
_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 + (
|
_CASH_TOOLS = _BASE + _LOOKUPS + (
|
||||||
"start_session", "add_buyin", "log_stack", "log_hand", "record_hand",
|
"start_session", "add_buyin", "log_stack", "log_hand", "record_hand",
|
||||||
"add_read", "analyze_spot", "session_stats", "end_session", "generate_recap",
|
"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
|
# 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 \
|
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 \
|
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 \
|
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(
|
TALK = Mode(
|
||||||
|
|||||||
+102
@@ -108,6 +108,20 @@ CREATE TABLE IF NOT EXISTS poker_stack_log (
|
|||||||
created_at TEXT NOT NULL
|
created_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_stacklog_session ON poker_stack_log(session_id);
|
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).
|
# 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,
|
def end_session(cash_out: float, mood: str | None = None,
|
||||||
session_id: int | None = None) -> dict:
|
session_id: int | None = None) -> dict:
|
||||||
"""Close a session: record cashout, compute net + hours. Returns the row."""
|
"""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)
|
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:
|
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."""
|
"""Generate Brian's .md recap from a session's structured data + conversation, store it."""
|
||||||
backend = backend or "cloud"
|
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(
|
reads = [dict(r) for r in _c().execute(
|
||||||
"SELECT seat, note FROM player_reads WHERE session_id = ?", (sid,)).fetchall()]
|
"SELECT seat, note FROM player_reads WHERE session_id = ?", (sid,)).fetchall()]
|
||||||
stats = session_stats(sid)
|
stats = session_stats(sid)
|
||||||
|
rituals = list_rituals(sid)
|
||||||
|
|
||||||
convo = ""
|
convo = ""
|
||||||
if s.get("chat_session_id"):
|
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"
|
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"
|
"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"
|
"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)")
|
"CONVERSATION DURING SESSION:\n" + (convo or "(none captured)")
|
||||||
)
|
)
|
||||||
md = llm.complete(
|
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).
|
# Context: how Brian runs at these stakes overall (closed sessions).
|
||||||
ctx = running_stats(stakes=s.get("stakes")) if s.get("stakes") else {}
|
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 {
|
return {
|
||||||
"session": {
|
"session": {
|
||||||
"id": sid, "venue": s.get("venue"), "stakes": s.get("stakes"),
|
"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,
|
"hands": hands,
|
||||||
"villains": _session_villains(sid),
|
"villains": _session_villains(sid),
|
||||||
"notes": notes,
|
"notes": notes,
|
||||||
|
"rituals": {
|
||||||
|
"alligator": alligator_active(sid),
|
||||||
|
"scars": by_kind("scar"),
|
||||||
|
"confidence": by_kind("confidence"),
|
||||||
|
"resets": by_kind("reset"),
|
||||||
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"hands_logged": stats.get("hands_logged", 0),
|
"hands_logged": stats.get("hands_logged", 0),
|
||||||
"tags": stats.get("tags", {}),
|
"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 ".")
|
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:
|
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, "")}
|
fields = {k: args.get(k) for k in poker._HAND_FIELDS if args.get(k) not in (None, "")}
|
||||||
hid = poker.log_hand(**fields)
|
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.",
|
"Tracks his stack over time and his live net while he's still sitting.",
|
||||||
{"amount": {**_N, "description": "Current total chip stack, in dollars"}},
|
{"amount": {**_N, "description": "Current total chip stack, in dollars"}},
|
||||||
["amount"])},
|
["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": {"handler": _log_hand, "spec": _f(
|
||||||
"log_hand",
|
"log_hand",
|
||||||
"Log a hand in the live session. All fields optional — capture whatever Brian gives you, even terse.",
|
"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; }
|
.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; }
|
.villain b { color: var(--text); } .villain .cat { color: var(--mid); font-size: .78rem; }
|
||||||
.note-meta { color: var(--fade); font-size: .72rem; }
|
.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; }
|
.empty { color: var(--fade); font-size: .92rem; }
|
||||||
.err { color: var(--low); text-align: center; padding: 30px; }
|
.err { color: var(--low); text-align: center; padding: 30px; }
|
||||||
.big-empty { text-align: center; padding: 50px 20px; color: var(--fade); }
|
.big-empty { text-align: center; padding: 50px 20px; color: var(--fade); }
|
||||||
@@ -139,11 +156,20 @@
|
|||||||
const villains = data.villains || [];
|
const villains = data.villains || [];
|
||||||
const notes = data.notes || [];
|
const notes = data.notes || [];
|
||||||
const stats = data.stats || {};
|
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 title = [s.stakes, s.game].filter(Boolean).join(' ') || 'Session';
|
||||||
const tagBits = Object.entries(stats.tags || {}).map(([k,v]) => `${k}×${v}`).join(' · ');
|
const tagBits = Object.entries(stats.tags || {}).map(([k,v]) => `${k}×${v}`).join(' · ');
|
||||||
|
|
||||||
root.innerHTML = `
|
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="card">
|
||||||
<div class="sess-top">
|
<div class="sess-top">
|
||||||
<span class="sess-title">${esc(title)}</span>
|
<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">in <b>${money(s.buy_in_total)}</b></span>
|
||||||
<span class="chip">${esc(s.format || 'cash')}</span>
|
<span class="chip">${esc(s.format || 'cash')}</span>
|
||||||
<span class="chip"><b>${hands.length}</b> hands</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,6 +207,23 @@
|
|||||||
: '<p class="empty">No hands logged yet.</p>'}
|
: '<p class="empty">No hands logged yet.</p>'}
|
||||||
</div>
|
</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">
|
<div class="card">
|
||||||
<p class="label">Villains seen</p>
|
<p class="label">Villains seen</p>
|
||||||
${villains.length ? `<ul class="rows">${villains.map(v => `
|
${villains.length ? `<ul class="rows">${villains.map(v => `
|
||||||
|
|||||||
@@ -106,3 +106,62 @@ def test_log_stack_tool_handler(lyra):
|
|||||||
assert "450" in out and "150" in out # confirms stack + live net
|
assert "450" in out and "150" in out # confirms stack + live net
|
||||||
# graceful when there's no number
|
# graceful when there's no number
|
||||||
assert "number" in tools.dispatch("log_stack", {}, {}).lower()
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user