f2944ed402
Phase 3. She can now log and resolve identity at the table: - add_read gains a `descriptor` param — a read on an unnamed player resolves to an existing descriptor villain (confident match) or opens a new one, so reads on "neck tattoo guy" accumulate and reuse across the night. - name_villain(descriptor, name): attach a real name once caught (history carries). - link_villains(a, b, same): merge on confirmed same-person, or mark distinct so she stops asking. Refuses to act on a vague reference — never merges on a guess. - Cash card PLAYERS guidance: log nameless villains by distinctive descriptor, cite the SCOUTING DESK note, ask before assuming a callback, confirm before merge. 4 tests. Full suite 150 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
3.4 KiB
Python
86 lines
3.4 KiB
Python
"""Confirm-loop tools: descriptor reads, name attach, merge/mark-distinct."""
|
|
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_descriptor_read_creates_then_reuses_nameless_villain(mods):
|
|
poker, tools = mods
|
|
poker.start_session(venue="Meadows", stakes="1/3", buy_in=300)
|
|
tools.dispatch("add_read", {"note": "opened UTG light",
|
|
"descriptor": "neck tattoo sleeve arm"}, {})
|
|
tools.dispatch("add_read", {"note": "showed a bluff",
|
|
"descriptor": "neck tattoo sleeve"}, {}) # rephrase → same guy
|
|
players = [p for p in poker.get_villain_file() if not p["named"]]
|
|
assert len(players) == 1 # one nameless villain, not two
|
|
reads = poker._c().execute(
|
|
"SELECT COUNT(*) n FROM player_reads WHERE player_id = ?", (players[0]["id"],)
|
|
).fetchone()["n"]
|
|
assert reads == 2
|
|
|
|
|
|
def test_name_villain_tool_attaches_name(mods):
|
|
poker, tools = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
poker.create_descriptor_villain("neck tattoo sleeve arm", venue="Meadows")
|
|
out = tools.dispatch("name_villain", {"descriptor": "neck tattoo sleeve arm",
|
|
"name": "Danny"}, {})
|
|
assert "Danny" in out
|
|
assert poker.resolve_villain("Danny")["band"] == "name"
|
|
|
|
|
|
def test_link_villains_merge_and_distinct(mods):
|
|
poker, tools = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
poker.upsert_player("Danny", venue="Meadows")
|
|
poker.upsert_player("Donny", venue="Meadows")
|
|
# same=false → recorded distinct
|
|
tools.dispatch("link_villains", {"player_a": "Danny", "player_b": "Donny",
|
|
"same": False, "note": "different builds"}, {})
|
|
a = poker.resolve_villain("Danny")["match_id"]
|
|
b = poker.resolve_villain("Donny")["match_id"]
|
|
assert poker.are_distinct(a, b)
|
|
# same=true on a fresh pair → merged
|
|
poker.upsert_player("Mike", venue="Meadows")
|
|
poker.upsert_player("Michael", venue="Meadows")
|
|
tools.dispatch("link_villains", {"player_a": "Mike", "player_b": "Michael",
|
|
"same": True}, {})
|
|
names = [p["name"] for p in poker.get_villain_file()]
|
|
assert ("Mike" in names) ^ ("Michael" in names) # one absorbed the other
|
|
|
|
|
|
def test_link_villains_refuses_when_reference_is_vague(mods):
|
|
poker, tools = mods
|
|
poker.start_session(venue="Meadows", buy_in=300)
|
|
poker.upsert_player("Danny", venue="Meadows")
|
|
out = tools.dispatch("link_villains", {"player_a": "Danny",
|
|
"player_b": "some guy", "same": True}, {})
|
|
assert "didn't merge" in out.lower() or "couldn't" in out.lower()
|