9b844bc356
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>
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""Live table roster: seat players, attach reads by handle, roster on the HUD."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
|
|
def _fake_embed(texts):
|
|
out = []
|
|
for t in texts:
|
|
v = np.zeros(64, dtype=np.float32)
|
|
for w in t.lower().split():
|
|
v[hash(w) % 64] += 1.0
|
|
out.append((v if v.any() else np.full(64, 1e-6, dtype=np.float32)).tolist())
|
|
return out
|
|
|
|
|
|
@pytest.fixture
|
|
def mods(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
|
|
from lyra import llm
|
|
monkeypatch.setattr(llm, "embed", _fake_embed)
|
|
import lyra.memory as memory
|
|
importlib.reload(memory)
|
|
import lyra.poker as poker
|
|
importlib.reload(poker)
|
|
import lyra.tools as tools
|
|
importlib.reload(tools)
|
|
return poker, tools
|
|
|
|
|
|
def test_seat_players_builds_roster(mods):
|
|
poker, _ = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
n = poker.seat_players(["TAG", "Jonathan", {"name": "Wheelz", "seat": "3"}])
|
|
assert n == 3
|
|
roster = poker.session_roster()
|
|
names = {r["name"] for r in roster}
|
|
assert names == {"TAG", "Jonathan", "Wheelz"}
|
|
assert next(r for r in roster if r["name"] == "Wheelz")["seat"] == "3"
|
|
|
|
|
|
def test_read_attaches_to_seated_player_by_handle(mods):
|
|
poker, _ = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
poker.seat_players(["TAG"])
|
|
poker.add_read(note="limped A4o from the SB, UTG straddle", name="TAG")
|
|
roster = poker.session_roster()
|
|
tag = next(r for r in roster if r["name"] == "TAG")
|
|
assert tag["reads"] == 1 and "A4o" in tag["last_note"]
|
|
# No duplicate TAG spawned — the read landed on the seated player.
|
|
assert sum(p["name"] == "TAG" for p in poker.get_villain_file()) == 1
|
|
|
|
|
|
def test_seat_players_tool_and_roster_in_hud(mods):
|
|
poker, tools = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
out = tools.dispatch("seat_players", {"players": [{"name": "TAG"}, {"name": "JD"}]}, {})
|
|
assert "TAG" in out and "JD" in out
|
|
assert len(poker.hud()["roster"]) == 2
|
|
|
|
|
|
def test_unseat_player_removes_from_roster_keeps_history(mods):
|
|
poker, _ = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
poker.seat_players(["TAG"])
|
|
poker.add_read(note="showed a bluff", name="TAG")
|
|
assert poker.unseat_player(name="TAG") is True
|
|
assert poker.session_roster() == [] # off the table
|
|
assert poker.player_profile("TAG")["reads"] # history intact
|
|
|
|
|
|
def test_seat_players_accepts_plain_name_list_via_tool(mods):
|
|
poker, tools = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
tools.dispatch("seat_players", {"players": "TAG, JD, Wheelz"}, {})
|
|
assert {r["name"] for r in poker.session_roster()} == {"TAG", "JD", "Wheelz"}
|