initial commit
This commit is contained in:
commit
42490e2b4b
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# crowns
|
||||||
|
|
||||||
|
AI and simulator for the card game [Five Crowns](https://setgame.com/sites/default/files/instructions/FIVE%20CROWNS%20-%20ENGLISH_2.pdf).
|
95
card.go
Normal file
95
card.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Suit int
|
||||||
|
|
||||||
|
func (s Suit) String() string {
|
||||||
|
switch s {
|
||||||
|
case 0:
|
||||||
|
return "♠"
|
||||||
|
case 1:
|
||||||
|
return "♥"
|
||||||
|
case 2:
|
||||||
|
return "♦"
|
||||||
|
case 3:
|
||||||
|
return "♣"
|
||||||
|
case 4:
|
||||||
|
return "★"
|
||||||
|
default:
|
||||||
|
panic("bad suit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Card int
|
||||||
|
|
||||||
|
func (c Card) Joker() bool {
|
||||||
|
return (c == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Card) Suit() Suit {
|
||||||
|
return Suit(int(c) % 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Card) Face() int {
|
||||||
|
return int(c) / 5
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Card) String() string {
|
||||||
|
if c.Joker() {
|
||||||
|
return "Jok" // 🂿 U+1F0BF
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == -1 {
|
||||||
|
return "INV" // Invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Face() {
|
||||||
|
case 11:
|
||||||
|
return fmt.Sprintf(" J%s", c.Suit().String())
|
||||||
|
|
||||||
|
case 12:
|
||||||
|
return fmt.Sprintf(" Q%s", c.Suit().String())
|
||||||
|
|
||||||
|
case 13:
|
||||||
|
return fmt.Sprintf(" K%s", c.Suit().String())
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%2d%s", c.Face(), c.Suit().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJoker() Card {
|
||||||
|
return Card(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCardFrom(face int, suit Suit) Card {
|
||||||
|
return Card(face*5 + int(suit))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Shuffle(c []Card) {
|
||||||
|
rand.Shuffle(len(c), func(i, j int) {
|
||||||
|
c[i], c[j] = c[j], c[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Deck() []Card {
|
||||||
|
// The full game deck is two 58-card decks
|
||||||
|
// Each 58-card deck has 3 jokers plus {3..10 J Q K} in 5 suits.
|
||||||
|
ret := make([]Card, 0, 58*2)
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
ret = append(ret, NewJoker()) // Joker
|
||||||
|
}
|
||||||
|
|
||||||
|
for s := 0; s < 5; s++ {
|
||||||
|
for f := 3; f < 14; f++ {
|
||||||
|
ret = append(ret, NewCardFrom(f, Suit(s)), NewCardFrom(f, Suit(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
20
card_test.go
Normal file
20
card_test.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCards(t *testing.T) {
|
||||||
|
d := Deck()
|
||||||
|
|
||||||
|
assert.Len(t, d, 58*2)
|
||||||
|
|
||||||
|
assert.EqualValues(t, []Card{0, 0, 0, 0, 0, 0, 15, 15, 20, 20, 25, 25, 30, 30, 35, 35, 40, 40, 45, 45, 50, 50, 55, 55, 60, 60, 65, 65, 16, 16, 21, 21, 26, 26, 31, 31, 36, 36, 41, 41, 46, 46, 51, 51, 56, 56, 61, 61, 66, 66, 17, 17, 22, 22, 27, 27, 32, 32, 37, 37, 42, 42, 47, 47, 52, 52, 57, 57, 62, 62, 67, 67, 18, 18, 23, 23, 28, 28, 33, 33, 38, 38, 43, 43, 48, 48, 53, 53, 58, 58, 63, 63, 68, 68, 19, 19, 24, 24, 29, 29, 34, 34, 39, 39, 44, 44, 49, 49, 54, 54, 59, 59, 64, 64, 69, 69}, d)
|
||||||
|
|
||||||
|
Shuffle(d)
|
||||||
|
|
||||||
|
assert.Len(t, d, 58*2)
|
||||||
|
|
||||||
|
}
|
154
game.go
Normal file
154
game.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PlayGame(numPlayers int) {
|
||||||
|
|
||||||
|
currentPlayer := 0
|
||||||
|
|
||||||
|
for round := 3; round < 14; round++ {
|
||||||
|
|
||||||
|
d := Deck()
|
||||||
|
Shuffle(d)
|
||||||
|
|
||||||
|
discard := []Card{}
|
||||||
|
|
||||||
|
fmt.Printf("# Round %d\n\n", round)
|
||||||
|
|
||||||
|
// Deal starting cards
|
||||||
|
|
||||||
|
hands := make([][]Card, numPlayers)
|
||||||
|
for p := 0; p < numPlayers; p++ {
|
||||||
|
hands[p] = d[0:round] // Deal from the bottom (0)
|
||||||
|
d = d[round:]
|
||||||
|
|
||||||
|
fmt.Printf("P%d starting hand: %v\n", p, hands[p])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deal one into the discard pile
|
||||||
|
|
||||||
|
discard = append(discard, d[0])
|
||||||
|
d = d[1:]
|
||||||
|
|
||||||
|
fmt.Printf("Discard stack: %v\n", discard)
|
||||||
|
|
||||||
|
fmt.Printf("Deck has %d cards remaining\n", len(d))
|
||||||
|
|
||||||
|
roundEndsWhenPlayerIs := -1
|
||||||
|
|
||||||
|
for {
|
||||||
|
if roundEndsWhenPlayerIs == currentPlayer {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score our hand as if we pick up the discard and discard it immediately
|
||||||
|
_, score := FindBestGrouping(hands[currentPlayer], round)
|
||||||
|
baseScore := score
|
||||||
|
bestDiscardBasedIdx := -1
|
||||||
|
bestDiscardBasedScore := score
|
||||||
|
|
||||||
|
// Score our hand as if we pick up the discard card and discard each other card
|
||||||
|
|
||||||
|
for cidx := 0; cidx < round; cidx++ {
|
||||||
|
// Pick up the last discard card, and discard card@cidx
|
||||||
|
theoretically := forkHand(hands[currentPlayer])
|
||||||
|
theoretically[cidx] = discard[len(discard)-1]
|
||||||
|
_, score := FindBestGrouping(theoretically, round)
|
||||||
|
|
||||||
|
if score < bestDiscardBasedScore {
|
||||||
|
// TODO if the points are equal or near-equal, prefer not to discard a wild for the next player
|
||||||
|
bestDiscardBasedIdx = cidx
|
||||||
|
bestDiscardBasedScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If taking the discard would let us win, or reduce our score by >5, take it
|
||||||
|
if bestDiscardBasedScore == 0 || (baseScore-bestDiscardBasedScore) > 5 {
|
||||||
|
|
||||||
|
fmt.Printf("P%d taking %v from the discard pile\n", currentPlayer, discard[len(discard)-1])
|
||||||
|
|
||||||
|
// We're going to win
|
||||||
|
if bestDiscardBasedIdx == -1 {
|
||||||
|
// Take the discard card + put it back again i.e. no change
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Take the discard card and discard cidx
|
||||||
|
newCard := discard[len(discard)-1]
|
||||||
|
discard = discard[0 : len(discard)-1]
|
||||||
|
|
||||||
|
unwantedCard := hands[currentPlayer][bestDiscardBasedIdx]
|
||||||
|
hands[currentPlayer][bestDiscardBasedIdx] = newCard
|
||||||
|
|
||||||
|
discard = append(discard, unwantedCard) // On top = available for next player
|
||||||
|
|
||||||
|
fmt.Printf("P%d discards %v\n", currentPlayer, unwantedCard)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// The discard card is not going to make us win or significantly lower our
|
||||||
|
// score
|
||||||
|
// Take from the deck
|
||||||
|
|
||||||
|
newCard := d[0]
|
||||||
|
d = d[1:]
|
||||||
|
hands[currentPlayer] = append(hands[currentPlayer], newCard)
|
||||||
|
|
||||||
|
fmt.Printf("P%d taking %v from the deck\n", currentPlayer, newCard)
|
||||||
|
|
||||||
|
bestRandomBasedIdx := -1
|
||||||
|
bestRandomBasedScore := (round * 50) + 51 // Worse than the worst possible score
|
||||||
|
|
||||||
|
// Look at what we can now drop (hand size + 1 new card). We have to drop something
|
||||||
|
for cidx := 0; cidx < round+1; cidx++ {
|
||||||
|
// Assume we discard that card,
|
||||||
|
theoretically := forkHand(hands[currentPlayer])
|
||||||
|
theoretically[cidx] = Card(-1) // Never matches + worth 0 points
|
||||||
|
_, score := FindBestGrouping(theoretically, round)
|
||||||
|
|
||||||
|
if score < bestRandomBasedScore {
|
||||||
|
// TODO if the points are equal or near-equal, prefer not to discard a wild for the next player
|
||||||
|
bestRandomBasedIdx = cidx
|
||||||
|
bestRandomBasedScore = score
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard our chosen card
|
||||||
|
// Slice tricks: Copy end card into this slot and reslice bounds
|
||||||
|
|
||||||
|
unwantedCard := hands[currentPlayer][bestRandomBasedIdx]
|
||||||
|
|
||||||
|
fmt.Printf("P%d discards %v\n", currentPlayer, unwantedCard)
|
||||||
|
|
||||||
|
hands[currentPlayer][bestRandomBasedIdx] = hands[currentPlayer][len(hands[currentPlayer])-1]
|
||||||
|
hands[currentPlayer] = hands[currentPlayer][0 : len(hands[currentPlayer])-1]
|
||||||
|
|
||||||
|
discard = append(discard, unwantedCard) // On top = available for next player
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check, one more time, if we have a winning hand
|
||||||
|
|
||||||
|
currentBestGrouping, currentScore := FindBestGrouping(hands[currentPlayer], round)
|
||||||
|
if currentScore == 0 {
|
||||||
|
|
||||||
|
// Declare victory
|
||||||
|
fmt.Printf("P%d declares victory\n- Hand: %v\n- Groupings: %v\n", currentPlayer, hands[currentPlayer], currentBestGrouping)
|
||||||
|
|
||||||
|
roundEndsWhenPlayerIs = currentPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next player
|
||||||
|
currentPlayer = (currentPlayer + 1) % numPlayers
|
||||||
|
}
|
||||||
|
|
||||||
|
// The round has ended
|
||||||
|
// Figure out what each player scored
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
9
game_test.go
Normal file
9
game_test.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPlayGame(t *testing.T) {
|
||||||
|
PlayGame(4)
|
||||||
|
}
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module code.ivysaur.me/crowns
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/mxschmitt/golang-combinations v1.2.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
11
go.sum
Normal file
11
go.sum
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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/mxschmitt/golang-combinations v1.2.0 h1:V5E7MncIK8Yr1SL/SpdqMuSquFsfoIs5auI7Y3n8z14=
|
||||||
|
github.com/mxschmitt/golang-combinations v1.2.0/go.mod h1:RCm5eR03B+JrBOMRDLsKZWShluXdrHu+qwhPEJ0miBM=
|
||||||
|
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
223
search.go
Normal file
223
search.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/mxschmitt/golang-combinations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeBooks(hand []Card, wildFace int) [][]int {
|
||||||
|
// Return all the possible single books that could be made from this hand.
|
||||||
|
// Caller should call this tree-recursively to find multiple grouping options.
|
||||||
|
pairings := [][]int{}
|
||||||
|
|
||||||
|
// Check for books
|
||||||
|
for i := 3; i < 14; i++ {
|
||||||
|
book := []int{}
|
||||||
|
for idx, c := range hand {
|
||||||
|
if c.Face() == i || c.Joker() || c.Face() == wildFace {
|
||||||
|
book = append(book, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(book) == 3 {
|
||||||
|
// That's an option
|
||||||
|
pairings = append(pairings, book)
|
||||||
|
|
||||||
|
} else if len(book) > 3 {
|
||||||
|
// For length over 3, all combinations of this are an option
|
||||||
|
|
||||||
|
for booksz := 3; booksz < len(book)+1; booksz++ {
|
||||||
|
pairings = append(pairings, combinations.Combinations(book, booksz)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Those are all the pairings we found
|
||||||
|
return pairings
|
||||||
|
}
|
||||||
|
|
||||||
|
func search(hand []Card, wildFace int, search Card) []int {
|
||||||
|
ret := []int{}
|
||||||
|
for idx, c := range hand {
|
||||||
|
if c == search || c.Joker() || c.Face() == wildFace {
|
||||||
|
ret = append(ret, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchExcept(hand []Card, wildFace int, search Card, blockindexes map[int]struct{}) []int {
|
||||||
|
ret := []int{}
|
||||||
|
for idx, c := range hand {
|
||||||
|
if _, blocked := blockindexes[idx]; blocked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == search || c.Joker() || c.Face() == wildFace {
|
||||||
|
ret = append(ret, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// take takes a single card out of a hand by its index, returning the remaining cards.
|
||||||
|
func take(hand []Card, index int) (Card, []Card) {
|
||||||
|
cc := hand[index]
|
||||||
|
|
||||||
|
rem := make([]Card, 0, len(hand)-1)
|
||||||
|
rem = append(rem, hand[0:index]...)
|
||||||
|
rem = append(rem, hand[index+1:]...)
|
||||||
|
|
||||||
|
return cc, rem
|
||||||
|
}
|
||||||
|
|
||||||
|
func forkPossibilities(in map[int]struct{}) map[int]struct{} {
|
||||||
|
ret := map[int]struct{}{}
|
||||||
|
for prev, _ := range in {
|
||||||
|
ret[prev] = struct{}{}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func forkHand(hand []Card) []Card {
|
||||||
|
ret := make([]Card, len(hand))
|
||||||
|
copy(ret, hand)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenPossibility(in map[int]struct{}) []int {
|
||||||
|
flat := make([]int, 0, len(in))
|
||||||
|
for cidx, _ := range in {
|
||||||
|
flat = append(flat, cidx)
|
||||||
|
}
|
||||||
|
sort.Ints(flat)
|
||||||
|
return flat
|
||||||
|
}
|
||||||
|
|
||||||
|
func continueRun(hand []Card, wildFace int, requireSuit Suit, searchNextFace int, takenIndexes map[int]struct{}) []map[int]struct{} {
|
||||||
|
|
||||||
|
possibilities := []map[int]struct{}{
|
||||||
|
takenIndexes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do you have any XX|wild
|
||||||
|
posns := searchExcept(hand, wildFace, NewCardFrom(searchNextFace, requireSuit), takenIndexes)
|
||||||
|
|
||||||
|
// For each match:
|
||||||
|
for _, midx := range posns {
|
||||||
|
|
||||||
|
// Take out that card
|
||||||
|
branchTaken := forkPossibilities(takenIndexes)
|
||||||
|
branchTaken[midx] = struct{}{}
|
||||||
|
|
||||||
|
// Try and continue the run upwards to the next card, using only the remaining parts of the hand
|
||||||
|
// (We will never find K+1)
|
||||||
|
continuations := continueRun(hand, wildFace, requireSuit, searchNextFace+1, branchTaken)
|
||||||
|
|
||||||
|
// We found those possibilities for this run
|
||||||
|
possibilities = append(possibilities, continuations...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return possibilities
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeRuns(hand []Card, wildFace int) [][]int {
|
||||||
|
pairings := [][]int{}
|
||||||
|
|
||||||
|
for s := 0; s < 5; s++ {
|
||||||
|
|
||||||
|
for f := 3; f < 12; f++ { // -2 from usual 14, as you can't start an upward run from Q/K
|
||||||
|
|
||||||
|
// Starting with this card, find all runs going upward
|
||||||
|
found := continueRun(hand, wildFace, Suit(s), f, map[int]struct{}{})
|
||||||
|
|
||||||
|
// For each possible run we could make starting here:
|
||||||
|
for _, ff := range found {
|
||||||
|
if len(ff) < 3 {
|
||||||
|
continue // Short runs are no good
|
||||||
|
}
|
||||||
|
pairings = append(pairings, flattenPossibility(ff))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairings
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeMultiGroups(hand []Card, wildface int) [][][]int {
|
||||||
|
// Depth-first recurse to find all groups in the hand
|
||||||
|
|
||||||
|
ret := [][][]int{}
|
||||||
|
|
||||||
|
poss := [][]int{}
|
||||||
|
poss = append(poss, MakeBooks(hand, wildface)...)
|
||||||
|
poss = append(poss, MakeRuns(hand, wildface)...)
|
||||||
|
|
||||||
|
for _, p := range poss {
|
||||||
|
// One groupup would be: to take that possibility alone
|
||||||
|
ret = append(ret, [][]int{p})
|
||||||
|
|
||||||
|
// Can we do anything more?
|
||||||
|
|
||||||
|
cloneHand := forkHand(hand)
|
||||||
|
for _, midx := range p {
|
||||||
|
cloneHand[midx] = Card(-1) // can't match with anything
|
||||||
|
}
|
||||||
|
|
||||||
|
chposs := MakeMultiGroups(cloneHand, wildface)
|
||||||
|
// Additional groupups would be: p plus chposs
|
||||||
|
for _, p2 := range chposs {
|
||||||
|
groupup := [][]int{p}
|
||||||
|
groupup = append(groupup, p2...)
|
||||||
|
ret = append(ret, groupup)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScoreAsUnpaired(hand []Card) int {
|
||||||
|
score := 0
|
||||||
|
for _, c := range hand {
|
||||||
|
if c.Joker() {
|
||||||
|
score += 50
|
||||||
|
} else if c == Card(-1) {
|
||||||
|
// skip (already accounted for)
|
||||||
|
} else {
|
||||||
|
score += c.Face()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindBestGrouping(hand []Card, wildface int) ([][]int, int) {
|
||||||
|
groupings := MakeMultiGroups(hand, wildface)
|
||||||
|
|
||||||
|
bestScore := 50 * len(hand) // Worst possible score
|
||||||
|
bestGroupIdx := -1
|
||||||
|
for ggidx, grouping := range groupings {
|
||||||
|
|
||||||
|
cloneHand := make([]Card, len(hand))
|
||||||
|
for _, group := range grouping {
|
||||||
|
for _, cidx := range group {
|
||||||
|
cloneHand[cidx] = Card(-1) // invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
score := ScoreAsUnpaired(cloneHand)
|
||||||
|
if score < bestScore {
|
||||||
|
bestScore = score
|
||||||
|
bestGroupIdx = ggidx
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestGroupIdx == -1 {
|
||||||
|
return [][]int{}, bestScore // No pairings could be found, full points
|
||||||
|
}
|
||||||
|
return groupings[bestGroupIdx], bestScore
|
||||||
|
}
|
45
search_test.go
Normal file
45
search_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindBooks(t *testing.T) {
|
||||||
|
|
||||||
|
hand := []Card{15, 16, 17} // [ 3♠ 3♥ 3♦]
|
||||||
|
|
||||||
|
t.Logf("Finding books in hand: %v", hand)
|
||||||
|
|
||||||
|
p := MakeBooks(hand, 10)
|
||||||
|
assert.EqualValues(t, [][]int{[]int{0, 1, 2}}, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindRuns(t *testing.T) {
|
||||||
|
hand := []Card{15, 20, 25, 25, 30} // [ 3♠ 4♠ 5♠ 5♠ 6♠]
|
||||||
|
|
||||||
|
t.Logf("Finding runs in hand: %v", hand)
|
||||||
|
|
||||||
|
p := MakeRuns(hand, 10)
|
||||||
|
assert.EqualValues(t, [][]int{[]int{0, 1, 2}, []int{0, 1, 2, 4}, []int{0, 1, 3}, []int{0, 1, 3, 4}, []int{1, 2, 4}, []int{1, 3, 4}}, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindMultiGroups(t *testing.T) {
|
||||||
|
hand := []Card{15, 20, 25, 30, 16, 17}
|
||||||
|
|
||||||
|
t.Logf("Finding all groupups in hand: %v", hand)
|
||||||
|
|
||||||
|
p := MakeMultiGroups(hand, 10)
|
||||||
|
assert.EqualValues(t, [][][]int{[][]int{[]int{0, 4, 5}}, [][]int{[]int{0, 4, 5}, []int{1, 2, 3}}, [][]int{[]int{0, 1, 2}}, [][]int{[]int{0, 1, 2, 3}}, [][]int{[]int{1, 2, 3}}, [][]int{[]int{1, 2, 3}, []int{0, 4, 5}}}, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSolveHand(t *testing.T) {
|
||||||
|
hand := []Card{15, 20, 25, 30, 16, 17}
|
||||||
|
|
||||||
|
t.Logf("Finding best groupups in hand: %v", hand)
|
||||||
|
|
||||||
|
grouping, score := FindBestGrouping(hand, 10)
|
||||||
|
assert.EqualValues(t, [][]int{[]int{0, 4, 5}, []int{1, 2, 3}}, grouping)
|
||||||
|
assert.EqualValues(t, 0, score)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user