Advent of Code 2023 - Day 2, in Kotlin - Cube Conundrum
Kotlin solutions to parts 1 and 2 of Advent of Code 2023, Day 2: 'Cube Conundrum'
Welcome back! If you’ve done Advent of Code long enough, you’ll run into a few parser-heavy puzzles each year. Some puzzles are easy to parse but some have complications making it more difficult. Today I’d classify this in the “more complicated than not” category. I got bit by a parser bug for a good ten minutes (thanks IntelliJ debugger, you saved me again!), so hopefully you won’t. :)
If you’d rather just view the code, my GitHub Repository is here.
Puzzle Input
We can bring our input
into our daily solution class as a List<String>
because each game is on its own line. From there, we will eventually need to store this information somewhere, so let’s define a data class called Game
to store the data we care about (its id
, and how many red
, green
, and blue
balls we’ve seen). We’ll make Game
private to Day02
in case we need to define another similarly named class at some point in the future (the names won’t collide this way).
class Day02(input: List<String>) {
private val games: List<Game> = input.map { Game.of(it) }
private data class Game(
val id: Int,
val red: Int,
val green: Int,
val blue: Int
) {}
}
We parse each row of input
by calling map
over each line and passing it to Game.of(...)
, which is our constructor function. We’ll define that as a companion object
on Game
.
// In Day02.Game
companion object {
fun of(input: String): Game {
val id = input.substringAfter(" ").substringBefore(":").toInt()
val colors = mutableMapOf<String, Int>()
input.substringAfter(":").split(";").forEach { turn ->
turn.split(",").map { it.trim() }.forEach { draw ->
val drawNum = draw.substringBefore(" ").toInt()
val color = draw.substringAfter(" ")
colors[color] = maxOf(drawNum, colors[color] ?: drawNum)
}
}
return Game(id, colors["red"] ?: 0, colors["green"] ?: 0, colors["blue"] ?: 0)
}
}
We’re only on Day 2 and we’re already seeing some of my favorite parser/helper functions - substringBefore
and substringAfter
. I like using them rather than writing complicated regular expressions (RegExs) because I think people find them easier to follow if you don’t have a good solid base of writing regular expressions. So for that reason, I tend to favor “manually” parsing using these functions (along with split
), which makes this code look longer but I think is easier to follow.
At any rate, the fist thing we do is parse out the id
of the game by narrowing the input
down to the characters immediately after the first space and before the first colon, and converting it to an integer via toInt
.
At this point we’ll define a MutableMap<String,Int>
called colors
that we will use to store how many of each type of ball we’ve seen until we’re ready to construct our class. Remember, at the end of each turn the elf returns all the balls we’ve seen to the bag, so we only want to store the highest number we’ve seen of each color.
Next up, we need to split
the remaining text by ;
in order to separate each turn
from the others. We then split
each turn on ,
and SUPER IMPORTANTLY (because this was my bug!), trim
the result of the split here to remove extraneous spaces. Now we have broken the turn
into a specific draw
, and we can parse out the number and color.
We can get the drawNum
by taking all of the text before the first space and converting it to an integer. We can get the color
by taking all of the text after the first space.
We can take this drawNum
of type color
only if the colors
map hasn’t seen it (stores null
for the color
), or has a lower value of that color
. To accomplish this we can store the maxOf
our drawNum
or whatever the colors
map is storing, defaulting to 0 if it stores nothing for that color. I suppose we could also do this same thing with map.compute
but I think what I have here is easier to follow and takes advantage of Kotlin’s excellent null support (via ?:
).
We now have everything we need to create and return our Game
instance. Be sure to default any color
we haven’t seen to 0.f
⭐ Day 2, Part 1
The puzzle text can be found here.
Part 1 asks us to find games that are possible. A game is only possible if we know the number of balls drawn is lower than the proposed amount. Otherwise, we’ll exceed the proposed amount and make the game impossible.
Let’s write a function called isPossible
that takes a proposed red
, green
, and blue
amount and does some checks. We’ll add this as a function to the Game
class.
// In Day02.Game:
fun isPossible(red: Int, green: Int, blue: Int) =
this.red <= red && this.green <= green && this.blue <= blue
Now we’re able to solve part 1 of the puzzle. First, we filter
out the games that are not possible and then we use sumOf
to sum the id
of each remaining (possible) game.
// In Day02
fun solvePart1(): Int =
games.filter {
it.isPossible(12, 13, 14)
}.sumOf { it.id }
Star earned! Onward!
⭐ Day 2, Part 2
The puzzle text can be found here.
Not much to do for part 2 except write a power
function in Game
…
// In Day02.Game:
fun power() =
red * blue * green
And then use sumOf
(again!) to sum the results of the powers of all the games…
// In Day02
fun solvePart2(): Int =
games.sumOf { it.power() }
Star earned! See you tomorrow!
Further Reading
- Index of All Solutions - All posts and solutions for 2023, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 2
- Advent of Code - Come join in and do these challenges yourself!