package main import "math/rand" // Бот Техасского Холдема — честного решателя GTO (оптимальной по // теории игр стратегии) здесь нет и быть не может в разумном // объёме кода; вместо этого используется оценка силы руки методом // Монте-Карло (симуляция множества случайных завершений раздачи // против случайных карт соперников) в сочетании с элементарным // расчётом шансов банка. Это не идеальная игра — блеф и адаптация к // стилю соперника учитываются лишь в грубом приближении — но // решения основаны на честной оценке вероятности выигрыша, а не на // произвольных правилах. // PokerDifficulty — уровень сложности бота. Игроку выбирать // сложность НЕ предлагается — при создании партии каждому боту // случайно назначается один из трёх уровней (см. NewPokerModel). type PokerDifficulty int const ( PokerDifficultyEasy PokerDifficulty = iota PokerDifficultyMedium PokerDifficultyHard ) // PokerBot — бот-соперник за одним из мест за столом. type PokerBot struct { Difficulty PokerDifficulty } // remainingDeck возвращает все карты колоды, ещё не известные как // использованные (не в своей руке и не на столе) — для честной // симуляции возможных карт соперников и оставшейся части борда. func remainingDeck(hole [2]Card, board []Card) []Card { used := map[Card]bool{hole[0]: true, hole[1]: true} for _, c := range board { used[c] = true } out := make([]Card, 0, 52-len(used)) for s := Clubs; s <= Spades; s++ { for r := Ace; r <= King; r++ { card := Card{Suit: s, Rank: r} if !used[card] { out = append(out, card) } } } return out } // pokerHandStrength оценивает вероятность выигрыша (или разделения // банка — считается за половину) руки hole на текущем борде board // против numOpponents случайных соперников, симулируя iterations // случайных завершений раздачи. func pokerHandStrength(hole [2]Card, board []Card, numOpponents, iterations int, rnd *rand.Rand) float64 { if numOpponents <= 0 { return 1 } wins := 0.0 for i := 0; i < iterations; i++ { deck := remainingDeck(hole, board) rnd.Shuffle(len(deck), func(a, b int) { deck[a], deck[b] = deck[b], deck[a] }) pos := 0 oppHoles := make([][2]Card, numOpponents) for o := 0; o < numOpponents; o++ { oppHoles[o] = [2]Card{deck[pos], deck[pos+1]} pos += 2 } fullBoard := append([]Card{}, board...) for len(fullBoard) < 5 { fullBoard = append(fullBoard, deck[pos]) pos++ } myValue := EvaluateBest5(append([]Card{hole[0], hole[1]}, fullBoard...)) winning, tie := true, false for _, oh := range oppHoles { ov := EvaluateBest5(append([]Card{oh[0], oh[1]}, fullBoard...)) switch { case ComparePokerHands(ov, myValue) > 0: winning = false case ComparePokerHands(ov, myValue) == 0: tie = true } if !winning { break } } if winning { if tie { wins += 0.5 } else { wins += 1.0 } } } return wins / float64(iterations) } // crudeStrength — грубая, не основанная на симуляции оценка силы // руки для лёгкого уровня: до флопа — простая таблица по паре и // старшинству карт, после флопа — приблизительная оценка по // категории собранной комбинации (без учёта кикеров). func crudeStrength(hole [2]Card, board []Card) float64 { if len(board) == 0 { r1, r2 := pokerRankValue(hole[0].Rank), pokerRankValue(hole[1].Rank) if r1 == r2 { return 0.5 + float64(r1)/28.0 } high, low := r1, r2 if low > high { high, low = low, high } strength := float64(high)/28.0 + float64(low)/56.0 if hole[0].Suit == hole[1].Suit { strength += 0.05 } if high-low <= 3 { strength += 0.03 } return strength } all := append([]Card{hole[0], hole[1]}, board...) v := EvaluateBest5(all) return 0.3 + float64(v.Category)/8.0*0.6 } // potSize — сколько всего фишек уже внесено в банк за эту раздачу // (по всем прошедшим раундам торгов вместе). func (g *PokerGameState) potSize() int { total := 0 for _, p := range g.Players { total += p.TotalContributed } return total } // countOpponentsInHand — сколько соперников (кроме seatIdx) всё ещё // не сбросили карты в этой раздаче. func (g *PokerGameState) countOpponentsInHand(seatIdx int) int { n := 0 for i, p := range g.Players { if i != seatIdx && !p.Folded && !p.Eliminated { n++ } } return n } // raiseTotalForStrength считает суммарную ставку раунда для рейза, // примерно пропорциональную банку и силе руки (от половины банка до // полного банка), с учётом ограничений минимального рейза и // собственного стека. func raiseTotalForStrength(g *PokerGameState, seatIdx int, strength float64) int { p := g.Players[seatIdx] pot := g.potSize() raiseSize := int(float64(pot) * (0.5 + strength*0.5)) if raiseSize < g.BigBlind { raiseSize = g.BigBlind } total := g.highestBet() + raiseSize maxTotal := p.CurrentBet + p.Stack if total > maxTotal { total = maxTotal } minTotal := g.highestBet() + g.BigBlind if g.highestBet() == 0 { minTotal = g.BigBlind } if total < minTotal { total = minTotal } if total > maxTotal { total = maxTotal } return total } // PokerBotAction — решение бота на один ход. type PokerBotAction struct { Kind string // "fold", "check", "call", "bet" BetAmount int // суммарная ставка раунда — только для "bet" } // DecideAction выбирает действие бота на его ходу seatIdx согласно // уровню сложности. func (b PokerBot) DecideAction(g *PokerGameState, seatIdx int, rnd *rand.Rand) PokerBotAction { p := g.Players[seatIdx] toCall := g.highestBet() - p.CurrentBet if b.Difficulty == PokerDifficultyEasy { return b.decideEasy(g, seatIdx, toCall) } return b.decideByStrength(g, seatIdx, toCall, rnd) } func (b PokerBot) decideEasy(g *PokerGameState, seatIdx int, toCall int) PokerBotAction { p := g.Players[seatIdx] strength := crudeStrength(p.HoleCards, g.Board) if toCall == 0 { if strength > 0.62 { return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, strength)} } return PokerBotAction{Kind: "check"} } callOdds := float64(toCall) / float64(g.potSize()+toCall) if strength+0.25 >= callOdds || toCall <= g.BigBlind*2 { return PokerBotAction{Kind: "call"} } return PokerBotAction{Kind: "fold"} } func (b PokerBot) decideByStrength(g *PokerGameState, seatIdx int, toCall int, rnd *rand.Rand) PokerBotAction { p := g.Players[seatIdx] iterations := 80 bluffChance := 0.0 aggression := 1.0 if b.Difficulty == PokerDifficultyHard { iterations = 200 bluffChance = 0.1 aggression = 1.15 } if bluffChance > 0 && rnd.Float64() < bluffChance { return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, 0.8)} } strength := pokerHandStrength(p.HoleCards, g.Board, g.countOpponentsInHand(seatIdx), iterations, rnd) if toCall == 0 { if strength > 0.65*aggression { return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, strength)} } return PokerBotAction{Kind: "check"} } potOddsNeeded := float64(toCall) / float64(g.potSize()+toCall) if strength < potOddsNeeded { return PokerBotAction{Kind: "fold"} } if strength > 0.75*aggression { return PokerBotAction{Kind: "bet", BetAmount: raiseTotalForStrength(g, seatIdx, strength)} } return PokerBotAction{Kind: "call"} } // PlayFullTurn исполняет один ход бота на его месте seatIdx. func (b PokerBot) PlayFullTurn(g *PokerGameState, seatIdx int, rnd *rand.Rand) error { action := b.DecideAction(g, seatIdx, rnd) switch action.Kind { case "fold": return g.Fold() case "check": return g.Check() case "call": return g.Call() case "bet": return g.Bet(action.BetAmount) } return nil }