Skip to Content

Advent of Code 2019 - Day 11, in Kotlin

Kotlin solutions to parts 1 and 2 of Advent of Code 2019, Day 11: 'Space Police'

Posted on

Why are the Space Police after us? This ship we’re in is legitimate salvage!

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

Problem Input

Because our IntCodeComputerMk2 uses Long for everything, we’ll parse the input the same way we did on Day 9:


@ExperimentalCoroutinesApi
class Day11(input: String) {

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

}

We’ll also annotate our class with @ExperimentalCoroutinesApi because we’ll end up using a property that is still experimental and requires us to opt-in.

Day 11, Part 1

The puzzle text can be found here.

Let’s define some constants and a helper class to get started.

Constants in Day11 are all Long, because that’s what IntCodeComputerMk2 speaks.

// In Day11

companion object {
    private const val black: Long = 0L
    private const val white: Long = 1L
    private const val left: Long = 0L
    private const val right: Long = 1L
}

Next we’ll define a new class called ScreenDirection. One of the things about Advent of Code is that puzzles generally (but not always) use “screen order”. Meaning that 0,0 is in the upper left hand corner, not the lower left hand corner. So y increases as we move down. In case we end up with a puzzle that doesn’t behave that way, I wanted to name our direction class something specific:

sealed class ScreenDirection {
    abstract fun turnAndMoveLeft(from: Point2D): Pair<ScreenDirection, Point2D>
    abstract fun turnAndMoveRight(from: Point2D): Pair<ScreenDirection, Point2D>

    object North : ScreenDirection() {
        override fun turnAndMoveLeft(from: Point2D) = Pair(West, from.left())
        override fun turnAndMoveRight(from: Point2D) = Pair(East, from.right())
    }
    object East : ScreenDirection() {
        override fun turnAndMoveLeft(from: Point2D) = Pair(North, from.down())
        override fun turnAndMoveRight(from: Point2D) = Pair(South, from.up())
    }
    object West : ScreenDirection() {
        override fun turnAndMoveLeft(from: Point2D) = Pair(South, from.up())
        override fun turnAndMoveRight(from: Point2D) = Pair(North, from.down())
    }
    object South : ScreenDirection() {
        override fun turnAndMoveLeft(from: Point2D) = Pair(East, from.right())
        override fun turnAndMoveRight(from: Point2D) = Pair(West, from.left())
    }
}

As you can see, we only implement the concepts of turning left and right. If we need more, we’ll add them later. The interesting part to notice is that if we’re facing West and want to turn left, towards the South, we need to move up instead of down which would seem more natural (because we’re using screen coordinates). This is an artifact of how I defined up and down a few days ago so feel free to change your own implementation if this strikes you as odd or confusing.

Painting The Ship

Our little painting robot works as follows:

  1. Tell it what color it’s looking at
  2. It tells us what color to paint.
  3. And then which direction to move (left or right).

We’re going to use coroutines again, since our IntCodeComputerMk2 already supports them. This will allow us to run the computer in one coroutine and loop around with the input in another coroutine.

private fun paintShip(startingWith: Long = black) = runBlocking {
    val ship = mutableMapOf(Point2D.ORIGIN to 0L)
    val computer = IntCodeComputerMk2(program)
    var location: Point2D = Point2D.ORIGIN
    var screenDirection: ScreenDirection = ScreenDirection.North
    launch {
        computer.runProgram()
    }

    computer.input.send(startingWith)
    while (!computer.output.isClosedForReceive) {
        val colorMsg = computer.output.receive()
        ship[location] = colorMsg
        when (val dir = computer.output.receive()) {
            left -> screenDirection.turnAndMoveLeft(location)
            right -> screenDirection.turnAndMoveRight(location)
            else -> throw IllegalStateException("Invalid direction: $dir")
        }.apply {
            screenDirection = first
            location = second
        }
        computer.input.send(ship.getOrDefault(location, black))
    }

    ship
}

Let’s go through that. First, we’ll take in an optional color to start off with (because I know things about Part 2). We’ll also run the entire function using runBlocking which is a coroutine context that will block so the callers to this function don’t have to support suspending calls.

Next, we set up our variables. Our ship is going to be a Map of Point2D to Long where the value is the color on that spot. The computer is an instance of our IntCodeComputerMk2. We define location, which we’ll initialize with 0,0 (the origin) and screenDirection, which is initialized to North.

We launch our computer in its own coroutine so it will run continuously in the background until it halts. Before starting our work loop, we send the computer the color we are starting with on its input.

Now for the loop described above. The isClosedForReceive property is still experimental, that’s why we have the annotation on our class. While the output channel from the computer is still open, we know there is still output being produced. We read the color from output and set the current location to that value. Next, we receive and interpret the direction. Our turnAndMove... functions return a Pair<ScreenDirection, Point2D> which represent what new direction we are facing and what new location we have.

I would love it if Kotlin supported destructuring assignments so we could do this:

(screenDirection, location) = when(val dir = computer.output.receive()) {
    left -> screenDirection.turnAndMoveLeft(location)
    right -> screenDirection.turnAndMoveRight(location)
    else -> throw IllegalStateException("Invalid direction: $dir")
}

But since it doesn’t, we’ll have to do that work in the apply. While we’re at this point we can send the computer our new location. Even though the computer might be done working, we can still send this because the input channel does not close until we turn off the computer.

Finally, we return the state of the ship.

Solve Part 1

Now we can solve Part 1:

fun solvePart1(): Int =
    paintShip().size

This tells us how many spots are painted. Star earned!

Day 11, Part 2

The puzzle text can be found here.

Remember what I said the other day about not liking visual interpretation of the output? I haven’t changed my mind. Nevertheless, let’s get cracking.

We have almost everything we need to solve this. First, let’s put a new Comparator in the companion for Point2D, one that will let us sort the points in screen order.

// In Point2D's companion:

val readerOrder: Comparator<Point2D> = Comparator { o1, o2 ->
    when {
        o1.y != o2.y -> o1.y - o2.y
        else -> o1.x - o2.x
    }   

Next, we’ll call our paintShip function, this time with white as the starting color.

fun solvePart2() {
    val ship = paintShip(white)
    val min = ship.keys.minWith(Point2D.readerOrder)!!
    val max = ship.keys.maxWith(Point2D.readerOrder)!!
    (min.y..max.y).forEach { y ->
        println(
            (min.x..max.x).map { x ->
                if (ship[Point2D(x, y)] == white) '#' else ' '
            }.joinToString(separator = "")
        )
    }
}

After getting the state of the ship back from our computer, we need to figure out how big of a picture to paint. We want the top left and lower right hand corners. To do that, we use minWith and maxWith over the set of points, comparing using our new readerOrder Comparator. Once we have those, we can set up ranges and draw our letters.

Go to the console and read what it says and that’s our answer. I’m a bit bummed mine didn’t say ROCINANTE…

Problem solved, Space Police escaped, and star earned! What a busy day!

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 11
  4. Advent of Code - Come join in and do these challenges yourself!