latest, found a bug, wrote a failing test

This commit is contained in:
Jeffrey Paul 2024-05-18 17:33:15 -07:00
parent 90a959448a
commit eb85e1d36e
22 changed files with 661 additions and 460 deletions

View File

@ -1,4 +1,4 @@
default: test
test:
cd pokercore && go test -v ./...
go test -v ./...

194
card.go Normal file
View File

@ -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
}

View File

@ -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)
}

View File

@ -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())
}
}

BIN
examples/searchbig/searchbig Executable file

Binary file not shown.

35
examples/sixhand/main.go Normal file
View File

@ -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())
}
}

BIN
examples/sixhand/sixhand Executable file

Binary file not shown.

View File

@ -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

2
go.mod
View File

@ -1,4 +1,4 @@
module git.eeqj.de/sneak/go-poker
module git.eeqj.de/sneak/pokercore
go 1.22.2

View File

@ -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
}

101
main.go
View File

@ -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())
}

View File

@ -3,7 +3,7 @@ package main
import (
"fmt"
"git.eeqj.de/sneak/go-poker/pokercore"
"git.eeqj.de/sneak/pokercore"
)
func main() {

23
pokercore.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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("<HandScore %d>", 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
}

View File

@ -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)
}

View File

@ -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("<PokerHand: %s (%s)>", 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",

145
rank.go Normal file
View File

@ -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
}

39
scoring.go Normal file
View File

@ -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("<HandScore %d>", x)
//return scoreToName(x)
}

View File

@ -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)
}

19
suit.go Normal file
View File

@ -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' // ♥
)