From 279d536458585869d53366ae15bda7f63f081aca Mon Sep 17 00:00:00 2001 From: mappu Date: Sun, 7 Jan 2024 15:33:45 +1300 Subject: [PATCH] faster run search, entropy parameter, refactor currentPlayer, round test --- card.go | 4 ++-- card_test.go | 7 +++++-- game.go | 50 ++++++++++++++++++++++++++------------------------ game_test.go | 14 ++++++++++++-- search.go | 24 +++++++++++++++--------- strategy.go | 26 ++++++++++++-------------- util.go | 16 +--------------- 7 files changed, 73 insertions(+), 68 deletions(-) diff --git a/card.go b/card.go index 25ac114..13067fe 100644 --- a/card.go +++ b/card.go @@ -87,8 +87,8 @@ 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) { +func Shuffle(c []Card, entropy *rand.Rand) { + entropy.Shuffle(len(c), func(i, j int) { c[i], c[j] = c[j], c[i] }) } diff --git a/card_test.go b/card_test.go index bc71d57..5eab3ad 100644 --- a/card_test.go +++ b/card_test.go @@ -1,6 +1,7 @@ package main import ( + "math/rand" "testing" "github.com/stretchr/testify/assert" @@ -10,10 +11,12 @@ func TestCards(t *testing.T) { d := Deck() assert.Len(t, d, DeckSize) - 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) + entropy := rand.New(rand.NewSource(0xdeadbeef)) + + Shuffle(d, entropy) assert.Len(t, d, DeckSize) + assert.EqualValues(t, []Card{50, 47, 24, 25, 22, 61, 33, 66, 36, 41, 16, 18, 58, 31, 16, 48, 38, 39, 33, 17, 0, 15, 32, 68, 23, 67, 27, 18, 22, 0, 46, 45, 31, 65, 60, 28, 68, 51, 26, 63, 41, 53, 65, 42, 59, 56, 40, 55, 30, 54, 57, 66, 61, 26, 51, 49, 55, 46, 35, 44, 42, 24, 45, 52, 59, 39, 19, 30, 36, 47, 49, 56, 0, 32, 62, 67, 50, 37, 58, 21, 69, 48, 53, 23, 57, 64, 34, 0, 21, 43, 27, 0, 64, 52, 60, 69, 17, 63, 35, 44, 25, 20, 43, 29, 19, 38, 34, 15, 54, 29, 28, 37, 20, 0, 62, 40}, d) } diff --git a/game.go b/game.go index 7f45eaf..8a74253 100644 --- a/game.go +++ b/game.go @@ -2,9 +2,10 @@ package main import ( "fmt" + "math/rand" ) -func PlayGame(numPlayers int) { +func PlayGame(numPlayers int, entropy *rand.Rand) { currentPlayer := 0 @@ -12,9 +13,10 @@ func PlayGame(numPlayers int) { for round := 3; round < 14; round++ { - r := NewRound(round, numPlayers, currentPlayer) - roundScores := r.Play() - currentPlayer = r.currentPlayer + r := NewRound(round, numPlayers, entropy) + nextPlayer, roundScores := r.Play(currentPlayer) + + currentPlayer = nextPlayer for p := 0; p < numPlayers; p++ { runningScores[p] += roundScores[p] @@ -28,24 +30,22 @@ func PlayGame(numPlayers int) { } type Round struct { - d []Card - discard []Card - round int - numPlayers int - currentPlayer int - hands [][]Card + d []Card + discard []Card + round int + numPlayers int + hands [][]Card } -func NewRound(round, numPlayers, currentPlayer int) *Round { +func NewRound(round, numPlayers int, entropy *rand.Rand) *Round { r := Round{ - round: round, - numPlayers: numPlayers, - currentPlayer: currentPlayer, + round: round, + numPlayers: numPlayers, } r.d = Deck() - Shuffle(r.d) + Shuffle(r.d, entropy) r.discard = []Card{} @@ -95,7 +95,9 @@ func FormatHandGroupings(hand []Card, groupings [][]int) string { return fmt.Sprintf("[ %v leftover %v ]", cgroups, leftover) } -func (r *Round) Play() []int { +func (r *Round) Play(startingPlayer int) (nextPlayer int, roundScores []int) { + + currentPlayer := startingPlayer fmt.Printf("Discard stack: %v\n", r.discard) @@ -104,34 +106,34 @@ func (r *Round) Play() []int { roundEndsWhenPlayerIs := -1 for { - if roundEndsWhenPlayerIs == r.currentPlayer { + if roundEndsWhenPlayerIs == currentPlayer { break } // Play the strategy for the current player - r.PlayDefaultStrategy() + r.PlayDefaultStrategy(currentPlayer) // Check, one more time, if we have a winning hand - currentBestGrouping, currentScore := FindBestGrouping(r.hands[r.currentPlayer], r.round) + currentBestGrouping, currentScore := FindBestGrouping(r.hands[currentPlayer], r.round) if currentScore == 0 { // Declare victory - fmt.Printf("P%d declares victory\n- Hand: %v\n- Groupings: %v\n", r.currentPlayer, r.hands[r.currentPlayer], currentBestGrouping) + fmt.Printf("P%d declares victory\n- Hand: %v\n- Groupings: %v\n", currentPlayer, r.hands[currentPlayer], currentBestGrouping) if roundEndsWhenPlayerIs == -1 { - roundEndsWhenPlayerIs = r.currentPlayer + roundEndsWhenPlayerIs = currentPlayer } // Otherwise, the round is already ending anyway } // Move to the next player - r.currentPlayer = (r.currentPlayer + 1) % r.numPlayers + currentPlayer = (currentPlayer + 1) % r.numPlayers } // The round has ended // Figure out what each player scored - roundScores := make([]int, r.numPlayers) + roundScores = make([]int, r.numPlayers) for p := 0; p < r.numPlayers; p++ { @@ -148,5 +150,5 @@ func (r *Round) Play() []int { } // Done - return roundScores + return currentPlayer, roundScores } diff --git a/game_test.go b/game_test.go index c2f1024..e477ebf 100644 --- a/game_test.go +++ b/game_test.go @@ -1,9 +1,19 @@ package main import ( + "math/rand" "testing" + + "github.com/stretchr/testify/assert" ) -func TestPlayGame(t *testing.T) { - PlayGame(4) +func TestPlayRound(t *testing.T) { + + entropy := rand.New(rand.NewSource(0xdeadbeef)) + + rr := NewRound(5, 4, entropy) + nextPlayer, scores := rr.Play(0) + + assert.EqualValues(t, 0, nextPlayer) + assert.EqualValues(t, []int{0, 28, 6, 6}, scores) } diff --git a/search.go b/search.go index c2d120a..6e82059 100644 --- a/search.go +++ b/search.go @@ -41,25 +41,29 @@ func MakeBooks(hand []Card, wildFace int) [][]int { return pairings } -func continueRun(hand []Card, wildFace int, requireSuit Suit, searchNextFace int, takenIndexes map[int]struct{}) []map[int]struct{} { +func continueRun(hand []Card, wildFace int, requireSuit Suit, searchNextFace int, takenIndexes []int) [][]int { - possibilities := []map[int]struct{}{ + possibilities := [][]int{ takenIndexes, } // Do you have any XX|wild - posns := searchExcept(hand, wildFace, NewCardFrom(searchNextFace, requireSuit), takenIndexes) + posns := search(hand, wildFace, NewCardFrom(searchNextFace, requireSuit)) // For each match: for _, midx := range posns { // Take out that card - branchTaken := forkPossibilities(takenIndexes) - branchTaken[midx] = struct{}{} + branchTaken := make([]int, len(takenIndexes)+1) + copy(branchTaken, takenIndexes) + branchTaken[len(branchTaken)-1] = midx + + branchTakenHand := forkHand(hand) + branchTakenHand[midx] = NewMasked() // 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) + continuations := continueRun(branchTakenHand, wildFace, requireSuit, searchNextFace+1, branchTaken) // We found those possibilities for this run possibilities = append(possibilities, continuations...) @@ -76,14 +80,14 @@ func MakeRuns(hand []Card, wildFace int) [][]int { 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{}{}) + found := continueRun(hand, wildFace, Suit(s), f, make([]int, 0)) // 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)) + pairings = append(pairings, ff) } } @@ -157,7 +161,9 @@ func ScoreAsUnpaired(hand []Card) int { func FindBestGrouping(hand []Card, wildface int) ([][]int, int) { groupings := MakeMultiGroups(hand, wildface) - fmt.Printf("Found %d possible groupings\n", len(groupings)) + if len(groupings) > 1000 { + fmt.Printf("Found %d possible groupings\n", len(groupings)) + } bestScore := 50 * len(hand) // Worst possible score bestGroupIdx := -1 diff --git a/strategy.go b/strategy.go index e450031..995092a 100644 --- a/strategy.go +++ b/strategy.go @@ -4,12 +4,10 @@ import ( "fmt" ) -func (r *Round) PlayDefaultStrategy() { - - currentPlayer := r.currentPlayer +func (r *Round) PlayDefaultStrategy(currentPlayer int) { // Score our hand as if we pick up the discard and discard it immediately - _, score := FindBestGrouping(r.hands[r.currentPlayer], r.round) + _, score := FindBestGrouping(r.hands[currentPlayer], r.round) baseScore := score bestDiscardBasedIdx := -1 bestDiscardBasedScore := score @@ -20,7 +18,7 @@ func (r *Round) PlayDefaultStrategy() { for cidx := 0; cidx < r.round; cidx++ { // Pick up the last discard card, and discard card@cidx - theoretically := forkHand(r.hands[r.currentPlayer]) + theoretically := forkHand(r.hands[currentPlayer]) theoretically[cidx] = r.discard[len(r.discard)-1] _, score := FindBestGrouping(theoretically, r.round) @@ -36,20 +34,20 @@ func (r *Round) PlayDefaultStrategy() { // 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", r.currentPlayer, r.discard[len(r.discard)-1]) + fmt.Printf("P%d taking %v from the discard pile\n", currentPlayer, r.discard[len(r.discard)-1]) // We're going to win if bestDiscardBasedIdx == -1 { // Take the discard card + put it back again i.e. no change - fmt.Printf("P%d discards %v immediately\n", r.currentPlayer, r.discard[len(r.discard)-1]) + fmt.Printf("P%d discards %v immediately\n", currentPlayer, r.discard[len(r.discard)-1]) } else { // Take the discard card and discard cidx newCard := r.discard[len(r.discard)-1] r.discard = r.discard[0 : len(r.discard)-1] - unwantedCard := r.hands[r.currentPlayer][bestDiscardBasedIdx] - r.hands[r.currentPlayer][bestDiscardBasedIdx] = newCard + unwantedCard := r.hands[currentPlayer][bestDiscardBasedIdx] + r.hands[currentPlayer][bestDiscardBasedIdx] = newCard r.discard = append(r.discard, unwantedCard) // On top = available for next player @@ -64,9 +62,9 @@ func (r *Round) PlayDefaultStrategy() { newCard := r.d[0] r.d = r.d[1:] - r.hands[r.currentPlayer] = append(r.hands[r.currentPlayer], newCard) + r.hands[currentPlayer] = append(r.hands[currentPlayer], newCard) - fmt.Printf("P%d taking %v from the deck\n", r.currentPlayer, newCard) + fmt.Printf("P%d taking %v from the deck\n", currentPlayer, newCard) bestRandomBasedIdx := -1 bestRandomBasedScore := (r.round * 50) + 51 // Worse than the worst possible score @@ -89,12 +87,12 @@ func (r *Round) PlayDefaultStrategy() { // Discard our chosen card // Slice tricks: Copy end card into this slot and reslice bounds - unwantedCard := r.hands[r.currentPlayer][bestRandomBasedIdx] + unwantedCard := r.hands[currentPlayer][bestRandomBasedIdx] fmt.Printf("P%d discards %v (position %d)\n", currentPlayer, unwantedCard, bestRandomBasedIdx) - r.hands[r.currentPlayer][bestRandomBasedIdx] = r.hands[r.currentPlayer][len(r.hands[r.currentPlayer])-1] - r.hands[r.currentPlayer] = r.hands[r.currentPlayer][0 : len(r.hands[r.currentPlayer])-1] + r.hands[currentPlayer][bestRandomBasedIdx] = r.hands[currentPlayer][len(r.hands[currentPlayer])-1] + r.hands[currentPlayer] = r.hands[currentPlayer][0 : len(r.hands[currentPlayer])-1] r.discard = append(r.discard, unwantedCard) // On top = available for next player diff --git a/util.go b/util.go index 90df277..0de7402 100644 --- a/util.go +++ b/util.go @@ -7,25 +7,11 @@ import ( 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.Masked() { continue // Doesn't match anything } - if c.Joker() || c == search || c.Face() == wildFace { + if c == search || c.Joker() || c.Face() == wildFace { ret = append(ret, idx) } }