split files, fix scoring
This commit is contained in:
parent
42490e2b4b
commit
f0d6923c71
22
card.go
22
card.go
@ -30,11 +30,23 @@ func (c Card) Joker() bool {
|
|||||||
return (c == 0)
|
return (c == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Card) Masked() bool {
|
||||||
|
return (c == -1)
|
||||||
|
}
|
||||||
|
|
||||||
func (c Card) Suit() Suit {
|
func (c Card) Suit() Suit {
|
||||||
|
if c.Joker() || c.Masked() {
|
||||||
|
panic("Suit on invalid")
|
||||||
|
}
|
||||||
|
|
||||||
return Suit(int(c) % 5)
|
return Suit(int(c) % 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Card) Face() int {
|
func (c Card) Face() int {
|
||||||
|
if c.Joker() || c.Masked() {
|
||||||
|
panic("Face on invalid")
|
||||||
|
}
|
||||||
|
|
||||||
return int(c) / 5
|
return int(c) / 5
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +55,7 @@ func (c Card) String() string {
|
|||||||
return "Jok" // 🂿 U+1F0BF
|
return "Jok" // 🂿 U+1F0BF
|
||||||
}
|
}
|
||||||
|
|
||||||
if c == -1 {
|
if c.Masked() {
|
||||||
return "INV" // Invalid
|
return "INV" // Invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +75,10 @@ func (c Card) String() string {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewMasked() Card {
|
||||||
|
return Card(-1)
|
||||||
|
}
|
||||||
|
|
||||||
func NewJoker() Card {
|
func NewJoker() Card {
|
||||||
return Card(0)
|
return Card(0)
|
||||||
}
|
}
|
||||||
@ -77,10 +93,12 @@ func Shuffle(c []Card) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DeckSize = 58 * 2
|
||||||
|
|
||||||
func Deck() []Card {
|
func Deck() []Card {
|
||||||
// The full game deck is two 58-card decks
|
// 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.
|
// 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++ {
|
for i := 0; i < 6; i++ {
|
||||||
ret = append(ret, NewJoker()) // Joker
|
ret = append(ret, NewJoker()) // Joker
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,11 @@ import (
|
|||||||
func TestCards(t *testing.T) {
|
func TestCards(t *testing.T) {
|
||||||
d := Deck()
|
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)
|
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)
|
Shuffle(d)
|
||||||
|
|
||||||
assert.Len(t, d, 58*2)
|
assert.Len(t, d, DeckSize)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
270
game.go
270
game.go
@ -8,147 +8,145 @@ func PlayGame(numPlayers int) {
|
|||||||
|
|
||||||
currentPlayer := 0
|
currentPlayer := 0
|
||||||
|
|
||||||
|
runningScores := make([]int, numPlayers)
|
||||||
|
|
||||||
for round := 3; round < 14; round++ {
|
for round := 3; round < 14; round++ {
|
||||||
|
|
||||||
d := Deck()
|
r := NewRound(round, numPlayers, currentPlayer)
|
||||||
Shuffle(d)
|
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++ {
|
for p := 0; p < numPlayers; p++ {
|
||||||
hands[p] = d[0:round] // Deal from the bottom (0)
|
runningScores[p] += roundScores[p]
|
||||||
d = d[round:]
|
|
||||||
|
|
||||||
fmt.Printf("P%d starting hand: %v\n", p, hands[p])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deal one into the discard pile
|
fmt.Printf("The round has ended\n")
|
||||||
|
fmt.Printf("Running total scores: %v\n", runningScores)
|
||||||
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 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
|
||||||
}
|
}
|
||||||
|
97
search.go
97
search.go
@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"fmt"
|
||||||
|
|
||||||
"github.com/mxschmitt/golang-combinations"
|
"github.com/mxschmitt/golang-combinations"
|
||||||
)
|
)
|
||||||
@ -15,7 +15,11 @@ func MakeBooks(hand []Card, wildFace int) [][]int {
|
|||||||
for i := 3; i < 14; i++ {
|
for i := 3; i < 14; i++ {
|
||||||
book := []int{}
|
book := []int{}
|
||||||
for idx, c := range hand {
|
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)
|
book = append(book, idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,64 +41,6 @@ func MakeBooks(hand []Card, wildFace int) [][]int {
|
|||||||
return pairings
|
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{} {
|
func continueRun(hand []Card, wildFace int, requireSuit Suit, searchNextFace int, takenIndexes map[int]struct{}) []map[int]struct{} {
|
||||||
|
|
||||||
possibilities := []map[int]struct{}{
|
possibilities := []map[int]struct{}{
|
||||||
@ -163,7 +109,20 @@ func MakeMultiGroups(hand []Card, wildface int) [][][]int {
|
|||||||
|
|
||||||
cloneHand := forkHand(hand)
|
cloneHand := forkHand(hand)
|
||||||
for _, midx := range p {
|
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)
|
chposs := MakeMultiGroups(cloneHand, wildface)
|
||||||
@ -185,30 +144,37 @@ func ScoreAsUnpaired(hand []Card) int {
|
|||||||
for _, c := range hand {
|
for _, c := range hand {
|
||||||
if c.Joker() {
|
if c.Joker() {
|
||||||
score += 50
|
score += 50
|
||||||
} else if c == Card(-1) {
|
} else if c.Masked() {
|
||||||
// skip (already accounted for)
|
// skip (already accounted for)
|
||||||
} else {
|
} else {
|
||||||
score += c.Face()
|
score += c.Face()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return score
|
return score
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
bestScore := 50 * len(hand) // Worst possible score
|
bestScore := 50 * len(hand) // Worst possible score
|
||||||
bestGroupIdx := -1
|
bestGroupIdx := -1
|
||||||
for ggidx, grouping := range groupings {
|
for ggidx, grouping := range groupings {
|
||||||
|
|
||||||
cloneHand := make([]Card, len(hand))
|
cloneHand := forkHand(hand)
|
||||||
for _, group := range grouping {
|
for _, group := range grouping {
|
||||||
for _, cidx := range group {
|
for _, cidx := range group {
|
||||||
cloneHand[cidx] = Card(-1) // invalid
|
cloneHand[cidx] = NewMasked() // invalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
score := ScoreAsUnpaired(cloneHand)
|
score := ScoreAsUnpaired(cloneHand)
|
||||||
|
if score == 0 {
|
||||||
|
return groupings[ggidx], 0 // Early exit
|
||||||
|
}
|
||||||
|
|
||||||
if score < bestScore {
|
if score < bestScore {
|
||||||
bestScore = score
|
bestScore = score
|
||||||
bestGroupIdx = ggidx
|
bestGroupIdx = ggidx
|
||||||
@ -217,7 +183,8 @@ func FindBestGrouping(hand []Card, wildface int) ([][]int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bestGroupIdx == -1 {
|
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
|
return groupings[bestGroupIdx], bestScore
|
||||||
}
|
}
|
||||||
|
@ -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, [][]int{[]int{0, 4, 5}, []int{1, 2, 3}}, grouping)
|
||||||
assert.EqualValues(t, 0, score)
|
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
103
strategy.go
Normal 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
67
util.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user