Skip to Content

Advent of Code 2019 - Day 17, in Kotlin

Kotlin solutions to parts 1 and 2 of Advent of Code 2019, Day 17: 'Set and Forget'

Posted on

Today we learn one of the most important lessons of being a software developer - that not every problem has code as its solution.

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

Problem Input

Another IntCode problem calls for another IntCode parser…

@FlowPreview
class Day17(input: String) {

    private val program: MutableMap<Long, Long> = input
        .split(",")
        .withIndex()
        .associateTo(mutableMapOf()) { it.index.toLong() to it.value.toLong() }

}

We will also annotate our class today with @FlowPreview so we can use a nice experimental feature of Kotlin Coroutines.

⭐ Day 17, Part 1

The puzzle text can be found here.

First, let’s start off by writing a function that starts and runs our computer. Since we’ll be consuming all of the output, we’ll need to tell our computer that we need an UNLIMITED channel (by default IntCodeComputerMk2 is conflated).

private fun mapScaffold(): Map<Point2D, Char> = runBlocking {
    val computer = IntCodeComputerMk2(program = program.copyOf(), output = Channel(Channel.UNLIMITED))
    launch {
        computer.runProgram()
    }
    takePicture(computer)
}

Next, we’ll write our takePicture function which read the output from the computer and turn it into a Map<Point2D, Char>, which we’ll use to represent our scaffold.

private suspend fun takePicture(computer: IntCodeComputerMk2): Map<Point2D, Char> =
    computer.output.consumeAsFlow()
        .map { it.toChar() }
        .toList()
        .joinToString("")
        .lines()
        .mapIndexed { y, row ->
            row.mapIndexed { x, c -> Point2D(x, y) to c }
        }
        .flatten()
        .toMap()

In this function, we use consumeAsFlow, which lets us consume the output from the computer as a stream vs. consuming it iteratively. We map each Long from the computer back out to a Char, join them into Strings. If we call lines() on our List<String>, we can map the stream of Char into our map. Think of this as the reverse of how we parse maze-based inputs from other challenges.

While we’re thinking about it, let’s write a function to print out the scaffold. Since this seems to be pretty generic, we’ll save it in our common extensions file.

// In Extensions.kt

fun <T> Map<Point2D, T>.print() {
    val maxX = this.keys.maxBy { it.x }!!.x
    val maxY = this.keys.maxBy { it.y }!!.y

    (0..maxY).forEach { y ->
        (0..maxX).forEach { x ->
            print(this.getOrDefault(Point2D(x, y), ' '))
        }
        println()
    }
}

Finally, we have enough to solve Part 1:

fun solvePart1(): Int {
    val scaffold = mapScaffold()
    scaffold.print()
    return scaffold
        .filter { it.value == '#' }
        .keys
        .filter { it.neighbors().all { neighbor -> scaffold[neighbor] == '#' } }
        .map { it.x * it.y }
        .sum()
}

Our solution involves creating the scaffold, printing it out (because we’ll use it in Part 2), and then counting the number of spaces on the scaffold that are on the scaffold and have scaffolds in every adjacent spot. Finally, we sum up the x*y coordinates for our answer!

Star earned!

⭐ Day 17, Part 2

The puzzle text can be found here.

This is the part where I decided to solve the problem manually for my input. I’m sure there is probably a way to automate all of this, but for me, I had fun trying to figure it out manually.

The method I used was:

  1. Print the scaffold
  2. Write instructions for how to follow the path
  3. Realize I made a trivial error and start over (do this like 3x)
  4. Identify commonly repeated parts of the instructions
  5. Feed our program to the computer
  6. Take the very last number the computer produces as output

When I print my scaffold, I get this:

..........................................#########......
..........................................#.......#......
..........................................#.......#......
..........................................#.......#......
..........................................#.......#......
..........................................#.......#......
..........................................#.......#......
..........................................#.......#......
......................................#############......
......................................#...#..............
......#.............................^######..............
......#...............................#..................
......#...............................#..................
......#...............................#..................
......#...............................#..................
......#...............................#..................
......#...............................#########..........
......#.......................................#..........
#######.................................#########........
#.......................................#.....#.#........
#.......................................#.....#.#........
#.......................................#.....#.#........
#.......................................#.....###########
#.......................................#.......#.......#
###########.............................#.......#.......#
..........#.............................#.......#.......#
..........#.............................###########.....#
..........#.....................................#.#.....#
..........#.....................................#.#.....#
..........#.....................................#.#.....#
..........#.....................................#########
..........#.......................................#......
..........###########.......................#######......
....................#.......................#............
....................#.............#########.#............
....................#.............#.......#.#............
....................#.............#.......#.#............
....................#.............#.......#.#............
....................#########.....#.......#.#............
............................#.....#.......#.#............
............................#.....#...#######............
............................#.....#...#...#..............
............................#.#############..............
............................#.#...#...#..................
............................#######...#..................
..............................#.......#..................
..............................#.......#..................
..............................#.......#..................
..............................#.......#..................
..............................#.......#..................
..............................#########..................

There are a few ways to traverse that path since we’re allowed to cross over our trail. However, I found that at least for my input, I was able to follow any line until its end and then turn. I never had to turn early.

Doing this, I was (eventually) able to create the following set of instructions:

R,6,L,10,R,8,R,8,R,12,L,8,L,8,R,6,L,10,R,8,R,8,R,12,L,8,L,8,L,10,R,6,R,6,L,8,
R,8,R,12,L,8,L,8,R,6,L,10,R,8,R,8,R,12,L,8,L,8,R,6,L,10,R,8,L,10,R,6,R,6,L,8

Then I used my text editor (Sublime Text ) to “find” the first few characters of this string on the theory that the program must start with a commonly used pattern. I was right and discovered that “R,6,L,10,R,8” repeats a few times. We’ll call this A. Next, I made the same assumption for the end of the program input - that it must end with a commonly used pattern. Finding that, leads to “L,10,R,6,R,6,L,8”, which we will call B. Finally, everything left is copies of “R,8,R,12,L,8,L,8”, which we will call C.

Repeating those letters in the right spots yields the following program: A,C,A,C,B,A,C,B,A,B.

Next, we need a way to fire up our computer, change the first instruction to 2, give it our input, and wait for it to finish.

private fun runWithInput(input: List<String>): Int = runBlocking {
    val computer = IntCodeComputerMk2(
        program.copyOf().apply { this[0] = 2L } // L33t H4x0rz
    )
    val cpu = launch {
        computer.runProgram()
    }
    input.forEach { line ->
        line.forEach { c ->
            computer.input.send(c.toLong())
        }
        computer.input.send(10)
    }
    cpu.join() // Wait for computer to finish
    computer.output.receive().toInt()
}

Because our IntCode computer wants ASCII input, we have to convert each character of our input program to its ASCII representation using toLong(). We send the input to the computer, wait for it to finish, and take the last digit it produces as our answer.

Note: The IntCode we’re given today is very good at providing coherent output and debug messages. If you run into trouble, try reading the output fully and printing it out (after converting it from Int to Char). This will tell you either what you did wrong, what the input prompts are, or give you a picture of the robot’s progress along the scaffold!

Sending our hand-calculated input to the computer we just created solves Part 2! Note that the final N in the input turns off the live video stream.

fun solvePart2(): Int =
    runWithInput(
        listOf(
            "A,C,A,C,B,A,C,B,A,B",
            "R,6,L,10,R,8",
            "L,10,R,6,R,6,L,8",
            "R,8,R,12,L,8,L,8",
            "N" // Do not want live video
        )
    )

Star earned!

Further Reading

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