Skip to Content

Advent of Code 2018 - Day 14, in Kotlin

Kotlin solutions to parts 1 and 2 of Advent of Code 2018, Day 14: 'Chocolate Charts'

Posted on

I’m a bit late posting this because I’ve been moving and have been extra busy.

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

Problem Input

We are given a number, and we need it as a String, an Int, and a List<Int> eventually. Let’s do that here, and we can refer to them in parts 1 and 2.

class Day14(stoppingPoint: String) {

    private val stopInt = stoppingPoint.toInt()
    private val stopList = stoppingPoint.map { it.toString().toInt() }.toList()

}

Turning a String into a List<Int> where the Int is a digit looks complicated, but String.toInt() and Char.toInt() mean different things. If you skip one of the toString() calls, you’ll get character values, not the digit it represents! Also, we can’t just turn our Int into a List<Int> with our asDigits() function below, because inputs might start with zero, and we would mis-calculate them.

Day 14, Part 1

You finally have a chance to look at all of the produce moving around. Chocolate, cinnamon, mint, chili peppers, nutmeg, vanilla… the Elves must be growing these plants to make hot chocolate! As you realize this, you hear a conversation in the distance. When you go to investigate, you discover two Elves in what appears to be a makeshift underground kitchen/laboratory.

The Elves are trying to come up with the ultimate hot chocolate recipe; they’re even maintaining a scoreboard which tracks the quality score (0-9) of each recipe.

Only two recipes are on the board: the first recipe got a score of 3, the second, 7. Each of the two Elves has a current recipe: the first Elf starts with the first recipe, and the second Elf starts with the second recipe.

To create new recipes, the two Elves combine their current recipes. This creates new recipes from the digits of the sum of the current recipes’ scores. With the current recipes’ scores of 3 and 7, their sum is 10, and so two new recipes would be created: the first with score 1 and the second with score 0. If the current recipes’ scores were 2 and 3, the sum, 5, would only create one recipe (with a score of 5) with its single digit.

The new recipes are added to the end of the scoreboard in the order they are created. So, after the first round, the scoreboard is 3, 7, 1, 0.

After all new recipes are added to the scoreboard, each Elf picks a new current recipe. To do this, the Elf steps forward through the scoreboard a number of recipes equal to 1 plus the score of their current recipe. So, after the first round, the first Elf moves forward 1 + 3 = 4 times, while the second Elf moves forward 1 + 7 = 8 times. If they run out of recipes, they loop back around to the beginning. After the first round, both Elves happen to loop around until they land on the same recipe that they had in the beginning; in general, they will move to different recipes.

Drawing the first Elf as parentheses and the second Elf as square brackets, they continue this process:

(3)[7]
(3)[7] 1  0 
 3  7  1 [0](1) 0 
 3  7  1  0 [1] 0 (1)
(3) 7  1  0  1  0 [1] 2 
 3  7  1  0 (1) 0  1  2 [4]
 3  7  1 [0] 1  0 (1) 2  4  5 
 3  7  1  0 [1] 0  1  2 (4) 5  1 
 3 (7) 1  0  1  0 [1] 2  4  5  1  5 
 3  7  1  0  1  0  1  2 [4](5) 1  5  8 
 3 (7) 1  0  1  0  1  2  4  5  1  5  8 [9]
 3  7  1  0  1  0  1 [2] 4 (5) 1  5  8  9  1  6 
 3  7  1  0  1  0  1  2  4  5 [1] 5  8  9  1 (6) 7 
 3  7  1  0 (1) 0  1  2  4  5  1  5 [8] 9  1  6  7  7 
 3  7 [1] 0  1  0 (1) 2  4  5  1  5  8  9  1  6  7  7  9 
 3  7  1  0 [1] 0  1  2 (4) 5  1  5  8  9  1  6  7  7  9  2 

The Elves think their skill will improve after making a few recipes (your puzzle input). However, that could take ages; you can speed this up considerably by identifying the scores of the ten recipes after that. For example:

If the Elves think their skill will improve after making 9 recipes, the scores of the ten recipes after the first nine on the scoreboard would be 5158916779 (highlighted in the last line of the diagram).

  • After 5 recipes, the scores of the next ten would be 0124515891.
  • After 18 recipes, the scores of the next ten would be 9251071085.
  • After 2018 recipes, the scores of the next ten would be 5941429882.

What are the scores of the ten recipes immediately after the number of recipes in your puzzle input?

The problem is pretty limiting in terms of approaches we can take. We could write this as a sequence, but it would be easier for part 2 if we pass in a function to tell us when to stop. Essentially, we will keep generating recipes and ask the stopCondition function if we should stop. When it tells us to stop, we return the entire history we’ve generated so far.

private fun recipes(stopCondition: (List<Int>) -> Boolean): List<Int> {
    val history = mutableListOf(3, 7)
    var elf1 = 0
    var elf2 = 1
    var stop = false

    while (!stop) {
        val nextValue = history[elf1] + history[elf2]
        nextValue.asDigits().forEach {
            if (!stop) {
                history.add(it)
                stop = stopCondition(history)
            }
        }
        elf1 = (elf1 + history[elf1] + 1) % history.size
        elf2 = (elf2 + history[elf2] + 1) % history.size
    }
    return history
}

The part that got me for a while is that we have to check stopCondition twice! Why? If the elves create a great recipe, they produce two digits and not just one. So we have to check the stop condition for each history item added, not just the first one.

We also define this extension function to bust up an Int into a List<Int>

private fun Int.asDigits(): List<Int> =
    this.toString().map { it.toString().toInt() }

And with part 1, our stop condition is just based on the length:

fun solvePart1(): String =
    recipes { it.size == stopInt + 10 }.takeLast(10).joinToString("")

We generate recipes until we have 10 more than the stopping point we have is, and then we just stringify the last 10 elements.

Star earned! Onward!

Day 14, Part 2

As it turns out, you got the Elves’ plan backwards. They actually want to know how many recipes appear on the scoreboard to the left of the first recipes whose scores are the digits from your puzzle input.

  • 51589 first appears after 9 recipes.
  • 01245 first appears after 5 recipes.
  • 92510 first appears after 18 recipes.
  • 59414 first appears after 2018 recipes.

How many recipes appear on the scoreboard to the left of the score sequence in your puzzle input?

See? I told you the higher-order function was the way to go. Defining a new function for part two solves the problem without altering our logic any more:

fun solvePart2(): Int =
    recipes { it.endsWith(stopList) }.size - stopList.size

We’ve got this handy extension function to quickly test that one List ends with another List. I found this to be fast enough to make part 2 not take too long.

private fun List<Int>.endsWith(other: List<Int>): Boolean =
    if (this.size < other.size) false
    else this.slice(this.size - other.size until this.size) == other

Star earned!

Further Reading

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