commit ad184a65b21e81695987e32d907bdab47075d175 Author: Trevor Slocum Date: Tue Jan 14 04:45:33 2020 -0800 Initial commit diff --git a/.builds/amd64_freebsd.yml b/.builds/amd64_freebsd.yml new file mode 100644 index 0000000..6069010 --- /dev/null +++ b/.builds/amd64_freebsd.yml @@ -0,0 +1,14 @@ +arch: amd64 +environment: + PROJECT_NAME: 'cards-cribbage' + CGO_ENABLED: '1' + GO111MODULE: 'on' +image: freebsd/latest +packages: + - go +sources: + - https://git.sr.ht/~tslocum/cards-cribbage +tasks: + - test: | + cd $PROJECT_NAME + go test ./... diff --git a/.builds/amd64_linux_alpine.yml b/.builds/amd64_linux_alpine.yml new file mode 100644 index 0000000..1008e99 --- /dev/null +++ b/.builds/amd64_linux_alpine.yml @@ -0,0 +1,14 @@ +arch: x86_64 +environment: + PROJECT_NAME: 'cards-cribbage' + CGO_ENABLED: '1' + GO111MODULE: 'on' +image: alpine/edge +packages: + - go +sources: + - https://git.sr.ht/~tslocum/cards-cribbage +tasks: + - test: | + cd $PROJECT_NAME + go test ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d95e50e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +*.sh diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..8da6a6f --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,2 @@ +0.1.0: +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7c20f89 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Trevor Slocum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff62e99 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# cards-cribbage +[![GoDoc](https://godoc.org/git.sr.ht/~tslocum/cards-cribbage?status.svg)](https://godoc.org/git.sr.ht/~tslocum/cards-cribbage) +[![builds.sr.ht status](https://builds.sr.ht/~tslocum/cards-cribbage.svg)](https://builds.sr.ht/~tslocum/cards-cribbage) +[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space) + +Cribbage rules library + +## Documentation + +Documentation is available via [godoc](https://godoc.org): + +- [cards](https://godoc.org/git.sr.ht/~tslocum/cards) +- [cards-cribbage](https://godoc.org/git.sr.ht/~tslocum/cards-cribbage) + +## Support + +Please share issues/suggestions [here](https://todo.sr.ht/~tslocum/cards-cribbage). diff --git a/card.go b/card.go new file mode 100644 index 0000000..f2caac8 --- /dev/null +++ b/card.go @@ -0,0 +1,12 @@ +package cribbage + +import "git.sr.ht/~tslocum/cards" + +// Value returns the cribbage value of a card. +func Value(c cards.Card) int { + v := int(c.Face) + if v > 10 { + v = 10 + } + return v +} diff --git a/cards.go b/cards.go new file mode 100644 index 0000000..185c3df --- /dev/null +++ b/cards.go @@ -0,0 +1,18 @@ +package cribbage + +import "git.sr.ht/~tslocum/cards" + +// Sum returns the total cribbage value of the supplied cards. +func Sum(c cards.Cards) int { + var v int + for _, card := range c { + v += Value(card) + } + return v +} + +func compareCards(i, j interface{}) bool { + icard := i.(cards.Card) + jcard := j.(cards.Card) + return icard.Value() < jcard.Value() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d5a5d9e --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.sr.ht/~tslocum/cards-cribbage + +go 1.13 + +require ( + git.sr.ht/~tslocum/cards v0.1.1-0.20200114033738-f9b90e62c278 + github.com/fighterlyt/permutation v0.0.0-20170407093504-ac78aa5051ae +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b6c4604 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +git.sr.ht/~tslocum/cards v0.1.1-0.20200114033738-f9b90e62c278 h1:WjWEU2uvliiJVeoUpjkZV/hS9f4X9qnOIS53n8lucdQ= +git.sr.ht/~tslocum/cards v0.1.1-0.20200114033738-f9b90e62c278/go.mod h1:ABrbSXnsABGTkuJbWZsI+oGtGTOMlAC+HZy3AysOe7c= +github.com/fighterlyt/permutation v0.0.0-20170407093504-ac78aa5051ae h1:wdS91f8H+bGgcjlx5G4LEUVXkmt/uz0VYkc6lZMIjD4= +github.com/fighterlyt/permutation v0.0.0-20170407093504-ac78aa5051ae/go.mod h1:KqCsX+AbfYLoAjwmUkE6ocQHwto7ibjZvTY/c5QhgZg= diff --git a/score.go b/score.go new file mode 100644 index 0000000..260c597 --- /dev/null +++ b/score.go @@ -0,0 +1,325 @@ +package cribbage + +import ( + "log" + "sort" + + . "git.sr.ht/~tslocum/cards" + "github.com/fighterlyt/permutation" +) + +// ScoringType represents a set of scoring rules. +type ScoringType int + +// Scoring types +const ( + Peg ScoringType = 1 + ShowHand ScoringType = 2 + ShowCrib ScoringType = 3 +) + +func (t ScoringType) String() string { + switch t { + case Peg: + return "Peg" + case ShowHand: + return "ShowHand" + case ShowCrib: + return "ShowCrib" + default: + return "?" + } +} + +// ScoreType represents a type of score. +type ScoreType int + +// Score types and their point values +const ( + Score15 ScoreType = 1 // 2 + ScorePair ScoreType = 2 // 2 + ScoreRun ScoreType = 3 // 1/card + ScoreFlush ScoreType = 4 // 1/card + ScoreNibs ScoreType = 5 // 2 + ScoreNobs ScoreType = 6 // 1 + Score31 ScoreType = 7 // 2 + ScoreGo ScoreType = 8 // 1 +) + +func (t ScoreType) String() string { + switch t { + case Score15: + return "15" + case ScorePair: + return "Pair" + case ScoreRun: + return "Run" + case ScoreFlush: + return "Flush" + case ScoreNibs: + return "Nibs" + case ScoreNobs: + return "Nobs" + case Score31: + return "31" + case ScoreGo: + return "Go" + default: + return "?" + } +} + +// Score returns the score of a pegging play or shown hand. +func Score(scoringType ScoringType, c Cards, starter Card) (int, ScoreResults) { + if (scoringType == ShowHand || scoringType == ShowCrib) && (starter.Face == 0 || starter.Suit == 0) { + return 0, nil + } + + var points int + var results ScoreResults + if c.Len() == 0 { + return points, results + } + + var scoreCards Cards + if scoringType == Peg { + scoreCards = c.Reverse() + } else { + scoreCards = append(c.Copy(), starter).Sort() + } + + // Score 15s + fifteenscore := 0 + fifteenvalue := 0 + + if scoringType != Peg { + var allusedcards []Cards + var usedcards Cards + + perm, err := permutation.NewPerm(scoreCards, compareCards) + if err != nil { + log.Panicf("failed to generate a permutation of cards %s", scoreCards) + } + + SCORE15: + for permhand, err := perm.Next(); err == nil; permhand, err = perm.Next() { + fifteenvalue = 0 + permhand := permhand.(Cards) + usedcards = Cards{} + + for _, card := range permhand { + usedcards = append(usedcards, card) + + fifteenvalue += Value(card) + if fifteenvalue >= 15 { + if fifteenvalue == 15 { + alreadyused := false + sort.Sort(usedcards) + for _, pastusedcards := range allusedcards { + if usedcards.Equal(pastusedcards) { + alreadyused = true + break + } + } + + if !alreadyused { + allusedcards = append(allusedcards, usedcards) + fifteenscore++ + + results = append(results, ScoreResult{Type: Score15, Cards: usedcards.Sort(), Points: 2}) + } + } + + continue SCORE15 + } + } + } + } else { + if Sum(scoreCards) == 15 { + results = append(results, ScoreResult{Type: Score15, Cards: scoreCards.Sort(), Points: 2}) + } + } + + // Score pairs + if scoringType != Peg { + var faces []CardFace + SCOREPAIR: + for _, card := range scoreCards { + for _, face := range faces { + if face == card.Face { + continue SCOREPAIR + } + } + + var paircards Cards + for _, compcard := range scoreCards { + if compcard.Face != card.Face { + continue + } + + paircards = append(paircards, compcard) + } + if len(paircards) > 1 { + pairmultiplier := 1 + if len(paircards) == 3 { + pairmultiplier = 2 + } else if len(paircards) == 4 { + pairmultiplier = 3 + } + results = append(results, ScoreResult{Type: ScorePair, Cards: paircards.Sort(), Points: len(paircards) * pairmultiplier}) + } + + faces = append(faces, card.Face) + } + } else { + if len(scoreCards) > 0 { + var paircards Cards + for _, compcard := range scoreCards[1:] { + if compcard.Face != scoreCards[0].Face { + break + } + + paircards = append(paircards, compcard) + } + pairmultiplier := 1 + if len(paircards) == 2 { + pairmultiplier = 2 + } else if len(paircards) == 3 { + pairmultiplier = 3 + } + if paircards != nil { + results = append(results, ScoreResult{Type: ScorePair, Cards: append(paircards, scoreCards[0]).Sort(), Points: (len(paircards) + 1) * pairmultiplier}) + } + } + } + + // Score runs + var allRunCards []Cards + var runCards Cards + var runScore int + if scoringType == Peg { + var compHand Cards + var compScore int + var runValue int + runScore = 1 + + // Examine the pile for a run by shortening the checked pile one card + // after each iteration. + SCOREPEGRUN: + for complen := 0; complen < len(scoreCards); complen++ { + compHand = scoreCards[0 : len(scoreCards)-complen] + compScore = 1 + runCards = nil + + for i, compcard := range compHand.Sort() { + if i > 0 { + if int(compcard.Face) == (runValue + 1) { + compScore++ + } else { + continue SCOREPEGRUN + } + } + + runValue = int(compcard.Face) + } + + if compScore > runScore { + runScore = compScore + + if runScore == len(scoreCards) { + runCards = compHand.Sort() + break SCOREPEGRUN + } + } + } + + if runScore >= 3 { + results = append(results, ScoreResult{Type: ScoreRun, Cards: runCards, Points: runScore}) + } + } else { + for runLength := 6; runLength > 3; runLength-- { + perm, err := permutation.NewPerm(scoreCards, compareCards) + if err != nil { + log.Panicf("failed to generate a permutation of cards %s", scoreCards) + } + + SCOREHANDRUN: + for permhand, err := perm.Next(); err == nil; permhand, err = perm.Next() { + permhand := permhand.(Cards) + runCards = Cards{} + + runScore = 0 + for i := range permhand { + if i > 0 && permhand[i].Face != permhand[i-1].Face-1 { + break + } + + runScore++ + runCards = append(runCards, permhand[i]) + } + + if runScore != runLength { + continue + } + + runCards = runCards.Sort() + for _, rc := range allRunCards { + containsAll := true + for _, runCard := range runCards { + if !rc.Contains(runCard) { + containsAll = false + break + } + } + if containsAll { + continue SCOREHANDRUN + } + } + + results = append(results, ScoreResult{Type: ScoreRun, Cards: runCards, Points: runScore}) + allRunCards = append(allRunCards, runCards) + } + } + } + + // Score flushes + if scoringType != Peg { + for _, suit := range StandardSuits { + suitvalue := 0 + var flushCards Cards + for _, card := range c { + if card.Suit == suit { + suitvalue++ + flushCards = append(flushCards, card) + } + } + if starter.Suit == suit { + suitvalue++ + flushCards = append(flushCards, starter) + } + if suitvalue == 5 || (suitvalue == 4 && scoringType == ShowHand) { + results = append(results, ScoreResult{Type: ScoreFlush, Cards: flushCards.Sort(), Points: suitvalue}) + break + } + } + } + + // Score nobs + if scoringType != Peg { + rightJack := Card{FaceJack, starter.Suit} + if c.Contains(rightJack) { + results = append(results, ScoreResult{Type: ScoreNobs, Cards: Cards{rightJack}, Points: 1}) + } + } + + // Score 31 + if scoringType == Peg && Sum(scoreCards) == 31 { + results = append(results, ScoreResult{Type: Score31, Cards: scoreCards.Sort(), Points: 2}) + } + + for _, r := range results { + points += r.Points + } + sort.Sort(results) + return points, results +} diff --git a/score_result.go b/score_result.go new file mode 100644 index 0000000..18f39fb --- /dev/null +++ b/score_result.go @@ -0,0 +1,47 @@ +package cribbage + +import ( + "fmt" + + . "git.sr.ht/~tslocum/cards" +) + +// ScoreResult is a score from pegging or showing a hand. +type ScoreResult struct { + Type ScoreType + Cards Cards + Points int +} + +func (r ScoreResult) String() string { + return fmt.Sprintf("%s for %d with %s", r.Type, r.Points, r.Cards) +} + +// ScoreResults is a slice of scores from pegging or showing a hand. +type ScoreResults []ScoreResult + +func (r ScoreResults) Len() int { + return len(r) +} + +func (r ScoreResults) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r ScoreResults) Less(i, j int) bool { + if r[i].Type != r[j].Type { + return r[i].Type < r[j].Type + } else if r[i].Points != r[j].Points { + return r[i].Points < r[j].Points + } else if len(r[i].Cards) != len(r[j].Cards) { + return len(r[i].Cards) < len(r[j].Cards) + } + + for k := len(r[i].Cards) - 1; k >= 0; k-- { + if r[i].Cards[k].Value() != r[j].Cards[k].Value() { + return r[i].Cards[k].Value() < r[j].Cards[k].Value() + } + } + + return i < j +} diff --git a/score_test.go b/score_test.go new file mode 100644 index 0000000..353e960 --- /dev/null +++ b/score_test.go @@ -0,0 +1,225 @@ +package cribbage + +import ( + "testing" + + . "git.sr.ht/~tslocum/cards" +) + +var ( + testHandA = Cards{Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}} + testHandB = Cards{Card{FaceJack, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}} + testHandC = Cards{Card{Face3, SuitHearts}, Card{Face3, SuitDiamonds}, Card{Face3, SuitClubs}, Card{Face3, SuitSpades}} + testHandD = Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}} + testHandE = Cards{Card{Face7, SuitHearts}, Card{Face8, SuitHearts}, Card{Face9, SuitHearts}, Card{Face10, SuitHearts}} + testHandF = Cards{Card{Face3, SuitHearts}, Card{Face6, SuitHearts}, Card{Face5, SuitHearts}, Card{Face4, SuitHearts}} + testHandG = Cards{Card{Face6, SuitHearts}, Card{Face5, SuitHearts}, Card{Face4, SuitHearts}} + testHandH = Cards{Card{FaceAce, SuitHearts}, Card{Face3, SuitHearts}, Card{Face2, SuitHearts}, Card{Face4, SuitHearts}} + testHandI = Cards{Card{FaceAce, SuitHearts}, Card{Face3, SuitHearts}, Card{FaceAce, SuitClubs}, Card{Face2, SuitHearts}, Card{Face4, SuitHearts}, Card{Face7, SuitHearts}} + testHandJ = Cards{Card{Face7, SuitHearts}, Card{Face3, SuitHearts}, Card{Face2, SuitHearts}, Card{Face4, SuitHearts}} + testHandK = Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{FaceJack, SuitSpades}} + testHandL = Cards{Card{Face2, SuitHearts}, Card{Face2, SuitDiamonds}, Card{Face7, SuitClubs}, Card{Face7, SuitSpades}} + testHandM = Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{FaceKing, SuitSpades}} +) + +type expectedPegScore struct { + Hand Cards + + Result []ScoreResult +} + +var expectedPegScores = []expectedPegScore{ + {testHandA, []ScoreResult{{Type: ScoreRun, Points: 4, Cards: Cards{Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}}}, + {testHandB, []ScoreResult{{Type: ScorePair, Points: 6, Cards: Cards{Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}}}, + {testHandC, []ScoreResult{{Type: ScorePair, Points: 12, Cards: Cards{Card{Face3, SuitHearts}, Card{Face3, SuitDiamonds}, Card{Face3, SuitClubs}, Card{Face3, SuitSpades}}}}}, + {testHandF, []ScoreResult{{Type: ScoreRun, Points: 4, Cards: Cards{Card{Face3, SuitHearts}, Card{Face4, SuitHearts}, Card{Face5, SuitHearts}, Card{Face6, SuitHearts}}}}}, + {testHandG, []ScoreResult{{Type: Score15, Points: 2, Cards: Cards{Card{Face4, SuitHearts}, Card{Face5, SuitHearts}, Card{Face6, SuitHearts}}}, {Type: ScoreRun, Points: 3, Cards: Cards{Card{Face4, SuitHearts}, Card{Face5, SuitHearts}, Card{Face6, SuitHearts}}}}}, + {testHandH, []ScoreResult{{Type: ScoreRun, Points: 4, Cards: Cards{Card{FaceAce, SuitHearts}, Card{Face2, SuitHearts}, Card{Face3, SuitHearts}, Card{Face4, SuitHearts}}}}}, + {testHandI, []ScoreResult{}}, + {testHandJ, []ScoreResult{{Type: ScoreRun, Points: 3}}}, + {testHandK, []ScoreResult{}}, + {testHandL, []ScoreResult{{Type: ScorePair, Points: 2, Cards: Cards{Card{Face7, SuitClubs}, Card{Face7, SuitSpades}}}}}, + {testHandM, []ScoreResult{}}, +} + +type expectedShowScore struct { + Starter Card + Hand Cards + + HandResult []ScoreResult + CribResult []ScoreResult +} + +var expectedShowScores = []expectedShowScore{ + {Card{FaceAce, SuitSpades}, testHandA, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{FaceAce, SuitSpades}, Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + {Type: ScoreRun, Points: 5, Cards: Cards{Card{FaceAce, SuitSpades}, Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + {Type: ScoreFlush, Points: 5, Cards: Cards{Card{FaceAce, SuitSpades}, Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + }, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{FaceAce, SuitSpades}, Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + {Type: ScoreRun, Points: 5, Cards: Cards{Card{FaceAce, SuitSpades}, Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + {Type: ScoreFlush, Points: 5, Cards: Cards{Card{FaceAce, SuitSpades}, Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + }}, + + {Card{FaceKing, SuitClubs}, testHandA, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitSpades}, Card{FaceKing, SuitClubs}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{FaceKing, SuitClubs}}}, + {Type: ScoreRun, Points: 4, Cards: Cards{Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + {Type: ScoreFlush, Points: 4, Cards: Cards{Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + }, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitSpades}, Card{FaceKing, SuitClubs}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{FaceKing, SuitClubs}}}, + {Type: ScoreRun, Points: 4, Cards: Cards{Card{Face2, SuitSpades}, Card{Face3, SuitSpades}, Card{Face4, SuitSpades}, Card{Face5, SuitSpades}}}, + }}, + + {Card{Face8, SuitClubs}, testHandE, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitHearts}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitClubs}}}, + {Type: ScorePair, Points: 2, Cards: Cards{Card{Face8, SuitHearts}, Card{Face8, SuitClubs}}}, + {Type: ScoreRun, Points: 4, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitHearts}, Card{Face9, SuitHearts}, Card{Face10, SuitHearts}}}, + {Type: ScoreRun, Points: 4, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitClubs}, Card{Face9, SuitHearts}, Card{Face10, SuitHearts}}}, + {Type: ScoreFlush, Points: 4, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitHearts}, Card{Face9, SuitHearts}, Card{Face10, SuitHearts}}}, + }, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitHearts}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitClubs}}}, + {Type: ScorePair, Points: 2, Cards: Cards{Card{Face8, SuitHearts}, Card{Face8, SuitClubs}}}, + {Type: ScoreRun, Points: 4, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitHearts}, Card{Face9, SuitHearts}, Card{Face10, SuitHearts}}}, + {Type: ScoreRun, Points: 4, Cards: Cards{Card{Face7, SuitHearts}, Card{Face8, SuitClubs}, Card{Face9, SuitHearts}, Card{Face10, SuitHearts}}}, + }}, + + {Card{Face5, SuitSpades}, testHandK, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitClubs}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitSpades}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: ScorePair, Points: 12, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: ScoreNobs, Points: 1, Cards: Cards{Card{FaceJack, SuitSpades}}}, + }, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitClubs}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitSpades}, Card{FaceJack, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: ScorePair, Points: 12, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: ScoreNobs, Points: 1, Cards: Cards{Card{FaceJack, SuitSpades}}}, + }}, + + {Card{Face5, SuitSpades}, testHandM, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitClubs}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitSpades}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: ScorePair, Points: 12, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + }, []ScoreResult{ + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitClubs}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitSpades}, Card{FaceKing, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: Score15, Points: 2, Cards: Cards{Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + {Type: ScorePair, Points: 12, Cards: Cards{Card{Face5, SuitHearts}, Card{Face5, SuitDiamonds}, Card{Face5, SuitClubs}, Card{Face5, SuitSpades}}}, + }}, +} + +func TestPegScoring(t *testing.T) { + t.Parallel() + + for i, expected := range expectedPegScores { + if Sum(expected.Hand) > 31 { + t.Errorf("case %d: invalid peg hand sum: got %d, want <=31: %s", i, Sum(expected.Hand), expected.Hand) + } + + pegPoints, pegResult := Score(Peg, expected.Hand, Card{}) + + if !resultsEqual(pegResult, expected.Result) { + t.Fatalf("case %d: incorrect peg result: got %s, want %s: %s", i, pegResult, expected.Result, expected.Hand) + } + + var expectedPoints int + for _, r := range expected.Result { + expectedPoints += r.Points + } + if pegPoints != expectedPoints { + t.Fatalf("case %d: incorrect peg score: got %d, want %d: %s", i, pegPoints, expectedPoints, expected.Hand) + } + } +} + +func TestScoreShowHand(t *testing.T) { + t.Parallel() + + var expectedPoints int + for i, expected := range expectedShowScores { + handPoints, handResult := Score(ShowHand, expected.Hand, expected.Starter) + + if !resultsEqual(handResult, expected.HandResult) { + t.Fatalf("case %d: incorrect hand result: got %s, want %s: %s - %s", i, handResult, expected.HandResult, expected.Starter, expected.Hand) + } + + expectedPoints = 0 + for _, r := range expected.HandResult { + expectedPoints += r.Points + } + if handPoints != expectedPoints { + t.Fatalf("case %d: incorrect hand score: got %d, want %d: %s - %s", i, handPoints, expectedPoints, expected.Starter, expected.Hand) + } + } +} + +func TestScoreShowCrib(t *testing.T) { + t.Parallel() + + var expectedPoints int + for i, expected := range expectedShowScores { + cribPoints, cribResult := Score(ShowCrib, expected.Hand, expected.Starter) + + if !resultsEqual(cribResult, expected.CribResult) { + t.Fatalf("case %d: incorrect crib result: got %s, want %s: %s - %s", i, cribResult, expected.CribResult, expected.Starter, expected.Hand) + } + + expectedPoints = 0 + for _, r := range expected.CribResult { + expectedPoints += r.Points + } + if cribPoints != expectedPoints { + t.Fatalf("case %d: incorrect crib score: got %d, want %d: %s - %s", i, cribPoints, expectedPoints, expected.Starter, expected.Hand) + } + } +} + +func resultsEqual(a []ScoreResult, b []ScoreResult) bool { + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i].Type != b[i].Type { + return false + } else if a[i].Points != b[i].Points { + return false + } else if len(a[i].Cards) != len(b[i].Cards) { + return false + } + + for j := range a[i].Cards { + if !a[i].Cards[j].Equal(b[i].Cards[j]) { + return false + } + } + } + + return true +}