Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions computer_player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -214,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 {
Expand Down
154 changes: 89 additions & 65 deletions game.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,9 @@ var computerNames = []string{
"WOPR",
"Cortana",
"Marvin",
"Siri",
"Alexa",
"Jeeves",
}

func (g *Game) getComputerPlayerSetup(computerNum int) (string, HitOrStayStrategy, ActionTargetStrategy, ActionTargetStrategy, error) {
Expand All @@ -603,114 +606,135 @@ 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.println(" 11) Gap Aware Stragegy")

g.print("Enter choice (1-11): ")

choice, err := g.getIntInput(1, 11)
if err != nil {
choice = 13
choice = 6
}

var strategy HitOrStayStrategy
var actionTargetStrategy ActionTargetStrategy
var positiveActionTargetStrategy ActionTargetStrategy
var targetScore int
var bustProbabilityThreshold float64
var gapTolerance int
var slackFactor int

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
}
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:
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
case 11:
name += fmt.Sprintf(" (gap%d_slack%d)", gapTolerance, slackFactor)
strategy = GapAwareStrategy(gapTolerance, slackFactor)
actionTargetStrategy = TargetLeaderStrategy
positiveActionTargetStrategy = TargetLastPlaceStrategy

default:
panic("invalid choice")
}
Expand Down
31 changes: 18 additions & 13 deletions player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down