diff --git a/.gitignore b/.gitignore index f1c181e..95cf6da 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +.DS_Store diff --git a/Makefile b/Makefile index d0ae92e..e5bf920 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ default: test test: - cd pokercore && make test + cd pokercore && go test -v ./... diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0ad8fc5 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module git.eeqj.de/sneak/go-poker + +go 1.22.2 + +require ( + github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/logrusorgru/aurora/v4 v4.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4d7d5e1 --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA= +github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index af490fa..f1023ab 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,14 @@ package main import ( "errors" "fmt" - log "github.com/sirupsen/logrus" - "github.com/sneak/gopoker/pokercore" "io" "net/http" "net/rpc" "net/rpc/jsonrpc" "os" "time" + + log "github.com/sirupsen/logrus" ) type JSONRPCServer struct { diff --git a/misc/generateTestSuite.go b/misc/generateTestSuite.go index 5ea67de..e130cc9 100644 --- a/misc/generateTestSuite.go +++ b/misc/generateTestSuite.go @@ -1,10 +1,18 @@ package main -import "fmt" -import "github.com/sneak/gopoker/pokercore" +import ( + "fmt" + + "git.eeqj.de/sneak/go-poker/pokercore" +) func main() { myDeck := pokercore.NewDeck() myDeck.ShuffleRandomly() - fmt.Println("%s", myDeck.Cards) + a := myDeck.Deal(5) + b := myDeck.Deal(7) + a.PrintToTerminal() + fmt.Printf("\n%#v\n", a) + b.PrintToTerminal() + fmt.Printf("\n%#v\n", b) } diff --git a/pokercore/Makefile b/pokercore/Makefile deleted file mode 100644 index 9772059..0000000 --- a/pokercore/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -default: test - -.PHONY: pkgs test - -fetch: - go get -t - -test: *.go - go test -v - diff --git a/pokercore/deck.go b/pokercore/deck.go new file mode 100644 index 0000000..1854c91 --- /dev/null +++ b/pokercore/deck.go @@ -0,0 +1,94 @@ +package pokercore + +import ( + "fmt" + rand "math/rand" + "sync" +) + +type Deck struct { + mu sync.Mutex + Cards Cards + ShuffleSeedVal int64 + Dealt int +} + +func NewDeckFromCards(cards Cards) *Deck { + d := new(Deck) + d.Cards = make([]Card, len(cards)) + copy(d.Cards, cards) + return d +} + +func NewEmptyDeck() *Deck { + d := new(Deck) + d.Cards = make([]Card, 0) + return d +} + +// NewDeck returns a new deck of 52 sorted cards. +func NewDeck() *Deck { + d := NewEmptyDeck() + + ranks := []Rank{ + ACE, DEUCE, THREE, FOUR, FIVE, + SIX, SEVEN, EIGHT, NINE, TEN, JACK, + QUEEN, KING} + + // This is the suit order used by dealers at + // The Golden Nugget in Las Vegas. + suits := []Suit{SPADE, HEART, DIAMOND, CLUB} + + for _, s := range suits { + for _, r := range ranks { + d.AddCard(Card{Rank: r, Suit: s}) + } + } + + return d +} + +func (d *Deck) AddCard(c Card) { + d.mu.Lock() + defer d.mu.Unlock() + d.Cards = append(d.Cards, c) +} + +// ShuffleRandomly shuffles the deck using cryptographically random numbers. +func (d *Deck) ShuffleRandomly() { + d.mu.Lock() + defer d.mu.Unlock() + rnd := rand.New(rand.NewSource(int64(cryptoUint64()))) + //FIXME(sneak) not sure if this is constant time or not + rnd.Shuffle(len(d.Cards), func(i, j int) { d.Cards[i], d.Cards[j] = d.Cards[j], d.Cards[i] }) +} + +// ShuffleDeterministically shuffles the deck using a deterministic seed for testing. +func (d *Deck) ShuffleDeterministically(seed int64) { + d.mu.Lock() + defer d.mu.Unlock() + r := rand.New(rand.NewSource(seed)) + //FIXME(sneak) not sure if this is constant time or not + r.Shuffle(len(d.Cards), func(i, j int) { d.Cards[i], d.Cards[j] = d.Cards[j], d.Cards[i] }) +} + +// Deal removes n cards from the top of the deck and returns them. The Deck is +// modified in place.to remove the dealt cards, just like in real life. +func (d *Deck) Deal(n int) (output Cards) { + d.mu.Lock() + defer d.mu.Unlock() + for i := 0; i < n; i++ { + output = append(output, d.Cards[i]) + } + d.Cards = d.Cards[n:] + d.Dealt += n + return output +} + +func (d *Deck) String() string { + return fmt.Sprintf("Deck{%s size=%d dealt=%d}", d.Cards, d.Count(), d.Dealt) +} + +func (d *Deck) Count() int { + return len(d.Cards) +} diff --git a/pokercore/pokercore.go b/pokercore/pokercore.go index 6512924..279b891 100644 --- a/pokercore/pokercore.go +++ b/pokercore/pokercore.go @@ -1,31 +1,37 @@ package pokercore -import "encoding/binary" -import "fmt" -import crand "crypto/rand" -import . "github.com/logrusorgru/aurora" -import log "github.com/sirupsen/logrus" -import rand "math/rand" +import ( + "encoding/binary" + "fmt" + "sort" -import "strings" + crand "crypto/rand" -type Suit rune -type Rank rune + "github.com/logrusorgru/aurora/v4" -const ( - CLUB Suit = '\u2663' - SPADE Suit = '\u2660' - DIAMOND Suit = '\u2666' - HEART Suit = '\u2665' + log "github.com/sirupsen/logrus" + + "strings" ) -/* -// emoji are cooler anyway -const CLUB = "C" -const SPADE = "S" -const DIAMOND = "D" -const HEART = "H" -*/ +type Suit rune + +func (s Suit) String() string { + return string(s) +} + +type Rank rune + +func (r Rank) String() string { + return string(r) +} + +const ( + CLUB Suit = '\u2663' // ♣ + SPADE Suit = '\u2660' // ♠ + DIAMOND Suit = '\u2666' // ♦ + HEART Suit = '\u2665' // ♥ +) const ( ACE Rank = 'A' @@ -43,53 +49,67 @@ const ( KING Rank = 'K' ) -type TestGenerationIteration struct { - Deck *Deck - Seed int64 -} - type Card struct { Rank Rank Suit Suit } -type Cards []*Card +type Cards []Card -type Deck struct { - Cards Cards - DealIndex int - ShuffleSeedVal int64 +func (r Rank) Int(x AcesHighOrLow) int { + return int(rankToScore(r, x)) } -func formatCardsForTerminal(c Cards) (output string) { +func (r Rank) HandScore(x AcesHighOrLow) HandScore { + return HandScore(r.Int(x)) +} + +func (c Cards) SortByRank(x AcesHighOrLow) Cards { + + newCards := make(Cards, len(c)) + copy(newCards, c) + + sort.Slice(newCards, func(i, j int) bool { + return rankToScore(newCards[i].Rank, x) > rankToScore(newCards[j].Rank, x) + }) + return newCards +} + +type TestGenerationIteration struct { + Deck *Deck + Seed int64 +} + +func (c Cards) PrintToTerminal() { + fmt.Printf("%s", c.FormatCardsForTerminal()) +} + +func (c Cards) FormatCardsForTerminal() string { var cardstrings []string for i := 0; i < len(c); i++ { - cardstrings = append(cardstrings, formatCardForTerminal(*c[i])) + cardstrings = append(cardstrings, c[i].FormatForTerminal()) } - output = strings.Join(cardstrings, ",") - return output - + return strings.Join(cardstrings, ",") } -func formatCardForTerminal(c Card) (output string) { +func (c Card) FormatForTerminal() string { var rank string var suit string - color := Red + color := aurora.Red switch c.Suit { case Suit(DIAMOND): - color = Blue + color = aurora.Blue case Suit(HEART): - color = Red + color = aurora.Red case Suit(CLUB): - color = Green + color = aurora.Green case Suit(SPADE): - color = Black + color = aurora.Black } - rank = fmt.Sprintf("%s", BgGray(Bold(color(c.Rank)))) - suit = fmt.Sprintf("%s", BgGray(Bold(color(c.Suit)))) - output = fmt.Sprintf("%s%s", rank, suit) - return output + rank = fmt.Sprintf("%s", aurora.Bold(color(c.Rank.String()))) + suit = fmt.Sprintf("%s", aurora.Bold(color(c.Suit.String()))) + return fmt.Sprintf("%s%s", rank, suit) } func cryptoUint64() (v uint64) { @@ -101,70 +121,13 @@ func cryptoUint64() (v uint64) { return v } -func (self *Card) String() string { - return fmt.Sprintf("%s%s", string(self.Rank), string(self.Suit)) +func (c *Card) String() string { + return fmt.Sprintf("%s%s", string(c.Rank), string(c.Suit)) } -func NewDeck() *Deck { - - self := new(Deck) - - ranks := []Rank{ - ACE, DEUCE, THREE, FOUR, FIVE, - SIX, SEVEN, EIGHT, NINE, TEN, JACK, - QUEEN, KING} - - suits := []Suit{HEART, DIAMOND, CLUB, SPADE} - - self.Cards = make([]*Card, 52) - - tot := 0 - for i := 0; i < len(ranks); i++ { - for n := 0; n < len(suits); n++ { - self.Cards[tot] = &Card{ - Rank: ranks[i], - Suit: suits[n], - } - tot++ - } - } - self.DealIndex = 0 - return self -} - -func (self *Deck) ShuffleRandomly() { - rnd := rand.New(rand.NewSource(int64(cryptoUint64()))) - //FIXME(sneak) not sure if this is constant time or not - rnd.Shuffle(len(self.Cards), func(i, j int) { self.Cards[i], self.Cards[j] = self.Cards[j], self.Cards[i] }) - self.DealIndex = 0 -} - -func (self *Deck) ShuffleDeterministically(seed int64) { - r := rand.New(rand.NewSource(seed)) - //FIXME(sneak) not sure if this is constant time or not - r.Shuffle(len(self.Cards), func(i, j int) { self.Cards[i], self.Cards[j] = self.Cards[j], self.Cards[i] }) - self.DealIndex = 0 -} - -func (self *Deck) Deal(n int) (output Cards) { - - if (self.DealIndex + n) > len(self.Cards) { - return output - } - - for i := 0; i < n; i++ { - output = append(output, self.Cards[self.DealIndex+1]) - self.DealIndex++ - } - return output -} - -func (self *Deck) Dealt() int { - return self.DealIndex -} - -func (self *Deck) Remaining() int { - return (len(self.Cards) - self.DealIndex) +func (c Cards) HighestRank(x AcesHighOrLow) Rank { + c = c.SortByRank(x) + return c[0].Rank } func (s Cards) String() (output string) { @@ -190,43 +153,23 @@ func proto() { fmt.Printf("community: %s\n", cmty) } -func scorePokerHand(input Cards) (score int) { - /* - - scoring system: - high card: - high card * 14^4 - + first kicker * 14^3 - + second kicker * 14^2 - + third kicker * 14 - + fourth kicker - max(AKQJ9): 576,011 - single pair: - pair value * 1,000,000 - + first kicker * 14^2 - + second kicker * 14 - + third kicker - max(AAKQJ): 14,002,727 - two pair: - higher of the two pair value * 100,000,000 - + lower of the two pair value * 14 - + kicker value - max(AAKKQ): 1,300,000,179 - trips: - trips value * 1,000,000,000 (min 2,000,000,000) - + first kicker * 14 - + second kicker - max (AAAKQ): 14,000,000,194 - straight: - highest card * 10,000,000,000 - straight to the ace: 140,000,000,000 - flush: - highest card * 100,000,000,000 - min(23457): 700,000,000,000 - max(AXXXX): 1,400,000,000,000 - boat: - - - */ - return 1 +func contains(ranks []Rank, rank Rank) bool { + for _, r := range ranks { + if r == rank { + return true + } + } + return false +} + +func unique(intSlice []int) []int { + keys := make(map[int]bool) + list := []int{} + for _, entry := range intSlice { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list } diff --git a/pokercore/pokercore_test.go b/pokercore/pokercore_test.go index e525155..80dbd88 100644 --- a/pokercore/pokercore_test.go +++ b/pokercore/pokercore_test.go @@ -1,7 +1,11 @@ package pokercore -import "github.com/stretchr/testify/assert" -import "testing" +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) type ShuffleTestResults []struct { SeedVal int64 @@ -10,20 +14,38 @@ type ShuffleTestResults []struct { func TestPokerDeck(t *testing.T) { d := NewDeck() + fmt.Printf("newdeck: %+v\n", d) d.ShuffleDeterministically(437) + fmt.Printf("deterministically shuffled deck: %+v\n", d) cards := d.Deal(7) - //expected := "7C,5S,QS,2D,6D,QC,3H" - expected := "7♣,5♠,Q♠,2♦,6♦,Q♣,3♥" - assert.Equal(t, cards.String(), expected) + expected := "6♥,A♦,7♥,9♣,6♠,9♥,8♣" + fmt.Printf("deterministically shuffled deck after dealing: %+v\n", d) + fmt.Printf("cards: %+v\n", cards) + assert.Equal(t, expected, cards.String()) - x := d.Remaining() + x := d.Count() assert.Equal(t, 45, x) d.ShuffleDeterministically(123456789) cards = d.Deal(10) - expected = "2♣,T♠,4♥,Q♣,9♦,7♥,7♠,6♥,5♥,5♠" + expected = "A♣,7♠,8♠,4♠,7♦,K♠,2♣,J♦,A♠,2♦" assert.Equal(t, expected, cards.String()) - x = d.Remaining() - assert.Equal(t, 42, x) + x = d.Count() + assert.Equal(t, 35, x) } + +func TestDealing(t *testing.T) { + d := NewDeckFromCards(Cards{ + Card{Rank: ACE, Suit: HEART}, + Card{Rank: DEUCE, Suit: HEART}, + Card{Rank: THREE, Suit: HEART}, + Card{Rank: FOUR, Suit: HEART}, + Card{Rank: FIVE, Suit: HEART}, + }) + cards := d.Deal(5) + expected := "A♥,2♥,3♥,4♥,5♥" + assert.Equal(t, expected, cards.String()) + x := d.Count() + assert.Equal(t, 0, x) +} diff --git a/pokercore/scoring.go b/pokercore/scoring.go new file mode 100644 index 0000000..8c0ed5b --- /dev/null +++ b/pokercore/scoring.go @@ -0,0 +1,263 @@ +package pokercore + +import "fmt" + +type HandScore int + +const ( + ScoreNoPair = HandScore(iota * 100_000_000_000) + ScorePair + ScoreTwoPair + ScoreThreeOfAKind + ScoreStraight + ScoreFlush + ScoreFullHouse + ScoreFourOfAKind + ScoreStraightFlush +) + +type AcesHighOrLow int + +const ( + AcesHigh AcesHighOrLow = iota + AcesLow +) + +func rankToScore(rank Rank, AcesHighOrLow AcesHighOrLow) HandScore { + switch rank { + case ACE: + if AcesHighOrLow == AcesHigh { + return 14 // Aces are high, so we give them the highest value + } else { + return 1 + } + case DEUCE: + return 2 + case THREE: + return 3 + case FOUR: + return 4 + case FIVE: + return 5 + case SIX: + return 6 + case SEVEN: + return 7 + case EIGHT: + return 8 + case NINE: + return 9 + case TEN: + return 10 + case JACK: + return 11 + case QUEEN: + return 12 + case KING: + return 13 + default: + panic("nope") + } +} + +func (c Cards) ScoreHand() (HandScore, error) { + if !c.IsFiveCardPokerHand() { + return 0, fmt.Errorf("hand must have 5 cards with no duplicates to be scored") + } + if c.containsRoyalFlush() { + return ScoreStraightFlush + 1000*ACE.HandScore(AcesHigh), nil + } + if c.containsStraightFlush() { + return ScoreStraightFlush + 1000*c.HighestRank(AcesHigh).HandScore(AcesHigh), nil + } + + panic("not implemented") + // FIXME finish this + return 0, nil +} + +func (hand Cards) containsDuplicates() bool { + seen := make(map[Card]bool) + for _, card := range hand { + if _, ok := seen[card]; ok { + return true + } + seen[card] = true + } + return false +} + +func (hand Cards) IsFiveCardPokerHand() bool { + return len(hand) == 5 && !hand.containsDuplicates() +} + +func (hand Cards) containsFlush() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + suit := hand[0].Suit + for i := 1; i < len(hand); i++ { + if hand[i].Suit != suit { + return false + } + } + return true +} + +func (hand Cards) containsStraight() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + sorted := hand.SortByRank(AcesHigh) + + if sorted[0].Rank == ACE && sorted[1].Rank == FIVE { + // special case for A-5 straight + if sorted[1].Rank == FIVE && sorted[2].Rank == FOUR && sorted[3].Rank == THREE && sorted[4].Rank == DEUCE { + return true + } + } + return sorted[0].Rank.Int(AcesHigh) == sorted[1].Rank.Int(AcesHigh)+1 && sorted[1].Rank.Int(AcesHigh) == sorted[2].Rank.Int(AcesHigh)+1 && sorted[2].Rank.Int(AcesHigh) == sorted[3].Rank.Int(AcesHigh)+1 && sorted[3].Rank.Int(AcesHigh) == sorted[4].Rank.Int(AcesHigh)+1 +} + +func (hand Cards) containsStraightFlush() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + if hand.containsStraight() && hand.containsFlush() { + return true + } + return false +} + +func (hand Cards) containsRoyalFlush() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + sorted := hand.SortByRank(AcesHigh) + if hand.containsStraightFlush() && sorted[0].Rank == ACE { + return true + } + return false +} + +func (hand Cards) containsFourOfAKind() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + sorted := hand.SortByRank(AcesHigh) + if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[2].Rank == sorted[3].Rank { + return true + } + if sorted[1].Rank == sorted[2].Rank && sorted[2].Rank == sorted[3].Rank && sorted[3].Rank == sorted[4].Rank { + return true + } + return false +} + +func (hand Cards) containsFullHouse() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + sorted := hand.SortByRank(AcesHigh) + if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank { + return true + } + if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank && sorted[3].Rank == sorted[4].Rank { + return true + } + return false +} + +func (hand Cards) containsPair() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + sorted := hand.SortByRank(AcesHigh) + if sorted[0].Rank == sorted[1].Rank { + return true + } + if sorted[1].Rank == sorted[2].Rank { + return true + } + if sorted[2].Rank == sorted[3].Rank { + return true + } + if sorted[3].Rank == sorted[4].Rank { + return true + } + return false +} + +func (hand Cards) containsThreeOfAKind() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + sorted := hand.SortByRank(AcesHigh) + if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank { + return true + } + if sorted[1].Rank == sorted[2].Rank && sorted[2].Rank == sorted[3].Rank { + return true + } + if sorted[2].Rank == sorted[3].Rank && sorted[3].Rank == sorted[4].Rank { + return true + } + return false +} + +func (hand Cards) containsTwoPair() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + sorted := hand.SortByRank(AcesHigh) + if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank { + return true + } + if sorted[0].Rank == sorted[1].Rank && sorted[3].Rank == sorted[4].Rank { + return true + } + if sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank { + return true + } + return false +} + +func (hand Cards) isUnmadeHand() bool { + if !hand.IsFiveCardPokerHand() { + panic("hand must have 5 cards to be scored") + } + return !hand.containsPair() && !hand.containsTwoPair() && !hand.containsThreeOfAKind() && !hand.containsStraight() && !hand.containsFlush() && !hand.containsFullHouse() && !hand.containsFourOfAKind() && !hand.containsStraightFlush() && !hand.containsRoyalFlush() +} + +// this method makes a n new hands where n is the number of cards in the hand +// each of the new hands has one card removed from the original hand +// then it calls the identifyBestFiveCardPokerHand method on each of the new hands +// and returns the best hand by score. this is recursion. +func (hand Cards) identifyBestFiveCardPokerHand() (Cards, error) { + newHands := make([]Cards, len(hand)) + for i := 0; i < len(hand); i++ { + newHand := hand[:i] + newHand = append(newHand, hand[i+1:]...) + newHands[i] = newHand + } + var bestHand Cards + var bestScore HandScore + + for _, h := range newHands { + if h.IsFiveCardPokerHand() { + score, _ := h.ScoreHand() + if score > bestScore { + bestScore = score + bestHand = h + } + } else { + rh, _ := h.identifyBestFiveCardPokerHand() + score, _ := rh.ScoreHand() + if score > bestScore { + bestScore = score + bestHand = rh + } + } + } + return bestHand, nil +} diff --git a/pokercore/scoring_test.go b/pokercore/scoring_test.go new file mode 100644 index 0000000..0a26909 --- /dev/null +++ b/pokercore/scoring_test.go @@ -0,0 +1,181 @@ +package pokercore + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAceLowStraight(t *testing.T) { + hand := Cards{ + AceOfSpades(), + DeuceOfHearts(), + ThreeOfDiamonds(), + FourOfClubs(), + FiveOfSpades(), + } + assert.True(t, hand.containsStraight(), "Expected hand to be a straight") +} + +func TestAceHighStraight(t *testing.T) { + hand := Cards{ + TenOfSpades(), + JackOfHearts(), + KingOfClubs(), + AceOfSpades(), + QueenOfDiamonds(), + } + + assert.True(t, hand.containsStraight(), "Expected hand to be a straight") + + newDeck := NewDeckFromCards(hand) + newDeck.ShuffleDeterministically(123456789) + fmt.Printf("Shuffled deck: %s\n", newDeck.String()) + fmt.Printf("new deck has %d cards\n", newDeck.Count()) + shuffledHand := newDeck.Deal(5) + assert.True(t, shuffledHand.containsStraight(), "Expected hand to still be a straight after shuffle") +} + +func TestOtherStraight(t *testing.T) { + hand := Cards{ + DeuceOfSpades(), + ThreeOfHearts(), + FourOfDiamonds(), + FiveOfClubs(), + SixOfSpades(), + } + assert.True(t, hand.containsStraight(), "Expected hand to be a straight") + + newDeck := NewDeckFromCards(hand) + newDeck.ShuffleDeterministically(123456789) + fmt.Printf("Shuffled deck: %s\n", newDeck.String()) + fmt.Printf("new deck has %d cards\n", newDeck.Count()) + shuffledHand := newDeck.Deal(5) + assert.True(t, shuffledHand.containsStraight(), "Expected hand to still be a straight after shuffle") + assert.False(t, shuffledHand.containsTwoPair(), "Expected hand to not be two pair") + +} + +func TestFlush(t *testing.T) { + hand := Cards{ + AceOfSpades(), + DeuceOfSpades(), + ThreeOfSpades(), + FourOfSpades(), + SixOfSpades(), + } + assert.True(t, hand.containsFlush(), "Expected hand to be a flush") + newDeck := NewDeckFromCards(hand) + newDeck.ShuffleDeterministically(123456789) + fmt.Printf("Shuffled deck: %s\n", newDeck.String()) + fmt.Printf("new deck has %d cards\n", newDeck.Count()) + shuffledHand := newDeck.Deal(5) + assert.True(t, shuffledHand.containsFlush(), "Expected hand to still be a flush after shuffle") + + x := ScoreFlush + var multiplier HandScore = 100 + x += multiplier * DEUCE.HandScore(AcesHigh) + multiplier *= 100 + x += multiplier * THREE.HandScore(AcesHigh) + multiplier *= 100 + x += multiplier * FOUR.HandScore(AcesHigh) + multiplier *= 100 + x += multiplier * SIX.HandScore(AcesHigh) + fmt.Printf("a-2-3-4-6 flush score should be: %d\n", x) +} + +func TestStraightFlush(t *testing.T) { + hand := Cards{ + SixOfSpades(), + DeuceOfSpades(), + ThreeOfSpades(), + FourOfSpades(), + FiveOfSpades(), + } + assert.True(t, hand.containsStraight(), "Expected hand to be a straight") + assert.True(t, hand.containsFlush(), "Expected hand to be a flush") + assert.True(t, hand.containsStraightFlush(), "Expected hand to be a straight flush") + assert.False(t, hand.containsRoyalFlush(), "Expected hand to not be a royal flush") + assert.False(t, hand.containsFourOfAKind(), "Expected hand to not be four of a kind") + assert.False(t, hand.containsFullHouse(), "Expected hand to not be a full house") + assert.False(t, hand.containsThreeOfAKind(), "Expected hand to not be three of a kind") + assert.False(t, hand.containsTwoPair(), "Expected hand to not be two pair") + assert.False(t, hand.containsPair(), "Expected hand to not be a pair") + + assert.True(t, hand.HighestRank(AcesHigh) == SIX, "Expected highest rank to be a six") + + nd := NewDeckFromCards(hand) + nd.ShuffleDeterministically(123456789) + fmt.Printf("Shuffled deck: %s\n", nd.String()) + fmt.Printf("new deck has %d cards\n", nd.Count()) + shuffledHand := nd.Deal(5) + assert.True(t, shuffledHand.containsStraightFlush(), "Expected hand to still be a straight flush after shuffle") + assert.True(t, shuffledHand.HighestRank(AcesHigh) == SIX, "Expected highest rank to still be a six after shuffle") + assert.True(t, shuffledHand.HighestRank(AcesLow) == SIX, "Expected highest rank to be a six after shuffle even with aces low") +} + +func TestRoyalFlush(t *testing.T) { + hand := Cards{ + TenOfSpades(), + JackOfSpades(), + QueenOfSpades(), + KingOfSpades(), + AceOfSpades(), + } + assert.True(t, hand.containsStraight(), "Expected hand to be a straight") + assert.True(t, hand.containsFlush(), "Expected hand to be a flush") + assert.True(t, hand.containsStraightFlush(), "Expected hand to be a straight flush") + assert.True(t, hand.containsRoyalFlush(), "Expected hand to be a royal flush") + + assert.False(t, hand.containsFourOfAKind(), "Expected hand to not be four of a kind") + assert.False(t, hand.containsFullHouse(), "Expected hand to not be a full house") + assert.False(t, hand.containsThreeOfAKind(), "Expected hand to not be three of a kind") + assert.False(t, hand.containsTwoPair(), "Expected hand to not be two pair") + assert.False(t, hand.containsPair(), "Expected hand to not be a pair") + + assert.True(t, hand.HighestRank(AcesHigh) == ACE, "Expected highest rank to be an ace") + assert.False(t, hand.HighestRank(AcesHigh) == TEN, "Expected highest rank to not be an ace") +} + +func TestUnmadeHand(t *testing.T) { + hand := Cards{ + TenOfSpades(), + JackOfDiamonds(), + QueenOfSpades(), + KingOfSpades(), + DeuceOfSpades(), + } + assert.False(t, hand.containsStraight(), "Expected hand to not be a straight") + assert.False(t, hand.containsFlush(), "Expected hand to not be a flush") + assert.False(t, hand.containsStraightFlush(), "Expected hand to not be a straight flush") + assert.False(t, hand.containsRoyalFlush(), "Expected hand to not be a royal flush") + assert.False(t, hand.containsFourOfAKind(), "Expected hand to not be four of a kind") + assert.False(t, hand.containsFullHouse(), "Expected hand to not be a full house") + assert.False(t, hand.containsThreeOfAKind(), "Expected hand to not be three of a kind") + assert.False(t, hand.containsTwoPair(), "Expected hand to not be two pair") + assert.False(t, hand.containsPair(), "Expected hand to not be a pair") + assert.True(t, hand.HighestRank(AcesHigh) == KING, "Expected highest rank to be a king") + assert.True(t, hand.isUnmadeHand(), "Expected hand to be unmade") +} + +func TestTwoPair(t *testing.T) { + hand := Cards{ + KingOfSpades(), + JackOfDiamonds(), + JackOfSpades(), + KingOfDiamonds(), + TenOfSpades(), + } + assert.False(t, hand.containsStraight(), "Expected hand to not be a straight") + assert.False(t, hand.containsFlush(), "Expected hand to not be a flush") + assert.False(t, hand.containsStraightFlush(), "Expected hand to not be a straight flush") + assert.False(t, hand.containsRoyalFlush(), "Expected hand to not be a royal flush") + assert.False(t, hand.containsFourOfAKind(), "Expected hand to not be four of a kind") + assert.False(t, hand.containsFullHouse(), "Expected hand to not be a full house") + assert.False(t, hand.containsThreeOfAKind(), "Expected hand to not be three of a kind") + assert.True(t, hand.containsTwoPair(), "Expected hand to be two pair") + assert.True(t, hand.containsPair(), "Expected hand to also be a pair") + assert.True(t, hand.HighestRank(AcesHigh) == KING, "Expected highest rank to be a king") + assert.False(t, hand.isUnmadeHand(), "Expected hand to not be unmade") +} diff --git a/pokercore/shortcuts.go b/pokercore/shortcuts.go new file mode 100644 index 0000000..3ff926a --- /dev/null +++ b/pokercore/shortcuts.go @@ -0,0 +1,57 @@ +package pokercore + +func AceOfSpades() Card { return Card{ACE, SPADE} } +func DeuceOfSpades() Card { return Card{DEUCE, SPADE} } +func ThreeOfSpades() Card { return Card{THREE, SPADE} } +func FourOfSpades() Card { return Card{FOUR, SPADE} } +func FiveOfSpades() Card { return Card{FIVE, SPADE} } +func SixOfSpades() Card { return Card{SIX, SPADE} } +func SevenOfSpades() Card { return Card{SEVEN, SPADE} } +func EightOfSpades() Card { return Card{EIGHT, SPADE} } +func NineOfSpades() Card { return Card{NINE, SPADE} } +func TenOfSpades() Card { return Card{TEN, SPADE} } +func JackOfSpades() Card { return Card{JACK, SPADE} } +func QueenOfSpades() Card { return Card{QUEEN, SPADE} } +func KingOfSpades() Card { return Card{KING, SPADE} } + +func AceOfHearts() Card { return Card{ACE, HEART} } +func DeuceOfHearts() Card { return Card{DEUCE, HEART} } +func ThreeOfHearts() Card { return Card{THREE, HEART} } +func FourOfHearts() Card { return Card{FOUR, HEART} } +func FiveOfHearts() Card { return Card{FIVE, HEART} } +func SixOfHearts() Card { return Card{SIX, HEART} } +func SevenOfHearts() Card { return Card{SEVEN, HEART} } +func EightOfHearts() Card { return Card{EIGHT, HEART} } +func NineOfHearts() Card { return Card{NINE, HEART} } +func TenOfHearts() Card { return Card{TEN, HEART} } +func JackOfHearts() Card { return Card{JACK, HEART} } +func QueenOfHearts() Card { return Card{QUEEN, HEART} } +func KingOfHearts() Card { return Card{KING, HEART} } + +func AceOfDiamonds() Card { return Card{ACE, DIAMOND} } +func DeuceOfDiamonds() Card { return Card{DEUCE, DIAMOND} } +func ThreeOfDiamonds() Card { return Card{THREE, DIAMOND} } +func FourOfDiamonds() Card { return Card{FOUR, DIAMOND} } +func FiveOfDiamonds() Card { return Card{FIVE, DIAMOND} } +func SixOfDiamonds() Card { return Card{SIX, DIAMOND} } +func SevenOfDiamonds() Card { return Card{SEVEN, DIAMOND} } +func EightOfDiamonds() Card { return Card{EIGHT, DIAMOND} } +func NineOfDiamonds() Card { return Card{NINE, DIAMOND} } +func TenOfDiamonds() Card { return Card{TEN, DIAMOND} } +func JackOfDiamonds() Card { return Card{JACK, DIAMOND} } +func QueenOfDiamonds() Card { return Card{QUEEN, DIAMOND} } +func KingOfDiamonds() Card { return Card{KING, DIAMOND} } + +func AceOfClubs() Card { return Card{ACE, CLUB} } +func DeuceOfClubs() Card { return Card{DEUCE, CLUB} } +func ThreeOfClubs() Card { return Card{THREE, CLUB} } +func FourOfClubs() Card { return Card{FOUR, CLUB} } +func FiveOfClubs() Card { return Card{FIVE, CLUB} } +func SixOfClubs() Card { return Card{SIX, CLUB} } +func SevenOfClubs() Card { return Card{SEVEN, CLUB} } +func EightOfClubs() Card { return Card{EIGHT, CLUB} } +func NineOfClubs() Card { return Card{NINE, CLUB} } +func TenOfClubs() Card { return Card{TEN, CLUB} } +func JackOfClubs() Card { return Card{JACK, CLUB} } +func QueenOfClubs() Card { return Card{QUEEN, CLUB} } +func KingOfClubs() Card { return Card{KING, CLUB} }