Advent of Code in Functional Kotlin: Day 2

Raphael De Lio
10 min readDec 2, 2023

--

Twitter | LinkedIn | YouTube | Instagram

The Puzzle (Part 1)

Imagine you have a bag containing multiple red, green, or blue cubes. You play a series of games where, in each game, an Elf shows you a few combinations of these colored cubes taken from the bag.

In each game, the Elf will take out some cubes, show them to you, and then put them back in the bag. This process happens multiple times in a single game. The key point is that the Elf can never show you more cubes of a particular color than what is inside the bag.

For example, in a bag that holds 12 red cubes, if, in any round of a game, the Elf shows you 15 red cubes, that game is not possible because the bag can only hold 12 red cubes.

The second puzzle of the Advent of Code 2023 is about figuring out which games can be played under certain conditions using cubes of different colors.

You have to review the record of several games. Each game’s record tells you the combinations of red, green, and blue cubes shown in each round of that game. Your task is to figure out which games could have happened with the given limitations on the number of each color of cubes in the bag.

After identifying the possible games, you must add their ID numbers to get the final answer.

So, to solve the puzzle, you need to check each game against the maximum number of cubes the bag can hold and see if the game follows the rules. If it does, it’s a possible game. If it doesn’t, it’s not possible. Then, sum up the ID numbers of all the possible games.

Example (Part 1)

Consider a bag that contains 12 red cubes, 13 green cubes, and 14 blue cubes when they provide the following records of games:

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green

What is the sum of the IDs of the games that could have been played with the given number of cubes?

In this example, games 1, 2, and 5 would have been possible.

However, game 3 would have been impossible because, at one point, the Elf showed you 20 red cubes at once, and you only have 12 red cubes in your bag; similarly, game 4 would also have been impossible because the Elf showed you 15 blue cubes at once.

If you add up the IDs of the games that would have been possible (1, 2, and 5), you get 8, the solution for the example.

Solution (Part 1)

First of all, let's decompose a line that represents a game:

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

Each Game is composed of a set that is, in turn, composed of colored cubes. That's what this game looks like when converted into JSON:

[
{
"gameId": 1,
"sets" : [
{
"blue": 3,
"red": 4
},
{
"red": 1,
"green": 1
"blue": 6
},
{
"green": 2
},
]
}
]

What I want to do is to parse each line into objects that represent a Game:

data class Game(val gameId: Int, val gameSets: List<GameSet>)
data class GameSet(val cubes: Cubes)
data class Cubes(val red: Int, val green: Int, val blue: Int)

Now, let’s examine what each line looks like and decide how we can parse it.

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

First, we can see that the line is split into two sides by a colon: The left side is always the "Game ID," and the right side represents the "GameSets.":

Left side: Game 1
Right side: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

Then, each game set is separate from the other by a semi-colon. If we split this string, this is what we get a list of three strings (GameSets):

0: 3 blue, 4 red
1: 1 red, 2 green, 6 blue
2: 2 green

Now, we can split each string (GameSet) by a comma, and we will end up with a list of strings that represent each cube and its respective count:

First set:
0: 3 blue
1: 4 red

Second set:
0: 1 red
1: 2 green
2: 6 blue

Third set:
0: 2 green

And finally, we can split each string (e.g. 3 blue) and end up with a pair of two elements: “3” and “blue.”

Parsing the Game

Let’s get started by parsing the game. We will first split the line using the colon. To avoid trimming the string afterward, we will add a space after the colon:

line.split(": ")

Let's call the result of this operation “gameIdOrSets.”

What we want to do now is start building our “Game” instance. For that, we will call fold on our result and perform a different operation on which of the sides of our “gameIdOrSets” object.

line.split(": ")
.fold(Game(0, emptyList())) { game, gameIdOrSets ->
if (game.gameId == 0)
game.copy(gameId = gameIdOrSets.asGameId) // assign game id
else
game.copy(gameSets = parseGameSets(gameIdOrSets)) // assign sets
}

