Skip to Content

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'

Posted on

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

  1. Index of All Solutions - All posts and solutions for 2023, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 2
  4. Advent of Code - Come join in and do these challenges yourself!