package main import ( "errors" "math/rand" "sort" ) // Техасский Холдем (No-Limit) — турнирный формат с выбыванием: игра // идёт до тех пор, пока не останется один игрок с фишками. Блайнды // фиксированы (не растут по ходу турнира) — это сознательное // упрощение ради управляемого объёма игры. // // ВАЖНОЕ УПРОЩЕНИЕ ПРАВИЛ: короткий олл-ин рейзом МЕНЬШЕ полного // размера предыдущего рейза, по строгим правилам, не должен заново // открывать торги игрокам, которые уже походили в этом раунде (они // обязаны лишь доколлировать либо сбросить, но не имеют права // повторно рейзить в ответ именно на такой недостаточный рейз). Эта // игра такого разграничения не делает — любой рейз (в том числе // короткий олл-ин) заново даёт всем активным игрокам право хода, // включая повторный рейз. Расхождение с турнирными правилами // возможно только в редких ситуациях с несколькими короткостёчными // олл-инами подряд. // PokerPhase — фаза текущей раздачи. type PokerPhase int const ( PokerPhasePreflop PokerPhase = iota PokerPhaseFlop PokerPhaseTurn PokerPhaseRiver PokerPhaseShowdown // раздача завершена, идёт показ карт/распределение банка PokerPhaseTournamentOver // остался один игрок с фишками — турнир окончен ) // PokerPlayer — один участник партии. type PokerPlayer struct { Name string Stack int // фишки, НЕ поставленные в текущий банк Bot *PokerBot HoleCards [2]Card Folded bool AllIn bool Eliminated bool // стек обнулился — выбыл из турнира насовсем CurrentBet int // ставка ТЕКУЩЕГО раунда торгов (preflop/flop/turn/river) TotalContributed int // сколько всего внесено в банк за ВСЮ раздачу (для сайд-потов) } // PokerSidePot — один "слой" банка с собственным списком, кто может // на него претендовать (см. computeSidePots). type PokerSidePot struct { Amount int EligiblePlayers []int } // PokerHandResult — итог завершённой раздачи (для отображения на // экране шоудауна/сброса). type PokerHandResult struct { Pots []PokerSidePot Winners map[int]int // индекс игрока -> сколько выиграл всего по всем банкам WentToShowdown bool HandValues map[int]PokerHandValue // только для тех, кто дошёл до шоудауна } // PokerGameState — состояние всей партии (турнира). type PokerGameState struct { Players []*PokerPlayer Deck []Card Board []Card DealerIdx int SmallBlind int BigBlind int Phase PokerPhase CurrentPlayerIdx int actedThisRound map[int]bool // кто уже отреагировал на ТЕКУЩИЙ уровень ставки в этом раунде торгов lastAggressorIdx int // кто последний раз ставил/рейзил в этом раунде (-1, если раунд ещё без ставок) Result *PokerHandResult } var ( ErrPokerWrongPhase = errors.New("недопустимое действие для текущей фазы") ErrPokerNotYourTurn = errors.New("сейчас не ваш ход") ErrPokerCannotCheck = errors.New("нельзя пасовать (чек) — на столе уже есть ставка, которую нужно уравнять или сбросить") ErrPokerBetTooLow = errors.New("ставка меньше минимально допустимой") ErrPokerNotEnoughChips = errors.New("недостаточно фишек для такой ставки") ) // NewPokerGame создаёт новый турнир: заданное число игроков (первый // — человек), фиксированный стартовый стек и блайнды. func NewPokerGame(numPlayers, startingStack, smallBlind, bigBlind int, rnd *rand.Rand) *PokerGameState { g := &PokerGameState{SmallBlind: smallBlind, BigBlind: bigBlind, DealerIdx: -1} for i := 0; i < numPlayers; i++ { g.Players = append(g.Players, &PokerPlayer{Stack: startingStack}) } g.startNewHand(rnd) return g } // activeTournamentPlayers — индексы игроков, ещё не выбывших из // турнира. func (g *PokerGameState) activeTournamentPlayers() []int { var out []int for i, p := range g.Players { if !p.Eliminated { out = append(out, i) } } return out } // nextActiveTournamentSeat находит следующее по кругу от from место // среди ещё не выбывших из турнира игроков. func (g *PokerGameState) nextActiveTournamentSeat(from int) int { n := len(g.Players) for i := 1; i <= n; i++ { idx := (from + i) % n if !g.Players[idx].Eliminated { return idx } } return from } // nextToAct находит следующего игрока по кругу от from, который ещё // может действовать в текущем раунде торгов (не сбросил, не в // олл-ине, не выбыл). func (g *PokerGameState) nextToAct(from int) int { n := len(g.Players) for i := 1; i <= n; i++ { idx := (from + i) % n p := g.Players[idx] if !p.Folded && !p.AllIn && !p.Eliminated { return idx } } return from } func (g *PokerGameState) countActingPlayers() int { n := 0 for _, p := range g.Players { if !p.Folded && !p.AllIn && !p.Eliminated { n++ } } return n } func (g *PokerGameState) countInHand() int { n := 0 for _, p := range g.Players { if !p.Folded && !p.Eliminated { n++ } } return n } // startNewHand начинает новую раздачу: сдвигает дилера, раздаёт // карты, ставит блайнды, определяет первый ход. func (g *PokerGameState) startNewHand(rnd *rand.Rand) { active := g.activeTournamentPlayers() if len(active) <= 1 { g.Phase = PokerPhaseTournamentOver return } for _, p := range g.Players { p.HoleCards = [2]Card{} p.Folded = p.Eliminated p.AllIn = false p.CurrentBet = 0 p.TotalContributed = 0 } g.Result = nil g.Board = nil g.Deck = shuffledDeck(rnd) if g.DealerIdx < 0 { g.DealerIdx = active[0] } else { g.DealerIdx = g.nextActiveTournamentSeat(g.DealerIdx) } // В игре один на один (heads-up) дилер ставит МАЛЫЙ блайнд сам // (а не "следующий" игрок) — общее правило "SB = следующий после // дилера" здесь даёт неверный результат, так как с двумя // игроками "следующий после дилера" и "следующий после этого // игрока" зацикливаются друг на друга, меняя роли блайндов местами. var sbIdx, bbIdx int if len(active) == 2 { sbIdx = g.DealerIdx bbIdx = g.nextActiveTournamentSeat(g.DealerIdx) } else { sbIdx = g.nextActiveTournamentSeat(g.DealerIdx) bbIdx = g.nextActiveTournamentSeat(sbIdx) } g.postBlind(sbIdx, g.SmallBlind) g.postBlind(bbIdx, g.BigBlind) for i := 0; i < 2; i++ { for _, idx := range g.activeTournamentPlayers() { g.Players[idx].HoleCards[i] = g.drawCard() } } g.Phase = PokerPhasePreflop g.lastAggressorIdx = bbIdx g.actedThisRound = map[int]bool{} g.CurrentPlayerIdx = g.nextToAct(bbIdx) } func (g *PokerGameState) postBlind(idx, amount int) { p := g.Players[idx] if amount >= p.Stack { amount = p.Stack p.AllIn = true } p.Stack -= amount p.CurrentBet = amount p.TotalContributed = amount } func (g *PokerGameState) drawCard() Card { card := g.Deck[0] g.Deck = g.Deck[1:] return card } func shuffledDeck(rnd *rand.Rand) []Card { deck := make([]Card, 0, 52) for s := Clubs; s <= Spades; s++ { for r := Ace; r <= King; r++ { deck = append(deck, Card{Suit: s, Rank: r}) } } rnd.Shuffle(len(deck), func(i, j int) { deck[i], deck[j] = deck[j], deck[i] }) return deck } // highestBet — самая большая ставка ЭТОГО раунда торгов среди всех // игроков. func (g *PokerGameState) highestBet() int { max := 0 for _, p := range g.Players { if p.CurrentBet > max { max = p.CurrentBet } } return max } // Fold — игрок сбрасывает карты. func (g *PokerGameState) Fold() error { if err := g.checkCanAct(); err != nil { return err } g.Players[g.CurrentPlayerIdx].Folded = true g.advanceAfterAction() return nil } // Check — пропустить ход без ставки (легально только если нечего уравнивать). func (g *PokerGameState) Check() error { if err := g.checkCanAct(); err != nil { return err } p := g.Players[g.CurrentPlayerIdx] if p.CurrentBet != g.highestBet() { return ErrPokerCannotCheck } g.advanceAfterAction() return nil } // Call — уравнять текущую наибольшую ставку (либо пойти в олл-ин, // если фишек не хватает на полное уравнивание). func (g *PokerGameState) Call() error { if err := g.checkCanAct(); err != nil { return err } p := g.Players[g.CurrentPlayerIdx] need := g.highestBet() - p.CurrentBet if need <= 0 { return g.Check() } if need >= p.Stack { need = p.Stack p.AllIn = true } p.Stack -= need p.CurrentBet += need p.TotalContributed += need g.advanceAfterAction() return nil } // Bet ставит (или рейзит) так, чтобы СУММАРНАЯ ставка игрока в этом // раунде торгов стала равна totalAmount. totalAmount должен быть не // меньше текущей наибольшей ставки плюс большой блайнд (минимальный // рейз в этой упрощённой реализации — см. пояснение в начале файла). func (g *PokerGameState) Bet(totalAmount int) error { if err := g.checkCanAct(); err != nil { return err } p := g.Players[g.CurrentPlayerIdx] minTotal := g.highestBet() + g.BigBlind if g.highestBet() == 0 { minTotal = g.BigBlind } maxTotal := p.CurrentBet + p.Stack if totalAmount > maxTotal { return ErrPokerNotEnoughChips } if totalAmount < minTotal && totalAmount < maxTotal { return ErrPokerBetTooLow } delta := totalAmount - p.CurrentBet p.Stack -= delta p.CurrentBet = totalAmount p.TotalContributed += delta if p.Stack == 0 { p.AllIn = true } g.lastAggressorIdx = g.CurrentPlayerIdx g.actedThisRound = map[int]bool{g.CurrentPlayerIdx: true} g.advanceAfterAction() return nil } // AllIn ставит весь оставшийся стек. func (g *PokerGameState) AllIn() error { if err := g.checkCanAct(); err != nil { return err } p := g.Players[g.CurrentPlayerIdx] return g.Bet(p.CurrentBet + p.Stack) } func (g *PokerGameState) checkCanAct() error { if g.Phase == PokerPhaseShowdown || g.Phase == PokerPhaseTournamentOver { return ErrPokerWrongPhase } p := g.Players[g.CurrentPlayerIdx] if p.Folded || p.AllIn || p.Eliminated { return ErrPokerNotYourTurn } return nil } // advanceAfterAction обрабатывает переход хода после действия // текущего игрока: завершение раздачи (остался один не сбросивший), // переход к следующей фазе (когда раунд торгов завершён), либо // передачу хода следующему активному игроку. func (g *PokerGameState) advanceAfterAction() { if g.countInHand() == 1 { g.resolveHandByFold() return } if g.actedThisRound == nil { g.actedThisRound = map[int]bool{} } g.actedThisRound[g.CurrentPlayerIdx] = true allActed := true for i, p := range g.Players { if !p.Folded && !p.AllIn && !p.Eliminated && !g.actedThisRound[i] { allActed = false break } } if allActed { g.advancePhase() return } g.CurrentPlayerIdx = g.nextToAct(g.CurrentPlayerIdx) } // advancePhase переходит к следующей фазе раздачи (флоп/тёрн/ривер/ // шоудаун), сбрасывая ставки раунда. Если активных (способных // действовать) игроков не осталось вовсе (все, кроме максимум // одного, в олл-ине) — карты дораздаются сразу до шоудауна без // дальнейших торгов. func (g *PokerGameState) advancePhase() { for _, p := range g.Players { p.CurrentBet = 0 } g.actedThisRound = map[int]bool{} switch g.Phase { case PokerPhasePreflop: g.Board = append(g.Board, g.drawCard(), g.drawCard(), g.drawCard()) g.Phase = PokerPhaseFlop case PokerPhaseFlop: g.Board = append(g.Board, g.drawCard()) g.Phase = PokerPhaseTurn case PokerPhaseTurn: g.Board = append(g.Board, g.drawCard()) g.Phase = PokerPhaseRiver case PokerPhaseRiver: g.resolveShowdown() return } if g.countActingPlayers() <= 1 { // все, кроме максимум одного, в олл-ине — дальше действовать // некому, доигрываем карты до шоудауна автоматически g.advancePhase() return } g.lastAggressorIdx = -1 g.CurrentPlayerIdx = g.nextToAct(g.DealerIdx) } // resolveHandByFold завершает раздачу, когда все, кроме одного, // сбросили карты — оставшийся игрок забирает весь банк без показа карт. func (g *PokerGameState) resolveHandByFold() { var winnerIdx int for i, p := range g.Players { if !p.Folded && !p.Eliminated { winnerIdx = i break } } pots := computeSidePots(g.Players) total := 0 for _, pot := range pots { total += pot.Amount } g.Players[winnerIdx].Stack += total g.Result = &PokerHandResult{ Pots: pots, Winners: map[int]int{winnerIdx: total}, WentToShowdown: false, } g.Phase = PokerPhaseShowdown g.markEliminated() } // resolveShowdown вскрывает карты и распределяет все банки (включая // сайд-поты) между лучшими руками среди дошедших до конца. func (g *PokerGameState) resolveShowdown() { pots := computeSidePots(g.Players) values := map[int]PokerHandValue{} for i, p := range g.Players { if !p.Folded && !p.Eliminated { all := append([]Card{p.HoleCards[0], p.HoleCards[1]}, g.Board...) values[i] = EvaluateBest5(all) } } winnings := map[int]int{} for _, pot := range pots { var best []int for _, idx := range pot.EligiblePlayers { if len(best) == 0 { best = []int{idx} continue } cmp := ComparePokerHands(values[idx], values[best[0]]) if cmp > 0 { best = []int{idx} } else if cmp == 0 { best = append(best, idx) } } share := pot.Amount / len(best) remainder := pot.Amount - share*len(best) for i, idx := range best { amt := share if i == 0 { amt += remainder // остаток от неровного деления — первому по очереди из победителей } winnings[idx] += amt g.Players[idx].Stack += amt } } g.Result = &PokerHandResult{ Pots: pots, Winners: winnings, WentToShowdown: true, HandValues: values, } g.Phase = PokerPhaseShowdown g.markEliminated() } func (g *PokerGameState) markEliminated() { for _, p := range g.Players { if p.Stack == 0 && !p.Eliminated { p.Eliminated = true } } } // NextHand начинает следующую раздачу турнира (или переводит в // PokerPhaseTournamentOver, если остался один игрок с фишками). func (g *PokerGameState) NextHand(rnd *rand.Rand) { if g.Phase != PokerPhaseShowdown { return } g.startNewHand(rnd) } // computeSidePots раскладывает общий банк на "слои" по уровням // взносов игроков за раздачу — стандартный алгоритм сайд-потов: // каждый слой формируют все, кто внёс хотя бы до этого уровня // (включая уже сбросивших — их фишки остаются в банке), а // претендовать на слой могут только НЕ сбросившие игроки, дошедшие // до этого уровня. func computeSidePots(players []*PokerPlayer) []PokerSidePot { levelSet := map[int]bool{} for _, p := range players { if p.TotalContributed > 0 { levelSet[p.TotalContributed] = true } } var levels []int for lvl := range levelSet { levels = append(levels, lvl) } sort.Ints(levels) var pots []PokerSidePot prev := 0 carry := 0 for _, level := range levels { layerSize := level - prev contributors := 0 var eligible []int for i, p := range players { if p.TotalContributed >= level { contributors++ if !p.Folded { eligible = append(eligible, i) } } } amount := layerSize*contributors + carry if len(eligible) == 0 { carry = amount } else { pots = append(pots, PokerSidePot{Amount: amount, EligiblePlayers: eligible}) carry = 0 } prev = level } if carry > 0 && len(pots) > 0 { pots[len(pots)-1].Amount += carry } return pots }