Compare commits

..

16 Commits

Author SHA1 Message Date
8dbfc45ebb latest 2026-01-06 10:13:38 -08:00
e26b79751f cleanups 2024-05-18 23:55:15 -07:00
6c66adf8d3 linting and formatting updates 2024-05-18 23:50:19 -07:00
ab92f29a88 latest. passes all tests, no known bugs, has examples. 2024-05-18 23:25:55 -07:00
f135f480be added more tests, cleaned up example app 2024-05-18 23:00:19 -07:00
0757f12dfa update gitignore 2024-05-18 23:00:14 -07:00
79cf2fd9aa remove some guardrails from internal methods now that tests are passing 2024-05-18 22:31:55 -07:00
f527c6eb07 fixed scoring bug
multiplier in scoring needs to increase by *100 for each item within
the hand type, because ranks are 2-14
2024-05-18 22:25:59 -07:00
f217a95e19 refactored some stuff around, still has a scoring bug 2024-05-18 22:23:14 -07:00
de1eeb214e make 'clean' makefile target rm test binary 2024-05-18 22:11:26 -07:00
14ffbe4eb4 added more tests, cleaned up some code, found another bug 2024-05-18 22:10:53 -07:00
5f7dba942c fixed bug which misidentified bigger pair in two pair 2024-05-18 21:18:16 -07:00
11c5886951 got a failing test for the two pair bigger pair bug 2024-05-18 21:17:38 -07:00
39e2f5a268 add useful gitignore 2024-05-18 20:32:46 -07:00
5a2560051d oops committed binaries, how did those get there 2024-05-18 20:32:10 -07:00
336c0d953b latest, passes all tests, no known bugs, woo 2024-05-18 20:26:08 -07:00
19 changed files with 1354 additions and 559 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,24 @@
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/...
lint:
golangci-lint run
fmt:
go fmt ./...

173
card.go
View File

