package holdem import "sort" // The betting rules. These are the fiddly ones — min-raise, short all-ins that // don't reopen the action, side pots — and they came over from gogobee, where // they had no tests at all. They have some now. // blinds posts the small and the big. Heads-up is the exception every poker // implementation gets wrong once: with two players the button *is* the small // blind and acts first before the flop, and last after it. func (s *State) blinds(evs *[]Event) (bb int) { var sb int if s.dealt() == 2 { sb, bb = s.Button, s.nextIn(s.Button) } else { sb = s.nextIn(s.Button) bb = s.nextIn(sb) } s.post(sb, s.Tier.SB, "small", evs) s.post(bb, s.Tier.BB, "big", evs) s.Bet = s.Tier.BB s.MinRaise = s.Tier.BB s.Aggressor = bb // the big blind has the option to raise their own blind return bb } // post puts a blind up. A player too short to cover it is all-in for what they // have, which is legal and is why the amount is clamped rather than refused. func (s *State) post(seat int, amount int64, which string, evs *[]Event) { p := &s.Seats[seat] if amount > p.Stack { amount = p.Stack } p.Stack -= amount p.Bet = amount p.Total = amount if p.Stack == 0 { p.State = AllIn } *evs = append(*evs, Event{Kind: "blind", Seat: seat, Amount: amount, Text: which}) } // firstPreFlop is under the gun: the seat after the big blind, or the button // itself when the table is heads-up. // // The button only gets it if the button can still act. A short stack can be // all-in on its own blind — post a small blind of 1 with 1 chip left and you are // in the hand with no chips and no say — and handing the action to a seat that // cannot act wedges the table. func (s *State) firstPreFlop(bb int) int { if s.dealt() != 2 { return s.nextCanAct(bb) } if s.Seats[s.Button].State == Active { return s.Button } return s.nextCanAct(s.Button) } // firstPostFlop is the first seat left of the button, on every street after the // flop. The button acts last from here on, which is the whole point of it. func (s *State) firstPostFlop() int { return s.nextCanAct(s.Button) } // ---- the five things a seat can do ---------------------------------------- func (s *State) fold(seat int, evs *[]Event) { p := &s.Seats[seat] p.State = Folded p.Acted = true s.History += "f" *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "fold"}) } func (s *State) check(seat int, evs *[]Event) error { p := &s.Seats[seat] if p.Bet < s.Bet { return ErrCantCheck } p.Acted = true s.History += "c" *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "check"}) return nil } func (s *State) call(seat int, evs *[]Event) error { p := &s.Seats[seat] owed := s.Bet - p.Bet if owed <= 0 { return ErrNothingToCall } if owed > p.Stack { owed = p.Stack // a call for less than the bet is a call all-in } p.Stack -= owed p.Bet += owed p.Total += owed p.Acted = true text := "call" if p.Stack == 0 { p.State = AllIn text = "allin" s.History += "a" } else { s.History += "c" } *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: text, Amount: owed, Total: p.Bet}) return nil } // raise raises *to* a total, not *by* an amount. Every poker interface in the // world means the total, and a browser that means the other thing bets wrong. func (s *State) raise(seat int, to int64, evs *[]Event) error { p := &s.Seats[seat] most := p.Bet + p.Stack if to > most { return ErrTooBig } if to < s.Bet+s.MinRaise && to < most { return ErrTooSmall // only a shove may be smaller than a legal raise } added := to - p.Bet over := to - s.Bet p.Stack -= added p.Bet = to p.Total += added p.Acted = true if over > 0 { s.MinRaise = over } s.Bet = to s.Aggressor = seat text := "raise" if p.Stack == 0 { p.State = AllIn text = "allin" s.History += "a" } else { // The policy was trained against a tree with two raise sizes in it, so the // history it reads has to say which one this was: R for a pot-sized raise // or bigger, r for anything smaller. if pot := s.inPlay(); pot > 0 && float64(over) >= float64(pot)*0.75 { s.History += "R" } else { s.History += "r" } } *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: text, Amount: added, Total: to}) return nil } // allin pushes the lot. // // A short all-in does not reopen the betting. If a player shoves for less than // a full raise over the current bet, players who have already acted may call it // but may not raise again — otherwise a tiny stack could be used to reopen the // action for a partner, which is the oldest collusion trick there is. func (s *State) allin(seat int, evs *[]Event) error { p := &s.Seats[seat] if p.Stack <= 0 { return ErrNoChips } added := p.Stack to := p.Bet + added p.Stack = 0 p.Bet = to p.Total += added p.State = AllIn p.Acted = true if to > s.Bet { if over := to - s.Bet; over >= s.MinRaise { s.MinRaise = over s.Aggressor = seat } s.Bet = to } s.History += "a" *evs = append(*evs, Event{Kind: "action", Seat: seat, Text: "allin", Amount: added, Total: to}) return nil } // ---- when is a street over ------------------------------------------------ // streetDone reports whether the betting round is finished, given the seat the // action would pass to next. // // The "has acted" check is the load-bearing half. The big blind has money in // front of them without having chosen to put it there, so a round where // everybody merely limps in has all bets matched while the blind has never had // a say. Without this, they never get their option. func (s *State) streetDone(next int) bool { if s.canActCount() == 0 { return true } for i := range s.Seats { p := &s.Seats[i] if p.State != Active { continue } if p.Bet != s.Bet || !p.Acted { return false } } // The last aggressor being all-in means the action can't get back to them: // everyone left has matched the bet above, so there is nothing more to do. if s.Seats[s.Aggressor].State == AllIn { return true } return next == s.Aggressor } // ---- side pots ------------------------------------------------------------- // sidePots slices the pot into layers, one per distinct all-in level. A player // can only win the part of the pot they could have lost, so each layer is // contested by exactly the players who paid into it. // // Folded players' chips stay in the pot — they paid for the right to fold — but // they are eligible for nothing. func (s *State) sidePots() { s.collect() var levels []int64 for i := range s.Seats { p := &s.Seats[i] if p.State == Folded || p.State == Out || p.Total == 0 { continue } levels = append(levels, p.Total) } if len(levels) == 0 { return } sort.Slice(levels, func(i, j int) bool { return levels[i] < levels[j] }) var pots []Pot var prev int64 for _, level := range levels { if level <= prev { continue } var amount int64 var eligible []int for i := range s.Seats { p := &s.Seats[i] paid := p.Total - prev if paid > level-prev { paid = level - prev } if paid > 0 { amount += paid // folded money counts toward the pot... } if p.State != Folded && p.State != Out && p.Total >= level { eligible = append(eligible, i) // ...but wins no part of it } } if amount > 0 { pots = append(pots, Pot{Amount: amount, Eligible: eligible}) } prev = level } if len(pots) > 0 { s.Side = pots s.Pot = 0 } }