// 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")
	ErrSeatTaken     = errors.New("holdem: that seat is taken")
)

// 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
	Paid  int64         `json:"paid"`  // rake lifted from pots this seat has won, all session
	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"}

// SeatConfig is one chair the table opens with. A shared table is seated by the
// runtime: humans in the chairs people have taken, bots in the rest. Solo play is
// just the case where exactly one chair is human, which is why there is no longer
// a separate solo constructor and no seat-zero-is-you convention — a table is a
// list of seats, and who is human is a property of each seat, not of its index.
type SeatConfig struct {
	Name string
	Bot  bool
	// Stack is the starting chips: a human's buy-in (already taken off their chip
	// stack by the caller), or a bot's stake from the house.
	Stack int64
}

// New opens a table and seats it. No hand is dealt yet — the table opens on
// PhaseHandOver, the state a table between hands is in, and the first Deal starts
// the first hand.
//
// The engine gives a human's chips back only by the runtime reading their stack
// when they leave; it never credits a chip stack itself. Bot stacks are house
// chips and not real money — the only real money at the table is the humans'.
func New(t Tier, seats []SeatConfig, rakePct float64, seed1, seed2 uint64) (State, []Event, error) {
	if len(seats) < 2 || len(seats) > MaxBots+1 {
		return State{}, nil, ErrTableFull
	}
	t.RakePct = rakePct

	s := State{
		Tier:  t,
		Phase: PhaseHandOver,
		Seed1: seed1,
		Seed2: seed2,
	}
	var evs []Event
	for _, sc := range seats {
		if !sc.Bot && (sc.Stack < t.MinBuy || sc.Stack > t.MaxBuy) {
			return State{}, nil, ErrBadBuyIn
		}
		i := len(s.Seats)
		s.Seats = append(s.Seats, Seat{Name: sc.Name, Bot: sc.Bot, Stack: sc.Stack})
		if !sc.Bot {
			// BoughtIn is the sum of what the humans brought — the audit's idea of the
			// stake. Per-seat border accounting lives in storage (game_seats.staked),
			// not here; the engine only ever moves chips within the table.
			s.BoughtIn += sc.Stack
			evs = append(evs, Event{Kind: "sit", Seat: i, Amount: sc.Stack, Text: t.Name})
		}
	}

	// The button starts at the last seat, so deal() moves it to the first: a full
	// table puts the earliest seat on the button, and heads-up puts it on the small
	// blind, in the action from the first card either way.
	s.Button = len(s.Seats) - 1
	return s, evs, nil
}

// soloSeats builds the seat list for a table of one human and n bots — the shape
// every table had before multiplayer, and the one the solo handler still opens.
// The human is seat zero and takes buyIn; the bots take the table maximum.
// SoloSeats is exported for the handler that still opens a solo table. See New.
func SoloSeats(t Tier, bots int, buyIn int64) []SeatConfig {
	return TableSeats(t, "You", bots, buyIn)
}

// TableSeats builds a table of one named human and n bots. It is what a player
// opening their own table gets: a chair with their name on it, and the rest of
// the felt filled with the house's regulars so the table is never empty. The
// human takes seat zero and their buy-in; each bot takes the table maximum in
// house chips, which are not real money and get rebought when a bot runs dry.
func TableSeats(t Tier, human string, bots int, buyIn int64) []SeatConfig {
	seats := []SeatConfig{{Name: human, Stack: buyIn}}
	for i := 0; i < bots && i < len(botNames); i++ {
		seats = append(seats, SeatConfig{Name: botNames[i], Bot: true, Stack: t.MaxBuy})
	}
	return seats
}

// freeBotName picks a regular not already sitting at the table, so vacating a
// seat never puts two Marjories on the felt. It falls back to a generic name if
// every regular is somehow taken, which a six-max table cannot actually manage.
func (s *State) freeBotName() string {
	used := make(map[string]bool, len(s.Seats))
	for i := range s.Seats {
		used[s.Seats[i].Name] = true
	}
	for _, n := range botNames {
		if !used[n] {
			return n
		}
	}
	return "The House"
}

// Vacate turns a human's chair back into the house's and returns the stack that
// goes home with them. It is how a shared table survives a player getting up: the
// seat keeps its place and its chips — which become house money, rebought like
// any bot's when they run low — so the others play on without a hole in the ring.
//
// The chips left in the seat are not a leak. The only real money at the table is
// in the human seats; the moment a seat is a bot's, its stack is house chips that
// nobody's balance is counting. What the player actually takes home is the
// returned stack, credited by the runtime in the same transaction that saves this
// state — see storage.LeaveTable.
//
// It refuses while a hand is live, because a seat with chips in the pot cannot be
// emptied without stranding them. PhaseHandOver is the only phase Leave was ever
// legal from.
func (s *State) Vacate(seat int) (int64, error) {
	if seat < 0 || seat >= len(s.Seats) {
		return 0, ErrUnknownMove
	}
	if s.Phase == PhaseBetting {
		return 0, ErrHandLive
	}
	p := &s.Seats[seat]
	if p.Bot {
		return 0, ErrUnknownMove
	}
	home := p.Stack
	p.Bot = true
	p.Name = s.freeBotName()
	return home, nil
}

