feat: live table roster — seat_players / unseat_player + HUD card
The missing backbone for read tracking: a place for "who's at the table" to live. When Brian reads the table off Bravo (handles like TAG), Lyra registers them as seated this session; reads/TAGs then attach to those players by handle instead of spawning duplicates or getting missed. - session_players table; seat_player/seat_players/unseat_player/session_roster; _resolve_or_create_player (shared name/descriptor resolution, dedupe guard). - tools seat_players (accepts objects or a plain name list) + unseat_player. - HUD gains `roster`; Session page shows a 🪑 Table card (seat, handle, category, read count, last read). - Cash card: capture the roster when he names the table; a Bravo handle like TAG is a PERSON, seated as a player — never the tight-aggressive style. 5 tests. Full suite 162 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -311,6 +311,26 @@ def _resolve_villain_ref(ref: str) -> tuple[int | None, str]:
|
||||
return None, res["band"]
|
||||
|
||||
|
||||
def _seat_players(args: dict, ctx: dict) -> str:
|
||||
players = args.get("players") or []
|
||||
# Accept a plain list of names too, for convenience.
|
||||
if isinstance(players, str):
|
||||
players = [p.strip() for p in re.split(r"[,\n]", players) if p.strip()]
|
||||
try:
|
||||
n = poker.seat_players(players)
|
||||
except ValueError:
|
||||
return "No live session — start one first, then I'll seat the table."
|
||||
roster = poker.session_roster()
|
||||
names = ", ".join(r["name"] for r in roster) or "—"
|
||||
return f"Seated {n}. Table now: {names}"
|
||||
|
||||
|
||||
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"
|
||||
return f"{who} is off the table." if ok else f"Couldn't find {who} on the roster."
|
||||
|
||||
|
||||
def _name_villain(args: dict, ctx: dict) -> str:
|
||||
ref = (args.get("descriptor") or "").strip()
|
||||
name = (args.get("name") or "").strip()
|
||||
@@ -653,6 +673,26 @@ TOOLS.update({
|
||||
"category": {**_S, "description": "feeder | risky | reg | unknown"},
|
||||
"venue": {**_S, "description": "Where they play"}},
|
||||
["note"])},
|
||||
"seat_players": {"handler": _seat_players, "spec": _f(
|
||||
"seat_players",
|
||||
"Register who's at the table this session — the roster Brian reads off the Bravo "
|
||||
"screen (handles like TAG, JD). Call this when he names the table (usually at the "
|
||||
"start) or when a new player sits. Each player is a real handle in `name`, or a "
|
||||
"`descriptor` if he only describes them. These become the roster his reads/TAGs "
|
||||
"attach to by name.",
|
||||
{"players": {"type": "array", "description": "Players to seat",
|
||||
"items": {"type": "object", "properties": {
|
||||
"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"}}}}},
|
||||
["players"])},
|
||||
"unseat_player": {"handler": _unseat_player, "spec": _f(
|
||||
"unseat_player",
|
||||
"Remove a player from the table roster when they bust or leave. Keeps their history.",
|
||||
{"name": {**_S, "description": "Their handle"},
|
||||
"descriptor": {**_S, "description": "Or a description if unnamed"}},
|
||||
[])},
|
||||
"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 "
|
||||
|
||||
Reference in New Issue
Block a user