split files, fix scoring

This commit is contained in:
mappu 2024-01-07 14:51:06 +13:00
parent 42490e2b4b
commit f0d6923c71
7 changed files with 370 additions and 206 deletions

22
card.go
View File

@ -30,11 +30,23 @@ func (c Card) Joker() bool {
return (c == 0)
}
func (c Card) Masked() bool {
return (c == -1)
}
func (c Card) Suit() Suit {
if c.Joker() || c.Masked() {
panic("Suit on invalid")
}
return Suit(int(c) % 5)
}
func (c Card) Face() int {
if c.Joker() || c.Masked() {
panic("Face on invalid")
}
return int(c) / 5
}
@ -43,7 +55,7 @@ func (c Card) String() string {
return "Jok" // 🂿 U+1F0BF
}
if c == -1 {
if c.Masked() {
return "INV" // Invalid
}
@ -63,6 +75,10 @@ func (c Card) String() string {
}
func NewMasked() Card {
return Card(-1)
}
func NewJoker() Card {
return Card(0)
}
@ -77,10 +93,12 @@ func Shuffle(c []Card) {
})
}
const DeckSize = 58 * 2
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)
ret := make([]Card, 0, DeckSize)
for i := 0; i < 6; i++ {
ret = append(ret, NewJoker()) // Joker
}

View File

@ -9,12 +9,11 @@ import (
func TestCards(t *testing.T) {
d := Deck()
assert.Len(t, d, 58*2)
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)
assert.Len(t, d, 58*2)
assert.Len(t, d, DeckSize)
}

270
game.go
View File

@ -8,147 +8,145 @@ func PlayGame(numPlayers int) {
currentPlayer := 0
runningScores := make([]int, numPlayers)
for round := 3; round < 14; round++ {
d := Deck()
Shuffle(d)
r := NewRound(round, numPlayers, currentPlayer)
roundScores := r.Play()
currentPlayer = r.currentPlayer
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])
runningScores[p] += roundScores[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
fmt.Printf("The round has ended\n")
fmt.Printf("Running total scores: %v\n", runningScores)
}
fmt.Printf("The game has ended\n")
}
type Round struct {
d []Card
discard []Card
round int
numPlayers int
currentPlayer int
hands [][]Card
}
func NewRound(round, numPlayers, currentPlayer int) *Round {
r := Round{
round: round,
numPlayers: numPlayers,
currentPlayer: currentPlayer,
}
r.d = Deck()
Shuffle(r.d)
r.discard = []Card{}
fmt.Printf("# Round %d\n\n", r.round)
// Deal starting cards
r.hands = make([][]Card, numPlayers)
for p := 0; p < numPlayers; p++ {
r.hands[p] = r.d[0:round] // Deal from the bottom (0)
r.d = r.d[round:]
fmt.Printf("P%d starting hand: %v\n", p, r.hands[p])
}
// Deal one into the discard pile
r.discard = append(r.discard, r.d[0])
r.d = r.d[1:]
// Done
return &r
}
func FormatHandGroupings(hand []Card, groupings [][]int) string {
tmp := forkHand(hand)
cgroups := [][]Card{}
for _, group := range groupings {
cgroup := []Card{}
for _, cidx := range group {
cgroup = append(cgroup, hand[cidx])
tmp[cidx] = Card(-1)
}
cgroups = append(cgroups, cgroup)
}
leftover := []Card{}
for _, cv := range tmp {
if cv != Card(-1) {
leftover = append(leftover, cv)
}
}
return fmt.Sprintf("[ %v leftover %v ]", cgroups, leftover)
}
func (r *Round) Play() []int {
fmt.Printf("Discard stack: %v\n", r.discard)
fmt.Printf("Deck has %d cards remaining\n", len(r.d))
roundEndsWhenPlayerIs := -1
for {
if roundEndsWhenPlayerIs == r.currentPlayer {
break
}
// Play the strategy for the current player
r.PlayDefaultStrategy()
// Check, one more time, if we have a winning hand
currentBestGrouping, currentScore := FindBestGrouping(r.hands[r.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)
if roundEndsWhenPlayerIs == -1 {
roundEndsWhenPlayerIs = r.currentPlayer
} // Otherwise, the round is already ending anyway
}
// Move to the next player
r.currentPlayer = (r.currentPlayer + 1) % r.numPlayers
}
// The round has ended
// Figure out what each player scored
roundScores := make([]int, r.numPlayers)
for p := 0; p < r.numPlayers; p++ {
gr, score := FindBestGrouping(r.hands[p], r.round)
fmt.Printf("P%d has %v (score: %d)\n", p, FormatHandGroupings(r.hands[p], gr) /*r.hands[p], gr,*/, score)
roundScores[p] = score
}
// Check stack alignment
if len(r.discard)+len(r.d)+(r.numPlayers*len(r.hands[0])) != DeckSize {
panic("Cards on the floor")
}
// Done
return roundScores
}

View File

