From 4aafad4e8d438b05e1c23b9544e2b167db7f5c46 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Thu, 7 Dec 2023 23:13:05 +0000 Subject: [PATCH] day-7 --- day-7/go.mod | 8 ++++ day-7/go.sum | 4 ++ day-7/hand_test.go | 100 +++++++++++++++++++++++++++++++++++++++++ day-7/hands.go | 108 +++++++++++++++++++++++++++++++++++++++++++++ day-7/one.go | 31 +++++++++++++ day-7/solution.go | 27 ++++++++++++ day-7/two.go | 74 +++++++++++++++++++++++++++++++ day-7/two_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++ day-7/util.go | 91 ++++++++++++++++++++++++++++++++++++++ day-7/util_test.go | 22 +++++++++ 10 files changed, 568 insertions(+) create mode 100644 day-7/go.mod create mode 100644 day-7/go.sum create mode 100644 day-7/hand_test.go create mode 100644 day-7/hands.go create mode 100644 day-7/one.go create mode 100644 day-7/solution.go create mode 100644 day-7/two.go create mode 100644 day-7/two_test.go create mode 100644 day-7/util.go create mode 100644 day-7/util_test.go diff --git a/day-7/go.mod b/day-7/go.mod new file mode 100644 index 0000000..5be8b24 --- /dev/null +++ b/day-7/go.mod @@ -0,0 +1,8 @@ +module github.com/onyx-and-iris/aoc2023/day-7 + +go 1.20 + +require ( + github.com/samber/lo v1.39.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect +) diff --git a/day-7/go.sum b/day-7/go.sum new file mode 100644 index 0000000..69409f9 --- /dev/null +++ b/day-7/go.sum @@ -0,0 +1,4 @@ +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= diff --git a/day-7/hand_test.go b/day-7/hand_test.go new file mode 100644 index 0000000..f3806f5 --- /dev/null +++ b/day-7/hand_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMatches(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("32T3K", 0) + h2 := newHand("ABC3D", 0) + + res1 := map[rune]int{ + '3': 2, + '2': 1, + 'T': 1, + 'K': 1, + } + res2 := map[rune]int{ + 'A': 1, + 'B': 1, + 'C': 1, + '3': 1, + 'D': 1, + } + + t.Run("Should produce equal maps", func(t *testing.T) { + assert.Equal(t, res1, h1.getMatches()) + }) + + t.Run("Should produce equal maps", func(t *testing.T) { + assert.Equal(t, res2, h2.getMatches()) + }) +} + +func TestIsHighCard(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("ABC12", 0) + + t.Run("Should be a high card", func(t *testing.T) { + assert.Equal(t, true, isHighCard(h1)) + }) + + t.Run("Should not be a four of a kind", func(t *testing.T) { + assert.Equal(t, false, isFourOfAKind(h1)) + }) +} + +func TestIsFourOfAKind(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("UUUU9", 0) + h2 := newHand("UUU99", 0) + + t.Run("Should be a four of a kind", func(t *testing.T) { + assert.Equal(t, true, isFourOfAKind(h1)) + }) + + t.Run("Should not be a four of a kind", func(t *testing.T) { + assert.Equal(t, false, isFourOfAKind(h2)) + }) +} + +func TestIsFullHouse(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("333KK", 0) + h2 := newHand("3338K", 0) + + t.Run("Should be a fullhouse", func(t *testing.T) { + assert.Equal(t, true, isFullHouse(h1)) + }) + + t.Run("Should not be a fullhouse", func(t *testing.T) { + assert.Equal(t, false, isFullHouse(h2)) + }) +} + +func TestIsTwoPair(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("33QQA", 0) + + t.Run("Should be a twopair", func(t *testing.T) { + assert.Equal(t, true, isTwoPair(h1)) + }) +} + +func TestIsOnePair(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("33QKA", 0) + + t.Run("Should be a onepair", func(t *testing.T) { + assert.Equal(t, true, isOnePair(h1)) + }) +} diff --git a/day-7/hands.go b/day-7/hands.go new file mode 100644 index 0000000..e2a2abd --- /dev/null +++ b/day-7/hands.go @@ -0,0 +1,108 @@ +package main + +var hands = []*hand{} + +type hand struct { + cards string + bid int + matches map[rune]int + numPairs int + _kind int +} + +func newHand(cards string, bid int) *hand { + h := &hand{cards: cards, bid: bid} + setKind(h) + return h +} + +func (h *hand) kind() int { + return h._kind +} + +func (h *hand) getMatches() map[rune]int { + if h.matches == nil { + h.matches = matches(h.cards) + } + return h.matches +} + +func setKind(h *hand) { + if isFiveOfAKind(h) { + h._kind = fiveofakind + } else if isHighCard(h) { + h._kind = highcard + } else if isFourOfAKind(h) { + h._kind = fourofakind + } else if isFullHouse(h) { + h._kind = fullhouse + } else if isThreeOfAKind(h) { + h._kind = threeofakind + } else if isTwoPair(h) { + h._kind = twopair + } else if isOnePair(h) { + h._kind = onepair + } +} + +func isFiveOfAKind(h *hand) bool { + return len(h.getMatches()) == 1 +} + +func isHighCard(h *hand) bool { + return len(h.getMatches()) == 5 +} + +func isFourOfAKind(h *hand) bool { + if len(h.getMatches()) == 2 { + for _, c := range h.matches { + if c == 4 { + return true + } + } + return false + } + return false +} + +func isFullHouse(h *hand) bool { + if len(h.getMatches()) == 2 { + return !isFourOfAKind(h) + } + return false +} + +func isThreeOfAKind(h *hand) bool { + if len(h.getMatches()) == 3 { + for _, c := range h.matches { + if c == 3 { + return true + } + } + return false + } + return false +} + +func getNumPairs(h *hand) int { + if h.numPairs == 0 { + allowed := []int{1, 2} + for _, n := range h.getMatches() { + if !contains(allowed, n) { + return -1 // there may be a pair but its a fullhouse + } + if n == 2 { + h.numPairs++ + } + } + } + return h.numPairs +} + +func isTwoPair(h *hand) bool { + return getNumPairs(h) == 2 +} + +func isOnePair(h *hand) bool { + return !isTwoPair(h) && h.numPairs == 1 +} diff --git a/day-7/one.go b/day-7/one.go new file mode 100644 index 0000000..7922fb0 --- /dev/null +++ b/day-7/one.go @@ -0,0 +1,31 @@ +package main + +import log "github.com/sirupsen/logrus" + +const ( + highcard = iota // 0 + onepair = iota // 1 + twopair = iota // 2 + threeofakind = iota // 3 + fullhouse = iota // 4 + fourofakind = iota // 5 + fiveofakind = iota // 6 + numKinds = iota +) + +// one returns the sum of products of hand values by bids +func one(lines []string) (int, error) { + parselines(lines) + + strength := []rune{'A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2'} + log.Debug("strength: ", strength) + sortByKindAndStrength(strength) + + sum := 0 + + for i, hand := range hands { + sum += (i + 1) * hand.bid + } + + return sum, nil +} diff --git a/day-7/solution.go b/day-7/solution.go new file mode 100644 index 0000000..47aec47 --- /dev/null +++ b/day-7/solution.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + + log "github.com/sirupsen/logrus" +) + +func init() { + log.SetLevel(log.InfoLevel) +} + +func main() { + lines := readlines() + + ans, err := one(lines) + if err != nil { + log.Fatal() + } + fmt.Printf("solution one: %d\n", ans) + + ans, err = two(lines) + if err != nil { + log.Fatal() + } + fmt.Printf("solution two: %d\n", ans) +} diff --git a/day-7/two.go b/day-7/two.go new file mode 100644 index 0000000..295dd92 --- /dev/null +++ b/day-7/two.go @@ -0,0 +1,74 @@ +package main + +import log "github.com/sirupsen/logrus" + +// promote alters a hands kind based on joker rules +func promote(hand *hand) { + m := matches(hand.cards) + log.Debug(m) + + switch hand.kind() { + case fiveofakind: + break + case fourofakind: + hand._kind = fiveofakind + case fullhouse: + if m['J'] == 2 || m['J'] == 3 { + hand._kind = fiveofakind + } + log.Debug(hand.cards, " was promoted to ", hand.kind()) + case threeofakind: + if m['J'] == 1 || m['J'] == 3 { + hand._kind = fourofakind + } + log.Debug(hand.cards, " was promoted to ", hand.kind()) + case twopair: + if m['J'] == 1 { + hand._kind = fullhouse + } else if m['J'] == 2 { + hand._kind = fourofakind + } + log.Debug(hand.cards, " was promoted to ", hand.kind()) + case onepair: + if m['J'] == 1 || m['J'] == 2 { + hand._kind = threeofakind + } + log.Debug(hand.cards, " was promoted to ", hand.kind()) + case highcard: + hand._kind = onepair + log.Debug(hand.cards, " was promoted to ", hand.kind()) + } +} + +// two coreturns the sum of products of hand values by bids +// it uses new joker rules +func two(lines []string) (int, error) { + for _, hand := range hands { + if containsChar(hand.cards, "J") { + promote(hand) + } + } + + strength := []rune{'A', 'K', 'Q', 'T', '9', '8', '7', '6', '5', '4', '3', '2', 'J'} + log.Debug("strength: ", strength) + sortByKindAndStrength(strength) + + sum := 0 + + var debugNames = map[int]string{ + 0: "highcard", + 1: "onepair", + 2: "twopair", + 3: "threeofakind", + 4: "fullhouse", + 5: "fourofakind", + 6: "fiveofakind", + } + + for i, hand := range hands { + log.Debug(hand.cards, ": ", hand.bid, " by ", i+1, " || ", debugNames[hand.kind()]) + sum += (i + 1) * hand.bid + } + + return sum, nil +} diff --git a/day-7/two_test.go b/day-7/two_test.go new file mode 100644 index 0000000..d6d8589 --- /dev/null +++ b/day-7/two_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPromoteHighCard(t *testing.T) { + //t.Skip("skipping test") + + h := newHand("1234J", 0) + promote(h) + + t.Run("Should promote to onepair", func(t *testing.T) { + assert.Equal(t, onepair, h.kind()) + }) +} + +func TestPromoteOnePair(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("1233J", 0) + promote(h1) + h2 := newHand("123JJ", 0) + promote(h2) + + t.Run("Should promote to threeofakind", func(t *testing.T) { + assert.Equal(t, threeofakind, h1.kind()) + }) + + t.Run("Should promote to threeofakind", func(t *testing.T) { + assert.Equal(t, threeofakind, h2.kind()) + }) +} + +func TestPromoteTwoPair(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("1133J", 0) + promote(h1) + h2 := newHand("133JJ", 0) + promote(h2) + + t.Run("Should promote to fullhouse", func(t *testing.T) { + assert.Equal(t, fullhouse, h1.kind()) + }) + + t.Run("Should promote to fourofakind", func(t *testing.T) { + assert.Equal(t, fourofakind, h2.kind()) + }) +} + +func TestPromoteThreeOfAKind(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("1333J", 0) + promote(h1) + h2 := newHand("13JJJ", 0) + promote(h2) + + t.Run("Should promote to fourofakind", func(t *testing.T) { + assert.Equal(t, fourofakind, h1.kind()) + }) + + t.Run("Should promote to fourofakind", func(t *testing.T) { + assert.Equal(t, fourofakind, h2.kind()) + }) +} + +func TestPromoteFullHOuse(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("J333J", 0) + promote(h1) + h2 := newHand("33JJJ", 0) + promote(h2) + + t.Run("Should promote to fiveofakind", func(t *testing.T) { + assert.Equal(t, fiveofakind, h1.kind()) + }) + + t.Run("Should promote to fiveofakind", func(t *testing.T) { + assert.Equal(t, fiveofakind, h2.kind()) + }) +} + +func TestPromoteFourOfAKind(t *testing.T) { + //t.Skip("skipping test") + + h1 := newHand("JJJJ3", 0) + promote(h1) + h2 := newHand("3333J", 0) + promote(h2) + + t.Run("Should promote to fiveofakind", func(t *testing.T) { + assert.Equal(t, fiveofakind, h1.kind()) + }) + + t.Run("Should promote to fiveofakind", func(t *testing.T) { + assert.Equal(t, fiveofakind, h2.kind()) + }) +} diff --git a/day-7/util.go b/day-7/util.go new file mode 100644 index 0000000..900ef3f --- /dev/null +++ b/day-7/util.go @@ -0,0 +1,91 @@ +package main + +import ( + "bufio" + "log" + "os" + "sort" + "strconv" + "strings" +) + +// readlines reads lines from stdin. +// Then it returns them as an array of strings +func readlines() []string { + lines := []string{} + + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + + return lines +} + +func parselines(lines []string) { + for _, line := range lines { + splitted := strings.Split(line, " ") + n, _ := strconv.Atoi(splitted[1]) + hands = append(hands, newHand(splitted[0], n)) + } +} + +// matches checks for number of matching cards in hand +func matches(cards string) map[rune]int { + m := map[rune]int{} + for _, r := range cards { + _, ok := m[r] + if !ok { + m[r] = strings.Count(cards, string(r)) + } + } + + return m +} + +// contains returns true if a slice of elements contains a given element +func contains[T comparable](elems []T, v T) bool { + for _, s := range elems { + if v == s { + return true + } + } + return false +} + +// indexOf returns the index of an element in a slice +func indexOf(collection []rune, elem rune) int { + for i, x := range collection { + if x == elem { + return i + } + } + return -1 +} + +func containsChar(s string, c string) bool { + for _, v := range s { + if c == string(v) { + return true + } + } + return false +} + +func sortByKindAndStrength(strength []rune) { + sort.Slice(hands, func(i, j int) bool { + if hands[i].kind() == hands[j].kind() { + for k := range hands[i].cards { + if hands[i].cards[k] == hands[j].cards[k] { + continue + } + return indexOf(strength, rune(hands[i].cards[k])) > indexOf(strength, rune(hands[j].cards[k])) + } + } + return hands[i].kind() < hands[j].kind() + }) +} diff --git a/day-7/util_test.go b/day-7/util_test.go new file mode 100644 index 0000000..fd5a979 --- /dev/null +++ b/day-7/util_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMatches(t *testing.T) { + //t.Skip("skipping test") + + m1 := matches("32T3K") + m2 := matches("JJU8J") + + t.Run("Should produce equal maps", func(t *testing.T) { + assert.Equal(t, map[rune]int{'3': 2, '2': 1, 'T': 1, 'K': 1}, m1) + }) + + t.Run("Should produce equal maps", func(t *testing.T) { + assert.Equal(t, map[rune]int{'J': 3, 'U': 1, '8': 1}, m2) + }) +}