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

View File

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

50
game.go
View File

@ -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
}

View File

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

View File

@ -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

View File

@ -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

16
util.go
View File

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