package holdem import ( "math" "sort" "strings" "github.com/chehsunliu/poker" "pete/internal/games/cards" ) // The bridge to the evaluator. // // The engine deals Pete's own cards.Card — the same one blackjack, solitaire and // the felt already speak — and converts at the door. Hand strength is the one // thing in this package that is genuinely hard to get right (7-card best-of-5, // 7,462 distinct hands), so it is not homegrown: github.com/chehsunliu/poker is // a lookup table and it is correct. // // The conversion is a table built once. Doing it per evaluation would matter: // a bot's equity estimate is a thousand seven-card evaluations, and it makes // several of those per hand. var ( pokerRanks = [14]string{"", "A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"} pokerSuits = [4]string{"s", "h", "d", "c"} // cards.Spades, Hearts, Diamonds, Clubs // A var initializer, not an init(). Go builds package-level variables before // it runs init functions, so anything else in this package that is itself a // var built out of this table — equity.go's deck52 is — would otherwise be // built out of an empty one. It was, briefly: every card came out identical, // every showdown tied, and every bot believed it held exactly 50% equity. pokerOf = func() (t [4][14]poker.Card) { for s := cards.Spades; s <= cards.Clubs; s++ { for r := cards.Ace; r <= cards.King; r++ { t[s][r] = poker.NewCard(pokerRanks[r] + pokerSuits[s]) } } return t }() ) // toPoker converts one card for the evaluator. func toPoker(c cards.Card) poker.Card { return pokerOf[c.Suit][c.Rank] } func toPokerAll(cs []cards.Card) []poker.Card { out := make([]poker.Card, len(cs)) for i, c := range cs { out[i] = toPoker(c) } return out } // rankOf evaluates a seat's best five from its hole cards and the board. Lower // is better — 1 is a royal flush — which is the evaluator's convention and not // worth inverting, since nothing outside this file ever sees the number. func rankOf(hole [2]cards.Card, board []cards.Card) (int32, string) { seven := make([]poker.Card, 0, 7) seven = append(seven, toPoker(hole[0]), toPoker(hole[1])) seven = append(seven, toPokerAll(board)...) r := poker.Evaluate(seven) return r, strings.ToLower(poker.RankString(r)) } // ---- showdown ------------------------------------------------------------- type ranked struct { seat int rank int32 desc string } // showdown turns the cards over, splits the pots, and pays. Every player still // in the hand shows, in the order the felt should turn them over: best hand // first, so the winner is the first card face the player sees. func (s *State) showdown(evs *[]Event) { s.collect() s.Street = Showdown // Cut the side pots, if nobody has cut them yet. // // runout() does this, but runout only happens when the betting stops because // there is nobody left able to bet. A hand can reach a showdown with an all-in // player in it and the betting having finished perfectly normally: a short stack // shoves, and two players who both have chips behind keep betting past them, // street after street, all the way to the river. Nothing has been cut, and the // short stack is sitting in a single pot marked eligible for all of it. // // Which means they can win every chip the deep players put in *after* they were // already all-in — money they could never have lost. All-in for 100 against two // players who each put in 500, and the best hand takes 1,100 instead of the 300 // they were playing for. The chips still balance, so conservation says nothing; // they just go to the wrong seat. if len(s.Side) == 0 && s.anyAllIn() { s.sidePots() } // Say so. The last street's bets are still sitting in front of the seats that // made them, as far as the felt knows, and nothing else in the script is going // to tell it they have been swept in. *evs = append(*evs, Event{Kind: "pot", Seat: -1, Amount: s.Total()}) var live []ranked for i := range s.Seats { p := &s.Seats[i] if p.State == Folded || p.State == Out { continue } r, desc := rankOf(p.Hole, s.Community) live = append(live, ranked{seat: i, rank: r, desc: desc}) } sort.Slice(live, func(i, j int) bool { return live[i].rank < live[j].rank }) for _, e := range live { *evs = append(*evs, Event{ Kind: "show", Seat: e.seat, Cards: []cards.Card{s.Seats[e.seat].Hole[0], s.Seats[e.seat].Hole[1]}, Text: e.desc, }) } pots := s.Side if len(pots) == 0 { all := make([]int, 0, len(live)) for _, e := range live { all = append(all, e.seat) } pots = []Pot{{Amount: s.Pot, Eligible: all}} s.Pot = 0 } s.Side = nil for _, pot := range pots { s.payPot(pot, live, evs) } s.endHand(evs) } // payPot rakes a pot and splits it between the best eligible hands. // // The rake comes out of the pot before it is split, which is what a cardroom // does and is also the only thing consistent with the rest of this casino: a // player pays it out of a pot they *win*, never out of a bet they lose. A hand // that dies before the flop is not raked at all — no flop, no drop — so folding // your blind round after round costs you exactly the blinds and no fee. func (s *State) payPot(pot Pot, live []ranked, evs *[]Event) { if pot.Amount <= 0 { return } eligible := make(map[int]bool, len(pot.Eligible)) for _, seat := range pot.Eligible { eligible[seat] = true } var winners []ranked best := int32(0) for _, e := range live { if !eligible[e.seat] { continue } if len(winners) == 0 || e.rank < best { best, winners = e.rank, []ranked{e} } else if e.rank == best { winners = append(winners, e) } } if len(winners) == 0 { return } amount := pot.Amount if s.Flopped { rake := int64(math.Floor(float64(amount) * s.Tier.RakePct)) if cap := s.Tier.BB * rakeCapBB; rake > cap { rake = cap } if rake > 0 { amount -= rake s.Rake += rake // every chip of it, so the table still balances // But only the part that came out of *your* winnings is money the house // actually made, and it is the only part the felt should quote you. The // bots' chips are not real — the only real money at this table is yours — // so raking a pot a bot won costs you nothing, and a counter that climbed // while you folded would be telling you it had. for _, w := range winners { if w.seat == You { s.Paid += rake / int64(len(winners)) } } *evs = append(*evs, Event{Kind: "rake", Seat: -1, Amount: rake}) } } share := amount / int64(len(winners)) odd := amount % int64(len(winners)) // the odd chip goes to the first seat left of the button for i, w := range winners { won := share if i == 0 { won += odd } s.Seats[w.seat].Stack += won s.Seats[w.seat].Won += won *evs = append(*evs, Event{Kind: "win", Seat: w.seat, Amount: won, Text: w.desc}) } } // takeit ends a hand nobody contested: everyone else folded, so the last player // standing takes the pot without showing. Their own uncalled bet comes back // first — it was never called, so it was never really in the pot. func (s *State) takeit(evs *[]Event) { s.uncalled(evs) s.collect() *evs = append(*evs, Event{Kind: "pot", Seat: -1, Amount: s.Total()}) winner := -1 for i := range s.Seats { if s.Seats[i].State != Folded && s.Seats[i].State != Out { winner = i break } } if winner < 0 { s.endHand(evs) return } // There are never side pots here: they are only cut once the betting is over // because everybody is all-in, and a table where everybody is all-in is a table // where nobody is left to fold. pot := Pot{Amount: s.Pot, Eligible: []int{winner}} s.Pot = 0 s.payPot(pot, []ranked{{seat: winner, rank: 0}}, evs) s.endHand(evs) } // uncalled returns the unmatched top of a bet. If you shove 500 into a player // with 200 behind, 300 of that was never contested and comes straight back. // // It must run *before* the bets are swept into the pot, and the matched level it // measures against counts the players who folded. Their chips are in the pot — // they paid to see the bet and then gave up — so the money they put in is money // that called. Miss that and a bet folded to on the river comes back whole, // including the part that was called on the flop, which mints chips out of air. // // The rake is the other reason this matters at all. When everybody folds, the // winner takes the pot back either way and the arithmetic looks the same — but a // pot with an uncalled bet still in it is a pot the house rakes, and it would be // raking the player on their own money that nobody ever contested. func (s *State) uncalled(evs *[]Event) { top, topSeat := int64(-1), -1 for i := range s.Seats { p := &s.Seats[i] if p.State == Folded || p.State == Out { continue } if p.Total > top { top, topSeat = p.Total, i } } if topSeat < 0 { return } var matched int64 // the most anybody else put in, whether or not they're still in for i := range s.Seats { if i == topSeat || s.Seats[i].State == Out { continue } if s.Seats[i].Total > matched { matched = s.Seats[i].Total } } excess := top - matched p := &s.Seats[topSeat] if excess <= 0 || excess > p.Bet { // An uncalled bet is always part of the street it was made on, so it cannot // be bigger than what that seat has in front of them right now. return } p.Stack += excess p.Total -= excess p.Bet -= excess if p.State == AllIn && p.Stack > 0 { p.State = Active // they were never really all-in against anybody } *evs = append(*evs, Event{Kind: "uncalled", Seat: topSeat, Amount: excess}) }