Files
gogobee/internal/plugin/holdem_equity.go
prosolis c7c1b76589 Add adventure plugin, holdem CFR fixes, and wordle plugin
Adventure: Complete v1 daily idle RPG with DM-driven gameplay, equipment
shop, treasure system, TwinBee NPC, streak/grudge/party mechanics,
flavor text, and scheduled morning/evening/midnight tickers.

Holdem CFR: Fix three critical training bugs (fold not forfeiting pot,
free calls after raises, training/runtime key mismatch). Add performance
optimizations (preflop lookup table, zero-alloc equity, integer keys,
raise cap, regret pruning). Enrich abstraction with 12 equity buckets,
board texture dimension, and 6-char action history. Replace validation
with proper multi-street simulation.

Also includes wordle plugin, holdem seed tooling, and schema additions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 10:09:57 -07:00

115 lines
2.6 KiB
Go

package plugin
import (
"math/rand/v2"
"github.com/chehsunliu/poker"
)
// EquityResult holds Monte Carlo simulation results.
type EquityResult struct {
Win float64
Tie float64
Loss float64
}
// allCards returns a fresh 52-card slice.
func allCards() []poker.Card {
suits := []string{"s", "h", "d", "c"}
ranks := []string{"2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A"}
cards := make([]poker.Card, 0, 52)
for _, r := range ranks {
for _, s := range suits {
cards = append(cards, poker.NewCard(r+s))
}
}
return cards
}
// Equity computes win/tie/loss fractions via Monte Carlo simulation.
func Equity(hole [2]poker.Card, community []poker.Card, numOpponents, iterations int) EquityResult {
if numOpponents < 1 {
numOpponents = 1
}
// Build set of known cards to exclude.
known := make(map[poker.Card]bool, 2+len(community))
known[hole[0]] = true
known[hole[1]] = true
for _, c := range community {
known[c] = true
}
// Remaining deck.
remaining := make([]poker.Card, 0, 52-len(known))
for _, c := range allCards() {
if !known[c] {
remaining = append(remaining, c)
}
}
boardNeeded := 5 - len(community)
cardsNeeded := numOpponents*2 + boardNeeded
var wins, ties, losses int
for i := 0; i < iterations; i++ {
// Fisher-Yates shuffle of first cardsNeeded elements.
for j := 0; j < cardsNeeded && j < len(remaining); j++ {
k := j + rand.IntN(len(remaining)-j)
remaining[j], remaining[k] = remaining[k], remaining[j]
}
// Deal opponent holes.
idx := 0
opponentHoles := make([][2]poker.Card, numOpponents)
for o := 0; o < numOpponents; o++ {
opponentHoles[o] = [2]poker.Card{remaining[idx], remaining[idx+1]}
idx += 2
}
// Complete board.
fullBoard := make([]poker.Card, 5)
copy(fullBoard, community)
for b := len(community); b < 5; b++ {
fullBoard[b] = remaining[idx]
idx++
}
// Evaluate hero.
heroCards := make([]poker.Card, 7)
heroCards[0] = hole[0]
heroCards[1] = hole[1]
copy(heroCards[2:], fullBoard)
heroRank := poker.Evaluate(heroCards)
// Evaluate opponents.
bestOpp := int32(7463) // worst possible rank
for _, oh := range opponentHoles {
oppCards := make([]poker.Card, 7)
oppCards[0] = oh[0]
oppCards[1] = oh[1]
copy(oppCards[2:], fullBoard)
oppRank := poker.Evaluate(oppCards)
if oppRank < bestOpp {
bestOpp = oppRank
}
}
if heroRank < bestOpp {
wins++
} else if heroRank == bestOpp {
ties++
} else {
losses++
}
}
total := float64(iterations)
return EquityResult{
Win: float64(wins) / total,
Tie: float64(ties) / total,
Loss: float64(losses) / total,
}
}