Skip to Content

Advent of Code 2020 - Day 12, in Kotlin - Rain Risk

Kotlin solutions to parts 1 and 2 of Advent of Code 2020, Day 12: 'Rain Risk'

Posted on

Today we’ll define some reusable classes for direction and points, which will help us today and hopefully in future puzzles as well.

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

Problem Input

We will take in the puzzle input as a List<String> and not alter it further. While we could convert our Strings into some kind of data class, we only have to go through the input once, and I’m not sure it is worth the effort.

Having said that, it’s time we commit to a Point2D implementation, and like previous years, a Direction sealed class structure. These strike me as generally useful to have.

First, let’s talk about our Direction sealed class:

// In Movement.kt

sealed class Direction {
    abstract val turnLeft: Direction
    abstract val turnRight: Direction
    abstract val offset: Point2D

    operator fun invoke(dir: String): Direction =
        when (dir) {
            "N" -> North
            "S" -> South
            "E" -> East
            "W" -> West
            else -> throw IllegalArgumentException("No such direction $dir")
        }

    object North : Direction() {
        override val turnLeft = West
        override val turnRight = East
        override val offset = Point2D(-1, 0)
    }

    object South : Direction() {
        override val turnLeft = East
        override val turnRight = West
        override val offset = Point2D(1, 0)
    }

    object West : Direction() {
        override val turnLeft = South
        override val turnRight = North
        override val offset = Point2D(0, -1)
    }

    object East : Direction() {
        override val turnLeft = North
        override val turnRight = South
        override val offset = Point2D(0, 1)
    }
}

One might ask why we use a sealed class with sealed objects here, rather than an enumeration. The problem with using an enumeration is that we will not be able to reference all of the enumerated values properly in turnLeft and turnRight if they haven’t been defined yet. So we’ve resorted to this sealed class.

The first thing to notice is the invoke operator. This lets us call Direction("W") to create a new Direction, instead of having to define a function in the companion and calling Direction.of("W") or something similar.

Next, we have four directions and each implements the abstract turnLeft, turnRight, and offset values which are specified in the parent class.

Because we reference Point2D in direction, now is a good time to discuss that implementation:

// In Movement.kt

data class Point2D(val x: Int, val y: Int) {
    operator fun plus(other: Point2D): Point2D =
        Point2D(x + other.x, y + other.y)

    operator fun times(by: Int): Point2D =
        Point2D(x * by, y * by)

    infix fun distanceTo(other: Point2D): Int =
        (x - other.x).absoluteValue + (y - other.y).absoluteValue

    companion object {
        val ORIGIN = Point2D(0, 0)
    }
}

Our simple Point2D has an x and a y coordinate. This should make point-based logic simpler for future puzzles instead of having to resort to Pair<Int,Int> like we have been. We’ll define a plus operator so we can add two points together, and we’ll add a times operator so we can multiply one point by a fixed amount (which we do in the puzzle). I can eventually see a need to multiply one Point2D by another, but we don’t need it today and we can easily add it later.

We will define an infix function so we can calculate the distance between two Point2D objects, as described in the puzzle. An infix function allows us to leave off the dot and the parenthesis when calling it, making it look like an operator. For example here distanceTo there, assuming here and there are Point2D objects.

Finally, we’ll declare an ORIGIN as I can see us using that a lot. I almost made 0,0 the default x,y values for this point but couldn’t come up with a great reason for doing that.

⭐ Day 12, Part 1

The puzzle text can be found here.

Now that we have a concept of Direction and place (via Point2D), we should have an easier time with future puzzles, starting with today!

Let’s define another data class for our Ship:

// In Day12

data class Ship(val at: Point2D = Point2D.ORIGIN, val facing: Direction = Direction.East) {

    fun forward(amount: Int): Ship =
        copy(at = at + (facing.offset * amount))

    fun move(dir: Direction, amount: Int): Ship =
        copy(at = at + (dir.offset * amount))

    fun turnLeft(times: Int): Ship =
        (0 until times).fold(this) { carry, _ ->
            carry.copy(facing = carry.facing.turnLeft)
        }

    fun turnRight(times: Int): Ship =
        (0 until times).fold(this) { carry, _ ->
            carry.copy(facing = carry.facing.turnRight)
        }
}

As you can see, our Ship is made up of its location (at) and the direction it is facing. Unfortunately we don’t get to put fun names or passengers in this ship. Maybe another day.

