335 lines
6.9 KiB
Go
335 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"cmp"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Card int
|
|
|
|
const (
|
|
Joker Card = iota + 1
|
|
Two
|
|
Three
|
|
Four
|
|
Five
|
|
Six
|
|
Seven
|
|
Eight
|
|
Nine
|
|
Ten
|
|
Jack
|
|
Queen
|
|
King
|
|
Ace
|
|
)
|
|
|
|
type HandKind int
|
|
|
|
const (
|
|
UnknownKind HandKind = iota
|
|
HighCard
|
|
OnePair
|
|
TwoPair
|
|
ThreeOfAKind
|
|
FullHouse
|
|
FourOfAKind
|
|
FiveOfAKind
|
|
)
|
|
|
|
type Hand []Card
|
|
|
|
type Player struct {
|
|
bid int
|
|
hand Hand
|
|
}
|
|
|
|
// Kind gets the "Kind" of the hand, which determines its value.
|
|
func (hand Hand) Kind() (HandKind, error) {
|
|
type kindMapping struct {
|
|
kind HandKind
|
|
distinctCounts []int
|
|
}
|
|
|
|
// Must iterate in order so we try each kind first
|
|
handCounts := []kindMapping{
|
|
{
|
|
kind: FiveOfAKind,
|
|
distinctCounts: []int{5},
|
|
},
|
|
{
|
|
kind: FourOfAKind,
|
|
distinctCounts: []int{1, 4},
|
|
},
|
|
{
|
|
kind: FullHouse,
|
|
distinctCounts: []int{2, 3},
|
|
},
|
|
{
|
|
kind: ThreeOfAKind,
|
|
distinctCounts: []int{1, 1, 3},
|
|
},
|
|
{
|
|
kind: TwoPair,
|
|
distinctCounts: []int{1, 2, 2},
|
|
},
|
|
{
|
|
kind: OnePair,
|
|
distinctCounts: []int{1, 1, 1, 2},
|
|
},
|
|
{
|
|
kind: HighCard,
|
|
distinctCounts: []int{1, 1, 1, 1, 1},
|
|
},
|
|
}
|
|
|
|
handWithoutJokers, numJokers := hand.WithoutCard(Joker)
|
|
distinctCounts := handWithoutJokers.CountDistinct()
|
|
cardCounts := mapValues(distinctCounts)
|
|
slices.Sort(cardCounts)
|
|
for _, mapping := range handCounts {
|
|
if canMakeKindWithCardCombo(cardCounts, mapping.distinctCounts, numJokers) {
|
|
return mapping.kind, nil
|
|
}
|
|
}
|
|
|
|
return UnknownKind, errors.New("no known kind for hand")
|
|
}
|
|
|
|
// WithoutCard will return a copy of the Hand without the given card, toRemove
|
|
func (hand Hand) WithoutCard(toRemove Card) (Hand, int) {
|
|
newHand := Hand{}
|
|
numRemoved := 0
|
|
for _, card := range hand {
|
|
if card == toRemove {
|
|
numRemoved++
|
|
continue
|
|
}
|
|
|
|
newHand = append(newHand, card)
|
|
}
|
|
|
|
return newHand, numRemoved
|
|
}
|
|
|
|
// CountDistinct returns a count of distinct cards by each card type
|
|
func (hand Hand) CountDistinct() map[Card]int {
|
|
buckets := make(map[Card]int)
|
|
for _, card := range hand {
|
|
buckets[card]++
|
|
}
|
|
|
|
return buckets
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) != 2 && len(os.Args) != 3 {
|
|
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
|
|
os.Exit(1)
|
|
}
|
|
|
|
inputFilename := os.Args[1]
|
|
inputFile, err := os.Open(inputFilename)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not open input file: %s", err))
|
|
}
|
|
|
|
defer inputFile.Close()
|
|
|
|
inputBytes, err := io.ReadAll(inputFile)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not read input file: %s", err))
|
|
}
|
|
|
|
input := strings.TrimSpace(string(inputBytes))
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to parse input: %s", err))
|
|
}
|
|
|
|
inputLines := strings.Split(input, "\n")
|
|
players, err := parsePlayers(inputLines)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to parse races: %s", err))
|
|
}
|
|
|
|
fmt.Printf("Part 1: %d\n", part1(players))
|
|
fmt.Printf("Part 2: %d\n", part2(players))
|
|
}
|
|
|
|
func part1(players []Player) int {
|
|
return findWinnings(players)
|
|
}
|
|
|
|
func part2(originalPlayers []Player) int {
|
|
players := makePart2Players(originalPlayers)
|
|
|
|
return findWinnings(players)
|
|
}
|
|
|
|
// findWinnings finds the winning for each game
|
|
func findWinnings(players []Player) int {
|
|
sortedPlayers := slices.Clone(players)
|
|
slices.SortFunc(sortedPlayers, func(a, b Player) int {
|
|
aKind, err := a.hand.Kind()
|
|
if err != nil {
|
|
panic(fmt.Errorf("invalid hand %+v: %w", a, err))
|
|
}
|
|
|
|
bKind, err := b.hand.Kind()
|
|
if err != nil {
|
|
panic(fmt.Errorf("invalid hand %+v: %w", b, err))
|
|
}
|
|
|
|
compareHands := cmp.Compare(aKind, bKind)
|
|
if compareHands == 0 {
|
|
return slices.Compare(a.hand, b.hand)
|
|
} else {
|
|
return compareHands
|
|
}
|
|
})
|
|
|
|
winnings := 0
|
|
for i, player := range sortedPlayers {
|
|
winnings += (i + 1) * player.bid
|
|
}
|
|
|
|
return winnings
|
|
}
|
|
|
|
// makePart2Players prepares the players for part 2 by replacing Jacks with Jokers
|
|
func makePart2Players(players []Player) []Player {
|
|
newPlayers := slices.Clone(players)
|
|
for i, player := range newPlayers {
|
|
updPlayer := player
|
|
updPlayer.hand = makePart2Hand(player.hand)
|
|
newPlayers[i] = updPlayer
|
|
}
|
|
|
|
return newPlayers
|
|
}
|
|
|
|
// makePart2Hand makes replaces Jacks with Jokers in each hand for part 2
|
|
func makePart2Hand(hand Hand) Hand {
|
|
newHand := slices.Clone(hand)
|
|
for i, card := range newHand {
|
|
if card == Jack {
|
|
newHand[i] = Joker
|
|
}
|
|
}
|
|
return newHand
|
|
}
|
|
|
|
// canMakeKindWithCardCombo checks that if we can make the kind given. This is done by taking a sorted list of
|
|
// each of the number of each type of card. For instance, 55766 would be [2, 2, 1]
|
|
func canMakeKindWithCardCombo(handDistinctCardCounts []int, kindCardCounts []int, numJokers int) bool {
|
|
if numJokers == 0 {
|
|
return slices.Equal(kindCardCounts, handDistinctCardCounts)
|
|
} else if len(handDistinctCardCounts) == 0 && numJokers == 5 && slices.Equal(kindCardCounts, []int{5}) {
|
|
// If we have five jokers, we must act as a five of a kind (most valuable)
|
|
return true
|
|
}
|
|
permutations := permutationsOfJokers(handDistinctCardCounts, numJokers)
|
|
|
|
for _, permutation := range permutations {
|
|
if slices.Equal(permutation, kindCardCounts) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Find all the positions where our sets of jokers could be located in in the distinct card count set
|
|
func permutationsOfJokers(distinctCardCounts []int, numJokers int) [][]int {
|
|
if numJokers == 0 {
|
|
return [][]int{distinctCardCounts}
|
|
}
|
|
|
|
permutations := [][]int{}
|
|
for i, count := range distinctCardCounts {
|
|
newCounts := slices.Clone(distinctCardCounts)
|
|
newCounts[i] = count + 1
|
|
permutations = append(permutations, permutationsOfJokers(newCounts, numJokers-1)...)
|
|
}
|
|
|
|
return permutations
|
|
}
|
|
|
|
func parsePlayers(inputLines []string) ([]Player, error) {
|
|
players := make([]Player, 0, len(inputLines))
|
|
for i, line := range inputLines {
|
|
player, err := parsePlayer(line)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse line %d: %w", i, err)
|
|
}
|
|
|
|
players = append(players, player)
|
|
}
|
|
|
|
return players, nil
|
|
}
|
|
|
|
func parsePlayer(inputLine string) (Player, error) {
|
|
lineComponents := strings.Split(inputLine, " ")
|
|
if len(lineComponents) != 2 {
|
|
return Player{}, errors.New("malformed player line")
|
|
}
|
|
|
|
hand, err := parseHand(lineComponents[0])
|
|
if err != nil {
|
|
return Player{}, fmt.Errorf("parse hand: %w", err)
|
|
}
|
|
|
|
bid, err := strconv.Atoi(lineComponents[1])
|
|
if err != nil {
|
|
return Player{}, fmt.Errorf("parse bid: %w", err)
|
|
}
|
|
|
|
return Player{bid: bid, hand: hand}, nil
|
|
}
|
|
|
|
func parseHand(handStr string) (Hand, error) {
|
|
cardMap := map[byte]Card{
|
|
'A': Ace,
|
|
'K': King,
|
|
'Q': Queen,
|
|
'J': Jack,
|
|
'T': Ten,
|
|
'9': Nine,
|
|
'8': Eight,
|
|
'7': Seven,
|
|
'6': Six,
|
|
'5': Five,
|
|
'4': Four,
|
|
'3': Three,
|
|
'2': Two,
|
|
}
|
|
|
|
hand := Hand{}
|
|
for _, handChar := range handStr {
|
|
card, ok := cardMap[byte(handChar)]
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid card character %c", handChar)
|
|
}
|
|
|
|
hand = append(hand, card)
|
|
}
|
|
|
|
return hand, nil
|
|
}
|
|
|
|
func mapValues[T comparable, U any](m map[T]U) []U {
|
|
values := make([]U, 0, len(m))
|
|
for _, value := range m {
|
|
values = append(values, value)
|
|
}
|
|
|
|
return values
|
|
}
|