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