mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
Add tip audit logging, CFR alignment tests, and multiway scenarios
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>
This commit is contained in:
177
internal/plugin/holdem_tip_cfr_test.go
Normal file
177
internal/plugin/holdem_tip_cfr_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user