mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Three confidence features for the poker tip system: - Tip audit table (holdem_tip_audit) logs every tip with full context for bulk review after real sessions - CFR alignment test validates all 32 scenarios against the trained 5M-iteration policy — rules engine never contradicts the bot's AI - 8 new multiway test scenarios covering all streets and key decisions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
4.3 KiB
Go
178 lines
4.3 KiB
Go
package plugin
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/chehsunliu/poker"
|
|
)
|
|
|
|
var testPolicy PolicyTable
|
|
|
|
func init() {
|
|
path := os.Getenv("HOLDEM_CFR_POLICY")
|
|
if path == "" {
|
|
path = "../../data/policy.gob"
|
|
}
|
|
p, err := LoadPolicy(path)
|
|
if err == nil {
|
|
testPolicy = p
|
|
}
|
|
}
|
|
|
|
// policyStrategy looks up the trained policy for the given game state,
|
|
// falling back to the pot-odds heuristic if no entry exists.
|
|
func policyStrategy(game *HoldemGame, heroIdx int) (probs [cfrNumActions]float64, source string) {
|
|
p := game.Players[heroIdx]
|
|
numOpp := game.activeCount() - 1
|
|
if numOpp < 1 {
|
|
numOpp = 1
|
|
}
|
|
eq := Equity(p.Hole, game.Community, numOpp, 2000)
|
|
|
|
if testPolicy != nil {
|
|
eqBkt := equityBucket(eq.Win + eq.Tie*0.5)
|
|
totalPot := game.Pot
|
|
for _, pp := range game.Players {
|
|
totalPot += pp.Bet
|
|
}
|
|
spr := 0.0
|
|
if totalPot > 0 {
|
|
spr = float64(p.Stack) / float64(totalPot)
|
|
}
|
|
sprBkt := sprBucket(spr)
|
|
pos := game.positionLabel(heroIdx)
|
|
boardTex := boardTexture(game.Community)
|
|
key := buildInfoSetKey(game.Street, pos, eqBkt, sprBkt, boardTex, truncateHistory(game.StreetHistory))
|
|
|
|
if entry, ok := testPolicy[key]; ok {
|
|
return filterLegalActions(entry, game, heroIdx), "trained"
|
|
}
|
|
}
|
|
|
|
eq2 := Equity(p.Hole, game.Community, numOpp, 2000)
|
|
return filterLegalActions(fallbackStrategy(eq2, game, heroIdx), game, heroIdx), "fallback"
|
|
}
|
|
|
|
// TestTipScenarios_CFRAlignment builds a minimal HoldemGame from each scenario,
|
|
// looks up the trained CFR policy (falling back to pot-odds heuristic), and
|
|
// verifies the rules-engine tip doesn't recommend an action that the CFR gives
|
|
// zero weight to.
|
|
func TestTipScenarios_CFRAlignment(t *testing.T) {
|
|
if testPolicy != nil {
|
|
t.Logf("loaded trained policy with %d info sets", len(testPolicy))
|
|
} else {
|
|
t.Log("no trained policy found, using fallback heuristic only")
|
|
}
|
|
|
|
for _, s := range tipScenarios {
|
|
t.Run(s.Name, func(t *testing.T) {
|
|
ctx := scenarioContext(t, s)
|
|
tip := strings.ToLower(generateRulesTip(ctx))
|
|
|
|
game := buildGameFromScenario(s)
|
|
heroIdx := 0
|
|
|
|
probs, source := policyStrategy(game, heroIdx)
|
|
|
|
tipAction := extractTipAction(tip)
|
|
cfrActions := mapTipActionToCFR(tipAction)
|
|
if len(cfrActions) == 0 {
|
|
return
|
|
}
|
|
|
|
totalWeight := 0.0
|
|
for _, cfrIdx := range cfrActions {
|
|
totalWeight += probs[cfrIdx]
|
|
}
|
|
|
|
if totalWeight < 0.01 {
|
|
t.Errorf("rules tip action %q has zero CFR weight (source: %s).\n tip: %s\n CFR probs: fold=%.2f call/chk=%.2f raise½=%.2f raisePot=%.2f allIn=%.2f",
|
|
tipAction, source, tip, probs[0], probs[1], probs[2], probs[3], probs[4])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// buildGameFromScenario constructs a minimal HoldemGame for CFR strategy lookup.
|
|
func buildGameFromScenario(s TipScenario) *HoldemGame {
|
|
hole := [2]poker.Card{
|
|
poker.NewCard(s.HoleStr[0]),
|
|
poker.NewCard(s.HoleStr[1]),
|
|
}
|
|
|
|
community := make([]poker.Card, len(s.BoardStr))
|
|
for i, cs := range s.BoardStr {
|
|
community[i] = poker.NewCard(cs)
|
|
}
|
|
|
|
numActive := s.NumActive
|
|
if numActive < 2 {
|
|
numActive = 2
|
|
}
|
|
|
|
players := make([]*HoldemPlayer, numActive)
|
|
players[0] = &HoldemPlayer{
|
|
DisplayName: "Hero",
|
|
Hole: hole,
|
|
Stack: s.Stack,
|
|
State: PlayerActive,
|
|
}
|
|
for i := 1; i < numActive; i++ {
|
|
players[i] = &HoldemPlayer{
|
|
DisplayName: "Villain",
|
|
Hole: [2]poker.Card{poker.NewCard("2c"), poker.NewCard("3d")},
|
|
Stack: s.Stack,
|
|
State: PlayerActive,
|
|
}
|
|
}
|
|
|
|
currentBet := int64(0)
|
|
if s.ToCall > 0 {
|
|
currentBet = s.ToCall
|
|
players[0].Bet = 0
|
|
}
|
|
|
|
pot := s.Pot
|
|
if s.ToCall > 0 {
|
|
pot -= s.ToCall
|
|
if pot < 0 {
|
|
pot = 0
|
|
}
|
|
}
|
|
|
|
dealerIdx := 0
|
|
if s.Position != "BTN" && s.Position != "SB" {
|
|
dealerIdx = numActive - 1
|
|
}
|
|
|
|
return &HoldemGame{
|
|
Players: players,
|
|
Community: community,
|
|
Street: s.Street,
|
|
Pot: pot,
|
|
CurrentBet: currentBet,
|
|
MinRaise: currentBet,
|
|
DealerIdx: dealerIdx,
|
|
ActionIdx: 0,
|
|
HandInProgress: true,
|
|
}
|
|
}
|
|
|
|
// mapTipActionToCFR maps a rules-engine action verb to corresponding CFR action indices.
|
|
func mapTipActionToCFR(action string) []int {
|
|
switch action {
|
|
case "fold":
|
|
return []int{cfrFold}
|
|
case "call", "check":
|
|
return []int{cfrCallCheck}
|
|
case "bet", "raise":
|
|
return []int{cfrRaiseHalf, cfrRaisePot, cfrAllIn}
|
|
case "shove":
|
|
return []int{cfrAllIn}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|