From 14ffbe4eb489ae90cfc3de02278937117ec9b791 Mon Sep 17 00:00:00 2001 From: sneak Date: Sat, 18 May 2024 22:10:53 -0700 Subject: [PATCH] added more tests, cleaned up some code, found another bug --- examples/simgame/main.go | 192 +++++++++++++++++++++++++++------------ handhelpers.go | 29 ------ pokerhand.go | 98 +++++++++++++------- scoring.go | 8 +- scoring_test.go | 55 ++++++++++- 5 files changed, 250 insertions(+), 132 deletions(-) diff --git a/examples/simgame/main.go b/examples/simgame/main.go index 5396913..ee6b3d9 100644 --- a/examples/simgame/main.go +++ b/examples/simgame/main.go @@ -7,74 +7,148 @@ import ( "git.eeqj.de/sneak/pokercore" ) +// just like on tv +var delayTime = 2 * time.Second +var playerCount = 8 + +type Player struct { + Hand pokercore.Cards + ScoredHand *pokercore.PokerHand + Position int + CommunityCardsAvailable pokercore.Cards +} + +type Game struct { + Deck *pokercore.Deck + Players []*Player + Community pokercore.Cards + Street int +} + +func NewGame(seed int) *Game { + g := &Game{} + g.Street = 0 + g.Deck = pokercore.NewDeck() + g.SpreadCards() + fmt.Printf("Shuffle up and deal!\n") + time.Sleep(delayTime) + fmt.Printf("Shuffling...\n") + g.Deck.ShuffleDeterministically(3141592653) + return g +} + +func (g *Game) StreetAsString() string { + switch g.Street { + case 0: + return "pre-flop" + case 1: + return "flop" + case 2: + return "turn" + case 3: + return "river" + } + return "unknown" +} + +func (g *Game) SpreadCards() { + fmt.Printf("deck before shuffling: %s\n", g.Deck.FormatForTerminal()) +} + +func (g *Game) DealPlayersIn() { + for i := 0; i < playerCount; i++ { + p := Player{Hand: g.Deck.Deal(2), Position: i + 1} + g.Players = append(g.Players, &p) + } +} + +func (g *Game) ShowGameStatus() { + fmt.Printf("Street: %s\n", g.StreetAsString()) + fmt.Printf("Community cards: %s\n", g.Community.FormatForTerminal()) + for _, p := range g.Players { + if p != nil { + fmt.Printf("Player %d: %s\n", p.Position, p.Hand.FormatForTerminal()) + if g.Street > 0 { + ac := append(p.Hand, g.Community...) + ph, err := ac.PokerHand() + if err != nil { + panic(err) + } + fmt.Printf("Player %d has %s\n", p.Position, ph.Description()) + fmt.Printf("Player %d Score: %d\n", p.Position, ph.Score) + } + } + } +} + +func (g *Game) DealFlop() { + g.Community = g.Deck.Deal(3) + g.Street = 1 +} + +func (g *Game) DealTurn() { + g.Community = append(g.Community, g.Deck.Deal(1)...) + g.Street = 2 +} + +func (g *Game) DealRiver() { + g.Community = append(g.Community, g.Deck.Deal(1)...) + g.Street = 3 +} + +func (g *Game) ShowWinner() { + var winner *Player + var winningHand *pokercore.PokerHand + for _, p := range g.Players { + if p != nil { + ac := append(p.Hand, g.Community...) + ph, err := ac.PokerHand() + if err != nil { + panic(err) + } + if winner == nil { + winner = p + winningHand = ph + } else { + if ph.Score > winningHand.Score { + winner = p + winningHand = ph + } + } + } + } + fmt.Printf("Winner: Player %d with %s.\n", winner.Position, winningHand.Description()) +} + func main() { - sleepTime := 2 * time.Second - playerCount := 8 - d := pokercore.NewDeck() - fmt.Printf("deck before shuffling: %s\n", d.FormatForTerminal()) // this "randomly chosen" seed somehow deals pocket queens, pocket kings, and pocket aces to three players. // what are the odds? lol - d.ShuffleDeterministically(1337) - fmt.Printf("deck after shuffling: %s\n", d.FormatForTerminal()) - var players []pokercore.Cards - for i := 0; i < playerCount; i++ { - players = append(players, d.Deal(2)) - } - for i, p := range players { - fmt.Printf("player %d: %s\n", i+1, p.FormatForTerminal()) - } - time.Sleep(2 * time.Second) + //g := NewGame(1337)) - fmt.Printf("##############################################\n") - // deal the flop - var community pokercore.Cards - community = d.Deal(3) - fmt.Printf("flop: %s\n", community.FormatForTerminal()) - time.Sleep(sleepTime) + // nothing up my sleeve: + g := NewGame(3141592653) - fmt.Printf("##############################################\n") - turn := d.Deal(1) - fmt.Printf("turn: %s\n", turn.FormatForTerminal()) - community = append(community, turn...) - fmt.Printf("board is now: %s\n", community.FormatForTerminal()) + g.ShowGameStatus() - time.Sleep(sleepTime) + g.DealPlayersIn() - fmt.Printf("##############################################\n") - river := d.Deal(1) - fmt.Printf("river: %s\n", river.FormatForTerminal()) - community = append(community, river...) - fmt.Printf("board is now: %s\n", community.FormatForTerminal()) + g.ShowGameStatus() + + g.DealFlop() + + g.ShowGameStatus() + + g.DealTurn() + + g.ShowGameStatus() + + g.DealRiver() + + g.ShowGameStatus() + + g.ShowWinner() - fmt.Printf("##############################################\n") - var winningPlayer int - var winningHand *pokercore.PokerHand - for i, p := range players { - fmt.Printf("player %d: %s\n", i+1, p.FormatForTerminal()) - playerCards, err := p.Append(community).IdentifyBestFiveCardPokerHand() - if err != nil { - panic("error evaluating hand: " + err.Error()) - } - ph, err := playerCards.PokerHand() - if err != nil { - panic("error evaluating hand: " + err.Error()) - } - fmt.Printf("player %d has a %s\n", i+1, ph.Description()) - if winningHand == nil { - winningPlayer = i - winningHand = ph - continue - } - if ph.Score > winningHand.Score { - winningPlayer = i - winningHand = ph - } - } - fmt.Printf("##############################################\n") - fmt.Printf("player %d wins with %s\n", winningPlayer+1, winningHand.Description()) - fmt.Printf("##############################################\n") fmt.Printf("What a strange game. The only winning move is to bet really big.\n") fmt.Printf("Insert coin to play again.\n") - } diff --git a/handhelpers.go b/handhelpers.go index 23fe752..386a703 100644 --- a/handhelpers.go +++ b/handhelpers.go @@ -1,34 +1,5 @@ package pokercore -func (c Cards) scoreStraightFlush() HandScore { - if !c.containsStraightFlush() { - panic("hand must be a straight flush to score it") - } - return ScoreStraightFlush + 1000*c.HighestRank().Score() -} - -func (c Cards) scoreFlush() HandScore { - if !c.containsFlush() { - panic("hand must be a flush to score it") - } - var score HandScore - for _, card := range c { - score += card.Rank.Score() - } - return ScoreFlush + score -} - -func (c Cards) scoreHighCard() HandScore { - if !c.isUnmadeHand() { - panic("hand must be a high card to score it") - } - var score HandScore - for _, card := range c { - score += card.Rank.Score() - } - return ScoreHighCard + score -} - func (c Cards) pairRank() Rank { if !c.containsPair() { panic("hand must have a pair to have a pair rank") diff --git a/pokerhand.go b/pokerhand.go index eed5969..2779dcf 100644 --- a/pokerhand.go +++ b/pokerhand.go @@ -1,6 +1,9 @@ package pokercore -import "fmt" +import ( + "errors" + "fmt" +) type PokerHandType int @@ -28,93 +31,118 @@ func (c Cards) Append(other Cards) Cards { } func (c Cards) PokerHand() (*PokerHand, error) { - if len(c) != 5 { - return nil, fmt.Errorf("hand must have 5 cards to be scored as a poker hand") - } if c.containsDuplicates() { - return nil, fmt.Errorf("hand must have no duplicates to be scored as a poker hand") + return nil, errors.New("hand must have no duplicates to be scored as a poker hand") + } + if len(c) < 5 { + return nil, errors.New("hand must have at least 5 cards to be scored as a poker hand") + } + + phc, err := c.IdentifyBestFiveCardPokerHand() + if err != nil { + // this should in theory never happen + return nil, err } ph := new(PokerHand) - ph.Hand = c - if c.containsRoyalFlush() { + ph.Hand = phc.SortByRankAscending() + if ph.Hand.containsRoyalFlush() { ph.Type = RoyalFlush ph.Score = ScoreRoyalFlush return ph, nil } - if c.containsStraightFlush() { + if ph.Hand.containsStraightFlush() { ph.Type = StraightFlush - ph.Score = ph.Hand.scoreStraightFlush() + ph.Score = ScoreStraightFlush + ph.Score += 1000 * ph.Hand.HighestRank().Score() return ph, nil } - if c.containsFourOfAKind() { + if ph.Hand.containsFourOfAKind() { ph.Type = FourOfAKind - ph.Score = ScoreFourOfAKind + 1000*c.fourOfAKindRank().Score() + c.fourOfAKindKicker().Score() + ph.Score = ScoreFourOfAKind + ph.Score += 1000 * ph.Hand.fourOfAKindRank().Score() + ph.Score += 100 * ph.Hand.fourOfAKindKicker().Score() return ph, nil } - if c.containsFullHouse() { + if ph.Hand.containsFullHouse() { ph.Type = FullHouse - ph.Score = ScoreFullHouse + 1000*c.fullHouseTripsRank().Score() + c.fullHousePairRank().Score() + ph.Score = ScoreFullHouse + // FIXME write a good test for this + ph.Score += 1000 * ph.Hand.fullHouseTripsRank().Score() + ph.Score += 100 * ph.Hand.fullHousePairRank().Score() return ph, nil } - if c.containsFlush() { + if ph.Hand.containsFlush() { ph.Type = Flush - ph.Score = c.scoreFlush() + ph.Score = ScoreFlush + // flush base score plus sum of card ranks + ph.Score += ph.Hand[0].Score() + ph.Score += ph.Hand[1].Score() + ph.Score += ph.Hand[2].Score() + ph.Score += ph.Hand[3].Score() + ph.Score += ph.Hand[4].Score() return ph, nil } - if c.containsStraight() { + if ph.Hand.containsStraight() { ph.Type = Straight + ph.Score = ScoreStraight // Straights are scored by the highest card in the straight // UNLESS the second highest card is a 5 and the highest card is an Ace // In that case, the straight is a 5-high straight, not an Ace-high straight - sorted := c.SortByRankAscending() + sorted := ph.Hand.SortByRankAscending() if sorted[3].Rank == FIVE && sorted[4].Rank == ACE { - // 5-high straight - ph.Score = ScoreStraight + 1000*sorted[3].Score() + // 5-high straight, scored by the five's rank + ph.Score += sorted[3].Score() } else { - // All other straights - ph.Score = ScoreStraight + 1000*sorted[4].Score() + // All other straights are scored by the highest card in the straight + ph.Score += sorted[4].Score() } return ph, nil } - if c.containsThreeOfAKind() { + if ph.Hand.containsThreeOfAKind() { ph.Type = ThreeOfAKind ph.Score = ScoreThreeOfAKind - ph.Score += 1000 * c.threeOfAKindTripsRank().Score() - ph.Score += 100 * c.threeOfAKindFirstKicker().Score() - ph.Score += 10 * c.threeOfAKindSecondKicker().Score() + ph.Score += 1000 * ph.Hand.threeOfAKindTripsRank().Score() + ph.Score += 100 * ph.Hand.threeOfAKindFirstKicker().Score() + ph.Score += 10 * ph.Hand.threeOfAKindSecondKicker().Score() return ph, nil } - if c.containsTwoPair() { + if ph.Hand.containsTwoPair() { ph.Type = TwoPair ph.Score = ScoreTwoPair - ph.Score += 1000 * c.twoPairBiggestPair().Score() - ph.Score += 100 * c.twoPairSmallestPair().Score() - ph.Score += 10 * c.twoPairKicker().Score() + ph.Score += 1000 * ph.Hand.twoPairBiggestPair().Score() + ph.Score += 100 * ph.Hand.twoPairSmallestPair().Score() + ph.Score += 10 * ph.Hand.twoPairKicker().Score() return ph, nil } - if c.containsPair() { + if ph.Hand.containsPair() { ph.Type = Pair ph.Score = ScorePair - ph.Score += 1000 * c.pairRank().Score() - ph.Score += 100 * c.pairFirstKicker().Score() - ph.Score += 10 * c.pairSecondKicker().Score() - ph.Score += c.pairThirdKicker().Score() + ph.Score += 1000 * ph.Hand.pairRank().Score() + ph.Score += 100 * ph.Hand.pairFirstKicker().Score() + ph.Score += 10 * ph.Hand.pairSecondKicker().Score() + ph.Score += ph.Hand.pairThirdKicker().Score() return ph, nil } ph.Type = HighCard - ph.Score = c.scoreHighCard() + ph.Score = ScoreHighCard // base score + // unmade hands are scored like flushes, just add up the values + ph.Score += ph.Hand[0].Score() + ph.Score += ph.Hand[1].Score() + ph.Score += ph.Hand[2].Score() + ph.Score += ph.Hand[3].Score() + ph.Score += ph.Hand[4].Score() return ph, nil } diff --git a/scoring.go b/scoring.go index c33d90c..2bc26a7 100644 --- a/scoring.go +++ b/scoring.go @@ -1,7 +1,6 @@ package pokercore import ( - "errors" "fmt" ) @@ -25,18 +24,13 @@ func (c Card) Score() HandScore { } func (c Cards) PokerHandScore() (HandScore, error) { - if len(c) != 5 { - return 0, errors.New("hand must have 5 cards to be scored as a poker hand") - } ph, err := c.PokerHand() if err != nil { return 0, err } - - //fmt.Println(ph) return ph.Score, nil } + func (x HandScore) String() string { return fmt.Sprintf("", x) - //return scoreToName(x) } diff --git a/scoring_test.go b/scoring_test.go index 011625b..febfd16 100644 --- a/scoring_test.go +++ b/scoring_test.go @@ -78,9 +78,9 @@ func TestAceLowStraight(t *testing.T) { assert.True(t, hand.containsStraight(), "Expected hand to be a straight") ph, err := hand.PokerHand() assert.Nil(t, err, "Expected no error") - assert.Greater(t, ph.Score, 0, "Expected score to be nonzero 0") + assert.Greater(t, ph.Score, 0, "Expected score to be greater than 0") assert.Less(t, ph.Score, 100000000000000000, "Expected score to be less than 100000000000000000") - assert.Equal(t, ph.Score, ScoreStraight+1000*FIVE.Score()) + assert.Equal(t, ph.Score, ScoreStraight+FIVE.Score()) assert.Equal(t, ph.Description(), "a five high straight") assert.True(t, hand.HighestRank() == ACE, "Expected highest rank to be an ace") assert.True(t, hand.SortByRankAscending().First().Rank == DEUCE, "Expected first card to be a deuce") @@ -296,3 +296,54 @@ func TestTwoPairBug(t *testing.T) { //fmt.Printf("PokerHand: %v+\n", ph) //fmt.Printf("PH score: %d\n", ph.Score) } + +func TestScoringStructureQuads(t *testing.T) { + handA := Cards{ + DeuceOfSpades(), + DeuceOfHearts(), + DeuceOfDiamonds(), + DeuceOfClubs(), + AceOfSpades(), + } + + handB := Cards{ + ThreeOfSpades(), + ThreeOfHearts(), + ThreeOfDiamonds(), + ThreeOfClubs(), + DeuceOfSpades(), + } + + phA, err := handA.PokerHand() + assert.Nil(t, err, "Expected no error") + phB, err := handB.PokerHand() + assert.Nil(t, err, "Expected no error") + assert.Greater(t, phB.Score, phA.Score, "Expected hand B to be higher than hand A") +} + +func TestScoringStructureFullHouse(t *testing.T) { + handA := Cards{ + DeuceOfSpades(), + DeuceOfHearts(), + DeuceOfDiamonds(), + AceOfSpades(), + AceOfHearts(), + } + + phA, err := handA.PokerHand() + assert.Nil(t, err, "Expected no error") + deucesFullOfAcesScore := phA.Score + + handB := Cards{ + ThreeOfSpades(), + ThreeOfHearts(), + ThreeOfDiamonds(), + DeuceOfSpades(), + DeuceOfHearts(), + } + phB, err := handB.PokerHand() + assert.Nil(t, err, "Expected no error") + threesFullOfDeucesScore := phB.Score + + assert.Greater(t, threesFullOfDeucesScore, deucesFullOfAcesScore, "Expected Threes full of deuces to beat deuces full of aces") +}