@ -1,7 +1,7 @@
package main
import (
"sort"
"fmt"
"github.com/mxschmitt/golang-combinations"
)
@ -15,7 +15,11 @@ func MakeBooks(hand []Card, wildFace int) [][]int {
for i := 3; i < 14; i++ {
book := []int{}
for idx, c := range hand {
if c.Face() == i || c.Joker() || c.Face() == wildFace {
if c.Masked() {
continue // Doesn't match anything
}
if c.Joker() || c.Face() == i || c.Face() == wildFace {
book = append(book, idx)
}
}
@ -37,64 +41,6 @@ func MakeBooks(hand []Card, wildFace int) [][]int {
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{}{
@ -163,7 +109,20 @@ func MakeMultiGroups(hand []Card, wildface int) [][][]int {
cloneHand := forkHand(hand)
for _, midx := range p {
cloneHand[midx] = Card(-1) // can't match with anything
cloneHand[midx] = NewMasked() // can't match with anything
}
// If that uses up all cards, then early-exit
anyUnpaired := false
for cidx := 0; cidx < len(cloneHand); cidx++ {
if cloneHand[cidx] != Card(-1) {
anyUnpaired = true
break
}
}
if !anyUnpaired {
return [][][]int{[][]int{p}} // Shut out all other possibilities, this is a winner
}
chposs := MakeMultiGroups(cloneHand, wildface)
@ -185,30 +144,37 @@ func ScoreAsUnpaired(hand []Card) int {
for _, c := range hand {
if c.Joker() {
score += 50
} else if c == Card(-1) {
} else if c.Masked() {
// skip (already accounted for)
} else {
score += c.Face()
}
}
return score
}
func FindBestGrouping(hand []Card, wildface int) ([][]int, int) {
groupings := MakeMultiGroups(hand, wildface)
fmt.Printf("Found %d possible groupings\n", len(groupings))
bestScore := 50 * len(hand) // Worst possible score
bestGroupIdx := -1
for ggidx, grouping := range groupings {
cloneHand := make([]Card, len(hand))
cloneHand := forkHand(hand)
for _, group := range grouping {
for _, cidx := range group {
cloneHand[cidx] = Card(-1) // invalid
cloneHand[cidx] = NewMasked() // invalid
}
}
score := ScoreAsUnpaired(cloneHand)
if score == 0 {
return groupings[ggidx], 0 // Early exit
}
if score < bestScore {
bestScore = score
bestGroupIdx = ggidx
@ -217,7 +183,8 @@ func FindBestGrouping(hand []Card, wildface int) ([][]int, int) {
}
if bestGroupIdx == -1 {
return [][]int{}, bestScore // No pairings could be found, full points
// No pairings could be found, score all remaining cards as ungrouped
return [][]int{}, ScoreAsUnpaired(hand)
}
return groupings[bestGroupIdx], bestScore
}

View File

@ -43,3 +43,15 @@ func TestSolveHand(t *testing.T) {
assert.EqualValues(t, [][]int{[]int{0, 4, 5}, []int{1, 2, 3}}, grouping)
assert.EqualValues(t, 0, score)
}
func TestScoreHand(t *testing.T) {
// P2 has [ [[ J★ 8♥ 8★] [ 9♥ 9♥ 9♠] [ Q♠ Jok Q♠]] leftover [10♣ 4♣] ] (score: 100)
hand := []Card{15, 20, 25, 30, 16, 17, 55, 60}
t.Logf("Finding score for hand: %v", hand)
grouping, score := FindBestGrouping(hand, 10)
assert.EqualValues(t, [][]int{[]int{0, 4, 5}, []int{1, 2, 3}}, grouping)
assert.EqualValues(t, 23, score)
}

103
strategy.go Normal file
View File

@ -0,0 +1,103 @@
package main
import (
"fmt"
)
func (r *Round) PlayDefaultStrategy() {
currentPlayer := r.currentPlayer
// Score our hand as if we pick up the discard and discard it immediately
_, score := FindBestGrouping(r.hands[r.currentPlayer], r.round)
baseScore := score
bestDiscardBasedIdx := -1
bestDiscardBasedScore := score
if score != 0 {
// Score our hand as if we pick up the discard card and discard each other card
for cidx := 0; cidx < r.round; cidx++ {
// Pick up the last discard card, and discard card@cidx
theoretically := forkHand(r.hands[r.currentPlayer])
theoretically[cidx] = r.discard[len(r.discard)-1]
_, score := FindBestGrouping(theoretically, r.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", r.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])
} 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
r.discard = append(r.discard, unwantedCard) // On top = available for next player
fmt.Printf("P%d discards %v (position %d)\n", currentPlayer, unwantedCard, bestDiscardBasedIdx)
}
} else {
// The discard card is not going to make us win or significantly lower our
// score
// Take from the deck
newCard := r.d[0]
r.d = r.d[1:]
r.hands[r.currentPlayer] = append(r.hands[r.currentPlayer], newCard)
fmt.Printf("P%d taking %v from the deck\n", r.currentPlayer, newCard)
bestRandomBasedIdx := -1
bestRandomBasedScore := (r.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 < r.round+1; cidx++ {
// Assume we discard that card,
theoretically := forkHand(r.hands[currentPlayer])
theoretically[cidx] = Card(-1) // Never matches + worth 0 points
_, score := FindBestGrouping(theoretically, r.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 := r.hands[r.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.discard = append(r.discard, unwantedCard) // On top = available for next player
}
}

67
util.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"sort"
)
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 {
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
}