From aefb22c82344bc1c2370860bba2dbbd72da93f68 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sat, 4 Jul 2026 07:01:11 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20clear=5Ftable=20=E2=80=94=20empty=20the?= =?UTF-8?q?=20roster=20on=20a=20table=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Clear the table" had no tool behind it, so she claimed she did it and nothing changed. Add clear_table (empties the roster, keeps the session/stack/reads) and a `replace` flag on seat_players for a one-shot table swap. Cash card: on a table change / "clear the table", call clear_table then seat the new table — never claim it without calling the tool. 2 tests. Full suite green. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyra/modes.py | 7 +++++-- lyra/poker.py | 14 ++++++++++++++ lyra/tools.py | 17 ++++++++++++++++- tests/test_roster.py | 22 ++++++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lyra/modes.py b/lyra/modes.py index af85d52..c1a11f5 100644 --- a/lyra/modes.py +++ b/lyra/modes.py @@ -47,7 +47,7 @@ _BASE = ("journal_write", "note", "think_about", "thought_response", "set_mode") # 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", "seat_players", "unseat_player", "name_villain", "link_villains", + "add_read", "seat_players", "unseat_player", "clear_table", "name_villain", "link_villains", "analyze_spot", "session_stats", "session_state", "end_session", "generate_recap", "scar_note", "confidence_bank", "alligator_blood", "reset_ritual", "undo_last", "update_session", @@ -84,7 +84,10 @@ THE TABLE ROSTER. When Brian names who's at the table — usually at the start, off the Bravo screen ("we've got TAG, JD, Wheelz, and a new guy in seat 3") — call seat_players \ to register them as seated this session. That roster is who his reads/TAGs attach to by name, \ and it's shown on his HUD. When someone busts or leaves, unseat_player; when a new player sits, \ -seat_players again. Keep it current as the table changes. A handle like "TAG" (all caps, off \ +seat_players again. When he CHANGES TABLES or says "clear the table", call clear_table to empty \ +the roster (the session and his stack keep going — only who's seated resets), then seat the new \ +table when he names it. Never claim you cleared or seated anyone without actually calling the \ +tool. Keep it current as the table changes. A handle like "TAG" (all caps, off \ Bravo) is a PERSON'S NAME — seat it as a player, never read it as the tight-aggressive style. LOGGING PLAYER ACTIONS IS A CORE JOB YOU KEEP MISSING. Whenever he tells you what another \ diff --git a/lyra/poker.py b/lyra/poker.py index 7452b60..bd17040 100644 --- a/lyra/poker.py +++ b/lyra/poker.py @@ -1888,6 +1888,20 @@ def unseat_player(name: str | None = None, descriptor: str | None = None, return True +def clear_roster(session_id: int | None = None) -> int: + """Empty the table roster (he changed tables) — unseat everyone at once. Keeps + the session and any reads logged; just resets who's currently seated. Returns + how many were cleared.""" + sid = _resolve(session_id) + if sid is None: + return 0 + conn = _c() + with conn: + cur = conn.execute( + "UPDATE session_players SET active = 0 WHERE session_id = ? AND active = 1", (sid,)) + return cur.rowcount + + def session_roster(session_id: int | None = None) -> list[dict]: """The live table roster: seated players with seat, dossier, and their latest read this session. This is 'who's at the table right now'.""" diff --git a/lyra/tools.py b/lyra/tools.py index 14f743d..e144589 100644 --- a/lyra/tools.py +++ b/lyra/tools.py @@ -317,6 +317,8 @@ def _seat_players(args: dict, ctx: dict) -> str: if isinstance(players, str): players = [p.strip() for p in re.split(r"[,\n]", players) if p.strip()] try: + if args.get("replace"): # a whole new table — wipe the roster first + poker.clear_roster() n = poker.seat_players(players) except ValueError: return "No live session — start one first, then I'll seat the table." @@ -325,6 +327,11 @@ def _seat_players(args: dict, ctx: dict) -> str: return f"Seated {n}. Table now: {names}" +def _clear_table(args: dict, ctx: dict) -> str: + n = poker.clear_roster() + return f"Table cleared — roster's empty ({n} removed). Tell me who's at the new one." + + def _unseat_player(args: dict, ctx: dict) -> str: ok = poker.unseat_player(name=args.get("name"), descriptor=args.get("descriptor")) who = args.get("name") or args.get("descriptor") or "player" @@ -685,7 +692,9 @@ TOOLS.update({ "name": {**_S, "description": "Handle as it appears on Bravo, e.g. 'TAG'"}, "descriptor": {**_S, "description": "Physical description if no name"}, "seat": {**_S, "description": "Seat number/label if known"}, - "category": {**_S, "description": "feeder | risky | reg | unknown"}}}}}, + "category": {**_S, "description": "feeder | risky | reg | unknown"}}}}, + "replace": {"type": "boolean", "description": "true = a brand-new table: clear the " + "current roster first, then seat these (use when he changes tables)"}}, ["players"])}, "unseat_player": {"handler": _unseat_player, "spec": _f( "unseat_player", @@ -693,6 +702,12 @@ TOOLS.update({ {"name": {**_S, "description": "Their handle"}, "descriptor": {**_S, "description": "Or a description if unnamed"}}, [])}, + "clear_table": {"handler": _clear_table, "spec": _f( + "clear_table", + "Empty the whole table roster at once — call this when Brian changes tables or says " + "to clear the table. The session, stack, and logged reads stay; only who's currently " + "seated resets. Then he'll tell you the new table.", + {}, [])}, "name_villain": {"handler": _name_villain, "spec": _f( "name_villain", "Attach a real name to a player you'd only known by description (e.g. you caught it " diff --git a/tests/test_roster.py b/tests/test_roster.py index 4a8409d..13fb553 100644 --- a/tests/test_roster.py +++ b/tests/test_roster.py @@ -72,6 +72,28 @@ def test_unseat_player_removes_from_roster_keeps_history(mods): assert poker.player_profile("TAG")["reads"] # history intact +def test_clear_table_empties_roster_keeps_reads(mods): + poker, tools = mods + poker.start_session(venue="Meadows", buy_in=300) + poker.seat_players(["TAG", "Jonathan"]) + poker.add_read(note="limped A4o", name="TAG") + out = tools.dispatch("clear_table", {}, {}) + assert "cleared" in out.lower() + assert poker.session_roster() == [] # roster emptied + assert poker.player_profile("TAG")["reads"] # reads kept + # A live session is untouched by clearing the table. + assert poker.live_session() is not None + + +def test_seat_players_replace_swaps_to_new_table(mods): + poker, tools = mods + poker.start_session(venue="Meadows", buy_in=300) + poker.seat_players(["TAG", "Jonathan"]) + tools.dispatch("seat_players", {"players": [{"name": "Doyle"}, {"name": "Ivey"}], + "replace": True}, {}) + assert {r["name"] for r in poker.session_roster()} == {"Doyle", "Ivey"} + + def test_seat_players_accepts_plain_name_list_via_tool(mods): poker, tools = mods poker.start_session(venue="Meadows", buy_in=300)