"""Deterministic poker evaluation + equity — the math Lyra must NEVER eyeball. Wraps `treys` so board reading (what each hand makes), who's ahead, exact equity, and outs are *computed*, not guessed by the LLM (which is unreliable at it). Cards are 'Rs' (rank + suit letter, e.g. 'Jh','Td'); a card with unknown suit ('Jx') is assigned an arbitrary free suit; a fully-unknown 'x' can't be used for equity. """ from __future__ import annotations from itertools import combinations from treys import Card, Evaluator _EV = Evaluator() _RANKS = "23456789TJQKA" _SUITS = "shdc" _DECK = [r + s for r in _RANKS for s in _SUITS] _SYM = {"♥": "h", "♦": "d", "♣": "c", "♠": "s"} class EquityError(ValueError): pass def _norm(tok: str) -> str: t = (tok or "").strip().replace("10", "T") for sym, ltr in _SYM.items(): t = t.replace(sym, ltr) return t def _resolve(groups: list[list[str]]) -> list[list[str]]: """Resolve card tokens across groups to concrete 'Rs' cards (assign suits to 'Rx', reject fully-unknown 'x'); raise on real duplicates/garbage.""" # concrete cards already named, so 'Rx' suit-assignment can avoid them concrete: set[str] = set() for g in groups: for tok in g: t = _norm(tok) if len(t) == 2 and t[0].upper() in _RANKS and t[1].lower() in _SUITS: concrete.add(t[0].upper() + t[1].lower()) placed: set[str] = set() out: list[list[str]] = [] cycle = 0 # rotate suit assignment for unknown suits so we don't fabricate flushes for g in groups: rg: list[str] = [] for tok in g: t = _norm(tok) if not t or t.lower() == "x": raise EquityError(f"card '{tok}' is fully unknown — need at least a rank") r = t[0].upper() if r not in _RANKS: raise EquityError(f"can't read card '{tok}'") if len(t) > 1 and t[1].lower() in _SUITS: card = r + t[1].lower() else: # unknown suit -> spread suits (rainbow) to avoid phantom flushes order = _SUITS[cycle % 4:] + _SUITS[:cycle % 4] cycle += 1 card = next((r + s for s in order if r + s not in concrete and r + s not in placed), None) if card is None: raise EquityError(f"no free suit left for {r}") if card in placed: raise EquityError(f"duplicate card {card}") placed.add(card) rg.append(card) out.append(rg) return out def _made(cards: list[str], board: list[str]) -> str: score = _EV.evaluate([Card.new(c) for c in board], [Card.new(c) for c in cards]) return _EV.class_to_string(_EV.get_rank_class(score)) def _equity(hero: list[str], vil: list[str], board: list[str]) -> tuple[float, float, float]: known = set(hero + vil + board) rem = [c for c in _DECK if c not in known] need = 5 - len(board) hw = vw = tie = 0 bh = [Card.new(c) for c in board] hh = [Card.new(c) for c in hero] vh = [Card.new(c) for c in vil] for extra in combinations(rem, need) if need else [()]: full = bh + [Card.new(c) for c in extra] h, v = _EV.evaluate(full, hh), _EV.evaluate(full, vh) if h < v: hw += 1 elif v < h: vw += 1 else: tie += 1 n = hw + vw + tie or 1 return round(100 * hw / n, 1), round(100 * vw / n, 1), round(100 * tie / n, 1) def _outs(hero: list[str], vil: list[str], board: list[str]) -> dict: """River cards (when one to come) that give hero the win. Lists them so a 'tricky' card (e.g. one that makes villain a flush) is visible by omission.""" if len(board) != 4: return {} known = set(hero + vil + board) bh = [Card.new(c) for c in board] hh = [Card.new(c) for c in hero] vh = [Card.new(c) for c in vil] winners = [] for c in (x for x in _DECK if x not in known): full = bh + [Card.new(c)] if _EV.evaluate(full, hh) < _EV.evaluate(full, vh): winners.append(c) return {"count": len(winners), "cards": winners} def analyze(hero: list[str], villain: list[str], board: list[str]) -> dict: """Made hands + exact equity + outs for a hero-vs-villain spot at a given board.""" h, v, b = _resolve([hero, villain, board]) allc = h + v + b if len(set(allc)) != len(allc): raise EquityError("duplicate cards across hands/board") res: dict = {"hero": h, "villain": v, "board": b} if len(b) >= 3: res["hero_hand"] = _made(h, b) res["villain_hand"] = _made(v, b) hs = _EV.evaluate([Card.new(c) for c in b], [Card.new(c) for c in h]) vs = _EV.evaluate([Card.new(c) for c in b], [Card.new(c) for c in v]) res["ahead"] = "hero" if hs < vs else "villain" if vs < hs else "tie" heq, veq, tie = _equity(h, v, b) res.update(hero_equity=heq, villain_equity=veq, tie_equity=tie) if len(b) == 4: res["hero_outs"] = _outs(h, v, b) return res