diff --git a/Makefile b/Makefile index e5bf920..087bb44 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ default: test test: - cd pokercore && go test -v ./... + go test -v ./... diff --git a/card.go b/card.go new file mode 100644 index 0000000..812147f --- /dev/null +++ b/card.go @@ -0,0 +1,194 @@ +package pokercore + +import ( + "fmt" + "slices" + "sort" + "strings" + + "github.com/logrusorgru/aurora/v4" +) + +type Card struct { + Rank Rank + Suit Suit +} + +func NewRankFromString(rank string) Rank { + switch rank { + case "2": + return Rank(DEUCE) + case "3": + return Rank(THREE) + case "4": + return Rank(FOUR) + case "5": + return Rank(FIVE) + case "6": + return Rank(SIX) + case "7": + return Rank(SEVEN) + case "8": + return Rank(EIGHT) + case "9": + return Rank(NINE) + case "T": + return Rank(TEN) + case "J": + return Rank(JACK) + case "Q": + return Rank(QUEEN) + case "K": + return Rank(KING) + case "A": + return Rank(ACE) + } + return Rank(0) +} + +func NewSuitFromString(suit string) Suit { + switch suit { + case string(SPADE): + return Suit(SPADE) + case string(CLUB): + return Suit(CLUB) + case string(HEART): + return Suit(HEART) + case string(DIAMOND): + return Suit(DIAMOND) + } + return Suit(0) +} + +func NewCardFromString(card string) (Card, error) { + // FIXME extend this later to common format strings like "9s" + if len(card) != 2 { + return Card{}, fmt.Errorf("Invalid card string %s", card) + } + rank := NewRankFromString(card[0:1]) + suit := NewSuitFromString(card[1:2]) + if rank == Rank(0) || suit == Suit(0) { + return Card{}, fmt.Errorf("Invalid card string %s", card) + } + return Card{Rank: rank, Suit: suit}, nil +} + +func NewCardsFromString(cards string) (Cards, error) { + // supports a string like 9♠,9♣,Q♥,Q♦,K♣ + // FIXME extend this later to common format strings like "9c Qh Qd Kc" + // with or without commas + var newCards Cards + cardStrings := strings.Split(cards, ",") + for _, cardString := range cardStrings { + card, err := NewCardFromString(cardString) + if err != nil { + return Cards{}, err + } + newCards = append(newCards, card) + } + if len(newCards) == 0 { + return Cards{}, fmt.Errorf("No cards found in string %s", cards) + } + return newCards, nil +} + +func (c *Card) String() string { + return fmt.Sprintf("%s%s", string(c.Rank), string(c.Suit)) +} + +type Cards []Card + +func (c Cards) First() Card { + return c[0] +} + +func (c Cards) Second() Card { + return c[1] +} + +func (c Cards) Third() Card { + return c[2] +} + +func (c Cards) Fourth() Card { + return c[3] +} + +func (c Cards) Fifth() Card { + return c[4] +} + +func (c Cards) Last() Card { + return c[len(c)-1] +} + +func (c Cards) SortByRankAscending() Cards { + newCards := make(Cards, len(c)) + copy(newCards, c) + sort.Slice(newCards, func(i, j int) bool { + return newCards[i].Rank.Score() < newCards[j].Rank.Score() + }) + + return newCards +} + +func (c Cards) PrintToTerminal() { + fmt.Printf("%s", c.FormatForTerminal()) +} + +type SortOrder int + +const ( + AceHighAscending SortOrder = iota + AceHighDescending +) + +func (c Cards) FormatForTerminalSorted(order SortOrder) string { + sorted := c.SortByRankAscending() // this is ascending + if order == AceHighDescending { + slices.Reverse(sorted) + } + return sorted.FormatForTerminal() +} + +func (c Cards) FormatForTerminal() string { + var cardstrings []string + for i := 0; i < len(c); i++ { + cardstrings = append(cardstrings, c[i].FormatForTerminal()) + } + return strings.Join(cardstrings, ",") +} + +func (c Card) FormatForTerminal() string { + var rank string + var suit string + color := aurora.Red + switch c.Suit { + case Suit(DIAMOND): + color = aurora.Blue + case Suit(HEART): + color = aurora.Red + case Suit(CLUB): + color = aurora.Green + case Suit(SPADE): + color = aurora.Black + } + + rank = fmt.Sprintf("%s", aurora.Bold(color(c.Rank.Symbol()))) + suit = fmt.Sprintf("%s", aurora.Bold(color(c.Suit.Symbol()))) + return fmt.Sprintf("%s%s", rank, suit) +} + +func (c Cards) HighestRank() Rank { + sorted := c.SortByRankAscending() + return sorted[len(sorted)-1].Rank +} + +func (s Cards) String() (output string) { + var cardstrings []string + for i := 0; i < len(s); i++ { + cardstrings = append(cardstrings, s[i].String()) + } + output = strings.Join(cardstrings, ",") + return output +} diff --git a/pokercore/shortcuts.go b/cardshortcuts.go similarity index 100% rename from pokercore/shortcuts.go rename to cardshortcuts.go diff --git a/pokercore/deck.go b/deck.go similarity index 96% rename from pokercore/deck.go rename to deck.go index 1854c91..2ede708 100644 --- a/pokercore/deck.go +++ b/deck.go @@ -85,6 +85,10 @@ func (d *Deck) Deal(n int) (output Cards) { return output } +func (d *Deck) FormatForTerminal() string { + return d.Cards.FormatForTerminal() +} + func (d *Deck) String() string { return fmt.Sprintf("Deck{%s size=%d dealt=%d}", d.Cards, d.Count(), d.Dealt) } diff --git a/examples/searchbig/main.go b/examples/searchbig/main.go new file mode 100644 index 0000000..bd2d899 --- /dev/null +++ b/examples/searchbig/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + + "git.eeqj.de/sneak/pokercore" +) + +func main() { + + for i := 0; i < 10; i++ { + d := pokercore.NewDeck() + fmt.Printf("deck before shuffling: %s\n", d.FormatForTerminal()) + d.ShuffleRandomly() + fmt.Printf("deck after shuffling: %s\n", d.FormatForTerminal()) + offTheTop := d.Deal(11) + fmt.Printf("off the top: %s\n", offTheTop.FormatForTerminal()) + wh, err := offTheTop.IdentifyBestFiveCardPokerHand() + if err != nil { + panic(err) + } + fmt.Printf("best hand: %s\n", wh.FormatForTerminalSorted(pokercore.AceHighAscending)) + score, err := wh.PokerHandScore() + if err != nil { + panic(err) + } + fmt.Printf("best hand score: %s\n", score) + ph, err := wh.PokerHand() + if err != nil { + panic(err) + } + fmt.Printf("best hand string: %s\n", ph.Description()) + } +} diff --git a/examples/searchbig/searchbig b/examples/searchbig/searchbig new file mode 100755 index 0000000..4bfafdf Binary files /dev/null and b/examples/searchbig/searchbig differ diff --git a/examples/sixhand/main.go b/examples/sixhand/main.go new file mode 100644 index 0000000..6a88afa --- /dev/null +++ b/examples/sixhand/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + + "git.eeqj.de/sneak/pokercore" +) + +func main() { + d := pokercore.NewDeck() + fmt.Printf("deck before shuffling: %s\n", d.FormatForTerminal()) + d.ShuffleDeterministically(1337 + 1) // 1337 gives too weird a result + fmt.Printf("deck after shuffling: %s\n", d.FormatForTerminal()) + var players []pokercore.Cards + for i := 0; i < 6; i++ { + players = append(players, d.Deal(2)) + } + for i, p := range players { + fmt.Printf("player %d: %s\n", i, p.FormatForTerminal()) + } + // deal the flop + var community pokercore.Cards + community = d.Deal(3) + fmt.Printf("flop: %s\n", community.FormatForTerminal()) + // evaluate the hands so far + for i, p := range players { + fmt.Printf("player %d: %s\n", i, p.FormatForTerminal()) + thisPlayersHand, err := p.Append(community).PokerHand() + if err != nil { + fmt.Printf("error evaluating hand: %s\n", err) + continue + } + fmt.Printf("player %d: %s\n", i, thisPlayersHand.String()) + } +} diff --git a/examples/sixhand/sixhand b/examples/sixhand/sixhand new file mode 100755 index 0000000..0d44b5b Binary files /dev/null and b/examples/sixhand/sixhand differ diff --git a/pokercore/findhand.go b/findhand.go similarity index 61% rename from pokercore/findhand.go rename to findhand.go index 7a69ec8..fe173d4 100644 --- a/pokercore/findhand.go +++ b/findhand.go @@ -1,24 +1,32 @@ package pokercore -import "errors" +import ( + "errors" +) var ErrDuplicateCard = errors.New("cannot score a poker hand out of a set of cards with duplicates") // 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) { +// and returns the best hand by score. this is recursion and is exponential in time complexity +func (hand Cards) IdentifyBestFiveCardPokerHand() (Cards, error) { + + //fmt.Println("hand: ", hand) if hand.containsDuplicates() { return nil, ErrDuplicateCard } newHands := make([]Cards, len(hand)) for i := 0; i < len(hand); i++ { - newHand := hand[:i] - newHand = append(newHand, hand[i+1:]...) + newHand := make(Cards, len(hand)-1) + copy(newHand, hand[:i]) + copy(newHand[i:], hand[i+1:]) + //fmt.Printf("generating new subset of hand: %+v\n", newHand) + //fmt.Printf("this subset drops the card at index %d: %s\n", i, hand[i].String()) newHands[i] = newHand } + //fmt.Printf("newHands: %+v\n", newHands) var bestHand Cards var bestScore HandScore @@ -30,7 +38,7 @@ func (hand Cards) identifyBestFiveCardPokerHand() (Cards, error) { bestHand = h } } else { - rh, _ := h.identifyBestFiveCardPokerHand() + rh, _ := h.IdentifyBestFiveCardPokerHand() score, _ := rh.PokerHandScore() if score > bestScore { bestScore = score diff --git a/go.mod b/go.mod index 0ad8fc5..5113c5a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.eeqj.de/sneak/go-poker +module git.eeqj.de/sneak/pokercore go 1.22.2 diff --git a/pokercore/handhelpers.go b/handhelpers.go similarity index 91% rename from pokercore/handhelpers.go rename to handhelpers.go index fe51826..57e1dd1 100644 --- a/pokercore/handhelpers.go +++ b/handhelpers.go @@ -33,7 +33,7 @@ func (c Cards) pairRank() Rank { if !c.containsPair() { panic("hand must have a pair to have a pair rank") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank { return sorted[0].Rank } @@ -53,7 +53,7 @@ func (c Cards) pairFirstKicker() Card { if !c.containsPair() { panic("hand must have a pair to have a first kicker") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank { return sorted[4] } @@ -73,7 +73,7 @@ func (c Cards) pairSecondKicker() Card { if !c.containsPair() { panic("hand must have a pair to have a second kicker") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank { // first kicker is [4] return sorted[3] @@ -97,7 +97,7 @@ func (c Cards) pairThirdKicker() Card { if !c.containsPair() { panic("hand must have a pair to have a third kicker") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank { // first kicker is [4] // second kicker is [3] @@ -125,7 +125,7 @@ func (c Cards) twoPairBiggestPair() Rank { if !c.containsTwoPair() { panic("hand must have two pair to have a biggest pair") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank { return sorted[2].Rank } @@ -143,7 +143,7 @@ func (c Cards) twoPairSmallestPair() Rank { if !c.containsTwoPair() { panic("hand must have two pair to have a smallest pair") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank { return sorted[2].Rank } @@ -160,7 +160,7 @@ func (c Cards) twoPairKicker() Card { if !c.containsTwoPair() { panic("hand must have two pair to have a twoPairKicker") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank { return sorted[4] } @@ -177,7 +177,7 @@ func (c Cards) threeOfAKindTripsRank() Rank { if !c.containsThreeOfAKind() { panic("hand must have three of a kind to have a trips rank") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank { return sorted[0].Rank } @@ -194,7 +194,7 @@ func (c Cards) threeOfAKindKickers() Cards { if !c.containsThreeOfAKind() { panic("hand must have three of a kind to have kickers") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank { return Cards{sorted[3], sorted[4]} } @@ -221,7 +221,7 @@ func (c Cards) fourOfAKindRank() Rank { if !c.containsFourOfAKind() { panic("hand must have four of a kind to have a four of a kind rank") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[2].Rank == sorted[3].Rank { return sorted[0].Rank } @@ -235,7 +235,7 @@ func (c Cards) fourOfAKindKicker() Card { if !c.containsFourOfAKind() { panic("hand must have four of a kind to have a four of a kind kicker") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[2].Rank == sorted[3].Rank { return sorted[4] } @@ -249,7 +249,7 @@ func (c Cards) fullHouseTripsRank() Rank { if !c.containsFullHouse() { panic("hand must have a full house to have a trips rank") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank { return sorted[0].Rank } @@ -263,7 +263,7 @@ func (c Cards) fullHousePairRank() Rank { if !c.containsFullHouse() { panic("hand must have a full house to have a pair rank") } - sorted := c.SortByRank() + sorted := c.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank { return sorted[4].Rank } @@ -305,7 +305,7 @@ func (hand Cards) containsStraight() bool { if !hand.IsFiveCardPokerHand() { panic("hand must have 5 cards to be scored") } - sorted := hand.SortByRank() + sorted := hand.SortByRankAscending() if sorted[4].Rank == ACE && sorted[3].Rank == FIVE { // special case for A-5 straight @@ -335,7 +335,13 @@ func (hand Cards) containsRoyalFlush() bool { if !hand.IsFiveCardPokerHand() { panic("hand must have 5 cards to be scored") } - if hand.containsStraightFlush() && hand.HighestRank() == ACE { + // This seems like it works, but a five-high straight flush is not a royal flush + // and the highest ranked card in five-high straigh flush is an ace. + //if hand.containsStraightFlush() && hand.HighestRank() == ACE { + // return true + //} + sorted := hand.SortByRankAscending() + if hand.containsStraightFlush() && hand.HighestRank() == ACE && sorted[0].Rank == TEN { return true } return false @@ -345,7 +351,7 @@ func (hand Cards) containsFourOfAKind() bool { if !hand.IsFiveCardPokerHand() { panic("hand must have 5 cards to be scored") } - sorted := hand.SortByRank() + sorted := hand.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[2].Rank == sorted[3].Rank { // the quads precede the kicker return true @@ -361,7 +367,7 @@ func (hand Cards) containsFullHouse() bool { if !hand.IsFiveCardPokerHand() { panic("hand must have 5 cards to be scored") } - sorted := hand.SortByRank() + sorted := hand.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank { // the trips precede the pair @@ -378,7 +384,7 @@ func (hand Cards) containsPair() bool { if !hand.IsFiveCardPokerHand() { panic("hand must have 5 cards to be scored") } - sorted := hand.SortByRank() + sorted := hand.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank { return true } @@ -398,7 +404,7 @@ func (hand Cards) containsThreeOfAKind() bool { if !hand.IsFiveCardPokerHand() { panic("hand must have 5 cards to be scored") } - sorted := hand.SortByRank() + sorted := hand.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank { return true } @@ -415,7 +421,7 @@ func (hand Cards) containsTwoPair() bool { if !hand.IsFiveCardPokerHand() { panic("hand must have 5 cards to be scored") } - sorted := hand.SortByRank() + sorted := hand.SortByRankAscending() if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank { return true } diff --git a/main.go b/main.go deleted file mode 100644 index f1023ab..0000000 --- a/main.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io" - "net/http" - "net/rpc" - "net/rpc/jsonrpc" - "os" - "time" - - log "github.com/sirupsen/logrus" -) - -type JSONRPCServer struct { - *rpc.Server -} - -func NewJSONRPCServer() *JSONRPCServer { - return &JSONRPCServer{rpc.NewServer()} -} - -func (s *JSONRPCServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { - log.Println("rpc server got a request") - conn, _, err := w.(http.Hijacker).Hijack() - if err != nil { - log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error()) - return - } - io.WriteString(conn, "HTTP/1.0 200 Connected to Go JSON-RPC\n\n") - codec := jsonrpc.NewServerCodec(conn) - log.Println("ServeCodec") - s.Server.ServeCodec(codec) - log.Println("finished serving request") -} - -type Args struct { - A, B int -} - -type Quotient struct { - Quo, Rem int -} - -type Arith int - -func (t *Arith) Multiply(args *Args, reply *int) error { - *reply = args.A * args.B - return nil -} - -func (t *Arith) Divide(args *Args, quo *Quotient) error { - if args.B == 0 { - return errors.New("divide by zero") - } - quo.Quo = args.A / args.B - quo.Rem = args.A % args.B - return nil -} - -func main() { - //log.SetFormatter(&log.JSONFormatter{}) - - // Output to stdout instead of the default stderr - // Can be any io.Writer, see below for File example - log.SetOutput(os.Stdout) - - // Only log the warning severity or above. - //log.SetLevel(log.WarnLevel) - - log.Infof("starting up") - - go runHttpServer() - - running := true - - for running { - time.Sleep(1 * time.Second) - } -} - -func runHttpServer() { - js := NewJSONRPCServer() - arith := new(Arith) - js.Register(arith) - - port := 8080 - - listenaddr := fmt.Sprintf("0.0.0.0:%d", port) - - s := &http.Server{ - Addr: listenaddr, - Handler: js, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - log.Infof("starting up http server %s", listenaddr) - log.Fatal(s.ListenAndServe()) -} diff --git a/misc/generateTestSuite.go b/misc/generateTestSuite.go index e130cc9..ce15afc 100644 --- a/misc/generateTestSuite.go +++ b/misc/generateTestSuite.go @@ -3,7 +3,7 @@ package main import ( "fmt" - "git.eeqj.de/sneak/go-poker/pokercore" + "git.eeqj.de/sneak/pokercore" ) func main() { diff --git a/pokercore.go b/pokercore.go new file mode 100644 index 0000000..ba0fc4c --- /dev/null +++ b/pokercore.go @@ -0,0 +1,23 @@ +package pokercore + +import ( + "encoding/binary" + + crand "crypto/rand" + + log "github.com/sirupsen/logrus" +) + +type TestGenerationIteration struct { + Deck *Deck + Seed int64 +} + +func cryptoUint64() (v uint64) { + err := binary.Read(crand.Reader, binary.BigEndian, &v) + if err != nil { + log.Fatal(err) + } + log.Debugf("crand cryptosource is returning Uint64: %d", v) + return v +} diff --git a/pokercore/pokercore.go b/pokercore/pokercore.go deleted file mode 100644 index f0d8641..0000000 --- a/pokercore/pokercore.go +++ /dev/null @@ -1,173 +0,0 @@ -package pokercore - -import ( - "encoding/binary" - "fmt" - "sort" - - crand "crypto/rand" - - "github.com/logrusorgru/aurora/v4" - - log "github.com/sirupsen/logrus" - - "strings" -) - -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' - DEUCE Rank = '2' - THREE Rank = '3' - FOUR Rank = '4' - FIVE Rank = '5' - SIX Rank = '6' - SEVEN Rank = '7' - EIGHT Rank = '8' - NINE Rank = '9' - TEN Rank = 'T' - JACK Rank = 'J' - QUEEN Rank = 'Q' - KING Rank = 'K' -) - -type Card struct { - Rank Rank - Suit Suit -} - -type Cards []Card - -func (r Rank) Int() int { - return int(r.Score()) -} - -func (r Rank) HandScore() HandScore { - return HandScore(r.Int()) -} - -func (c Cards) SortByRank() Cards { - newCards := make(Cards, len(c)) - copy(newCards, c) - sort.Slice(newCards, func(i, j int) bool { - return newCards[i].Rank.Score() > newCards[j].Rank.Score() - }) - 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, c[i].FormatForTerminal()) - } - return strings.Join(cardstrings, ",") -} - -func (c Card) FormatForTerminal() string { - var rank string - var suit string - color := aurora.Red - switch c.Suit { - case Suit(DIAMOND): - color = aurora.Blue - case Suit(HEART): - color = aurora.Red - case Suit(CLUB): - color = aurora.Green - case Suit(SPADE): - color = aurora.Black - } - - 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) { - err := binary.Read(crand.Reader, binary.BigEndian, &v) - if err != nil { - log.Fatal(err) - } - log.Debugf("crand cryptosource is returning Uint64: %d", v) - return v -} - -func (c *Card) String() string { - return fmt.Sprintf("%s%s", string(c.Rank), string(c.Suit)) -} - -func (c Cards) HighestRank() Rank { - c = c.SortByRank() - return c[0].Rank -} - -func (s Cards) String() (output string) { - var cardstrings []string - for i := 0; i < len(s); i++ { - cardstrings = append(cardstrings, s[i].String()) - } - output = strings.Join(cardstrings, ",") - return output -} - -func generate() { - log.SetLevel(log.DebugLevel) -} - -func proto() { - myDeck := NewDeck() - myDeck.ShuffleDeterministically(42) - myHand := myDeck.Deal(2) - //spew.Dump(myHand) - fmt.Printf("my hand: %s\n", myHand) - cmty := myDeck.Deal(5) - fmt.Printf("community: %s\n", cmty) -} - -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/scoring.go b/pokercore/scoring.go deleted file mode 100644 index 06ae6ef..0000000 --- a/pokercore/scoring.go +++ /dev/null @@ -1,147 +0,0 @@ -package pokercore - -import "fmt" - -type HandScore int - -const ( - ScoreHighCard = HandScore(iota * 100_000_000_000) - ScorePair - ScoreTwoPair - ScoreThreeOfAKind - ScoreStraight - ScoreFlush - ScoreFullHouse - ScoreFourOfAKind - ScoreStraightFlush - ScoreRoyalFlush -) - -func (c Card) Score() HandScore { - return HandScore(c.Rank.Score()) -} - -func (c Cards) PokerHandScore() (HandScore, error) { - if len(c) != 5 { - return 0, fmt.Errorf("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 scoreToName(x HandScore) string { - switch x { - case x >= ScoreRoyalFlush: - return "Royal Flush" - case x >= ScoreStraightFlush: - return "Straight Flush" - case x >= ScoreFourOfAKind: - return "Four of a Kind" - case x >= ScoreFullHouse: - return "Full House" - case x >= ScoreFlush: - return "Flush" - case x >= ScoreStraight: - return "Straight" - case x >= ScoreThreeOfAKind: - return "Three of a Kind" - case x >= ScoreTwoPair: - return "Two Pair" - case x >= ScorePair: - return "Pair" - case x >= ScoreHighCard: - return "High Card" - } -} -*/ - -func (r Rank) Article() string { - switch r { - case ACE: - return "an" - case EIGHT: - return "an" - default: - return "a" - } -} - -func (r Rank) WithArticle() string { - return fmt.Sprintf("%s %s", r.Article(), r) -} - -func (r Rank) Pluralize() string { - switch r { - case ACE: - return "Aces" - case DEUCE: - return "Deuces" - case THREE: - return "Threes" - case FOUR: - return "Fours" - case FIVE: - return "Fives" - case SIX: - return "Sixes" - case SEVEN: - return "Sevens" - case EIGHT: - return "Eights" - case NINE: - return "Nines" - case TEN: - return "Tens" - case JACK: - return "Jacks" - case QUEEN: - return "Queens" - case KING: - return "Kings" - default: - panic("nope") - } -} - -func (x HandScore) String() string { - return fmt.Sprintf("", x) - //return scoreToName(x) -} - -func (r Rank) Score() HandScore { - switch r { - 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 - case ACE: - return 14 - } - return 0 -} diff --git a/pokercore/pokercore_test.go b/pokercore_test.go similarity index 50% rename from pokercore/pokercore_test.go rename to pokercore_test.go index 80dbd88..51e7355 100644 --- a/pokercore/pokercore_test.go +++ b/pokercore_test.go @@ -49,3 +49,42 @@ func TestDealing(t *testing.T) { x := d.Count() assert.Equal(t, 0, x) } + +func TestSpecialCaseOfFiveHighStraightFlush(t *testing.T) { + // actual bug from first implementation + 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()) + ph, err := cards.PokerHand() + assert.Nil(t, err) + description := ph.Description() + assert.Equal(t, "a five high straight flush in ♥", description) +} + +func TestSpecialCaseOfFiveHighStraight(t *testing.T) { + // actual bug from first implementation + d := NewDeckFromCards(Cards{ + Card{Rank: ACE, Suit: HEART}, + Card{Rank: DEUCE, Suit: HEART}, + Card{Rank: THREE, Suit: SPADE}, + Card{Rank: FOUR, Suit: HEART}, + Card{Rank: FIVE, Suit: CLUB}, + }) + d.ShuffleDeterministically(123456789) + cards := d.Deal(5) + + cards = cards.SortByRankAscending() + expected := "2♥,3♠,4♥,5♣,A♥" + assert.Equal(t, expected, cards.String()) + ph, err := cards.PokerHand() + assert.Nil(t, err) + description := ph.Description() + assert.Equal(t, "a five high straight", description) +} diff --git a/pokercore/pokerhand.go b/pokerhand.go similarity index 71% rename from pokercore/pokerhand.go rename to pokerhand.go index f7b384d..eed5969 100644 --- a/pokercore/pokerhand.go +++ b/pokerhand.go @@ -23,6 +23,10 @@ type PokerHand struct { Score HandScore } +func (c Cards) Append(other Cards) Cards { + return append(c, other...) +} + 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") @@ -65,7 +69,19 @@ func (c Cards) PokerHand() (*PokerHand, error) { if c.containsStraight() { ph.Type = Straight - ph.Score = ScoreStraight + 1000*c.HighestRank().Score() + + // 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() + + if sorted[3].Rank == FIVE && sorted[4].Rank == ACE { + // 5-high straight + ph.Score = ScoreStraight + 1000*sorted[3].Score() + } else { + // All other straights + ph.Score = ScoreStraight + 1000*sorted[4].Score() + } return ph, nil } @@ -102,17 +118,40 @@ func (c Cards) PokerHand() (*PokerHand, error) { return ph, nil } -func (c PokerHand) HighestRank() Rank { - return c.Hand.HighestRank() +func (ph PokerHand) ToSortedCards() Cards { + sorted := ph.Hand.SortByRankAscending() + return sorted } -func (c PokerHand) String() string { - sortedHand := c.Hand.SortByRank() +func (ph PokerHand) Compare(other PokerHand) int { + if ph.Score > other.Score { + return 1 + } + if ph.Score < other.Score { + return -1 + } + return 0 +} + +func (ph PokerHand) HighestRank() Rank { + return ph.Hand.HighestRank() +} + +func (ph PokerHand) String() string { + return fmt.Sprintf("", ph.Hand.String(), ph.Description()) +} + +func (c PokerHand) Description() string { + sortedHand := c.Hand.SortByRankAscending() if c.Type == RoyalFlush { return fmt.Sprintf("a royal flush in %s", c.Hand[0].Suit) } if c.Hand.containsStraightFlush() { - return fmt.Sprintf("%s high straight flush in %s", c.HighestRank().WithArticle(), sortedHand[0].Suit) + if sortedHand[3].Rank == FIVE && sortedHand[4].Rank == ACE { + // special case for steel wheel + return fmt.Sprintf("%s high straight flush in %s", sortedHand[3].Rank.WithArticle(), sortedHand[4].Suit) + } + return fmt.Sprintf("%s high straight flush in %s", c.HighestRank().WithArticle(), sortedHand[4].Suit) } if c.Hand.containsFourOfAKind() { return fmt.Sprintf("four %s with %s", c.Hand.fourOfAKindRank().Pluralize(), c.Hand.fourOfAKindKicker().Rank.WithArticle()) @@ -121,9 +160,13 @@ func (c PokerHand) String() string { return fmt.Sprintf("a full house, %s full of %s", c.Hand.fullHouseTripsRank().Pluralize(), c.Hand.fullHousePairRank().Pluralize()) } if c.Hand.containsFlush() { - return fmt.Sprintf("%s high flush in %s", c.HighestRank().WithArticle(), sortedHand[0].Suit) + return fmt.Sprintf("%s high flush in %s", c.HighestRank().WithArticle(), sortedHand[4].Suit) } if c.Hand.containsStraight() { + if sortedHand[3].Rank == FIVE && sortedHand[4].Rank == ACE { + // special case for wheel straight + return fmt.Sprintf("%s high straight", sortedHand[3].Rank.WithArticle()) + } return fmt.Sprintf("%s high straight", c.HighestRank().WithArticle()) } if c.Hand.containsThreeOfAKind() { @@ -151,6 +194,7 @@ func (c PokerHand) String() string { c.Hand.pairThirdKicker().Rank.WithArticle(), ) } + return fmt.Sprintf( // "ace high with an eight, a seven, a six, and a deuce" "%s high with %s, %s, %s, and %s", diff --git a/rank.go b/rank.go new file mode 100644 index 0000000..990a412 --- /dev/null +++ b/rank.go @@ -0,0 +1,145 @@ +package pokercore + +import "fmt" + +type Rank rune + +const ( + ACE Rank = 'A' + DEUCE Rank = '2' + THREE Rank = '3' + FOUR Rank = '4' + FIVE Rank = '5' + SIX Rank = '6' + SEVEN Rank = '7' + EIGHT Rank = '8' + NINE Rank = '9' + TEN Rank = 'T' + JACK Rank = 'J' + QUEEN Rank = 'Q' + KING Rank = 'K' +) + +func (r Rank) String() string { + switch r { + case ACE: + return "ace" + case DEUCE: + return "deuce" + case THREE: + return "three" + case FOUR: + return "four" + case FIVE: + return "five" + case SIX: + return "six" + case SEVEN: + return "seven" + case EIGHT: + return "eight" + case NINE: + return "nine" + case TEN: + return "ten" + case JACK: + return "jack" + case QUEEN: + return "queen" + case KING: + return "king" + } + return "" +} + +func (r Rank) Symbol() string { + return string(r) +} + +func (r Rank) Int() int { + return int(r.Score()) +} + +func (r Rank) HandScore() HandScore { + return HandScore(r.Int()) +} + +func (r Rank) Article() string { + switch r { + case ACE: + return "an" + case EIGHT: + return "an" + default: + return "a" + } +} + +func (r Rank) WithArticle() string { + return fmt.Sprintf("%s %s", r.Article(), r) +} + +func (r Rank) Pluralize() string { + switch r { + case ACE: + return "aces" + case DEUCE: + return "deuces" + case THREE: + return "threes" + case FOUR: + return "fours" + case FIVE: + return "fives" + case SIX: + return "sixes" + case SEVEN: + return "sevens" + case EIGHT: + return "eights" + case NINE: + return "nines" + case TEN: + return "tens" + case JACK: + return "jacks" + case QUEEN: + return "queens" + case KING: + return "kings" + default: + panic("nope") + } +} + +func (r Rank) Score() HandScore { + switch r { + 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 + case ACE: + return 14 + } + return 0 +} diff --git a/scoring.go b/scoring.go new file mode 100644 index 0000000..7d10d17 --- /dev/null +++ b/scoring.go @@ -0,0 +1,39 @@ +package pokercore + +import "fmt" + +type HandScore int + +const ( + ScoreHighCard = HandScore(iota * 100_000_000_000) + ScorePair + ScoreTwoPair + ScoreThreeOfAKind + ScoreStraight + ScoreFlush + ScoreFullHouse + ScoreFourOfAKind + ScoreStraightFlush + ScoreRoyalFlush +) + +func (c Card) Score() HandScore { + return HandScore(c.Rank.Score()) +} + +func (c Cards) PokerHandScore() (HandScore, error) { + if len(c) != 5 { + return 0, fmt.Errorf("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/pokercore/scoring_test.go b/scoring_test.go similarity index 77% rename from pokercore/scoring_test.go rename to scoring_test.go index ac26b77..5561520 100644 --- a/pokercore/scoring_test.go +++ b/scoring_test.go @@ -16,6 +16,19 @@ func TestAceLowStraight(t *testing.T) { FiveOfSpades(), } 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.Less(t, ph.Score, 100000000000000000, "Expected score to be less than 100000000000000000") + assert.Equal(t, ph.Score, ScoreStraight+100*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") + assert.True(t, hand.SortByRankAscending().Last().Rank == ACE, "Expected last card in sorted to be a ace") + assert.True(t, hand.SortByRankAscending().Second().Rank == THREE, "Expected second card to be a three") + assert.True(t, hand.SortByRankAscending().Third().Rank == FOUR, "Expected third card to be a four") + assert.True(t, hand.SortByRankAscending().Fourth().Rank == FIVE, "Expected fourth card to be a five") + assert.True(t, hand.SortByRankAscending().Fifth().Rank == ACE, "Expected fifth card to be an ace") } func TestAceHighStraight(t *testing.T) { @@ -28,13 +41,17 @@ func TestAceHighStraight(t *testing.T) { } 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.True(t, shuffledHand.HighestRank() == ACE, "Expected highest rank to be an ace") + sortedHand := shuffledHand.SortByRankAscending() + assert.True(t, sortedHand[0].Rank == TEN, "Expected lowest rank to be a ten") + assert.True(t, sortedHand[1].Rank == JACK, "Expected second lowest rank to be a jack") + assert.True(t, sortedHand[2].Rank == QUEEN, "Expected third lowest rank to be a queen") + assert.True(t, sortedHand[3].Rank == KING, "Expected fourth lowest rank to be a king") + assert.True(t, sortedHand[4].Rank == ACE, "Expected highest rank to be an ace") } func TestOtherStraight(t *testing.T) { @@ -197,3 +214,18 @@ func TestHandScore(t *testing.T) { fmt.Printf("PokerHand: %v+\n", ph) fmt.Printf("PH score: %d\n", ph.Score) } + +func TestTwoPairBug(t *testing.T) { + // this is an actual bug in the first implementation + c, err := NewCardsFromString("9♠,9♣,Q♥,Q♦,K♣") + assert.Nil(t, err, "Expected no error") + + ph, err := c.PokerHand() + assert.Nil(t, err, "Expected no error") + assert.Greater(t, ph.Score, 0, "Expected score to be nonzero 0") + desc := ph.Description() + assert.Equal(t, desc, "two pair, queens and nines with a king") + + fmt.Printf("PokerHand: %v+\n", ph) + fmt.Printf("PH score: %d\n", ph.Score) +} diff --git a/suit.go b/suit.go new file mode 100644 index 0000000..15996c4 --- /dev/null +++ b/suit.go @@ -0,0 +1,19 @@ +package pokercore + +type Suit rune + +func (s Suit) String() string { + return string(s) +} + +func (s Suit) Symbol() string { + // this is just to match Rank's Symbol() method + return string(s) +} + +const ( + CLUB Suit = '\u2663' // ♣ + SPADE Suit = '\u2660' // ♠ + DIAMOND Suit = '\u2666' // ♦ + HEART Suit = '\u2665' // ♥ +)