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

264 lines
8.9 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 "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
}