The result of the split is a list with two strings. The fold function receives an initial object (Game(0, emptyList()), which is then reused in each iteration of our list “gameIdOrSets.”

Parsing the Game ID
We know that on the first iteration of our list, the gameId of the Game object we are creating is 0. We also know that the first element of our “gameIdOrSets” list is the gameId of the game we are parsing (“Game 1”. Therefore, during the first iteration, we parse the game ID with the following implementation:

val String.asGameId: Int get() = split(" ").last().toInt()

Even though I can trust this is always the case, I’m unhappy with this part of the solution. So, please let me know in the comments if you can think of a better one.

In the end, that’s what our function will look like:

fun parseGame(line: String) = line.split(": ")
.fold(Game(0, emptyList())) { game, gameIdOrSets ->
if (game.gameId == 0)
game.copy(gameId = gameIdOrSets.asGameId)
else
game.copy(gameSets = parseGameSets(gameIdOrSets))
}

Parsing the Game Sets

Now, we can focus on the second element of our line, the game sets. The string of the game sets will look something like:

3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green

Again, we will start by splitting this string with a semi-colon:

sets.split("; ")

This split returns a list of strings, each representing a Game Set. Therefore, we want to map them into GameSets:

sets.split("; ")
.map { set ->

}

Each set is composed of different cube colors and their respective counts.

3 blue, 4 red

A comma splits them:

sets.split("; ")
.map { set ->
set.split(", ")

}

We already have an object that represents our cubes, so we will use fold to iterate over each of the colors and their respective counts to build our Cubes object:

sets.split("; ")
.map { set ->
set.split(", ")
.fold(Cubes(0, 0, 0)) { cubes, countColor ->

}
}

Now, let’s parse each countColor (e.g. 3 blue). To do it, we have to split our string with a space and then map the result into a pair. Let’s implement a separate function to do it:

fun parseColorCount(countColor: String) = countColor.split(" ")
.let { it.first().toInt() to it.last().lowercase() }

With this pair in hands, let’s implement a function in our Cubes class that will put this colorCount pair into its respective color in the Cube:

data class Cubes(val red: Int, val green: Int, val blue: Int) {
fun addCubes(color: String, count: Int) = when (color) {
"red" -> copy(red = red + count)
"green" -> copy(green = green + count)
"blue" -> copy(blue = blue + count)
else -> this
}
}

And call this function in our parser:

sets.split("; ")
.map { set ->
set.split(", ")
.fold(Cubes(0, 0, 0)) { cubes, countColor ->
with(parseColorCount(countColor)) {
val count = first
val color = second
cubes.addCubes(color, count)
}
}
}

And finally, we want to build our GameSet that will be returned to our map. Let’s also define our function here:

fun parseGameSets(sets: String) = sets.split("; ")
.map { set ->
set.split(", ")
.fold(Cubes(0, 0, 0)) { cubes, countColor ->
with(parseColorCount(countColor)) {
val count = first
val color = second
cubes.addCubes(color, count)
}
}.let { cubes -> GameSet(cubes) }
}

If we run our parseGame function on the first line of our input, and then print the result, this is what we should see:

Game(gameId=1, gameSets=[GameSet(cubes=Cubes(red=4, green=0, blue=3)), GameSet(cubes=Cubes(red=1, green=2, blue=6)), GameSet(cubes=Cubes(red=0, green=2, blue=0))])

Cool!

Verifying if games can be played

Now that we can parse our games let’s implement a function in our Game class to determine whether a game is possible under a specific condition:

data class Game(
val gameId: Int,
val gameSets: List<GameSet>
) {
fun canBePlayed(maxCubes: Cubes) = gameSets.all { set ->
set.cubes.red <= maxCubes.red &&
set.cubes.green <= maxCubes.green &&
set.cubes.blue <= maxCubes.blue
}
}

The canBePlayed function will determine if all the sets are playable with the given maximum number of cubes of each color.

Now, let’s merge everything together and come up with the sum of all playable game IDs.

First, let’s define the maximum number of cubes:

val maxCubes = Cubes(12, 13, 14)

Then, for each parsed line, we only want to take the games that can be played:

val maxCubes = Cubes(12, 13, 14)

parseGame(line).takeIf {
it.canBePlayed(maxCubes)
}

What we want from each of these games is the gameId, and in case it couldn’t be taken, and the returned result was null, let’s assign 0 to it:

val maxCubes = Cubes(12, 13, 14)

parseGame(line).takeIf {
it.canBePlayed(maxCubes)
}?.gameId ?: 0

Then, let’s sum the result of each parsed and print it out:

val maxCubes = Cubes(12, 13, 14)

input.sumOf { line ->
parseGame(line).takeIf {
it.canBePlayed(maxCubes)
}?.gameId ?: 0
}.println()

And the result should be 8. 🚀

Part 2

In this part of the puzzle, our job is to determine the minimum number of red, green, and blue cubes required for each game to be possible. This is based on the combinations shown in each round of the game. For example, if a round shows 4 red cubes, you know there must be at least 4 red cubes in the bag.

Once you find the minimum number of each color cube needed for a game, you calculate the ‘power’ of that set by multiplying the numbers of red, green, and blue cubes together.

Your task is to do this for each game: find the ‘power’ of their minimum sets of cubes and then add these powers together to get a total sum. This total sum is the final answer you need to find.

Again, an example is provided for the same list of games:

Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
  • In game 1, the game could have been played with as few as 4 red, 2 green, and 6 blue cubes. If any color had even one fewer cube, the game would have been impossible.
  • Game 2 could have been played with a minimum of 1 red, 3 green, and 4 blue cubes.
  • Game 3 must have been played with at least 20 red, 13 green, and 6 blue cubes.
  • Game 4 required at least 14 red, 3 green, and 15 blue cubes.
  • Game 5 needed no fewer than 6 red, 3 green, and 2 blue cubes in the bag.

The power of a set of cubes is equal to the numbers of red, green, and blue cubes multiplied together. The power of the minimum set of cubes in game 1 is 48. In games 2-5 it was 12, 1560, 630, and 36, respectively. Adding up these five powers produces the sum 2286.

Let’s crack it!

Solution (Part 2)

For the second part, we don’t have to parse the game again; we can reuse the same functions we had already implemented.

All we have to do is implement functions to check the minimum number of cubes of each color for each game and then multiply them to come up with a result:

data class Game(
val gameId: Int,
val gameSets: List<GameSet>
) {
fun minCubes() = minBlues() * minGreens() * minReds()

private fun minBlues() = gameSets.maxOf { it.cubes.blue }
private fun minGreens() = gameSets.maxOf { it.cubes.green }
private fun minReds() = gameSets.maxOf { it.cubes.red }
}

Then, we just need to sum up these results:

input.sumOf { line -> parseGame(line).minCubes() }.println()

The result will be 2286. Pretty easy, right? 😋

What do you think of this solution? Let me know in the comments!

Stay curious!

If you want to check the full solution, you can find it in my GitHub Repository:

Contribute

Writing takes time and effort. I love writing and sharing knowledge, but I also have bills to pay. If you like my work, please, consider donating through Buy Me a Coffee: https://www.buymeacoffee.com/RaphaelDeLio

Or by sending me BitCoin: 1HjG7pmghg3Z8RATH4aiUWr156BGafJ6Zw

Follow Me on Social Media

Stay connected and dive deeper into the world of Kotlin with me! Follow my journey across all major social platforms for exclusive content, tips, and discussions.

Twitter | LinkedIn | YouTube | Instagram

--

--

Raphael De Lio
Raphael De Lio

Written by Raphael De Lio

Software Engineer | Developer Advocate | International Conference Speaker | Tech Content Creator | Working @ Redis | https://linktr.ee/raphaeldelio

No responses yet