@@ -5,8 +5,9 @@ import (
"slices"
"sort"
"strings"
"unicode/utf8"
"github.com/logrusorgru/aurora/v4"
aurora "github.com/logrusorgru/aurora/v4"
)
type Card struct {
@@ -14,62 +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 {
return Card{}, fmt.Errorf("Invalid card string %s", card)
length := utf8.RuneCountInString(card)
if length != 2 {
return Card{}, fmt.Errorf("invalid card string %s, must be 2 characters", card)
}
rank := NewRankFromString(card[0:1])
suit := NewSuitFromString(card[1:2])
rankMap := map[rune]Rank{
rune(DEUCE): DEUCE,
rune(THREE): THREE,
rune(FOUR): FOUR,
rune(FIVE): FIVE,
rune(SIX): SIX,
rune(SEVEN): SEVEN,
rune(EIGHT): EIGHT,
rune(NINE): NINE,
rune(TEN): TEN,
rune(JACK): JACK,
rune(QUEEN): QUEEN,
rune(KING): KING,
rune(ACE): ACE,
}
suitMap := map[rune]Suit{
rune(SPADE): SPADE,
rune(HEART): HEART,
rune(DIAMOND): DIAMOND,
rune(CLUB): CLUB,
}
var rank Rank
var suit Suit
for r := range rankMap {
if strings.ContainsRune(card, r) {
rank = rankMap[r]
break
}
}
for s := range suitMap {
if strings.ContainsRune(card, s) {
suit = suitMap[s]
break
}
}
if rank == Rank(0) || suit == Suit(0) {
return Card{}, fmt.Errorf("Invalid card string %s", card)
return Card{}, fmt.Errorf("invalid card string %s", card)
}
return Card{Rank: rank, Suit: suit}, nil
}
@@ -77,17 +72,17 @@ 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 {
newCards := make(Cards, len(cardStrings))
for i, cardString := range cardStrings {
card, err := NewCardFromString(cardString)
if err != nil {
return Cards{}, err
return nil, err
}
newCards = append(newCards, card)
newCards[i] = card
}
if len(newCards) == 0 {
return Cards{}, fmt.Errorf("No cards found in string %s", cards)
return nil, fmt.Errorf("no cards found in string %s", cards)
}
return newCards, nil
}
@@ -98,42 +93,41 @@ func (c *Card) String() string {
type Cards []Card
func (c Cards) First() Card {
return c[0]
func (cards Cards) First() Card {
return cards[0]
}
func (c Cards) Second() Card {
return c[1]
func (cards Cards) Second() Card {
return cards[1]
}
func (c Cards) Third() Card {
return c[2]
func (cards Cards) Third() Card {
return cards[2]
}
func (c Cards) Fourth() Card {
return c[3]
func (cards Cards) Fourth() Card {
return cards[3]
}
func (c Cards) Fifth() Card {
return c[4]
func (cards Cards) Fifth() Card {
return cards[4]
}
func (c Cards) Last() Card {
return c[len(c)-1]
func (cards Cards) Last() Card {
return cards[len(cards)-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()
func (cards Cards) SortByRankAscending() Cards {
sortedCards := make(Cards, len(cards))
copy(sortedCards, cards)
sort.Slice(sortedCards, func(i, j int) bool {
return sortedCards[i].Rank.Score() < sortedCards[j].Rank.Score()
})
return newCards
return sortedCards
}
func (c Cards) PrintToTerminal() {
fmt.Printf("%s", c.FormatForTerminal())
func (cards Cards) PrintToTerminal() {
fmt.Printf("%s", cards.FormatForTerminal())
}
type SortOrder int
@@ -143,18 +137,18 @@ const (
AceHighDescending
)
func (c Cards) FormatForTerminalSorted(order SortOrder) string {
sorted := c.SortByRankAscending() // this is ascending
func (cards Cards) FormatForTerminalSorted(order SortOrder) string {
sorted := cards.SortByRankAscending() // this is ascending
if order == AceHighDescending {
slices.Reverse(sorted)
}
return sorted.FormatForTerminal()
}
func (c Cards) FormatForTerminal() string {
func (cards Cards) FormatForTerminal() string {
var cardstrings []string
for i := 0; i < len(c); i++ {
cardstrings = append(cardstrings, c[i].FormatForTerminal())
for i := 0; i < len(cards); i++ {
cardstrings = append(cardstrings, cards[i].FormatForTerminal())
}
return strings.Join(cardstrings, ",")
}
@@ -179,16 +173,15 @@ func (c Card) FormatForTerminal() string {
return fmt.Sprintf("%s%s", rank, suit)
}
func (c Cards) HighestRank() Rank {
sorted := c.SortByRankAscending()
func (cards Cards) HighestRank() Rank {
sorted := cards.SortByRankAscending()
return sorted[len(sorted)-1].Rank
}
func (s Cards) String() (output string) {
func (cards Cards) String() string {
var cardstrings []string
for i := 0; i < len(s); i++ {
cardstrings = append(cardstrings, s[i].String())
for i := 0; i < len(cards); i++ {
cardstrings = append(cardstrings, cards[i].String())
}
output = strings.Join(cardstrings, ",")
return output
return strings.Join(cardstrings, ",")
}

119
examples/flip/main.go Normal file
View File

@@ -0,0 +1,119 @@
package main
import (
"fmt"
"git.eeqj.de/sneak/pokercore"
)
var playerCount = 2
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 NewGame() *Game {
g := &Game{}
g.Street = 0
g.Deck = pokercore.NewDeck()
g.Deck.ShuffleRandomly()
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) 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.DealPlayersIn()
g.DealFlop()
g.DealTurn()
g.DealRiver()
g.ShowGameStatus()
g.ShowWinner()
}

155
examples/montecarlo/main.go Normal file
View File

@@ -0,0 +1,155 @@
package main
import (
"fmt"
"time"
"github.com/rcrowley/go-metrics"
"github.com/schollz/progressbar/v3"
"sneak.berlin/go/pokercore"
)
var gameCount = 50_000
var playerCount = 2
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 NewGame() *Game {
g := &Game{}
g.Street = 0
g.Deck = pokercore.NewDeck()
g.Deck.ShuffleRandomly()
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) 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) 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() int {
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
}
}
}
}
return winner.Position
}
func main() {
oneWins := 0
twoWins := 0
registry := metrics.NewRegistry()
timer := metrics.NewTimer()
registry.Register("pokerGame", timer)
bar := progressbar.NewOptions(
gameCount,
progressbar.OptionSetDescription("Gambling..."),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetPredictTime(true),
progressbar.OptionClearOnFinish(),
progressbar.OptionThrottle(
100*time.Millisecond,
), // Update every 100ms
)
for i := 0; i < gameCount; i++ {
start := time.Now()
w := runFlip()
duration := time.Since(start)
timer.Update(duration)
bar.Add(1)
if w == 1 {
oneWins++
} else {
twoWins++
}
}
fmt.Printf("%d games played\n", gameCount)
fmt.Printf("Min: %d ns\n", timer.Min())
fmt.Printf("Max: %d ns\n", timer.Max())
fmt.Printf("Mean: %0.2f ns\n", timer.Mean())
fmt.Printf("StdDev: %0.2f ns\n", timer.StdDev())
fmt.Printf(
"Percentiles: 50%%: %0.2f ns, 75%%: %0.2f ns, 95%%: %0.2f ns, 99%%: %0.2f ns\n",
timer.Percentile(0.50),
timer.Percentile(0.75),
timer.Percentile(0.95),
timer.Percentile(0.99),
)
oneWinPercentage := float64(oneWins) / float64(gameCount) * 100
twoWinPercentage := float64(twoWins) / float64(gameCount) * 100
fmt.Printf("Player 1 won: %d (%0.2f%%)\n", oneWins, oneWinPercentage)
fmt.Printf("Player 2 won: %d (%0.2f%%)\n", twoWins, twoWinPercentage)
}
func runFlip() int {
g := NewGame()
g.DealPlayersIn()
g.DealFlop()
g.DealTurn()
g.DealRiver()
winnerPosition := g.ShowWinner()
return winnerPosition
}

Binary file not shown.

56
examples/sf/main.go Normal file
View File

@@ -0,0 +1,56 @@
package main
import (
"fmt"
"sneak.berlin/go/pokercore"
)
func main() {
var sfp bool
var tries int
var found int
const maxTries = 100_000_000
for i := 0; i < maxTries; i++ {
sfp = searchStraightFlush()
if sfp {
found++
}
tries++
if tries%1000 == 0 {
fmt.Printf("Tries: %d, Found: %d\n", tries, found)
}
}
fmt.Printf("Tries: %d, Found: %d\n", tries, found)
}
func searchStraightFlush() bool {
d := pokercore.NewDeck()
d.ShuffleRandomly()
var hand pokercore.Cards
hand = d.Deal(7)
ph, err := hand.PokerHand()
if err != nil {
fmt.Println("Error: ", err)
return false
}
if ph.Type == pokercore.StraightFlush ||
ph.Type == pokercore.RoyalFlush {
fmt.Println("straight flush found")
fmt.Println("Hand: ", hand.FormatForTerminal())
fmt.Println("PokerHand: ", ph)
return true
}
return false
}

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)

14
go.mod
View File

@@ -1,17 +1,23 @@
module git.eeqj.de/sneak/pokercore
module sneak.berlin/go/pokercore
go 1.22.2
require (
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/logrusorgru/aurora/v4 v4.0.0
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/schollz/progressbar/v3 v3.14.2
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.0
sneak.berlin/go/timingbench v0.0.0-20240522212031-a6243a470213
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/logrusorgru/aurora/v4 v4.0.0 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // 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
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.20.0 // indirect
gonum.org/v1/gonum v0.15.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

30
go.sum
View File

@@ -1,25 +1,45 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
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-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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
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.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.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=
gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ=
gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sneak.berlin/go/timingbench v0.0.0-20240522212031-a6243a470213 h1:jgfwL2lUUp6aII87vgkgFenfKftsbKvUR3jlsRdS2yo=
sneak.berlin/go/timingbench v0.0.0-20240522212031-a6243a470213/go.mod h1:W+0S+VhiuNIU/06KPhWJCmNhMaCztg2MuHitNEVEFG0=

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

83
perf_test.go Normal file
View File

@@ -0,0 +1,83 @@
package pokercore
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"sneak.berlin/go/timingbench"
)
func TestShuffleSpeed(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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"
@@ -13,14 +12,15 @@ type ShuffleTestResults []struct {
}
func TestPokerDeck(t *testing.T) {
t.Parallel()
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()
@@ -36,6 +36,7 @@ func TestPokerDeck(t *testing.T) {
}
func TestDealing(t *testing.T) {
t.Parallel()
d := NewDeckFromCards(Cards{
Card{Rank: ACE, Suit: HEART},
Card{Rank: DEUCE, Suit: HEART},
@@ -51,6 +52,7 @@ func TestDealing(t *testing.T) {
}
func TestSpecialCaseOfFiveHighStraightFlush(t *testing.T) {
t.Parallel()
// actual bug from first implementation
d := NewDeckFromCards(Cards{
Card{Rank: ACE, Suit: HEART},
@@ -69,6 +71,7 @@ func TestSpecialCaseOfFiveHighStraightFlush(t *testing.T) {
}
func TestSpecialCaseOfFiveHighStraight(t *testing.T) {
t.Parallel()
// actual bug from first implementation
d := NewDeckFromCards(Cards{
Card{Rank: ACE, Suit: HEART},

View File

@@ -1,6 +1,9 @@
package pokercore
import "fmt"
import (
"errors"
"fmt"
)
type PokerHandType int
@@ -28,102 +31,44 @@ 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
// 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
}
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()
ph.Hand = phc.SortByRankAscending()
} else if len(c) == 5 {
ph.Hand = c.SortByRankAscending()
} else {
// All other straights
ph.Score = ScoreStraight + 1000*sorted[4].Score()
return nil, errors.New("hand must have at least 5 cards to be scored as a poker hand")
}
// this doesn't return an error because we've already checked for
// duplicates and we have already identified the best 5-card hand
// this code used to be here, but got factored out so it can go in
// scoring.go
ph.calculateScore()
return ph, nil
}
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
func (ph *PokerHand) ToSortedCards() Cards {
// 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()
}
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
}
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
}
func (ph PokerHand) Compare(other PokerHand) int {
func (ph *PokerHand) Compare(other PokerHand) int {
if ph.Score > other.Score {
return 1
}
@@ -133,75 +78,74 @@ func (ph PokerHand) Compare(other PokerHand) int {
return 0
}
func (ph PokerHand) HighestRank() Rank {
func (ph *PokerHand) HighestRank() Rank {
return ph.Hand.HighestRank()
}
func (ph PokerHand) String() string {
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)
func (ph *PokerHand) Description() string {
if ph.Type == RoyalFlush {
return fmt.Sprintf("a royal flush in %s", ph.Hand[0].Suit)
}
if c.Hand.containsStraightFlush() {
if sortedHand[3].Rank == FIVE && sortedHand[4].Rank == ACE {
if ph.Hand.containsStraightFlush() {
if ph.Hand[3].Rank == FIVE && ph.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", ph.Hand[3].Rank.WithArticle(), ph.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", ph.HighestRank().WithArticle(), ph.Hand[4].Suit)
}
if c.Hand.containsFourOfAKind() {
return fmt.Sprintf("four %s with %s", c.Hand.fourOfAKindRank().Pluralize(), c.Hand.fourOfAKindKicker().Rank.WithArticle())
if ph.Hand.containsFourOfAKind() {
return fmt.Sprintf("four %s with %s", ph.Hand.fourOfAKindRank().Pluralize(), ph.Hand.fourOfAKindKicker().Rank.WithArticle())
}
if c.Hand.containsFullHouse() {
return fmt.Sprintf("a full house, %s full of %s", c.Hand.fullHouseTripsRank().Pluralize(), c.Hand.fullHousePairRank().Pluralize())
if ph.Hand.containsFullHouse() {
return fmt.Sprintf("a full house, %s full of %s", ph.Hand.fullHouseTripsRank().Pluralize(), ph.Hand.fullHousePairRank().Pluralize())
}
if c.Hand.containsFlush() {
return fmt.Sprintf("%s high flush in %s", c.HighestRank().WithArticle(), sortedHand[4].Suit)
if ph.Hand.containsFlush() {
return fmt.Sprintf("%s high flush in %s", ph.HighestRank().WithArticle(), ph.Hand[4].Suit)
}
if c.Hand.containsStraight() {
if sortedHand[3].Rank == FIVE && sortedHand[4].Rank == ACE {
if ph.Hand.containsStraight() {
if ph.Hand[3].Rank == FIVE && ph.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", ph.Hand[3].Rank.WithArticle())
}
return fmt.Sprintf("%s high straight", c.HighestRank().WithArticle())
return fmt.Sprintf("%s high straight", ph.HighestRank().WithArticle())
}
if c.Hand.containsThreeOfAKind() {
if ph.Hand.containsThreeOfAKind() {
return fmt.Sprintf(
"three %s with %s and %s",
c.Hand.threeOfAKindTripsRank().Pluralize(),
c.Hand.threeOfAKindFirstKicker().Rank.WithArticle(),
c.Hand.threeOfAKindSecondKicker().Rank.WithArticle(),
ph.Hand.threeOfAKindTripsRank().Pluralize(),
ph.Hand.threeOfAKindFirstKicker().Rank.WithArticle(),
ph.Hand.threeOfAKindSecondKicker().Rank.WithArticle(),
)
}
if c.Hand.containsTwoPair() {
if ph.Hand.containsTwoPair() {
return fmt.Sprintf(
"two pair, %s and %s with %s",
c.Hand.twoPairBiggestPair().Pluralize(),
c.Hand.twoPairSmallestPair().Pluralize(),
c.Hand.twoPairKicker().Rank.WithArticle(),
ph.Hand.twoPairBiggestPair().Pluralize(),
ph.Hand.twoPairSmallestPair().Pluralize(),
ph.Hand.twoPairKicker().Rank.WithArticle(),
)
}
if c.Hand.containsPair() {
if ph.Hand.containsPair() {
return fmt.Sprintf(
"a pair of %s with %s, %s, and %s",
c.Hand.pairRank().Pluralize(),
c.Hand.pairFirstKicker().Rank.WithArticle(),
c.Hand.pairSecondKicker().Rank.WithArticle(),
c.Hand.pairThirdKicker().Rank.WithArticle(),
ph.Hand.pairRank().Pluralize(),
ph.Hand.pairFirstKicker().Rank.WithArticle(),
ph.Hand.pairSecondKicker().Rank.WithArticle(),
ph.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",
sortedHand[4].Rank,
sortedHand[3].Rank.WithArticle(),
sortedHand[2].Rank.WithArticle(),
sortedHand[1].Rank.WithArticle(),
sortedHand[0].Rank.WithArticle(),
ph.Hand[4].Rank,
ph.Hand[3].Rank.WithArticle(),
ph.Hand[2].Rank.WithArticle(),
ph.Hand[1].Rank.WithArticle(),
ph.Hand[0].Rank.WithArticle(),
)
}

View File

@@ -1,6 +1,8 @@
package pokercore
import "fmt"
import (
"fmt"
)
type HandScore int
@@ -22,18 +24,122 @@ 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()
}

View File

@@ -1,13 +1,74 @@
package pokercore
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHandDescriptionBug(t *testing.T) {
t.Parallel()
playerCount := 8
d := NewDeck()
d.ShuffleDeterministically(1337)
players := make([]*Cards, playerCount)
for i := 0; i < playerCount; i++ {
c := d.Deal(2)
players[i] = &c
t.Logf("Player %d dealt: %+v\n", i+1, c)
}
t.Logf("Players: %+v\n", players)
community := d.Deal(5)
t.Logf("Community: %+v\n", community)
var playerResults []*PokerHand
for i := 0; i < playerCount; i++ {
t.Logf("Player %d hole cards: %+v\n", i+1, *players[i])
pc := append(*players[i], community...)
t.Logf("Player %d cards available: %+v\n", i+1, pc)
hand, err := pc.IdentifyBestFiveCardPokerHand()
assert.NoError(t, err, "Expected no error")
ph, err := hand.PokerHand()
assert.NoError(t, err, "Expected no error")
t.Logf("Player %d five cards used: %+v\n", i+1, hand)
t.Logf("Player %d poker hand: %+v\n", i+1, ph)
t.Logf("Player %d best hand description: %s\n", i+1, 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, "two pair, tens and sevens with a nine", weirdOne.Description())
scoreShouldBe := ScoreTwoPair
scoreShouldBe += 10000 * TEN.Score()
scoreShouldBe += 100 * SEVEN.Score()
scoreShouldBe += NINE.Score()
assert.Equal(t, scoreShouldBe, weirdOne.Score)
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, TEN, bp, "Expected biggest pair to be a ten")
sp := cards.twoPairSmallestPair() // returns Rank, because describing a pair
assert.Equal(t, SEVEN, sp, "Expected smallest pair to be a seven")
k := cards.twoPairKicker() // returns Card, because describing a single card
assert.Equal(t, NINE, k.Rank, "Expected kicker to be a nine")
}
func TestAceLowStraight(t *testing.T) {
t.Parallel()
t.Run("Test Ace-Low Straight", func(t *testing.T) {
hand := Cards{
AceOfSpades(),
DeuceOfHearts(),
@@ -17,21 +78,25 @@ 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.NoError(t, err, "Expected no error")
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.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")
assert.Equal(t, ph.Score, ScoreStraight+FIVE.Score())
assert.Equal(t, "a five high straight", ph.Description())
assert.Equal(t, ACE, hand.HighestRank(), "Expected highest rank to be an ace")
s := hand.SortByRankAscending()
assert.Equal(t, DEUCE, s.First().Rank, "Expected first card to be a deuce")
assert.Equal(t, ACE, s.Last().Rank, "Expected last card in sorted to be an ace")
assert.Equal(t, THREE, s.Second().Rank, "Expected second card to be a three")
assert.Equal(t, FOUR, s.Third().Rank, "Expected third card to be a four")
assert.Equal(t, FIVE, s.Fourth().Rank, "Expected fourth card to be a five")
assert.Equal(t, ACE, s.Fifth().Rank, "Expected fifth card to be an ace")
})
}
func TestAceHighStraight(t *testing.T) {
t.Parallel()
t.Run("Test Ace-High Straight", func(t *testing.T) {
hand := Cards{
TenOfSpades(),
JackOfHearts(),
@@ -45,16 +110,19 @@ func TestAceHighStraight(t *testing.T) {
newDeck.ShuffleDeterministically(123456789)
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")
assert.Equal(t, ACE, shuffledHand.HighestRank(), "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")
assert.Equal(t, TEN, sortedHand[0].Rank, "Expected lowest rank to be a ten")
assert.Equal(t, JACK, sortedHand[1].Rank, "Expected second lowest rank to be a jack")
assert.Equal(t, QUEEN, sortedHand[2].Rank, "Expected third lowest rank to be a queen")
assert.Equal(t, KING, sortedHand[3].Rank, "Expected fourth lowest rank to be a king")
assert.Equal(t, ACE, sortedHand[4].Rank, "Expected highest rank to be an ace")
})
}
func TestOtherStraight(t *testing.T) {
t.Parallel()
t.Run("Test Other Straight", func(t *testing.T) {
hand := Cards{
DeuceOfSpades(),
ThreeOfHearts(),
@@ -66,15 +134,18 @@ 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())
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.Equal(t, SIX, shuffledHand.HighestRank(), "Expected highest rank to be a six")
assert.Equal(t, DEUCE, shuffledHand.SortByRankAscending().First().Rank, "Expected first card to be a deuce")
})
}
func TestFlush(t *testing.T) {
t.Parallel()
t.Run("Test Flush", func(t *testing.T) {
hand := Cards{
AceOfSpades(),
DeuceOfSpades(),
@@ -85,24 +156,27 @@ 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())
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()
ph, err := shuffledHand.PokerHand()
assert.NoError(t, err, "Expected no error")
assert.Greater(t, ph.Score, 0, "Expected score to be nonzero")
assert.Equal(t, ph.Score, x)
})
}
func TestStraightFlush(t *testing.T) {
t.Parallel()
t.Run("Test Straight Flush", func(t *testing.T) {
hand := Cards{
SixOfSpades(),
DeuceOfSpades(),
@@ -120,19 +194,52 @@ func TestStraightFlush(t *testing.T) {
assert.False(t, hand.containsTwoPair(), "Expected hand to not be two pair")
assert.False(t, hand.containsPair(), "Expected hand to not be a pair")
assert.True(t, hand.HighestRank() == SIX, "Expected highest rank to be a six")
assert.Equal(t, SIX, hand.HighestRank(), "Expected highest rank to be a six")
nd := NewDeckFromCards(hand)
nd.ShuffleDeterministically(123456789)
fmt.Printf("Shuffled deck: %s\n", nd.String())
fmt.Printf("new deck has %d cards\n", nd.Count())
shuffledHand := nd.Deal(5)
assert.True(t, shuffledHand.containsStraightFlush(), "Expected hand to still be a straight flush after shuffle")
assert.True(t, shuffledHand.HighestRank() == 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")
assert.Equal(t, SIX, shuffledHand.HighestRank(), "Expected highest rank to still be a six after shuffle")
assert.Equal(t, SIX, shuffledHand.HighestRank(), "Expected highest rank to be a six after shuffle even with aces low")
})
}
func TestFourOfAKind(t *testing.T) {
t.Parallel()
t.Run("Test Four of a Kind", func(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.Equal(t, SIX, hand.HighestRank(), "Expected highest rank to be a six")
})
}
func TestRoyalFlush(t *testing.T) {
t.Parallel()
t.Run("Test Royal Flush", func(t *testing.T) {
hand := Cards{
TenOfSpades(),
JackOfSpades(),
@@ -151,11 +258,14 @@ func TestRoyalFlush(t *testing.T) {
assert.False(t, hand.containsTwoPair(), "Expected hand to not be two pair")
assert.False(t, hand.containsPair(), "Expected hand to not be a pair")
assert.True(t, hand.HighestRank() == ACE, "Expected highest rank to be an ace")
assert.False(t, hand.HighestRank() == TEN, "Expected highest rank to not be an ace")
assert.Equal(t, ACE, hand.HighestRank(), "Expected highest rank to be an ace")
assert.NotEqual(t, TEN, hand.HighestRank(), "Expected highest rank to not be a ten")
})
}
func TestUnmadeHand(t *testing.T) {
t.Parallel()
t.Run("Test Unmade Hand", func(t *testing.T) {
hand := Cards{
TenOfSpades(),
JackOfDiamonds(),
@@ -172,11 +282,14 @@ func TestUnmadeHand(t *testing.T) {
assert.False(t, hand.containsThreeOfAKind(), "Expected hand to not be three of a kind")
assert.False(t, hand.containsTwoPair(), "Expected hand to not be two pair")
assert.False(t, hand.containsPair(), "Expected hand to not be a pair")
assert.True(t, hand.HighestRank() == KING, "Expected highest rank to be a king")
assert.Equal(t, KING, hand.HighestRank(), "Expected highest rank to be a king")
assert.True(t, hand.isUnmadeHand(), "Expected hand to be unmade")
})
}
func TestTwoPair(t *testing.T) {
t.Parallel()
t.Run("Test Two Pair", func(t *testing.T) {
hand := Cards{
KingOfSpades(),
JackOfDiamonds(),
@@ -193,11 +306,28 @@ func TestTwoPair(t *testing.T) {
assert.False(t, hand.containsThreeOfAKind(), "Expected hand to not be three of a kind")
assert.True(t, hand.containsTwoPair(), "Expected hand to be two pair")
assert.True(t, hand.containsPair(), "Expected hand to also be a pair")
assert.True(t, hand.HighestRank() == KING, "Expected highest rank to be a king")
assert.Equal(t, KING, hand.HighestRank(), "Expected highest rank to be a king")
assert.False(t, hand.isUnmadeHand(), "Expected hand to not be unmade")
})
}
func TestDetectDuplicates(t *testing.T) {
t.Parallel()
t.Run("Test Detect Duplicates", func(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) {
t.Parallel()
t.Run("Test Hand Score", func(t *testing.T) {
hand := Cards{
KingOfSpades(),
JackOfDiamonds(),
@@ -206,26 +336,88 @@ func TestHandScore(t *testing.T) {
TenOfSpades(),
}
ph, error := hand.PokerHand()
assert.Nil(t, error, "Expected no error")
assert.True(t, ph.Score > 0, "Expected score to be nonzero 0")
ph, err := hand.PokerHand()
assert.NoError(t, err, "Expected no error")
assert.True(t, ph.Score > 0, "Expected score to be nonzero")
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)
// write more assertions FIXME
})
}
func TestTwoPairBug(t *testing.T) {
t.Parallel()
t.Run("Test Two Pair Bug", func(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")
assert.NoError(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")
assert.NoError(t, err, "Expected no error")
assert.Greater(t, ph.Score, 0, "Expected score to be nonzero")
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)
assert.Equal(t, "two pair, queens and nines with a king", desc)
})
}
func TestScoringStructureQuads(t *testing.T) {
t.Parallel()
t.Run("Test Scoring Structure Quads", func(t *testing.T) {
// this test case was for a bug, but is now fixed
handA := Cards{
DeuceOfSpades(),
DeuceOfHearts(),
DeuceOfDiamonds(),
DeuceOfClubs(),
AceOfSpades(),
}
handB := Cards{
ThreeOfSpades(),
ThreeOfHearts(),
ThreeOfDiamonds(),
ThreeOfClubs(),
DeuceOfSpades(),
}
phA, err := handA.PokerHand()
assert.NoError(t, err, "Expected no error")
phB, err := handB.PokerHand()
assert.NoError(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) {
t.Parallel()
t.Run("Test Scoring Structure Full House", func(t *testing.T) {
// this test case documents an actual bug i found in my scoring code
// related to the fact that i was multiplying by 100 then by 1000,
// instead of by 100 then by 10000 in the scoring. because the ranks
// are 2-14, the different levels of kickers (or in the case of a full
// house, the pair) were not distinguishing sufficiently.
handA := Cards{
DeuceOfSpades(),
DeuceOfHearts(),
DeuceOfDiamonds(),
AceOfSpades(),
AceOfHearts(),
}
phA, err := handA.PokerHand()
assert.NoError(t, err, "Expected no error")
deucesFullOfAcesScore := phA.Score
handB := Cards{
ThreeOfSpades(),
ThreeOfHearts(),
ThreeOfDiamonds(),
DeuceOfSpades(),
DeuceOfHearts(),
}
phB, err := handB.PokerHand()
assert.NoError(t, err, "Expected no error")
threesFullOfDeucesScore := phB.Score
assert.Greater(t, threesFullOfDeucesScore, deucesFullOfAcesScore, "Expected Threes full of deuces to beat deuces full of aces")
})
}