File size: 7,144 Bytes
3680f5b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | """Poker decision engine with a single, calibrated risk knob (r in [0, 1]).
Design (v0, documented limitations at bottom):
- Equity: Monte Carlo vs uniform random opponent hole card(s) and remaining
board, via the `treys` evaluator.
- The knob r simultaneously and MONOTONICALLY controls:
calling threshold : need = pot_odds - 0.20*r (risk-seeking calls thinner)
raise threshold : 0.62 - 0.10*r (aggression scales up)
bet threshold : 0.60 - 0.12*r (bluffier at high r)
bet sizing : 0.45 + 0.55 of pot, scaled by equity excess
- Position modifier: BTN -0.03, SB/BB +0.03, middle +0.00 on all thresholds.
- EV model (heuristic, for policy comparison only — not ground truth):
fold: 0
call: eq*(pot+to_call) - to_call
bet X: p_fold*pot + (1-p_fold)*(eq*(pot+2X) - X),
p_fold = clamp(0.6 - 1.2*eq, 0.05, 0.6) (opponent folds more vs big eq)
"""
import random
from dataclasses import dataclass
from treys import Card as TCard
from treys import Evaluator
@dataclass
class TableState:
hero: list # ['AS', 'KD'] ASCII card strings
board: list # up to 5 ASCII cards
pot: int # chips in middle, including current street bets
stack: int # hero chips behind
to_call: int # 0 if checked to hero
n_players: int # players still in the hand
position: str # BTN, SB, BB, UTG, MP, MP2, CO
street: str # preflop, flop, turn, river
# treys card ints are bit-encoded, not 0..51 — build the real 52-card deck once
_ALL_CARDS = [TCard.new(r + s) for r in "23456789TJQKA" for s in "shdc"]
_POS_MOD = {"BTN": -0.03, "CO": -0.02, "SB": 0.03, "BB": 0.03,
"UTG": 0.02, "UTG+1": 0.02, "MP": 0.0, "MP2": 0.0}
def parse_card(c):
c = c.strip().upper()
if c[-1] in "SHDC" and len(c) == 2:
return TCard.new(c[:-1] + c[-1].lower())
# unicode suit form 'A♠'
suit = {"♠": "s", "♥": "h", "♦": "d", "♣": "c"}.get(c[-1])
rank = c[:-1].replace("10", "T")
return TCard.new(rank + suit.lower())
class RiskEngine:
def __init__(self, n_sims=1500, seed=7):
self.ev = Evaluator()
self.n_sims = n_sims
self.rng = random.Random(seed)
def equity(self, hero, board, n_opponents=1):
"""Win/tie probability vs uniform random opponents. ~0.15-0.3 s for 1500 sims."""
hero_c = [parse_card(c) for c in hero]
board_c = [parse_card(c) for c in board]
known = set(hero_c + board_c)
deck = [c for c in _ALL_CARDS if c not in known]
wins = ties = 0
n_sims = self.n_sims
for _ in range(n_sims):
self.rng.shuffle(deck)
opp_hands = []
need = 2 * n_opponents + (5 - len(board_c))
used = deck[:need]
k = 0
full_board = board_c + used[k:k + (5 - len(board_c))]
k += (5 - len(board_c))
best = None
hero_score = self.ev.evaluate(hero_c, full_board)
all_scores = [hero_score]
for o in range(n_opponents):
oc = used[k:k + 2]
k += 2
all_scores.append(self.ev.evaluate(oc, full_board))
m = min(all_scores)
if all_scores[0] == m and all_scores.count(m) == 1:
wins += 1
elif all_scores[0] == m:
ties += 1
return (wins + 0.5 * ties) / n_sims
def thresholds(self, r, position):
mod = _POS_MOD.get(position, 0.0)
return {
"call_margin": 0.20 * r, # equity buffer below pot odds
"raise_thr": max(0.45, 0.62 - 0.10 * r - mod),
"bet_thr": max(0.42, 0.60 - 0.12 * r - mod),
"bet_frac": 0.45 + 0.55 * r, # of pot
}
def decide(self, state: TableState, r: float, eq=None):
"""Action for the state at risk knob r. eq may be precomputed."""
t = self.thresholds(r, state.position)
if eq is None:
eq = self.equity(state.hero, state.board,
max(1, state.n_players - 1))
pot, to_call, stack = state.pot, state.to_call, state.stack
pot_odds = to_call / (pot + to_call) if to_call > 0 else 0.0
need = max(0.0, pot_odds - t["call_margin"])
if to_call > 0:
if eq >= t["raise_thr"] and stack > to_call:
amt = int(min(stack - to_call,
(0.45 + 0.55 * r) * (pot + 2 * to_call)
* (0.5 + eq)))
return {"action": "raise", "amount": max(1, amt), "equity": eq,
"thresholds": t}
if eq >= need:
return {"action": "call", "amount": to_call, "equity": eq,
"thresholds": t}
return {"action": "fold", "amount": 0, "equity": eq,
"thresholds": t}
# checked to hero
if eq >= t["bet_thr"]:
amt = int(min(stack, t["bet_frac"] * pot * (0.5 + eq)))
return {"action": "bet" if state.street != "preflop" else "raise",
"amount": max(1, amt), "equity": eq, "thresholds": t}
return {"action": "check", "amount": 0, "equity": eq, "thresholds": t}
# --- heuristic EV (policy comparison only) ---
def ev_of(self, state, action, eq):
pot, to_call, stack = state.pot, state.to_call, state.stack
if action["action"] in ("fold", "check"):
return 0.0
if action["action"] == "call":
return eq * (pot + to_call) - to_call
X = min(stack, action["amount"])
p_fold = min(0.6, max(0.05, 0.6 - 1.2 * eq))
return p_fold * pot + (1 - p_fold) * (eq * (pot + 2 * X) - X)
def best_ev(self, state, eq):
"""Best heuristic EV among the candidate actions (oracle for decision loss)."""
cands = []
t = self.thresholds(0.5, state.position)
cands.append({"action": "fold", "amount": 0})
if state.to_call > 0:
cands.append({"action": "call", "amount": state.to_call})
if state.stack > state.to_call:
cands.append({"action": "raise",
"amount": min(state.stack - state.to_call,
int(0.8 * (state.pot + 2 * state.to_call)))})
else:
cands.append({"action": "check", "amount": 0})
cands.append({"action": "bet",
"amount": min(state.stack, int(0.8 * state.pot))})
return max(self.ev_of(state, c, eq) for c in cands)
LIMITATIONS = """v0 limitations, by design:
- One opponent model: uniform random hole cards. No opponent ranges/reads.
- Single-street decisions: no future-street planning, no ICM, no multiway
pot dynamics beyond equity-vs-n-opponents.
- EV model is heuristic (fold-equity linear in equity) and is used only to
COMPARE policies on the same footing, never as ground truth.
- Preflop uses the same equity MC (no chart knowledge).
"""
|