import java.io.File typealias Hand = List fun main() { val input = File("input") val bets = parseHands(input) println(part1(bets)) println(part2(bets)) } fun part1(bets: List>): Int { return bets.sortedBy { handValue(it.first) }.foldIndexed(0) { i, acc, bet -> acc + (i + 1) * bet.second } } fun part2(bets: List>): 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 = 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 // 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. 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> { return input.useLines { it .map { val (hand, bid) = it.split(' ') hand.toList() to bid.toInt() } .toList() } }