advent-of-code-2023/day7/main.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
}