Compare commits

...

12 Commits

16 changed files with 853 additions and 327 deletions

42
.gitignore vendored
View File

@ -5,9 +5,45 @@
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Go workspace file
go.work
go.work.sum
# Dependency directories
vendor/
# Go build artifacts
bin/
build/
dist/
# Configuration files for editors and tools
.vscode/
.idea/
*.swp
*.swo
# Logs
*.log
# Temporary files
*.tmp
*.temp
*.bak
# Test binary, coverage, and results
*.test
*.cover
*.cov
*.profile
*.prof
# OS-specific files
.DS_Store
Thumbs.db
# i don't know what keeps creating this file
examples/simgame/simgame

View File

@ -1,4 +1,18 @@
default: test
.PHONY: examples
default: run
run: simgame
simgame: examples
RANDOM_SHUFFLE=1 ./bin/simgame
clean:
rm -rf bin *.test
test:
go test -v ./...
go test -count=1 .
examples:
test -d bin || mkdir bin
go build -o bin/ ./examples/...

97
card.go
View File

@ -5,6 +5,7 @@ import (
"slices"
"sort"
"strings"
"unicode/utf8"
"github.com/logrusorgru/aurora/v4"
)
@ -14,59 +15,56 @@ type Card struct {
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 {
length := utf8.RuneCountInString(card)
if length != 2 {
return Card{}, fmt.Errorf("Invalid card string %s", card)
}
rank := NewRankFromString(card[0:1])
suit := NewSuitFromString(card[1:2])
var rank Rank
var suit Suit
if strings.ContainsRune(card, rune(SPADE)) {
suit = Suit(SPADE)
} else if strings.ContainsRune(card, rune(HEART)) {
suit = Suit(HEART)
} else if strings.ContainsRune(card, rune(DIAMOND)) {
suit = Suit(DIAMOND)
} else if strings.ContainsRune(card, rune(CLUB)) {
suit = Suit(CLUB)
} else {
return Card{}, fmt.Errorf("Invalid card string %s", card)
}
if strings.ContainsRune(card, rune(DEUCE)) {
rank = Rank(DEUCE)
} else if strings.ContainsRune(card, rune(THREE)) {
rank = Rank(THREE)
} else if strings.ContainsRune(card, rune(FOUR)) {
rank = Rank(FOUR)
} else if strings.ContainsRune(card, rune(FIVE)) {
rank = Rank(FIVE)
} else if strings.ContainsRune(card, rune(SIX)) {
rank = Rank(SIX)
} else if strings.ContainsRune(card, rune(SEVEN)) {
rank = Rank(SEVEN)
} else if strings.ContainsRune(card, rune(EIGHT)) {
rank = Rank(EIGHT)
} else if strings.ContainsRune(card, rune(NINE)) {
rank = Rank(NINE)
} else if strings.ContainsRune(card, rune(TEN)) {
rank = Rank(TEN)
} else if strings.ContainsRune(card, rune(JACK)) {
rank = Rank(JACK)
} else if strings.ContainsRune(card, rune(QUEEN)) {
rank = Rank(QUEEN)
} else if strings.ContainsRune(card, rune(KING)) {
rank = Rank(KING)
} else if strings.ContainsRune(card, rune(ACE)) {
rank = Rank(ACE)
} else {
return Card{}, fmt.Errorf("Invalid card string %s", card)
}
if rank == Rank(0) || suit == Suit(0) {
return Card{}, fmt.Errorf("Invalid card string %s", card)
}
@ -78,6 +76,7 @@ func NewCardsFromString(cards string) (Cards, error) {
// FIXME extend this later to common format strings like "9c Qh Qd Kc"
// with or without commas
var newCards Cards
newCards = make(Cards, 0)
cardStrings := strings.Split(cards, ",")
for _, cardString := range cardStrings {
card, err := NewCardFromString(cardString)

Binary file not shown.

170
examples/simgame/main.go Normal file
View File

@ -0,0 +1,170 @@
package main
import (
"fmt"
"os"
"time"
"git.eeqj.de/sneak/pokercore"
)
// just like on tv
var delayTime = 2 * time.Second
var playerCount = 8
type Player struct {
Hand pokercore.Cards
ScoredHand *pokercore.PokerHand
Position int
}
type Game struct {
Deck *pokercore.Deck
Players []*Player
Community pokercore.Cards
Street int
}
func suspense() {
fmt.Printf("########################################\n")
if os.Getenv("NO_LAG") == "" {
time.Sleep(delayTime)
}
}
func NewGame() *Game {
// this "randomly chosen" seed somehow deals pocket queens, pocket kings, and pocket aces to three players.
// what are the odds? lol
//g := NewGame(1337))
// nothing up my sleeve:
seed := 3141592653
g := &Game{}
g.Street = 0
g.Deck = pokercore.NewDeck()
g.SpreadCards()
fmt.Printf("Shuffle up and deal!\n")
suspense()
fmt.Printf("Shuffling...\n")
if os.Getenv("RANDOM_SHUFFLE") != "" {
g.Deck.ShuffleRandomly()
} else {
fmt.Printf("Using deterministic shuffle seed: %d\n", seed)
g.Deck.ShuffleDeterministically(int64(seed))
}
return g
}
func (g *Game) StreetAsString() string {
switch g.Street {
case 0:
return "pre-flop"
case 1:
return "flop"
case 2:
return "turn"
case 3:
return "river"
}
return "unknown"
}
func (g *Game) SpreadCards() {
fmt.Printf("deck before shuffling: %s\n", g.Deck.FormatForTerminal())
}
func (g *Game) DealPlayersIn() {
for i := 0; i < playerCount; i++ {
p := Player{Hand: g.Deck.Deal(2), Position: i + 1}
g.Players = append(g.Players, &p)
}
}
func (g *Game) ShowGameStatus() {
fmt.Printf("Street: %s\n", g.StreetAsString())
fmt.Printf("Community cards: %s\n", g.Community.FormatForTerminal())
for _, p := range g.Players {
if p != nil {
fmt.Printf("Player %d: %s\n", p.Position, p.Hand.FormatForTerminal())
if g.Street > 0 {
ac := append(p.Hand, g.Community...)
ph, err := ac.PokerHand()
if err != nil {
panic(err)
}
fmt.Printf("Player %d has %s\n", p.Position, ph.Description())
fmt.Printf("Player %d Score: %d\n", p.Position, ph.Score)
}
}
}
}
func (g *Game) DealFlop() {
g.Community = g.Deck.Deal(3)
g.Street = 1
}
func (g *Game) DealTurn() {
g.Community = append(g.Community, g.Deck.Deal(1)...)
g.Street = 2
}
func (g *Game) DealRiver() {
g.Community = append(g.Community, g.Deck.Deal(1)...)
g.Street = 3
}
func (g *Game) ShowWinner() {
var winner *Player
var winningHand *pokercore.PokerHand
for _, p := range g.Players {
if p != nil {
ac := append(p.Hand, g.Community...)
ph, err := ac.PokerHand()
if err != nil {
panic(err)
}
if winner == nil {
winner = p
winningHand = ph
} else {
if ph.Score > winningHand.Score {
winner = p
winningHand = ph
}
}
}
}
fmt.Printf("Winner: Player %d with %s.\n", winner.Position, winningHand.Description())
}
func main() {
g := NewGame()
g.ShowGameStatus()
g.DealPlayersIn()
suspense()
g.ShowGameStatus()
g.DealFlop()
suspense()
g.ShowGameStatus()
g.DealTurn()
suspense()
g.ShowGameStatus()
g.DealRiver()
suspense()
g.ShowGameStatus()
g.ShowWinner()
fmt.Printf("What a strange game. The only winning move is to bet really big.\n")
fmt.Printf("Insert coin to play again.\n")
}

View File

@ -1,35 +0,0 @@
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())
}
}

Binary file not shown.

View File

@ -17,6 +17,14 @@ func (hand Cards) IdentifyBestFiveCardPokerHand() (Cards, error) {
return nil, ErrDuplicateCard
}
if len(hand) == 5 {
return hand, nil
}
if len(hand) < 5 {
return nil, errors.New("hand must have at least 5 cards to identify the best 5 card poker hand")
}
newHands := make([]Cards, len(hand))
for i := 0; i < len(hand); i++ {
newHand := make(Cards, len(hand)-1)

23
go.mod
View File

@ -4,14 +4,33 @@ go 1.22.2
require (
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/logrusorgru/aurora/v4 v4.0.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.0
)
require (
git.eeqj.de/sneak/timingbench v0.0.0-20240519025145-fb13c5c56a02 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/logrusorgru/aurora/v4 v4.0.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/k0kubun/pp/v3 v3.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.33.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/schollz/progressbar/v3 v3.14.2 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.20.0 // indirect
gonum.org/v1/gonum v0.15.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

108
go.sum
View File

@ -1,24 +1,132 @@
git.eeqj.de/sneak/timingbench v0.0.0-20240519025145-fb13c5c56a02 h1:b/v1EDAlsfvINIeV4znI/vH7SY7mUJOO1KWeBD+IW90=
git.eeqj.de/sneak/timingbench v0.0.0-20240519025145-fb13c5c56a02/go.mod h1:iKAlgt/liDtXifmn7fPJK+KYMr0c4lXYFJ+j5d3gfEQ=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs=
github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA=
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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.14.2 h1:EducH6uNLIWsr560zSV1KrTeUb/wZGAHqyMFIEa99ks=
github.com/schollz/progressbar/v3 v3.14.2/go.mod h1:aQAZQnhF4JGFtRJiw/eobaXpsqpVQAftEQ+hLGXaRc4=
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=

View File

@ -1,38 +1,12 @@
package pokercore
func (c Cards) scoreStraightFlush() HandScore {
if !c.containsStraightFlush() {
panic("hand must be a straight flush to score it")
}
return ScoreStraightFlush + 1000*c.HighestRank().Score()
}
func (c Cards) scoreFlush() HandScore {
if !c.containsFlush() {
panic("hand must be a flush to score it")
}
var score HandScore
for _, card := range c {
score += card.Rank.Score()
}
return ScoreFlush + score
}
func (c Cards) scoreHighCard() HandScore {
if !c.isUnmadeHand() {
panic("hand must be a high card to score it")
}
var score HandScore
for _, card := range c {
score += card.Rank.Score()
}
return ScoreHighCard + score
}
// these helper functions are used in a Cards.PokerHand() constructor and in
// the calculateScore() function called from it.
// they only work with exactly five cards, no duplicates, and no wild cards.
// they are not intended to be used in any other context, which is why
// they are not exported
func (c Cards) pairRank() Rank {
if !c.containsPair() {
panic("hand must have a pair to have a pair rank")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank {
return sorted[0].Rank
@ -50,9 +24,6 @@ func (c Cards) pairRank() Rank {
}
func (c Cards) pairFirstKicker() Card {
if !c.containsPair() {
panic("hand must have a pair to have a first kicker")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank {
return sorted[4]
@ -70,9 +41,6 @@ func (c Cards) pairFirstKicker() Card {
}
func (c Cards) pairSecondKicker() Card {
if !c.containsPair() {
panic("hand must have a pair to have a second kicker")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank {
// first kicker is [4]
@ -94,9 +62,6 @@ func (c Cards) pairSecondKicker() Card {
}
func (c Cards) pairThirdKicker() Card {
if !c.containsPair() {
panic("hand must have a pair to have a third kicker")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank {
// first kicker is [4]
@ -122,14 +87,25 @@ func (c Cards) pairThirdKicker() Card {
}
func (c Cards) twoPairBiggestPair() Rank {
if !c.containsTwoPair() {
panic("hand must have two pair to have a biggest pair")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank {
return sorted[2].Rank
}
if sorted[0].Rank == sorted[1].Rank && sorted[3].Rank == sorted[4].Rank {
return sorted[3].Rank
}
if sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank {
return sorted[3].Rank
}
panic("nope")
}
func (c Cards) twoPairSmallestPair() Rank {
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank {
return sorted[0].Rank
}
if sorted[0].Rank == sorted[1].Rank && sorted[3].Rank == sorted[4].Rank {
return sorted[0].Rank
}
@ -139,27 +115,7 @@ func (c Cards) twoPairBiggestPair() Rank {
panic("nope")
}
func (c Cards) twoPairSmallestPair() Rank {
if !c.containsTwoPair() {
panic("hand must have two pair to have a smallest pair")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank {
return sorted[2].Rank
}
if sorted[0].Rank == sorted[1].Rank && sorted[3].Rank == sorted[4].Rank {
return sorted[3].Rank
}
if sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank {
return sorted[3].Rank
}
panic("nope")
}
func (c Cards) twoPairKicker() Card {
if !c.containsTwoPair() {
panic("hand must have two pair to have a twoPairKicker")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank {
return sorted[4]
@ -174,9 +130,6 @@ func (c Cards) twoPairKicker() Card {
}
func (c Cards) threeOfAKindTripsRank() Rank {
if !c.containsThreeOfAKind() {
panic("hand must have three of a kind to have a trips rank")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank {
return sorted[0].Rank
@ -191,9 +144,6 @@ func (c Cards) threeOfAKindTripsRank() Rank {
}
func (c Cards) threeOfAKindKickers() Cards {
if !c.containsThreeOfAKind() {
panic("hand must have three of a kind to have kickers")
}
sorted := c.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank {
return Cards{sorted[3], sorted[4]}
@ -284,14 +234,7 @@ func (hand Cards) containsDuplicates() bool {
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 {
@ -302,9 +245,6 @@ func (hand Cards) containsFlush() bool {
}
func (hand Cards) containsStraight() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
sorted := hand.SortByRankAscending()
if sorted[4].Rank == ACE && sorted[3].Rank == FIVE {
@ -322,9 +262,8 @@ func (hand Cards) containsStraight() bool {
}
func (hand Cards) containsStraightFlush() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
// this of course only works on five card hands
// but these hand helpers are only ever called from a five card hand
if hand.containsStraight() && hand.containsFlush() {
return true
}
@ -332,9 +271,6 @@ func (hand Cards) containsStraightFlush() bool {
}
func (hand Cards) containsRoyalFlush() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
// 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 {
@ -348,9 +284,6 @@ func (hand Cards) containsRoyalFlush() bool {
}
func (hand Cards) containsFourOfAKind() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
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
@ -364,9 +297,6 @@ func (hand Cards) containsFourOfAKind() bool {
}
func (hand Cards) containsFullHouse() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
sorted := hand.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank && sorted[3].Rank == sorted[4].Rank {
@ -381,9 +311,6 @@ func (hand Cards) containsFullHouse() bool {
}
func (hand Cards) containsPair() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
sorted := hand.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank {
return true
@ -401,9 +328,6 @@ func (hand Cards) containsPair() bool {
}
func (hand Cards) containsThreeOfAKind() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
sorted := hand.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[1].Rank == sorted[2].Rank {
return true
@ -418,9 +342,6 @@ func (hand Cards) containsThreeOfAKind() bool {
}
func (hand Cards) containsTwoPair() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
sorted := hand.SortByRankAscending()
if sorted[0].Rank == sorted[1].Rank && sorted[2].Rank == sorted[3].Rank {
return true
@ -435,8 +356,6 @@ func (hand Cards) containsTwoPair() bool {
}
func (hand Cards) isUnmadeHand() bool {
if !hand.IsFiveCardPokerHand() {
panic("hand must have 5 cards to be scored")
}
// i suspect this is expensive but we use it only in tests
return !hand.containsPair() && !hand.containsTwoPair() && !hand.containsThreeOfAKind() && !hand.containsStraight() && !hand.containsFlush() && !hand.containsFullHouse() && !hand.containsFourOfAKind() && !hand.containsStraightFlush() && !hand.containsRoyalFlush()
}

79
perf_test.go Normal file
View File

@ -0,0 +1,79 @@
package pokercore
import (
"context"
"testing"
"time"
"git.eeqj.de/sneak/timingbench"
"github.com/stretchr/testify/assert"
)
func TestShuffleSpeed(t *testing.T) {
iterations := 1000
t.Logf("Running %d iterations of shuffle speed test", iterations)
// Create a context with a timeout for cancellation.
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
// Measure the execution times of the sample function.
d := NewDeck()
result, err := timingbench.TimeFunction(ctx, func() { d.ShuffleRandomly() }, iterations)
if err != nil {
t.Fatalf("Error measuring function: %v", err)
}
// Print the timing results.
t.Logf(result.String())
}
func TestHandFindingSpeedFiveCard(t *testing.T) {
iterations := 1000
t.Logf("Running %d iterations of hand finding speed test for 5 card hand", iterations)
measureHandFinding(t, iterations, 5)
}
func TestHandFindingSpeedSevenCard(t *testing.T) {
iterations := 1000
t.Logf("Running %d iterations of hand finding speed test for 7 card hand", iterations)
measureHandFinding(t, iterations, 7)
}
func TestHandFindingSpeedNineCard(t *testing.T) {
iterations := 100
t.Logf("Running %d iterations of hand finding speed test for 9 card hand", iterations)
measureHandFinding(t, iterations, 9)
}
func measureHandFinding(t *testing.T, iterations int, cardCount int) {
// Create a context with a timeout for cancellation.
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
// Measure the execution times of the sample function.
result, err := timingbench.TimeFunction(ctx, func() {
findHandInRandomCards(t, int(123456789), cardCount) // check for hand in 10 cards
}, iterations)
if err != nil {
t.Fatalf("Error measuring function: %v", err)
}
t.Logf("Searching %d random cards for a hand takes on average %s", cardCount, result.Mean)
t.Logf("Over %d iterations the min was %s and the max was %s", iterations, result.Min, result.Max)
t.Logf("The standard deviation was %s", result.StdDev)
t.Logf("The median was %s", result.Median)
// Print the timing results.
//t.Logf(result.String())
}
func findHandInRandomCards(t *testing.T, shuffleSeed int, cardCount int) {
d := NewDeck()
d.ShuffleDeterministically(int64(shuffleSeed))
cards := d.Deal(cardCount)
hand, err := cards.IdentifyBestFiveCardPokerHand()
assert.Nil(t, err, "Expected no error")
ph, err := hand.PokerHand()
assert.Nil(t, err, "Expected no error")
//assert.Greater(t, ph.Score, 0, "Expected score to be nonzero 0")
desc := ph.Description()
assert.NotEmpty(t, desc, "Expected description to not be empty")
}

View File

@ -1,7 +1,6 @@
package pokercore
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@ -14,13 +13,13 @@ type ShuffleTestResults []struct {
func TestPokerDeck(t *testing.T) {
d := NewDeck()
fmt.Printf("newdeck: %+v\n", d)
//fmt.Printf("newdeck: %+v\n", d)
d.ShuffleDeterministically(437)
fmt.Printf("deterministically shuffled deck: %+v\n", d)
//fmt.Printf("deterministically shuffled deck: %+v\n", d)
cards := d.Deal(7)
expected := "6♥,A♦,7♥,9♣,6♠,9♥,8♣"
fmt.Printf("deterministically shuffled deck after dealing: %+v\n", d)
fmt.Printf("cards: %+v\n", cards)
//fmt.Printf("deterministically shuffled deck after dealing: %+v\n", d)
//fmt.Printf("cards: %+v\n", cards)
assert.Equal(t, expected, cards.String())
x := d.Count()

View File

@ -1,6 +1,9 @@
package pokercore
import "fmt"
import (
"errors"
"fmt"
)
type PokerHandType int
@ -28,99 +31,41 @@ func (c Cards) Append(other Cards) Cards {
}
func (c Cards) PokerHand() (*PokerHand, error) {
if len(c) != 5 {
return nil, fmt.Errorf("hand must have 5 cards to be scored as a poker hand")
}
if c.containsDuplicates() {
return nil, fmt.Errorf("hand must have no duplicates to be scored as a poker hand")
return nil, errors.New("hand must have no duplicates to be scored as a poker hand")
}
ph := new(PokerHand)
ph.Hand = c
if c.containsRoyalFlush() {
ph.Type = RoyalFlush
ph.Score = ScoreRoyalFlush
return ph, nil
}
if c.containsStraightFlush() {
ph.Type = StraightFlush
ph.Score = ph.Hand.scoreStraightFlush()
return ph, nil
}
if c.containsFourOfAKind() {
ph.Type = FourOfAKind
ph.Score = ScoreFourOfAKind + 1000*c.fourOfAKindRank().Score() + c.fourOfAKindKicker().Score()
return ph, nil
}
if c.containsFullHouse() {
ph.Type = FullHouse
ph.Score = ScoreFullHouse + 1000*c.fullHouseTripsRank().Score() + c.fullHousePairRank().Score()
return ph, nil
}
if c.containsFlush() {
ph.Type = Flush
ph.Score = c.scoreFlush()
return ph, nil
}
if c.containsStraight() {
ph.Type = Straight
// 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()
// IdentifyBestFiveCardPokerHand() calls us to score hands
// but only if the hand is 5 cards exactly which avoids recursion loop
if len(c) > 5 {
phc, err := c.IdentifyBestFiveCardPokerHand()
if err != nil {
// this should in theory never happen
return nil, err
}
return ph, nil
ph.Hand = phc.SortByRankAscending()
} else if len(c) == 5 {
ph.Hand = c.SortByRankAscending()
} else {
return nil, errors.New("hand must have at least 5 cards to be scored as a poker hand")
}
if c.containsThreeOfAKind() {
ph.Type = ThreeOfAKind
ph.Score = ScoreThreeOfAKind
ph.Score += 1000 * c.threeOfAKindTripsRank().Score()
ph.Score += 100 * c.threeOfAKindFirstKicker().Score()
ph.Score += 10 * c.threeOfAKindSecondKicker().Score()
return ph, nil
}
// this doesn't return an error because we've already checked for
// duplicates and we have already identified the best 5-card hand
if c.containsTwoPair() {
ph.Type = TwoPair
ph.Score = ScoreTwoPair
ph.Score += 1000 * c.twoPairBiggestPair().Score()
ph.Score += 100 * c.twoPairSmallestPair().Score()
ph.Score += 10 * c.twoPairKicker().Score()
return ph, nil
}
// this code used to be here, but got factored out so it can go in
// scoring.go
ph.calculateScore()
if c.containsPair() {
ph.Type = Pair
ph.Score = ScorePair
ph.Score += 1000 * c.pairRank().Score()
ph.Score += 100 * c.pairFirstKicker().Score()
ph.Score += 10 * c.pairSecondKicker().Score()
ph.Score += c.pairThirdKicker().Score()
return ph, nil
}
ph.Type = HighCard
ph.Score = c.scoreHighCard()
return ph, nil
}
func (ph PokerHand) ToSortedCards() Cards {
sorted := ph.Hand.SortByRankAscending()
return sorted
// I believe ph.Hand is already sorted, but in any case it's only 5
// cards and sorting it costs ~nothing
return ph.Hand.SortByRankAscending()
}
func (ph PokerHand) Compare(other PokerHand) int {
@ -142,16 +87,15 @@ func (ph PokerHand) String() string {
}
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() {
if sortedHand[3].Rank == FIVE && sortedHand[4].Rank == ACE {
if c.Hand[3].Rank == FIVE && c.Hand[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.Hand[3].Rank.WithArticle(), c.Hand[4].Suit)
}
return fmt.Sprintf("%s high straight flush in %s", c.HighestRank().WithArticle(), sortedHand[4].Suit)
return fmt.Sprintf("%s high straight flush in %s", c.HighestRank().WithArticle(), c.Hand[4].Suit)
}
if c.Hand.containsFourOfAKind() {
return fmt.Sprintf("four %s with %s", c.Hand.fourOfAKindRank().Pluralize(), c.Hand.fourOfAKindKicker().Rank.WithArticle())
@ -160,12 +104,12 @@ func (c PokerHand) Description() 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[4].Suit)
return fmt.Sprintf("%s high flush in %s", c.HighestRank().WithArticle(), c.Hand[4].Suit)
}
if c.Hand.containsStraight() {
if sortedHand[3].Rank == FIVE && sortedHand[4].Rank == ACE {
if c.Hand[3].Rank == FIVE && c.Hand[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.Hand[3].Rank.WithArticle())
}
return fmt.Sprintf("%s high straight", c.HighestRank().WithArticle())
}
@ -198,10 +142,10 @@ func (c PokerHand) Description() string {
return fmt.Sprintf(
// "ace high with an eight, a seven, a six, and a deuce"
"%s high with %s, %s, %s, and %s",
sortedHand[4].Rank,
sortedHand[3].Rank.WithArticle(),
sortedHand[2].Rank.WithArticle(),
sortedHand[1].Rank.WithArticle(),
sortedHand[0].Rank.WithArticle(),
c.Hand[4].Rank,
c.Hand[3].Rank.WithArticle(),
c.Hand[2].Rank.WithArticle(),
c.Hand[1].Rank.WithArticle(),
c.Hand[0].Rank.WithArticle(),
)
}

View File

@ -1,6 +1,8 @@
package pokercore
import "fmt"
import (
"fmt"
)
type HandScore int
@ -22,18 +24,123 @@ func (c Card) Score() HandScore {
}
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)
}
func (ph *PokerHand) calculateScore() {
// sanity check, we should only be called in the PokerHand() method from
// a Cards, but just in case
if len(ph.Hand) != 5 {
// normally we don't panic in a library but this is a "should never
// happen"
panic("PokerHand.calculateScore() called on a PokerHand with != 5 cards")
}
if ph.Hand.containsRoyalFlush() {
ph.Type = RoyalFlush
ph.Score = ScoreRoyalFlush
return
}
if ph.Hand.containsStraightFlush() {
ph.Type = StraightFlush
ph.Score = ScoreStraightFlush
ph.Score += ph.Hand.HighestRank().Score()
return
}
if ph.Hand.containsFourOfAKind() {
ph.Type = FourOfAKind
ph.Score = ScoreFourOfAKind
ph.Score += 10000 * ph.Hand.fourOfAKindRank().Score()
ph.Score += 100 * ph.Hand.fourOfAKindKicker().Score()
return
}
if ph.Hand.containsFullHouse() {
ph.Type = FullHouse
ph.Score = ScoreFullHouse
ph.Score += 10000 * ph.Hand.fullHouseTripsRank().Score()
ph.Score += 100 * ph.Hand.fullHousePairRank().Score()
return
}
if ph.Hand.containsFlush() {
ph.Type = Flush
ph.Score = ScoreFlush
// flush base score plus sum of card ranks
ph.Score += ph.Hand[0].Score()
ph.Score += ph.Hand[1].Score()
ph.Score += ph.Hand[2].Score()
ph.Score += ph.Hand[3].Score()
ph.Score += ph.Hand[4].Score()
return
}
if ph.Hand.containsStraight() {
ph.Type = Straight
ph.Score = ScoreStraight
// note that ph.Hand is already sorted by rank ascending with ace
// high
// 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
if ph.Hand[3].Rank == FIVE && ph.Hand[4].Rank == ACE {
// 5-high straight, scored by the five's rank
ph.Score += ph.Hand[3].Score()
} else {
// All other straights are scored by the highest card in the straight
ph.Score += ph.Hand[4].Score()
}
return
}
if ph.Hand.containsThreeOfAKind() {
ph.Type = ThreeOfAKind
ph.Score = ScoreThreeOfAKind
ph.Score += 10000 * ph.Hand.threeOfAKindTripsRank().Score()
ph.Score += 100 * ph.Hand.threeOfAKindFirstKicker().Score()
ph.Score += ph.Hand.threeOfAKindSecondKicker().Score()
return
}
if ph.Hand.containsTwoPair() {
ph.Type = TwoPair
ph.Score = ScoreTwoPair
ph.Score += 10000 * ph.Hand.twoPairBiggestPair().Score()
ph.Score += 100 * ph.Hand.twoPairSmallestPair().Score()
ph.Score += ph.Hand.twoPairKicker().Score()
return
}
if ph.Hand.containsPair() {
ph.Type = Pair
ph.Score = ScorePair
ph.Score += 10000 * ph.Hand.pairRank().Score()
ph.Score += 100 * ph.Hand.pairFirstKicker().Score()
ph.Score += ph.Hand.pairSecondKicker().Score()
ph.Score += ph.Hand.pairThirdKicker().Score()
return
}
ph.Type = HighCard
ph.Score = ScoreHighCard // base score
// unmade hands are scored like flushes, just add up the values
ph.Score += ph.Hand[0].Score()
ph.Score += ph.Hand[1].Score()
ph.Score += ph.Hand[2].Score()
ph.Score += ph.Hand[3].Score()
ph.Score += ph.Hand[4].Score()
return
}

View File

@ -1,12 +1,72 @@
package pokercore
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHandDescripionBug(t *testing.T) {
playerCount := 8
d := NewDeck()
d.ShuffleDeterministically(1337)
var players []*Cards
players = make([]*Cards, playerCount)
for i := 1; i-1 < playerCount; i++ {
c := d.Deal(2)
players[i-1] = &c
t.Logf("Player %d dealt: %+v\n", i, c)
}
t.Logf("Players: %+v\n", players)
community := d.Deal(5)
t.Logf("Community: %+v\n", community)
var playerResults []*PokerHand
for i := 1; i-1 < playerCount; i++ {
t.Logf("Player %d hole cards: %+v\n", i, *players[i-1])
pc := append(*players[i-1], community...)
t.Logf("Player %d cards available: %+v\n", i, pc)
hand, err := pc.IdentifyBestFiveCardPokerHand()
assert.Nil(t, err, "Expected no error")
ph, err := hand.PokerHand()
assert.Nil(t, err, "Expected no error")
t.Logf("Player %d five cards used: %+v\n", i, hand)
t.Logf("Player %d poker hand: %+v\n", i, ph)
t.Logf("Player %d best hand description: %s\n", i, ph.Description())
playerResults = append(playerResults, ph)
}
weirdOne := playerResults[7]
t.Logf("Weird one: %v\n", weirdOne)
t.Logf("Weird one description: %s\n", weirdOne.Description())
// T♠,7♠,9♦,7♣,T♥
assert.Equal(t, weirdOne.Description(), "two pair, tens and sevens with a nine")
scoreShouldBe := ScoreTwoPair
scoreShouldBe += 10000 * TEN.Score()
scoreShouldBe += 100 * SEVEN.Score()
scoreShouldBe += NINE.Score()
assert.Equal(t, weirdOne.Score, scoreShouldBe)
cards := weirdOne.Hand
assert.True(t, cards.containsTwoPair(), "Expected hand to be two pair")
bp := cards.twoPairBiggestPair() // returns Rank, because describing a pair
assert.Equal(t, bp, TEN, "Expected biggest pair to be a ten")
sp := cards.twoPairSmallestPair() // returns Rank, because describing a pair
assert.Equal(t, sp, SEVEN, "Expected smallest pair to be a seven")
k := cards.twoPairKicker() // returns Card, because describing a single card
assert.Equal(t, k.Rank, NINE, "Expected kicker to be a nine")
}
func TestAceLowStraight(t *testing.T) {
hand := Cards{
AceOfSpades(),
@ -18,17 +78,18 @@ func TestAceLowStraight(t *testing.T) {
assert.True(t, hand.containsStraight(), "Expected hand to be a straight")
ph, err := hand.PokerHand()
assert.Nil(t, err, "Expected no error")
assert.Greater(t, ph.Score, 0, "Expected score to be nonzero 0")
assert.Greater(t, ph.Score, 0, "Expected score to be greater than 0")
assert.Less(t, ph.Score, 100000000000000000, "Expected score to be less than 100000000000000000")
assert.Equal(t, ph.Score, ScoreStraight+100*FIVE.Score())
assert.Equal(t, ph.Score, ScoreStraight+FIVE.Score())
assert.Equal(t, ph.Description(), "a five high straight")
assert.True(t, hand.HighestRank() == ACE, "Expected highest rank to be an ace")
assert.True(t, hand.SortByRankAscending().First().Rank == DEUCE, "Expected first card to be a deuce")
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")
s := hand.SortByRankAscending()
assert.True(t, s.First().Rank == DEUCE, "Expected first card to be a deuce")
assert.True(t, s.Last().Rank == ACE, "Expected last card in sorted to be a ace")
assert.True(t, s.Second().Rank == THREE, "Expected second card to be a three")
assert.True(t, s.Third().Rank == FOUR, "Expected third card to be a four")
assert.True(t, s.Fourth().Rank == FIVE, "Expected fourth card to be a five")
assert.True(t, s.Fifth().Rank == ACE, "Expected fifth card to be an ace")
}
func TestAceHighStraight(t *testing.T) {
@ -66,12 +127,14 @@ func TestOtherStraight(t *testing.T) {
newDeck := NewDeckFromCards(hand)
newDeck.ShuffleDeterministically(123456789)
fmt.Printf("Shuffled deck: %s\n", newDeck.String())
fmt.Printf("new deck has %d cards\n", newDeck.Count())
//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")
assert.False(t, shuffledHand.containsPair(), "Expected hand to not be a pair")
assert.True(t, shuffledHand.HighestRank() == SIX, "Expected highest rank to be a six")
assert.True(t, shuffledHand.SortByRankAscending().First().Rank == DEUCE, "Expected first card to be a deuce")
}
func TestFlush(t *testing.T) {
@ -85,21 +148,24 @@ func TestFlush(t *testing.T) {
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())
//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")
// flush value is the sum of the ranks, just like high card
x := ScoreFlush
var multiplier HandScore = 100
x += multiplier * DEUCE.Score()
multiplier *= 100
x += multiplier * THREE.Score()
multiplier *= 100
x += multiplier * FOUR.Score()
multiplier *= 100
x += multiplier * SIX.Score()
fmt.Printf("a-2-3-4-6 flush score should be: %d\n", x)
x += ACE.Score()
x += DEUCE.Score()
x += THREE.Score()
x += FOUR.Score()
x += SIX.Score()
//fmt.Printf("a-2-3-4-6 flush score should be: %d\n", x)
ph, err := shuffledHand.PokerHand()
assert.Nil(t, err, "Expected no error")
assert.Greater(t, ph.Score, 0, "Expected score to be nonzero 0")
assert.Equal(t, ph.Score, x)
}
func TestStraightFlush(t *testing.T) {
@ -124,14 +190,43 @@ func TestStraightFlush(t *testing.T) {
nd := NewDeckFromCards(hand)
nd.ShuffleDeterministically(123456789)
fmt.Printf("Shuffled deck: %s\n", nd.String())
fmt.Printf("new deck has %d cards\n", nd.Count())
//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() == SIX, "Expected highest rank to still be a six after shuffle")
assert.True(t, shuffledHand.HighestRank() == SIX, "Expected highest rank to be a six after shuffle even with aces low")
}
func TestFourOfAKind(t *testing.T) {
hand := Cards{
SixOfSpades(),
SixOfHearts(),
SixOfDiamonds(),
SixOfClubs(),
FiveOfSpades(),
}
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.True(t, hand.containsFourOfAKind(), "Expected hand to be four of a kind")
assert.False(t, hand.containsFullHouse(), "Expected hand to not be a full house")
// note that these are *expected* to be true. the contains* functions
// are used in the PokerHand.calculateScore method to determine the best hand
// and are checked in sequence of descending value, so if a hand is four of a kind
// it will not be checked for full house, three of a kind, etc.
// technically quads *is* two pair also, and a hand with quads does
// indeed contain three of a kind, and contains a pair.
assert.True(t, hand.containsThreeOfAKind(), "Expected hand to contain three of a kind")
assert.True(t, hand.containsTwoPair(), "Expected hand to contain two pair")
assert.True(t, hand.containsPair(), "Expected hand to contain a pair")
assert.True(t, hand.HighestRank() == SIX, "Expected highest rank to be a six")
}
func TestRoyalFlush(t *testing.T) {
hand := Cards{
TenOfSpades(),
@ -197,6 +292,17 @@ func TestTwoPair(t *testing.T) {
assert.False(t, hand.isUnmadeHand(), "Expected hand to not be unmade")
}
func TestDetectDuplicates(t *testing.T) {
hand := Cards{
KingOfSpades(),
JackOfDiamonds(),
JackOfSpades(),
KingOfSpades(),
TenOfSpades(),
}
assert.True(t, hand.containsDuplicates(), "Expected hand to contain duplicates")
}
func TestHandScore(t *testing.T) {
hand := Cards{
KingOfSpades(),
@ -211,8 +317,8 @@ func TestHandScore(t *testing.T) {
assert.True(t, ph.Score > 0, "Expected score to be nonzero 0")
assert.True(t, ph.Score < 100000000000000000, "Expected score to be less than 100000000000000000")
fmt.Printf("PokerHand: %v+\n", ph)
fmt.Printf("PH score: %d\n", ph.Score)
//fmt.Printf("PokerHand: %v+\n", ph)
//fmt.Printf("PH score: %d\n", ph.Score)
}
func TestTwoPairBug(t *testing.T) {
@ -220,12 +326,65 @@ func TestTwoPairBug(t *testing.T) {
c, err := NewCardsFromString("9♠,9♣,Q♥,Q♦,K♣")
assert.Nil(t, err, "Expected no error")
//fmt.Printf("Cards: %v+\n", c)
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)
//fmt.Printf("PokerHand: %v+\n", ph)
//fmt.Printf("PH score: %d\n", ph.Score)
}
func TestScoringStructureQuads(t *testing.T) {
handA := Cards{
DeuceOfSpades(),
DeuceOfHearts(),
DeuceOfDiamonds(),
DeuceOfClubs(),
AceOfSpades(),
}
handB := Cards{
ThreeOfSpades(),
ThreeOfHearts(),
ThreeOfDiamonds(),
ThreeOfClubs(),
DeuceOfSpades(),
}
phA, err := handA.PokerHand()
assert.Nil(t, err, "Expected no error")
phB, err := handB.PokerHand()
assert.Nil(t, err, "Expected no error")
assert.Greater(t, phB.Score, phA.Score, "Expected hand B to be higher than hand A")
}
func TestScoringStructureFullHouse(t *testing.T) {
handA := Cards{
DeuceOfSpades(),
DeuceOfHearts(),
DeuceOfDiamonds(),
AceOfSpades(),
AceOfHearts(),
}
phA, err := handA.PokerHand()
assert.Nil(t, err, "Expected no error")
deucesFullOfAcesScore := phA.Score
handB := Cards{
ThreeOfSpades(),
ThreeOfHearts(),
ThreeOfDiamonds(),
DeuceOfSpades(),
DeuceOfHearts(),
}
phB, err := handB.PokerHand()
assert.Nil(t, err, "Expected no error")
threesFullOfDeucesScore := phB.Score
assert.Greater(t, threesFullOfDeucesScore, deucesFullOfAcesScore, "Expected Threes full of deuces to beat deuces full of aces")
}