# Advent of Code 2023 - Day 7, in Kotlin - Camel Cards

Kotlin solutions to parts 1 and 2 of Advent of Code 2023, Day 7: 'Camel Cards'

Posted on

Another Advent of Code card game! I liked this one and hope my solution is clear. This post is pretty long, but really only because I refactored a lot between part 1 and part 2.

If you’d rather just view the code, my GitHub Repository is here.

Puzzle Input

We will take in the `input` today as a `List<String>`.

``````class Day07(private val input: List<String>) {

}
``````

Strategy

Since it can always be determine which hand of cards beats another, independent of the rather unique rules today’s puzzle gives us, that means there exists a natural ordering for all hands. Meaning: if we generated every hand possible, we could number them, and put them in an order.

So let’s do that. We will define a `Hand` class which contains the `cards` and the `bid`. When sorting the `Hand` objects we will do it in a way that puts them into their natural order, starting with weaker hands first and progressing up to stronger hands.

#### ⭐ Day 7, Part 1

The puzzle text can be found here.

Let’s start with our `Hand` class, since it will end up doing all of the work. We’ll have to rework it for part 2. As in previous days, we will make this class private to our `Day07` class.

``````// In Day07

private class Hand(cards: String, val bid: Int) : Comparable<Hand> {
private val identity: Int = calculateIdentity(cards)

override fun compareTo(other: Hand): Int =
this.identity - other.identity

private fun calculateIdentity(cards: String): Int {
val category = categoryCalculation(cards)
return cards.fold(CATEGORIES.indexOf(category)) { acc, card ->
(acc shl 4) or STRENGTH.indexOf(card)
}
}

private fun calculateCategory(cards: String): List<Int> =
cards.groupingBy { it }.eachCount().values.sortedDescending()

companion object {
private val CATEGORIES = listOf(
listOf(1, 1, 1, 1, 1),
listOf(2, 1, 1, 1),
listOf(2, 2, 1),
listOf(3, 1, 1),
listOf(3, 2),
listOf(4, 1),
listOf(5)
)

private const val STRENGTH = "23456789TJQKA"
}
}
``````

Our `Hand` class takes in the `cards` that make it up as well as the `bid`, which we define as a property. It also implements `Comparable<Hand>` which is used to put them into a natural order.

The only other property we define is `identity`, which means of all the possible hands we could generate, which one is this? We’ll calculate it in a minute, let’s fist go over the tools we need.

If you look in the `companion object`, you will find two things. First, `CATEGORIES` which is a `List<List<Int>>`. Each of the inner lists represents counts of cards. So if we have 5 individual cards that are not pairs, we have `List(1, 1, 1, 1, 1)`. On the other end of the range, we have `List(5)` to represent when all 5 cards are the same. We have lists for a single pair, two pair, three of a kind, full house, and four of a kind. Thankfully, there are no straights in Camel Cards like in poker, or this model would’t really work.

Second, we have `STRENGTH` which puts the cards in order from weakest to strongest. We’ll use their index to assign them a value later.

The first order of business is to calculate the `category`. We do this in the `calculateCategory` function (this will change names in part 2). To get the `List<Int>` that represents what kind of hand we have, we use `groupingBy` from the Kotlin Standard Library, which allows us to group the cards (effectively, not actually) into a `Map<Char, List<Char>>`. We can then call `eachCount` to reduce the value of `List<Char>` to an `Int`, representing the size of the list. Since we only care about the `values` we take them, and then `sortDescending` to put these into the order you see in `CATEGORIES`.

Overview of Identity

In order to build our `identity`, we will convert the `category` of the hand 4 of a kind, etc) and the relative value of each card into a single `Int`. Picture each of these taking up 4 bits (one half byte).

``````0000 0000 0000 0000 0000 CC11 2233 4455 -> 32 bits
--byte1--|--byte2--|--byte3--|--byte4--

Where - 0 = unused bit
C = Category
1 = Card 1
2 = Card 2
3 = Card 3
4 = Card 4
5 = Card 5
``````

Putting the bits into this order will guarantee the order we want. All 5 of a kind hands come before all 4 of a kind hands, and within those categories, the hands are in order.

To accomplish this, we will use `fold`, seeding it with the `category`. For each loop through the `cards`, we shift the `acc` (accumulation) number to the left by 4 bits and perform a binary OR with that number and the number representing the strength of the individual card.

Comparing Hands

We override `compareTo` from the `Comparable` interface, which lets us compare two `Hand` objects. We will put these into an order such that weaker hands come before stronger hands. So the weakest hand (“23456”) comes before the second weakest hand (“23457”).

Playing Camel Cards

Finally, some parsing!

``````// In Day07

