Files
Pete/internal/games/holdem/holdem.go
prosolis 903c5accdb games: the rake you pay, and the rake the table lifts
They are different numbers and the felt was quoting the wrong one. Every raked
pot has chips lifted off it, whoever wins — that has to stay true or the table
stops balancing. But the bots' chips are not real, so a pot a bot wins costs
you nothing, and the counter under your stack was climbing anyway while you sat
there folding.

Rake is now every chip off the table (so the chips conserve) and Paid is the
part that came out of a pot you won, which is the only part that is money and
the only part worth telling you about. A chop costs you half of it. The audit
log takes Paid too: the house's income is what it made off the player, not what
it lifted off a bot.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
2026-07-14 09:11:14 -07:00

859 lines
26 KiB
Go

// Package holdem is a pure Texas Hold'em engine, played for chips against bots.
//
// Same seam as every other table in the casino: ApplyMove(state, move) (state,
// events, error), where an error means the move was illegal and nothing else.
// No HTTP, no timers, no sockets. The state is a plain value, so a hand survives
// a redeploy and replays from its seed.
//
// Three things make hold'em different from the five tables already on the felt.
//
// It is a cash game, not a stake. Every other game here takes a bet, plays once
// and pays a multiple. Poker isn't that: you buy chips onto the table, you play
// as many hands as you like, and you leave with whatever is in front of you. So
// the session is the unit, not the hand — the live row lives across hands, the
// buy-in is the only chip movement at the start, and the stack going home is the
// only one at the end. In between the money is entirely inside this engine.
//
// The bots move inside ApplyMove, as they do in UNO. One call plays the player's
// action and every bot action behind it, deals whatever streets that completes,
// and hands the lot back as a script of events. Poker is where you would reach
// for a socket, and this is what not reaching for one costs.
//
// The bots are the trained ones. gogobee spent a long time running CFR against
// this game and the policy it converged on is the best asset in either repo; it
// is embedded here whole (see cfr.go). What that means for the player: the house
// edge at this table is not a rule, it is an opponent. There is no 3:2 and no
// multiple. If you beat the bots you win, and the only thing the house takes is
// the rake on the pots you win.
package holdem
import (
"errors"
"math/rand/v2"
"pete/internal/games/cards"
)
// Errors an illegal move can produce. An error means nothing happened.
var (
ErrOver = errors.New("holdem: you've left the table")
ErrNotYourTurn = errors.New("holdem: it isn't your turn")
ErrHandLive = errors.New("holdem: finish the hand first")
ErrNoHand = errors.New("holdem: no hand in progress")
ErrCantCheck = errors.New("holdem: there's a bet to you")
ErrNothingToCall = errors.New("holdem: nothing to call")
ErrTooSmall = errors.New("holdem: that's under the minimum raise")
ErrTooBig = errors.New("holdem: you don't have that many chips")
ErrNoChips = errors.New("holdem: you have no chips left")
ErrUnknownMove = errors.New("holdem: unknown move")
ErrUnknownTier = errors.New("holdem: no such table")
ErrBadBuyIn = errors.New("holdem: that isn't a legal buy-in")
ErrTableFull = errors.New("holdem: too many seats")
)
// You are always seat zero. The bots are the seats after you.
const You = 0
// rakeCapBB caps the rake on any one pot at three big blinds, which is what a
// cardroom does. Without a cap, five percent of a big pot is a lot of money to
// take off a player for winning it.
const rakeCapBB = 3
// Street is how far the board has come.
type Street uint8
const (
PreFlop Street = iota
Flop
Turn
River
Showdown
)
var streetNames = [5]string{"preflop", "flop", "turn", "river", "showdown"}
func (s Street) String() string {
if int(s) >= len(streetNames) {
return "?"
}
return streetNames[s]
}
// SeatState is where a player stands in the hand being played.
type SeatState uint8
const (
Active SeatState = iota // still has chips and a say
Folded // out of this hand
AllIn // in the hand, but has nothing left to bet
Out // not dealt in at all
)
// Seat is one player at the table — you, or one of the bots.
//
// Hole is on the server and stays there. The view layer sends your two cards to
// you and sends nobody else's to anybody, right up until a showdown turns them
// over. A bot's cards are most of the information in this game; a browser that
// held them would make counting cards a matter of reading the network tab.
type Seat struct {
Name string `json:"name"`
Bot bool `json:"bot"`
Stack int64 `json:"stack"`
Hole [2]cards.Card `json:"hole"`
Bet int64 `json:"bet"` // put in on this street
Total int64 `json:"total"` // put in across this hand
Won int64 `json:"won"` // taken out of the pot this hand
State SeatState `json:"state"`
Acted bool `json:"acted"` // has chosen to do something this street
}
// Pot is a pot and the seats that may win it. A hand with no all-in has exactly
// one; every all-in at a distinct level adds another.
type Pot struct {
Amount int64 `json:"amount"`
Eligible []int `json:"eligible"`
}
// Tier is a table you can sit at. The dial is the stakes, and the stakes are
// what make a chip mean something: at 25/50 a careless call costs more than a
// whole session at 1/2.
//
// The buy-in range is the standard 20 to 100 big blinds. Sitting down short is
// a real strategy (fewer decisions, less to lose) and sitting down deep is the
// other one, so the range is a choice and not a formality.
type Tier struct {
Slug string `json:"slug"`
Name string `json:"name"`
SB int64 `json:"sb"`
BB int64 `json:"bb"`
MinBuy int64 `json:"min_buy"`
MaxBuy int64 `json:"max_buy"`
RakePct float64 `json:"rake_pct"`
Blurb string `json:"blurb"`
}
// Tiers are the three tables. The rake is the casino's five percent everywhere,
// capped at three big blinds a pot, and taken only from a pot that saw a flop.
//
// RakePct is a *fraction*, 0.05, because that is what it is everywhere else in
// the casino — blackjack's DefaultRules says 0.05 and New() takes its word for it.
// It was 5 here for an afternoon, meaning percent, and since New overwrites the
// tier's value with the one it is handed, every rake worked out to five percent of
// a hundredth of the pot, which integer division rounded to nothing. The house took
// nothing at all and no test noticed, because every test set the tier up by hand.
var Tiers = []Tier{
{Slug: "micro", Name: "The Kitchen Table", SB: 1, BB: 2, MinBuy: 40, MaxBuy: 200, RakePct: 0.05,
Blurb: "1/2 blinds. Cheap enough to learn what the bots do to you."},
{Slug: "low", Name: "The Back Room", SB: 5, BB: 10, MinBuy: 200, MaxBuy: 1000, RakePct: 0.05,
Blurb: "5/10. A bluff here costs real chips, which is the only reason a bluff works."},
{Slug: "high", Name: "The High Roller", SB: 25, BB: 50, MinBuy: 1000, MaxBuy: 5000, RakePct: 0.05,
Blurb: "25/50. Three streets of this and you know whether you can play."},
}
// TierBySlug finds a table by the name the browser sent.
func TierBySlug(slug string) (Tier, error) {
for _, t := range Tiers {
if t.Slug == slug {
return t, nil
}
}
return Tier{}, ErrUnknownTier
}
// MaxBots is five, which with you makes a six-handed table — the size most
// online poker is actually played at, and as many opponents as the felt can
// show without the cards getting too small to read.
const MaxBots = 5
// Phase is what the table is waiting for.
type Phase string
const (
PhaseBetting Phase = "betting" // a hand is live and it's your turn
PhaseHandOver Phase = "handover" // the hand is paid; deal, top up, or leave
PhaseDone Phase = "done" // you're up from the table; the stack goes home
)
// State is the whole table. It never leaves the server: the deck is in here,
// and so is every bot's hand.
type State struct {
Tier Tier `json:"tier"`
Seats []Seat `json:"seats"`
Button int `json:"button"`
HandNo int `json:"hand_no"`
Deck cards.Deck `json:"deck"`
Community []cards.Card `json:"community"`
Street Street `json:"street"`
Flopped bool `json:"flopped"` // this hand saw a flop, so its pot is rakeable
Pot int64 `json:"pot"`
Side []Pot `json:"side,omitempty"`
Bet int64 `json:"bet"` // the bet to match on this street
MinRaise int64 `json:"min_raise"` // the smallest legal raise over it
Aggressor int `json:"aggressor"`
ToAct int `json:"to_act"`
// History is the action so far on this street, as the f/c/r/R/a characters
// the CFR policy was trained to read. It is the bots' memory and nothing else.
History string `json:"history"`
Phase Phase `json:"phase"`
// The money that crosses the border. BoughtIn is every chip staked onto this
// table; Payout is the stack that goes home, and is only set once you're up.
BoughtIn int64 `json:"bought_in"`
Payout int64 `json:"payout"`
// Two rakes, and they are different numbers.
//
// Rake is every chip the house has lifted off this table. It exists so the
// chips balance: a pot that is raked is a pot that pays out less than it holds,
// and the difference has to be somewhere.
//
// Paid is the part of that which came out of a pot *you* won — the only part
// that is real money, and the only part worth quoting you. Rake a pot a bot
// wins and you have paid nothing; a counter that climbed anyway while you sat
// folding would be lying to you.
Rake int64 `json:"rake"`
Paid int64 `json:"paid"`
// The seed rides in the state for the same reason it does in UNO: the bots
// choose and the deck is reshuffled every hand, so the engine needs randomness
// mid-session — and there is no generator alive between two HTTP requests to
// hand it. Each step derives its own from the seed and the step count, so the
// session still replays exactly as it fell.
Seed1 uint64 `json:"seed1"`
Seed2 uint64 `json:"seed2"`
Step uint64 `json:"step"`
}
// Event is one beat of the script the felt plays back. Seat is -1 when the beat
// belongs to the table rather than a player.
type Event struct {
Kind string `json:"kind"`
Seat int `json:"seat"`
Cards []cards.Card `json:"cards,omitempty"`
Amount int64 `json:"amount,omitempty"`
Total int64 `json:"total,omitempty"`
Text string `json:"text,omitempty"`
}
// The moves a player can make. The betting five, plus the three that are about
// the session rather than the hand.
const (
Fold = "fold"
Check = "check"
Call = "call"
Raise = "raise"
Shove = "allin" // the move; AllIn is the seat state it puts you in
Deal = "deal" // next hand
TopUp = "topup" // put more chips on the table, between hands
Leave = "leave" // get up; the stack goes back to your stack
)
// Move is what the browser sends. To is the total a raise raises *to*, and
// Amount is chips added in a top-up.
type Move struct {
Kind string `json:"move"`
To int64 `json:"to,omitempty"`
Amount int64 `json:"amount,omitempty"`
}
// botNames are the regulars. Six of them so a full table never has two.
var botNames = []string{"Dice", "Marjorie", "Ox", "Sunny", "Pinch", "The Reverend"}
// New sits you down. The buy-in is chips the caller has already taken off the
// player's stack; this engine only ever gives them back through Leave.
//
// No hand is dealt yet — the table opens on PhaseHandOver, which is the state a
// table between hands is in, and the first Deal move starts the first hand.
func New(t Tier, bots int, buyIn int64, rakePct float64, seed1, seed2 uint64) (State, []Event, error) {
if bots < 1 || bots > MaxBots {
return State{}, nil, ErrTableFull
}
if buyIn < t.MinBuy || buyIn > t.MaxBuy {
return State{}, nil, ErrBadBuyIn
}
t.RakePct = rakePct
s := State{
Tier: t,
Button: 0,
Phase: PhaseHandOver,
BoughtIn: buyIn,
Seed1: seed1,
Seed2: seed2,
}
s.Seats = append(s.Seats, Seat{Name: "You", Stack: buyIn})
for i := 0; i < bots; i++ {
s.Seats = append(s.Seats, Seat{Name: botNames[i], Bot: true, Stack: t.MaxBuy})
}
// The button starts to your right, so the first hand deals you the small blind
// heads-up and the button on a full table — either way you are in the action
// from the first card rather than folding your way in.
s.Button = len(s.Seats) - 1
evs := []Event{{Kind: "sit", Seat: You, Amount: buyIn, Text: t.Name}}
return s, evs, nil
}
// ApplyMove is the whole engine. It plays your move, then every bot behind you,
// deals whatever streets that finishes, and stops either when it is your turn
// again or when the hand is over.
func ApplyMove(s State, m Move) (State, []Event, error) {
if s.Phase == PhaseDone {
return s, nil, ErrOver
}
rng := s.next()
evs := []Event{}
switch m.Kind {
case Deal:
if s.Phase != PhaseHandOver {
return s, nil, ErrHandLive
}
s.deal(&evs, rng)
s.advance(&evs, rng, true)
case TopUp:
if s.Phase != PhaseHandOver {
return s, nil, ErrHandLive
}
if m.Amount <= 0 || s.Seats[You].Stack+m.Amount > s.Tier.MaxBuy {
return s, nil, ErrBadBuyIn
}
s.Seats[You].Stack += m.Amount
s.BoughtIn += m.Amount
evs = append(evs, Event{Kind: "topup", Seat: You, Amount: m.Amount})
case Leave:
if s.Phase != PhaseHandOver {
return s, nil, ErrHandLive
}
s.Phase = PhaseDone
s.Payout = s.Seats[You].Stack
evs = append(evs, Event{Kind: "leave", Seat: You, Amount: s.Payout})
case Fold, Check, Call, Raise, Shove:
if s.Phase != PhaseBetting {
return s, nil, ErrNoHand
}
if s.ToAct != You {
return s, nil, ErrNotYourTurn
}
if err := s.act(You, m, &evs); err != nil {
return s, nil, err // nothing happened; the caller keeps the old state
}
s.ToAct = s.nextCanAct(You)
s.advance(&evs, rng, true)
default:
return s, nil, ErrUnknownMove
}
return s, evs, nil
}
// Step plays a move for whoever is to act — bot or player — and advances only as
// far as the next decision, whoever's it is. Nobody's turn is taken for them.
//
// This is the seam the trainer plays through, and it exists so that the trainer
// is playing *this* game: the same betting rules, the same street completion, the
// same side pots, the same money. The alternative is a second, simplified model
// of poker written for the trainer alone — which is what gogobee had, and it is
// why its policy encoded a game nobody was dealing.
func Step(s State, m Move) (State, []Event, error) {
if s.Phase != PhaseBetting {
return s, nil, ErrNoHand
}
rng := s.next()
evs := []Event{}
seat := s.ToAct
if err := s.act(seat, m, &evs); err != nil {
return s, nil, err
}
s.ToAct = s.nextCanAct(seat)
s.advance(&evs, rng, false)
return s, evs, nil
}
// Open deals one heads-up hand at the given stacks, stopping at the first
// decision. For the trainer: a table with no history and no button rotation.
func Open(t Tier, stack0, stack1 int64, seed1, seed2 uint64) (State, error) {
if stack0 <= 0 || stack1 <= 0 {
return State{}, ErrBadBuyIn
}
s := State{
Tier: t,
Phase: PhaseHandOver,
Seats: []Seat{
{Name: "0", Stack: stack0},
{Name: "1", Stack: stack1, Bot: true},
},
Button: 1, // deal() moves it, so seat 0 takes the button and the small blind
Seed1: seed1,
Seed2: seed2,
}
rng := s.next()
evs := []Event{}
s.deal(&evs, rng)
s.advance(&evs, rng, false)
return s, nil
}
// Clone deep-copies the table. CFR walks a tree of what-ifs, and a shallow copy
// would have every branch writing into the same deck.
func (s State) Clone() State {
out := s
out.Seats = append([]Seat(nil), s.Seats...)
out.Deck = append(cards.Deck(nil), s.Deck...)
out.Community = append([]cards.Card(nil), s.Community...)
out.Side = make([]Pot, len(s.Side))
for i, p := range s.Side {
out.Side[i] = Pot{Amount: p.Amount, Eligible: append([]int(nil), p.Eligible...)}
}
return out
}
// act applies one seat's action, whoever it belongs to.
func (s *State) act(seat int, m Move, evs *[]Event) error {
switch m.Kind {
case Fold:
s.fold(seat, evs)
return nil
case Check:
return s.check(seat, evs)
case Call:
return s.call(seat, evs)
case Raise:
return s.raise(seat, m.To, evs)
case Shove:
return s.allin(seat, evs)
}
return ErrUnknownMove
}
// advance runs the table forward until you have a decision to make, or until
// there is nothing left to decide.
//
// This is the loop the whole design turns on. Every other engine here returns
// after one move because there is nobody else at the table; this one keeps going
// — bot, bot, flop, bot, turn — and only stops when the answer has to come from
// the player. Which is why one HTTP request can be a whole hand: shove all-in
// and the board runs out and the pot is paid inside a single call.
//
// With bots false it stops at every decision instead of playing the bots' for
// them. That is the trainer's way in: it wants to choose both seats' moves.
func (s *State) advance(evs *[]Event, rng *rand.Rand, bots bool) {
for {
// Everyone else folded. Nobody shows; the last one standing takes it.
if s.liveCount() <= 1 {
s.takeit(evs)
return
}
// Nobody left with a decision to make: the rest of the board is a formality,
// so deal it and turn the cards over.
//
// The subtle half is the lone player who still has chips. They only have a
// decision if there is a bet to them — call it or fold. If there isn't, they
// have nobody left to bet *into*, because everyone else is already all-in,
// and poker does not let you put chips in a pot nobody can contest.
switch s.canActCount() {
case 0:
s.runout(evs)
return
case 1:
lone := s.onlyActor()
if s.Owed(lone) == 0 {
s.runout(evs)
return
}
s.ToAct = lone
}
if s.streetDone(s.ToAct) {
if s.Street == River {
s.showdown(evs)
return
}
s.street(evs)
continue
}
if s.ToAct == You || !bots {
return // a decision that isn't ours to make
}
s.botActs(s.ToAct, evs, rng)
s.ToAct = s.nextCanAct(s.ToAct)
}
}
// deal starts a hand: rebuy the broke bots, move the button, shuffle, post the
// blinds, and put two cards in front of everybody.
func (s *State) deal(evs *[]Event, rng *rand.Rand) {
s.HandNo++
for i := range s.Seats {
p := &s.Seats[i]
// A bot that has been ground down to nothing reloads. It has to: a table
// where you have taken everybody's chips is a table with no game left in it,
// and their chips were never real anyway — the only real money at this table
// is yours, and the only thing the house takes is the rake.
if p.Bot && p.Stack < s.Tier.BB {
add := s.Tier.MaxBuy - p.Stack
p.Stack = s.Tier.MaxBuy
*evs = append(*evs, Event{Kind: "rebuy", Seat: i, Amount: add, Total: p.Stack})
}
p.Bet, p.Total, p.Won, p.Acted = 0, 0, 0, false
p.Hole = [2]cards.Card{}
p.State = Active
if p.Stack <= 0 {
p.State = Out
}
}
s.Community = nil
s.Side = nil
s.Pot = 0
s.Street = PreFlop
s.Flopped = false
s.History = ""
s.Phase = PhaseBetting
s.Deck = cards.NewDeck(1)
s.Deck.Shuffle(rng)
s.Button = s.nextIn(s.Button)
*evs = append(*evs, Event{Kind: "hand", Seat: s.Button, Amount: int64(s.HandNo)})
bb := s.blinds(evs)
// Two cards each, one at a time round the table, as they are actually dealt.
for round := 0; round < 2; round++ {
for i := 0; i < len(s.Seats); i++ {
seat := (s.Button + 1 + i) % len(s.Seats)
p := &s.Seats[seat]
if p.State == Out {
continue
}
c, _ := s.Deck.Draw()
p.Hole[round] = c
}
}
// Only your cards go into the script. The bots' are in the state, on this side
// of the wire, and the only thing that ever turns them over is a showdown.
*evs = append(*evs, Event{Kind: "hole", Seat: You,
Cards: []cards.Card{s.Seats[You].Hole[0], s.Seats[You].Hole[1]}})
s.ToAct = s.firstPreFlop(bb)
}
// street burns one and deals the next board card or three.
func (s *State) street(evs *[]Event) {
s.collect()
s.resetBets()
s.Deck.Draw() // the burn card, as printed in the rules and as dealt in a casino
switch s.Street {
case PreFlop:
s.Street = Flop
s.Flopped = true
for i := 0; i < 3; i++ {
c, _ := s.Deck.Draw()
s.Community = append(s.Community, c)
}
case Flop:
s.Street = Turn
c, _ := s.Deck.Draw()
s.Community = append(s.Community, c)
case Turn:
s.Street = River
c, _ := s.Deck.Draw()
s.Community = append(s.Community, c)
}
// Total, not Pot: by the time a board runs out behind an all-in the money has
// already been cut into side pots, and s.Pot is zero.
*evs = append(*evs, Event{Kind: s.Street.String(), Seat: -1,
Cards: s.Community[len(s.Community)-cardsOn(s.Street):], Amount: s.Total()})
s.ToAct = s.firstPostFlop()
s.Aggressor = s.ToAct // nobody has bet yet, so the option ends where it starts
}
func cardsOn(st Street) int {
if st == Flop {
return 3
}
return 1
}
// runout deals the rest of the board with no more betting, because there is no
// longer anybody able to bet. The side pots are built first: once the chips stop
// moving, who can win what is already decided.
func (s *State) runout(evs *[]Event) {
allIn := false
for i := range s.Seats {
if s.Seats[i].State == AllIn {
allIn = true
break
}
}
if allIn {
// A shove nobody could cover comes back first — while it is still a bet in
// front of a seat, and before the pots are cut around it.
s.uncalled(evs)
}
s.collect()
if allIn {
s.sidePots()
}
for s.Street < River {
s.street(evs)
}
s.showdown(evs)
}
// endHand pays out and parks the table between hands.
func (s *State) endHand(evs *[]Event) {
s.Pot = 0
s.Side = nil
s.Phase = PhaseHandOver
*evs = append(*evs, Event{Kind: "end", Seat: -1, Amount: s.Seats[You].Stack})
// Busting is the end of the session, not the end of a hand. There is nothing
// to deal you and nothing to give back, so the table closes and you sit down
// again — which is a buy-in, and a buy-in is a decision worth making on purpose.
if s.Seats[You].Stack <= 0 {
s.Phase = PhaseDone
s.Payout = 0
*evs = append(*evs, Event{Kind: "bust", Seat: You})
}
}
// ---- the small stuff -------------------------------------------------------
// next derives this step's generator and advances the step count.
func (s *State) next() *rand.Rand {
s.Step++
return cards.NewRNG(s.Seed1, s.Seed2^s.Step)
}
// collect sweeps the street's bets into the pot.
func (s *State) collect() {
for i := range s.Seats {
s.Pot += s.Seats[i].Bet
s.Seats[i].Bet = 0
}
}
// resetBets opens a new street: nothing to call, and nobody has spoken.
func (s *State) resetBets() {
for i := range s.Seats {
s.Seats[i].Bet = 0
s.Seats[i].Acted = false
}
s.Bet = 0
s.MinRaise = s.Tier.BB
s.History = ""
}
// inPlay is the pot plus everything bet on this street — what a bot is actually
// deciding against, and what a pot-sized raise is a size of.
func (s State) inPlay() int64 {
total := s.Pot
for i := range s.Seats {
total += s.Seats[i].Bet
}
return total
}
// Pot returns the money on the table, however it is currently sliced.
func (s State) Total() int64 {
total := s.inPlay()
for _, p := range s.Side {
total += p.Amount
}
return total
}
// liveCount is the seats still in the hand, whether or not they can bet.
func (s State) liveCount() int {
n := 0
for i := range s.Seats {
if st := s.Seats[i].State; st == Active || st == AllIn {
n++
}
}
return n
}
// canActCount is the seats that still have chips and a decision.
func (s State) canActCount() int {
n := 0
for i := range s.Seats {
if s.Seats[i].State == Active {
n++
}
}
return n
}
// dealt is the seats in this hand at all.
func (s State) dealt() int {
n := 0
for i := range s.Seats {
if s.Seats[i].State != Out {
n++
}
}
return n
}
// nextIn is the next seat that was dealt in — used to move the button, which
// moves past a seat that is sitting out rather than landing on it.
func (s State) nextIn(from int) int {
n := len(s.Seats)
for i := 1; i <= n; i++ {
next := (from + i) % n
if st := s.Seats[next].State; st == Active || st == AllIn {
return next
}
}
return from
}
// onlyActor is the one seat that can still act. Call it when canActCount is 1.
func (s State) onlyActor() int {
for i := range s.Seats {
if s.Seats[i].State == Active {
return i
}
}
return s.ToAct
}
// canBet reports whether there is anybody left to bet *into*. With one player
// still holding chips and the rest all-in, a raise is chips nobody can call, so
// no raise is offered — to the player or to a bot.
func (s State) canBet() bool { return s.canActCount() > 1 }
// nextCanAct is the next seat with a decision to make.
func (s State) nextCanAct(from int) int {
n := len(s.Seats)
for i := 1; i <= n; i++ {
next := (from + i) % n
if s.Seats[next].State == Active {
return next
}
}
return from
}
// Owed is what the seat must put in to call.
func (s State) Owed(seat int) int64 {
owed := s.Bet - s.Seats[seat].Bet
if owed < 0 {
return 0
}
if owed > s.Seats[seat].Stack {
return s.Seats[seat].Stack
}
return owed
}
// MinRaiseTo is the smallest total a raise may raise to, clamped to a shove when
// the seat cannot cover a full one.
func (s State) MinRaiseTo(seat int) int64 {
to := s.Bet + s.MinRaise
if most := s.Seats[seat].Bet + s.Seats[seat].Stack; to > most {
return most
}
return to
}
// MaxRaiseTo is everything the seat has.
func (s State) MaxRaiseTo(seat int) int64 {
return s.Seats[seat].Bet + s.Seats[seat].Stack
}
// CanRaise reports whether the seat may raise: they need chips behind the call,
// and somebody left to bet into. The felt asks so it never offers a button the
// table would refuse.
func (s State) CanRaise(seat int) bool {
return s.mask(seat)[actRaiseHalf]
}
// InPosition reports whether the seat acts last after the flop, which is the
// only thing about position the trained bots actually know.
//
// The postflop order runs from the seat left of the button all the way round to
// the button itself, so the player in position is simply the last one still in
// the hand — the button, or whoever is nearest to it once the button has folded.
// The policy was trained heads-up, where this is exactly the button; applying it
// to a six-handed table is an approximation, and this is where the approximation
// lives.
func (s State) InPosition(seat int) bool {
last := -1
for i := 1; i <= len(s.Seats); i++ {
at := (s.Button + i) % len(s.Seats)
if st := s.Seats[at].State; st == Active || st == AllIn {
last = at
}
}
return seat == last
}
// Position is the seat's label at this table — BTN, SB, BB, and so on. It is for
// the felt to print. The bots do not use it: see InPosition, and the note on
// infoSet about what happens when you confuse the two.
func (s State) Position(seat int) string {
n := s.dealt()
if n < 2 {
return ""
}
if seat == s.Button {
return "BTN"
}
if n == 2 {
return "BB" // heads-up, the other seat is always the big blind
}
sb := s.nextIn(s.Button)
bb := s.nextIn(sb)
switch seat {
case sb:
return "SB"
case bb:
return "BB"
}
utg := s.nextIn(bb)
if seat == utg {
return "UTG"
}
// Everyone between UTG and the button is somewhere in the middle; the seat
// closest to the button is the cutoff.
dist, cur := 0, utg
for i := 0; i < n; i++ {
cur = s.nextIn(cur)
dist++
if cur == seat {
break
}
}
if remaining := n - 4; remaining > 0 && dist >= remaining {
return "CO"
}
return "MP"
}