The Ship has a few functions and each one of them returns a new instance of Ship, making this an immutable data structure. The forward function generates a new Ship in a new location, taking advantage of both the plus and times operators we defined in Point2D! Move works similarly, but uses the specified direction rather than the ship’s facing.

The turnLeft and turnRight functions use a fold to generate new Ship objects that are turned in the direction we specify.

Now that we have all that, we can write a solution to part 1:

// In Day12

fun solvePart1(): Int =
    input.fold(Ship(Point2D.ORIGIN, Direction.East)) { ship, instruction ->
        val command = instruction.first()
        val amount = instruction.substring(1).toInt()
        when (command) {
            'N' -> ship.move(Direction.North, amount)
            'S' -> ship.move(Direction.South, amount)
            'E' -> ship.move(Direction.East, amount)
            'W' -> ship.move(Direction.West, amount)
            'F' -> ship.forward(amount)
            'L' -> ship.turnLeft(amount / 90)
            'R' -> ship.turnRight(amount / 90)
            else -> throw IllegalArgumentException("Unknown instruction: $instruction")
        }
    }.at distanceTo Point2D.ORIGIN

Because every time we call a function on our Ship, we can fold over the input, generating a new Ship each time through. We’ll parse out the command (the first character of the input) and the amount (anything after the command), and use a when expression to interpret the commands. I won’t go into detail, mostly this should be self explanatory as we’re delegating the work to our Ship class, and in turn to either Direction or Point2D.

We can use our infix distanceTo function to figure out the distance from the origin point to the final Ship that our fold generates, giving us the answer to part 1. Star earned!

Intermission

If you’ve earned every star up until this point, congratulations! We are half way through with Advent of Code 2020!

⭐ Day 12, Part 2

The puzzle text can be found here.

While most of the code we’ve written is useful for Part 2, we’ll need to make a couple of changes. First, we need to add the concept of rotation to our Point2D class, by adding some new functions:

// In Point2D

fun rotateLeft(): Point2D =
    Point2D(x = y * -1, y = x)

fun rotateRight(): Point2D =
    Point2D(x = y, y = x * -1)

These functions too me a while to write because I was having a hard time understanding the instructions as they were written. But after I wrote down and closely followed what we were being told, it eventually made sense. Essentially, we flip the x and y axis and negate one of them, depending on which direction we are turning.

And because we have a Ship, we might as well have a Waypoint in today’s puzzle as well:

// In Day12

data class Waypoint(val at: Point2D = Point2D(-1, 10)) {
    fun move(dir: Direction, amount: Int): Waypoint =
        Waypoint(at + (dir.offset * amount))

    fun turnLeft(amount: Int): Waypoint =
        (0 until amount).fold(this) { carry, _ ->
            Waypoint(carry.at.rotateLeft())
        }

    fun turnRight(amount: Int): Waypoint =
        (0 until amount).fold(this) { carry, _ ->
            Waypoint(carry.at.rotateRight())
        }
}

We only have to have three functions for the Waypoint - how to move and how to turn. The move calculates a new location by multiplying the directional offset, and the rotations happen similar to how we implemented them earlier, depending on our Point2D functions.

Now we can solve part 2:

// In Day12

fun solvePart2(): Int {
    var waypoint = Waypoint()
    var ship = Ship()
    input.forEach { instruction ->
        val command = instruction.first()
        val amount = instruction.substring(1).toInt()
        when (command) {
            'N' -> waypoint = waypoint.move(Direction.North, amount)
            'S' -> waypoint = waypoint.move(Direction.South, amount)
            'E' -> waypoint = waypoint.move(Direction.East, amount)
            'W' -> waypoint = waypoint.move(Direction.West, amount)
            'F' -> ship = ship.copy(at = ship.at + (waypoint.at * amount))
            'L' -> waypoint = waypoint.turnLeft(amount / 90)
            'R' -> waypoint = waypoint.turnRight(amount / 90)
            else -> throw IllegalArgumentException("Unknown instruction: $instruction")
        }
    }
    return Point2D.ORIGIN distanceTo ship.at
}

This looks a lot like the solution to part 1, except we aren’t doing it as a single expression, so I won’t go over it in great details. Because some instructions have us move the Ship and others have us move the Waypoint, I felt it was cleaner to make them both vars and set new values when we moved them, rather than trying to coordinate this all in a single expression. This version works similarly to part 1, but most of the instructions involved the waypoint instead of the ship.

Running this solves part 2.

See you tomorrow!

Further Reading

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