adventofcode-2023/day7/day7.kt

141 lines
4.4 KiB
Kotlin
Raw Normal View History

2023-12-07 17:22:15 +00:00
import java.io.File
typealias Hand = List<Char>
fun main() {
val input = File("input")
val bets = parseHands(input)
println(part1(bets))
println(part2(bets))
}
fun part1(bets: List<Pair<Hand, Int>>): Int {
return bets.sortedBy { handValue(it.first) }.foldIndexed(0) { i, acc, bet ->
acc + (i + 1) * bet.second
}
}
fun part2(bets: List<Pair<Hand, Int>>): Int {
return bets.sortedBy { handValueJoker(it.first) }.foldIndexed(0) { i, acc, bet ->
acc + (i + 1) * bet.second
}
}
fun cardValue(card: Char): Int {
return when (card) {
'A' -> 12
'K' -> 11
'Q' -> 10
'J' -> 9
'T' -> 8
in '2'..'9' -> card.digitToInt() - 2
else -> throw Exception("Invalid card")
}
}
fun type(cards: Hand): Int {
return cards.groupBy { it }.mapValues { it.value.count() }.entries.fold(0) { acc, entry ->
acc +
when (entry.value) {
5 -> 6000000
4 -> 5000000
3 -> 3000000
2 -> 1000000
1 -> 0
else -> throw Exception("Too many cards")
}
}
}
fun handValue(cards: Hand): Int {
// Each hand has 5 cards, the possible card values are 2-9+TJQKA,
// and the order matters.
//
// This means the hands themselves are basically a 5-digit base 13
// number.
//
// In addition, particular groupings of the cards matter. This is
// represented by the type - to condense this into an easily
// usable number, the type is therefore encoded in digits just
// beyond the max of the 5-digit base 13 numbers. For convenient
// programming, this means the type is encoded by the millions
// digit in base 10, hence the odd type return numbers.
return type(cards) + cards.fold(0) { acc, label -> acc * 13 + cardValue(label) }
}
fun cardValueJoker(card: Char): Int {
return when (card) {
'A' -> 12
'K' -> 11
'Q' -> 10
'T' -> 9
in '2'..'9' -> card.digitToInt() - 1
'J' -> 0
else -> throw Exception("Invalid card")
}
}
fun typeJoker(cards: Hand): Int {
var groups: MutableMap<Char, Int> =
cards.groupBy { it }.mapValues { it.value.count() }.toMutableMap()
// If there is only one group, we return early with the highest
// value (this is to catch edge cases where there are only jokers)
if (groups.entries.size == 1) {
return 6000000
}
val jokers = groups.remove('J') ?: 0
2023-12-08 18:22:09 +00:00
// Jokers act as a kind of tie breaker here - similar to the full
// scenario, the label we want to add the joker to is the one with
// the highest card value *as well as* the highest type.
//
// So we sort it first by value (descending so the highest type
// gets i == 0), and second by the value of the card (so that if
// two hands have the same type the one with the higher card value
// wins).
//
// The hands are base 13 numbers, so multiplying the value puts it
// one digit ahead of the card value.
2023-12-07 17:22:15 +00:00
return groups.entries.sortedByDescending { 13 * it.value + cardValueJoker(it.key) }.foldIndexed(
0
) { i, acc, entry ->
acc +
when (entry.value + if (i == 0) jokers else 0) {
5 -> 6000000
4 -> 5000000
3 -> 3000000
2 -> 1000000
1 -> 0
else -> throw Exception("Too many cards")
}
}
}
fun handValueJoker(cards: Hand): Int {
// This is just a bit more complicated; the card value doesn't
// change much, but the *type* changes quite a bit.
//
// Rather than just giving the best type the highest value, we now
// need to take into account scenarios in which ties are broken by
// jokers. To do this, we sort the groups by the value of their
// cards and the number of them.
//
// Since the tie breaker in this case is again the value of the
// card, this is kind of recursive.
return typeJoker(cards) + cards.fold(0) { acc, label -> acc * 13 + cardValueJoker(label) }
}
fun parseHands(input: File): List<Pair<Hand, Int>> {
return input.useLines {
it
.map {
val (hand, bid) = it.split(' ')
hand.toList() to bid.toInt()
}
.toList()
}
}