From 1a235592ef165d82665bba9fea1158e113ec6315 Mon Sep 17 00:00:00 2001 From: Daniel Haskin Date: Thu, 28 Aug 2025 17:18:03 -0600 Subject: [PATCH 1/3] Add prompts for targets scores, bust thresholds; add bayes strat --- computer_player.go | 34 +++++++++++++ game.go | 121 +++++++++++++++++++++------------------------ player.go | 31 +++++++----- 3 files changed, 108 insertions(+), 78 deletions(-) diff --git a/computer_player.go b/computer_player.go index 09fc78a..6e604d7 100644 --- a/computer_player.go +++ b/computer_player.go @@ -64,12 +64,46 @@ func PlayRoundTo(n int) HitOrStayStrategy { } } +func BayesianGainStrategy(self PlayerInterface, gameState *GameState) bool { + bustProb := CalculateLimitedKnowledgeBustProbability(self) + currentScore := self.CalculateRoundScore() + expectedBustCost := float64(currentScore) * bustProb + + // Approximate expected value of next number card + // \sum(i^2)/\sum(i) for i in [1..12] = 8.33 + baseNextCardValue := 8.33 + + if self.NumberOfNumberCards() > 6 { + baseNextCardValue += 15 + } + + if hasMultiplier(self) { + baseNextCardValue *= 2 + } + + expectedScoreIncrease := float64(baseNextCardValue) * (1 - bustProb) + expectedNetGain := expectedScoreIncrease - expectedBustCost + return expectedNetGain > 0.0 +} + func PlayToBustProbability(p float64) HitOrStayStrategy { return func(self PlayerInterface, gameState *GameState) bool { return CalculateBustProbability(self, gameState) < p } } +func CalculateLimitedKnowledgeBustProbability(player PlayerInterface) float64 { + numberCardSum := 0 + for _, card := range player.GetHand() { + if card.Type == NumberCard { + numberCardSum += card.Value + } + } + + // 978 is \sum(i) for i in [1..12] + return float64(numberCardSum) / float64(50) +} + // Helper functions for advanced strategies func CalculateBustProbability(player PlayerInterface, gameState *GameState) float64 { numberCards := make(map[int]bool) diff --git a/game.go b/game.go index 8740c63..0923cde 100644 --- a/game.go +++ b/game.go @@ -603,114 +603,105 @@ func (g *Game) getComputerPlayerSetup(computerNum int) (string, HitOrStayStrateg g.printf("\nComputer Player %d:\n", computerNum) g.println("Choose AI strategy:") - g.println(" 1) Plays to 20") - g.println(" 2) Plays to 25") - g.println(" 3) Plays to 30") - g.println(" 4) Plays to 35") - g.println(" 5) Hit p(BUST) < 0.2") - g.println(" 6) Hit p(BUST) < 0.25") - g.println(" 7) Hit p(BUST) < 0.3") - g.println(" 8) Hit p(BUST) < 0.35") - g.println(" 9) Hit p(BUST) < 0.4") - g.println(" 10) FLIP 7") - g.println(" 11) Random") - g.println(" 12) Adaptive Bust Prob (0.3)") - g.println(" 13) Expected Value") - g.println(" 14) Hybrid Strategy") - g.println(" 15) Gap-Based Strategy") - g.println(" 16) Optimal Strategy") - g.print("Enter choice (1-18): ") - - choice, err := g.getIntInput(1, 18) + g.println(" 1) Plays to some score") + g.println(" 2) Plays to against some bust probability threshold (counts cards)") + g.println(" 3) FLIP 7 (always hits)") + g.println(" 4) Random") + g.println(" 5) Adaptive Bust Prob (0.3)") + g.println(" 6) Expected Value") + g.println(" 7) Hybrid Strategy") + g.println(" 8) Gap-Based Strategy") + g.println(" 9) Optimal Strategy") + g.println(" 10) Bayesian Gain Strategy") + + g.print("Enter choice (1-10): ") + + choice, err := g.getIntInput(1, 10) if err != nil { - choice = 13 + choice = 6 } var strategy HitOrStayStrategy var actionTargetStrategy ActionTargetStrategy var positiveActionTargetStrategy ActionTargetStrategy + var targetScore int + var bustProbabilityThreshold float64 + + if choice == 1 { + g.print("Enter Target Score: ") + score, err := g.getIntInput(1, 100) + if err != nil { + score = 30 + } + strategy = PlayRoundTo(score) + targetScore = score + } + + if choice == 2 { + g.print("Enter Bust Probability Threshold: ") + probInput, err := g.getStringInput() + if err != nil { + probInput = "0.33" + } + prob, err := strconv.ParseFloat(probInput, 64) + if err != nil || prob < 0.1 || prob > 0.5 { + prob = 0.33 + } + bustProbabilityThreshold = prob + } switch choice { case 1: - name += " (20)" - strategy = PlayRoundTo(20) + name += " (" + strconv.Itoa(targetScore) + ")" + strategy = PlayRoundTo(targetScore) actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy case 2: - name += " (25)" - strategy = PlayRoundTo(25) + name += " p(" + fmt.Sprintf("%.2f", bustProbabilityThreshold) + ")" + strategy = PlayToBustProbability(bustProbabilityThreshold) actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy case 3: - name += " (30)" - strategy = PlayRoundTo(30) - actionTargetStrategy = TargetLeaderStrategy - positiveActionTargetStrategy = TargetLastPlaceStrategy - case 4: - name += " (35)" - strategy = PlayRoundTo(35) - actionTargetStrategy = TargetLeaderStrategy - positiveActionTargetStrategy = TargetLastPlaceStrategy - case 5: - name += " (p0.2)" - strategy = PlayToBustProbability(0.2) - actionTargetStrategy = TargetLeaderStrategy - positiveActionTargetStrategy = TargetLastPlaceStrategy - case 6: - name += " (p0.25)" - strategy = PlayToBustProbability(0.25) - actionTargetStrategy = TargetLeaderStrategy - positiveActionTargetStrategy = TargetLastPlaceStrategy - case 7: - name += " (p0.3)" - strategy = PlayToBustProbability(0.3) - actionTargetStrategy = TargetLeaderStrategy - positiveActionTargetStrategy = TargetLastPlaceStrategy - case 8: - name += " (p0.35)" - strategy = PlayToBustProbability(0.35) - actionTargetStrategy = TargetLeaderStrategy - positiveActionTargetStrategy = TargetLastPlaceStrategy - case 9: - name += " (p0.4)" - strategy = PlayToBustProbability(0.4) - actionTargetStrategy = TargetLeaderStrategy - positiveActionTargetStrategy = TargetLastPlaceStrategy - case 10: name += " (hit)" strategy = AlwaysHitStrategy actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy - case 11: + case 4: name += " (rand)" strategy = RandomHitOrStayStrategy actionTargetStrategy = TargetRandomStrategy positiveActionTargetStrategy = TargetRandomStrategy - case 12: + case 5: name += " (adapt0.3)" strategy = AdaptiveBustProbabilityStrategy(0.3) actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy - case 13: + case 6: name += " (exp)" strategy = ExpectedValueStrategy actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy - case 14: + case 7: name += " (hybrid)" strategy = HybridStrategy actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy - case 15: + case 8: name += " (gap)" strategy = GapBasedStrategy actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy - case 16: + case 9: name += " (opt)" strategy = OptimalStrategy actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy + case 10: + name += " (bayes)" + strategy = BayesianGainStrategy + actionTargetStrategy = TargetLeaderStrategy + positiveActionTargetStrategy = TargetLastPlaceStrategy + default: panic("invalid choice") } diff --git a/player.go b/player.go index 92ef8eb..095df5d 100644 --- a/player.go +++ b/player.go @@ -24,25 +24,26 @@ const ( ) type PlayerInterface interface { - GetName() string - HasSecondChance() bool - GetTotalScore() int - GetPlayerIcon() string AddCard(card *Card) error - UseSecondChance() *Card - Stay() + AddToTotalScore() Bust() CalculateRoundScore() int - AddToTotalScore() - ResetForNewRound() []*Card - IsActive() bool - HasCards() bool - ShowHand() - GetHand() []*Card - GetHandSummary() string ChooseActionTarget(gameState *GameState, actionType ActionType) (PlayerInterface, error) ChoosePositiveActionTarget(gameState *GameState, actionType ActionType) (PlayerInterface, error) + GetHand() []*Card + GetHandSummary() string + GetName() string + GetPlayerIcon() string + GetTotalScore() int + HasCards() bool + HasSecondChance() bool + IsActive() bool MakeHitStayDecision(gameState *GameState) (bool, error) + NumberOfNumberCards() int + ResetForNewRound() []*Card + ShowHand() + Stay() + UseSecondChance() *Card } // Player represents a game player @@ -119,6 +120,10 @@ func (p *BasePlayer) GetHand() []*Card { return slices.Concat(p.NumberCards, p.ModifierCards, p.ActionCards) } +func (p *BasePlayer) NumberOfNumberCards() int { + return len(p.NumberCards) +} + // UseSecondChance uses the second chance card to avoid busting func (p *BasePlayer) UseSecondChance() *Card { if !p.HasSecondChance() { From 23ae71961d20430b44c73d86c150cde2e423939f Mon Sep 17 00:00:00 2001 From: Daniel Haskin Date: Thu, 28 Aug 2025 17:26:06 -0600 Subject: [PATCH 2/3] Make sure there are 18 computer player names --- game.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/game.go b/game.go index 0923cde..35dbe1f 100644 --- a/game.go +++ b/game.go @@ -594,6 +594,9 @@ var computerNames = []string{ "WOPR", "Cortana", "Marvin", + "Siri", + "Alexa", + "Jeeves", } func (g *Game) getComputerPlayerSetup(computerNum int) (string, HitOrStayStrategy, ActionTargetStrategy, ActionTargetStrategy, error) { From 992bfde86cf063190330b959e603e8063954dbb7 Mon Sep 17 00:00:00 2001 From: Daniel Haskin Date: Fri, 29 Aug 2025 09:23:46 -0600 Subject: [PATCH 3/3] Add Gap Aware Strategy --- computer_player.go | 18 ++++++++++++++++++ game.go | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/computer_player.go b/computer_player.go index 6e604d7..877f4aa 100644 --- a/computer_player.go +++ b/computer_player.go @@ -248,6 +248,24 @@ func HybridStrategy(self PlayerInterface, gameState *GameState) bool { return bustProb < baseBustThreshold } +func GapAwareStrategy(TargetScore int, GapThreshold int) HitOrStayStrategy { + slackTarget := TargetScore - GapThreshold + aggressiveTarget := TargetScore - GapThreshold + slackTargetStrategy := PlayRoundTo(TargetScore) + aggressiveTargetStrategy := PlayRoundTo(aggressiveTarget) + normalTargetStrategy := PlayRoundTo(TargetScore) + return func(self PlayerInterface, gameState *GameState) bool { + score := self.CalculateRoundScore() + if score <= aggressiveTarget { + return aggressiveTargetStrategy(self, gameState) + } else if score >= slackTarget { + return slackTargetStrategy(self, gameState) + } else { + return normalTargetStrategy(self, gameState) + } + } +} + // GapBasedStrategy focuses on the score gap to other players func GapBasedStrategy(self PlayerInterface, gameState *GameState) bool { if gameState.CurrentLeader == nil { diff --git a/game.go b/game.go index 35dbe1f..e0b1d62 100644 --- a/game.go +++ b/game.go @@ -616,10 +616,11 @@ func (g *Game) getComputerPlayerSetup(computerNum int) (string, HitOrStayStrateg g.println(" 8) Gap-Based Strategy") g.println(" 9) Optimal Strategy") g.println(" 10) Bayesian Gain Strategy") + g.println(" 11) Gap Aware Stragegy") - g.print("Enter choice (1-10): ") + g.print("Enter choice (1-11): ") - choice, err := g.getIntInput(1, 10) + choice, err := g.getIntInput(1, 11) if err != nil { choice = 6 } @@ -629,6 +630,8 @@ func (g *Game) getComputerPlayerSetup(computerNum int) (string, HitOrStayStrateg var positiveActionTargetStrategy ActionTargetStrategy var targetScore int var bustProbabilityThreshold float64 + var gapTolerance int + var slackFactor int if choice == 1 { g.print("Enter Target Score: ") @@ -652,6 +655,28 @@ func (g *Game) getComputerPlayerSetup(computerNum int) (string, HitOrStayStrateg } bustProbabilityThreshold = prob } + if choice == 11 { + g.print("Enter Target Score: ") + score, err := g.getIntInput(1, 100) + if err != nil { + score = 30 + } + strategy = PlayRoundTo(score) + targetScore = score + + g.print("Enter Gap Tolerance (e.g., 5): ") + gapTol, err := g.getIntInput(1, 50) + if err != nil { + gapTol = 20 + } + gapTolerance = gapTol + g.print("Enter Slack Factor (e.g., 5): ") + slack, err := g.getIntInput(1, 20) + if err != nil { + slack = 5 + } + slackFactor = slack + } switch choice { case 1: @@ -704,6 +729,11 @@ func (g *Game) getComputerPlayerSetup(computerNum int) (string, HitOrStayStrateg strategy = BayesianGainStrategy actionTargetStrategy = TargetLeaderStrategy positiveActionTargetStrategy = TargetLastPlaceStrategy + case 11: + name += fmt.Sprintf(" (gap%d_slack%d)", gapTolerance, slackFactor) + strategy = GapAwareStrategy(gapTolerance, slackFactor) + actionTargetStrategy = TargetLeaderStrategy + positiveActionTargetStrategy = TargetLastPlaceStrategy default: panic("invalid choice")