go_games_collection/poker_game.go
2026-07-10 09:12:06 +03:00

562 lines
18 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}