// Occupy seats a human in a chair a bot was keeping warm, with the buy-in they
// brought. It is the join half of Vacate: a player sitting down at somebody
// else's table takes an open seat rather than opening a felt of their own.
//
// Like Vacate it is a between-hands move — you cannot sit into a live hand — and
// the buy-in has to be legal for the table, because the chips are already off the
// player's stack by the time this runs and an illegal seat would strand them.
func (s *State) Occupy(seat int, name string, buyIn int64) error {
	if seat < 0 || seat >= len(s.Seats) {
		return ErrUnknownMove
	}
	if s.Phase == PhaseBetting {
		return ErrHandLive
	}
	if !s.Seats[seat].Bot {
		return ErrSeatTaken
	}
	if buyIn < s.Tier.MinBuy || buyIn > s.Tier.MaxBuy {
		return ErrBadBuyIn
	}
	p := &s.Seats[seat]
	p.Bot = false
	p.Name = name
	p.Stack = buyIn
	p.State = Out // between hands; the next deal brings them in
	s.BoughtIn += buyIn
	return nil
}

// ApplyMove is the whole engine. It plays the acting seat's move, then every bot
// behind them, deals whatever streets that finishes, and stops when the action
// reaches a human — the same one again, or another at the table — or when the
// hand is over.
//
// seat is who is acting. A betting move is legal only from the seat whose turn it
// is; the session moves (Deal, TopUp, Leave) belong to the seat that sent them.
// This is the one place seat identity enters the engine — everything below works
// on seat indices already.
func ApplyMove(s State, seat int, m Move) (State, []Event, error) {
	if seat < 0 || seat >= len(s.Seats) {
		return s, nil, ErrUnknownMove
	}
	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[seat].Stack+m.Amount > s.Tier.MaxBuy {
			return s, nil, ErrBadBuyIn
		}
		s.Seats[seat].Stack += m.Amount
		s.BoughtIn += m.Amount
		evs = append(evs, Event{Kind: "topup", Seat: seat, Amount: m.Amount})

	case Leave:
		if s.Phase != PhaseHandOver {
			return s, nil, ErrHandLive
		}
		// Getting up at a solo table ends the session and pays the stack out; the
		// runtime reads Payout and crosses the border. At a shared table leaving is a
		// storage operation (LeaveTable swaps the seat for a bot in the settle tx),
		// and this branch is not the path taken — see the handler.
		s.Phase = PhaseDone
		s.Payout = s.Seats[seat].Stack
		evs = append(evs, Event{Kind: "leave", Seat: seat, Amount: s.Payout})

	case Fold, Check, Call, Raise, Shove:
		if s.Phase != PhaseBetting {
			return s, nil, ErrNoHand
		}
		if s.ToAct != seat {
			return s, nil, ErrNotYourTurn
		}
		if err := s.act(seat, m, &evs); err != nil {
			return s, nil, err // nothing happened; the caller keeps the old state
		}
		s.ToAct = s.nextCanAct(seat)
		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.Seats[s.ToAct].Bot || !bots {
			return // a decision a human has to make, or the trainer wants to
		}

		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
		}
	}
	// A hole event per dealt seat, each carrying that seat's cards. The engine no
	// longer decides who may see what — it cannot, now that a table has more than
	// one human — so it emits every hand and the view layer redacts per viewer,
	// nulling the cards of every seat but the one watching. That per-seat redaction
	// is the whole security boundary once these events fan out over SSE, and it has
	// a test that renders each seat's view and greps for anybody else's cards.
	for i := range s.Seats {
		if s.Seats[i].State == Out {
			continue
		}
		*evs = append(*evs, Event{Kind: "hole", Seat: i,
			Cards: []cards.Card{s.Seats[i].Hole[0], s.Seats[i].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})

	// Busting is the end of a session, not of a hand. At a solo table there is
	// nothing to deal the one human and nothing to give back, so the table closes
	// and they sit down again — a buy-in, and a buy-in is a decision worth making
	// on purpose. At a shared table one human busting is not everyone's business:
	// their seat simply goes Out and the runtime offers the rebuy while the table
	// plays on. So the whole-session bust only fires when there is a single human.
	humans := s.humanSeats()
	if len(humans) == 1 && s.Seats[humans[0]].Stack <= 0 {
		s.Phase = PhaseDone
		s.Payout = 0
		*evs = append(*evs, Event{Kind: "bust", Seat: humans[0]})
	}
}

// humanSeats lists the seats a person is sitting in. Bots are the house; the only
// real money at the table is in these.
func (s State) humanSeats() []int {
	var out []int
	for i := range s.Seats {
		if !s.Seats[i].Bot {
			out = append(out, i)
		}
	}
	return out
}

// ---- 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
}

// nextDealt is the next seat holding cards this hand, folded or not. Where
// nextIn asks "who is still in the betting", this asks "who was dealt in", and
// the pair is easy to confuse: fold three seats and nextIn walks straight past
// them, so anything counting seats round the table lands somewhere different
// depending on how the hand has gone. Position needs the fixed one — where you
// sit is decided when the button moves and does not change because somebody
// mucked.
func (s State) nextDealt(from int) int {
	n := len(s.Seats)
	for i := 1; i <= n; i++ {
		next := (from + i) % n
		if s.Seats[next].State != Out {
			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
}

// anyAllIn reports whether anybody is in the hand with no chips left, which is
// the only thing that makes side pots necessary.
func (s State) anyAllIn() bool {
	for i := range s.Seats {
		if s.Seats[i].State == AllIn {
			return true
		}
	}
	return false
}

// 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.nextDealt(s.Button)
	bb := s.nextDealt(sb)
	switch seat {
	case sb:
		return "SB"
	case bb:
		return "BB"
	}

	utg := s.nextDealt(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.nextDealt(cur)
		dist++
		if cur == seat {
			break
		}
	}
	if remaining := n - 4; remaining > 0 && dist >= remaining {
		return "CO"
	}
	return "MP"
}
