faster run search, entropy parameter, refactor currentPlayer, round test

This commit is contained in:
mappu 2024-01-07 15:33:45 +13:00
parent 2f2c3cdd22
commit 279d536458
7 changed files with 73 additions and 68 deletions

View File

@ -87,8 +87,8 @@ func NewCardFrom(face int, suit Suit) Card {
return Card(face*5 + int(suit)) return Card(face*5 + int(suit))
} }
func Shuffle(c []Card) { func Shuffle(c []Card, entropy *rand.Rand) {
rand.Shuffle(len(c), func(i, j int) { entropy.Shuffle(len(c), func(i, j int) {
c[i], c[j] = c[j], c[i] c[i], c[j] = c[j], c[i]
}) })
} }

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"math/rand"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -10,10 +11,12 @@ func TestCards(t *testing.T) {
d := Deck() d := Deck()
assert.Len(t, d, DeckSize) 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) 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.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)
} }

50
game.go
View File

@ -2,9 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"math/rand"
) )
func PlayGame(numPlayers int) { func PlayGame(numPlayers int, entropy *rand.Rand) {
currentPlayer := 0 currentPlayer := 0
@ -12,9 +13,10 @@ func PlayGame(numPlayers int) {
for round := 3; round < 14; round++ { for round := 3; round < 14; round++ {
r := NewRound(round, numPlayers, currentPlayer) r := NewRound(round, numPlayers, entropy)
roundScores := r.Play() nextPlayer, roundScores := r.Play(currentPlayer)
currentPlayer = r.currentPlayer
currentPlayer = nextPlayer
for p := 0; p < numPlayers; p++ { for p := 0; p < numPlayers; p++ {
runningScores[p] += roundScores[p] runningScores[p] += roundScores[p]
@ -28,24 +30,22 @@ func PlayGame(numPlayers int) {
} }
type Round struct { type Round struct {
d []Card d []Card
discard []Card discard []Card
round int round int
numPlayers int numPlayers int
currentPlayer int hands [][]Card
hands [][]Card
} }
func NewRound(round, numPlayers, currentPlayer int) *Round { func NewRound(round, numPlayers int, entropy *rand.Rand) *Round {
r := Round{ r := Round{
round: round, round: round,
numPlayers: numPlayers, numPlayers: numPlayers,
currentPlayer: currentPlayer,
} }
r.d = Deck() r.d = Deck()
Shuffle(r.d) Shuffle(r.d, entropy)
r.discard = []Card{} r.discard = []Card{}
@ -95,7 +95,9 @@ func FormatHandGroupings(hand []Card, groupings [][]int) string {
return fmt.Sprintf("[ %v leftover %v ]", cgroups, leftover) 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) fmt.Printf("Discard stack: %v\n", r.discard)
@ -104,34 +106,34 @@ func (r *Round) Play() []int {
roundEndsWhenPlayerIs := -1 roundEndsWhenPlayerIs := -1
for { for {
if roundEndsWhenPlayerIs == r.currentPlayer { if roundEndsWhenPlayerIs == currentPlayer {
break break
} }
// Play the strategy for the current player // Play the strategy for the current player
r.PlayDefaultStrategy() r.PlayDefaultStrategy(currentPlayer)
// Check, one more time, if we have a winning hand // 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 { if currentScore == 0 {
// Declare victory // 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 { if roundEndsWhenPlayerIs == -1 {
roundEndsWhenPlayerIs = r.currentPlayer roundEndsWhenPlayerIs = currentPlayer
} // Otherwise, the round is already ending anyway } // Otherwise, the round is already ending anyway
} }
// Move to the next player // Move to the next player
r.currentPlayer = (r.currentPlayer + 1) % r.numPlayers currentPlayer = (currentPlayer + 1) % r.numPlayers
} }
// The round has ended // The round has ended
// Figure out what each player scored // Figure out what each player scored
roundScores := make([]int, r.numPlayers) roundScores = make([]int, r.numPlayers)
for p := 0; p < r.numPlayers; p++ { for p := 0; p < r.numPlayers; p++ {
@ -148,5 +150,5 @@ func (r *Round) Play() []int {
} }
// Done // Done
return roundScores return currentPlayer, roundScores
} }

View File

@ -1,9 +1,19 @@
package main package main
import ( import (
"math/rand"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestPlayGame(t *testing.T) { func TestPlayRound(t *testing.T) {
PlayGame(4)
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)
} }

View File

@ -41,25 +41,29 @@ func MakeBooks(hand []Card, wildFace int) [][]int {
return pairings 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, takenIndexes,
} }
// Do you have any XX|wild // 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 each match:
for _, midx := range posns { for _, midx := range posns {
// Take out that card // Take out that card
branchTaken := forkPossibilities(takenIndexes) branchTaken := make([]int, len(takenIndexes)+1)
branchTaken[midx] = struct{}{} 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 // Try and continue the run upwards to the next card, using only the remaining parts of the hand
// (We will never find K+1) // (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 // We found those possibilities for this run
possibilities = append(possibilities, continuations...) 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 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 // 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 each possible run we could make starting here:
for _, ff := range found { for _, ff := range found {
if len(ff) < 3 { if len(ff) < 3 {
continue // Short runs are no good 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) { func FindBestGrouping(hand []Card, wildface int) ([][]int, int) {
groupings := MakeMultiGroups(hand, wildface) 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 bestScore := 50 * len(hand) // Worst possible score
bestGroupIdx := -1 bestGroupIdx := -1

View File

@ -4,12 +4,10 @@ import (
"fmt" "fmt"
) )
func (r *Round) PlayDefaultStrategy() { func (r *Round) PlayDefaultStrategy(currentPlayer int) {
currentPlayer := r.currentPlayer
// Score our hand as if we pick up the discard and discard it immediately // 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 baseScore := score
bestDiscardBasedIdx := -1 bestDiscardBasedIdx := -1
bestDiscardBasedScore := score bestDiscardBasedScore := score
@ -20,7 +18,7 @@ func (r *Round) PlayDefaultStrategy() {
for cidx := 0; cidx < r.round; cidx++ { for cidx := 0; cidx < r.round; cidx++ {
// Pick up the last discard card, and discard card@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] theoretically[cidx] = r.discard[len(r.discard)-1]
_, score := FindBestGrouping(theoretically, r.round) _, 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 taking the discard would let us win, or reduce our score by >5, take it
if bestDiscardBasedScore == 0 || (baseScore-bestDiscardBasedScore) > 5 { 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 // We're going to win
if bestDiscardBasedIdx == -1 { if bestDiscardBasedIdx == -1 {
// Take the discard card + put it back again i.e. no change // 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 { } else {
// Take the discard card and discard cidx // Take the discard card and discard cidx
newCard := r.discard[len(r.discard)-1] newCard := r.discard[len(r.discard)-1]
r.discard = r.discard[0 : len(r.discard)-1] r.discard = r.discard[0 : len(r.discard)-1]
unwantedCard := r.hands[r.currentPlayer][bestDiscardBasedIdx] unwantedCard := r.hands[currentPlayer][bestDiscardBasedIdx]
r.hands[r.currentPlayer][bestDiscardBasedIdx] = newCard r.hands[currentPlayer][bestDiscardBasedIdx] = newCard
r.discard = append(r.discard, unwantedCard) // On top = available for next player r.discard = append(r.discard, unwantedCard) // On top = available for next player
@ -64,9 +62,9 @@ func (r *Round) PlayDefaultStrategy() {
newCard := r.d[0] newCard := r.d[0]
r.d = r.d[1:] 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 bestRandomBasedIdx := -1
bestRandomBasedScore := (r.round * 50) + 51 // Worse than the worst possible score bestRandomBasedScore := (r.round * 50) + 51 // Worse than the worst possible score
@ -89,12 +87,12 @@ func (r *Round) PlayDefaultStrategy() {
// Discard our chosen card // Discard our chosen card
// Slice tricks: Copy end card into this slot and reslice bounds // 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) 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[currentPlayer][bestRandomBasedIdx] = r.hands[currentPlayer][len(r.hands[currentPlayer])-1]
r.hands[r.currentPlayer] = r.hands[r.currentPlayer][0 : len(r.hands[r.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 r.discard = append(r.discard, unwantedCard) // On top = available for next player

16
util.go
View File

@ -7,25 +7,11 @@ import (
func search(hand []Card, wildFace int, search Card) []int { func search(hand []Card, wildFace int, search Card) []int {
ret := []int{} ret := []int{}
for idx, c := range hand { 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() { if c.Masked() {
continue // Doesn't match anything 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) ret = append(ret, idx)
} }
} }