private fun playCamelCards(): Int =
input
.asSequence()
.map { row -> row.split(" ") }
.map { parts -> Hand(parts.first(), parts.last().toInt()) }
.sorted()
.mapIndexed { index, hand -> hand.bid * (index + 1) }
.sum()
``````

We look through our `input` and `split` each `row` by space. We then `map` that array into a `Hand` object, make sure the results are `sorted` and then use `mapIndexed` to calculate the value of each hand. Remember, `mapIndexed` starts at 0, but we want to start at 1, so we have to add 1 to the result before getting the `sum`.

All that’s left to do is `playCamelCards`!

``````// In Day07

fun solvePart1(): Int =
playCamelCards()
``````

Star earned! Onward!

#### ⭐ Day 7, Part 2

The puzzle text can be found here.

Luckily, despite the somewhat extensive refactoring, we’re not far off from the answer.

The only real change is in how we calculate the category. All other aspects of `Hand` stay the same.

Here is a full version of `Hand` after refactoring. I’ll go over interesting parts below.

``````// In Day07

private class Hand(cards: String, val bid: Int, jokersWild: Boolean) : Comparable<Hand> {
private val identity: Int = calculateIdentity(
cards,
if (jokersWild) STRENGTH_WITH_JOKERS else STRENGTH_WITHOUT_JOKERS,
if (jokersWild) this::calculateCategoryWithJokers else this::calculateCategoryWithoutJokers
)

override fun compareTo(other: Hand): Int =
this.identity - other.identity

private fun calculateIdentity(
cards: String,
strength: String,
categoryCalculation: (String) -> List<Int>
): Int {
val category = categoryCalculation(cards)
return cards.fold(CATEGORIES.indexOf(category)) { acc, card ->
(acc shl 4) or strength.indexOf(card)
}
}

private fun calculateCategoryWithoutJokers(cards: String): List<Int> =
cards.groupingBy { it }.eachCount().values.sortedDescending()

private fun calculateCategoryWithJokers(cards: String): List<Int> {
val cardsWithoutJokers = cards.filterNot { it == 'J' }
val numberOfJokers = cards.length - cardsWithoutJokers.length

return if (numberOfJokers == 5) listOf(5)
else calculateCategoryWithoutJokers(cardsWithoutJokers).toMutableList().apply {
this[0] += numberOfJokers
}
}

companion object {
private val CATEGORIES = listOf(
listOf(1, 1, 1, 1, 1),
listOf(2, 1, 1, 1),
listOf(2, 2, 1),
listOf(3, 1, 1),
listOf(3, 2),
listOf(4, 1),
listOf(5)
)

private const val STRENGTH_WITHOUT_JOKERS = "23456789TJQKA"
private const val STRENGTH_WITH_JOKERS = "J23456789TQKA"
}
}

``````

The first thing to notice is that we now have to account for whether jokers are wild or not. So we will add a `jokersWild` variable to our `Hand` constructor. The `calculateIdentity` function also changes to take in the proper `strength` mapping and a function to calculate the `category`.

The `calculateCategoryWitoutJokers` function is the same as the `calculateCategory` function above, just renamed. Similarly, `STRENGTH_WITHOUT_JOKERS` is simply `STRENGTH` renamed. We’ve added `STRENGTH_WITH_JOKERS` which gives `J` the lowest point value. The `compareTo` function does not change at all, nor does `CATEGORIES`.

To calculate the category of a hand with jokers, we implement `calculateCategoryWithJokers`. The first thing we do is remove all of the jokers from the list of cards. If we have a hand with all jokers, we don’t have any calculating to do - we can return a `List(5)` directly (yes, we could also do the same for no jokers). Otherwise, we take the cards that aren’t jokers and use the `calculateCategoryWitoutJokers` function to get the category list. Because we always want to use the joker to represent the best card, we add the number of jokers to the leftmost value in the `category`. This has the effect of promoting the jokers to whatever the best card in the hand is, whatever it is.

We also need to make some minor changes to `playCamelCards`:

``````// In Day07

private fun playCamelCards(jokersWild: Boolean = false): Int =
input
.asSequence()
.map { row -> row.split(" ") }
.map { parts -> Hand(parts.first(), parts.last().toInt(), jokersWild) }
.sorted()
.mapIndexed { index, hand -> hand.bid * (index + 1) }
.sum()
``````

We will take in a `jokersWild` boolean flag and default it to `false`. We pass that value to the `Hand` constructor.

Calling `playCamelCards` indicating `true` for jokers are wild gives us the solution to part 2.

``````// In Day07

fun solvePart2(): Int =
playCamelCards(true)
``````

Star earned! See you tomorrow!