package holdem import ( "math/rand/v2" "github.com/chehsunliu/poker" "pete/internal/games/cards" ) // How good is this hand, really? // // A bot's decision starts here: deal the cards it cannot see, a thousand times, // and count how often it wins. That number — plus the board's texture, the // stack-to-pot ratio and the action so far — is the key it looks its trained // strategy up under. // // Monte Carlo rather than exhaustive because exhaustive is 2.1 million river // runouts against one opponent and rather more against five, and a thousand // samples puts the estimate inside a percentage point or so. The bot does not // need the fourth decimal place; it needs to know whether it is ahead. // Equity is the fraction of runouts a hand wins, ties and loses against the // given number of unknown opponents. type Equity struct { Win float64 Tie float64 Loss float64 } // Strength collapses a result into the one number the policy is keyed on: a tie // is worth half a win, because that is what half a pot is. func (e Equity) Strength() float64 { return e.Win + e.Tie*0.5 } // deck52 is the evaluator's whole deck, built once. var deck52 = func() []poker.Card { d := make([]poker.Card, 0, 52) for s := cards.Spades; s <= cards.Clubs; s++ { for r := cards.Ace; r <= cards.King; r++ { d = append(d, pokerOf[s][r]) } } return d }() // equityOf runs the simulation. The RNG is threaded like everything else here, // so a bot's decision replays from the session's seed along with the deal. func equityOf(hole [2]cards.Card, board []cards.Card, opponents, iterations int, rng *rand.Rand) Equity { if opponents < 1 { opponents = 1 } h0, h1 := toPoker(hole[0]), toPoker(hole[1]) // Seven cards, checked by hand. A map here would be the most expensive thing // in the trainer, which calls this function millions of times. var known [7]poker.Card n := 2 known[0], known[1] = h0, h1 pb := make([]poker.Card, len(board)) for i, c := range board { pb[i] = toPoker(c) known[n] = pb[i] n++ } rest := make([]poker.Card, 0, 52) for _, c := range deck52 { seen := false for _, k := range known[:n] { if k == c { seen = true break } } if !seen { rest = append(rest, c) } } need := opponents*2 + (5 - len(pb)) if need > len(rest) { return Equity{Tie: 1} } hero := make([]poker.Card, 7) hero[0], hero[1] = h0, h1 villain := make([]poker.Card, 7) full := make([]poker.Card, 5) var wins, ties int for i := 0; i < iterations; i++ { // A partial Fisher-Yates: only the cards actually needed get shuffled into // place, which is the difference between this being cheap and being the // slowest thing in the request. for j := 0; j < need; j++ { k := j + rng.IntN(len(rest)-j) rest[j], rest[k] = rest[k], rest[j] } copy(full, pb) at := opponents * 2 for b := len(pb); b < 5; b++ { full[b] = rest[at] at++ } copy(hero[2:], full) mine := poker.Evaluate(hero) best := int32(7463) // one worse than the worst real hand for o := 0; o < opponents; o++ { villain[0], villain[1] = rest[o*2], rest[o*2+1] copy(villain[2:], full) if r := poker.Evaluate(villain); r < best { best = r } } switch { case mine < best: wins++ case mine == best: ties++ } } total := float64(iterations) return Equity{ Win: float64(wins) / total, Tie: float64(ties) / total, Loss: float64(iterations-wins-ties) / total, } }