Skip to Content

Advent of Code 2021 - Day 13, in Kotlin - Transparent Origami

Kotlin solutions to parts 1 and 2 of Advent of Code 2021, Day 13: 'Transparent Origami'

Posted on

When I saw the title of today’s puzzle (“Transparent Origami”) I thought we would be folding up some crabs, octopuses, or whales. Instead, we’re pretending we’ve bought a cool game from the 1980s and need to look up a code in the decoder ring to play it. Except this time it’s a sub-mounted thermal camera.

I like to think there’s a nonzero chance that Eric Wastl decided to have us fold paper to confuse the functional programmers out there who use the fold function a lot.

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

Problem Input

Since our input is a single file with two different parts, we’ll write two functions to do the parsing. We could write a single function that returns a Pair, but I think this just looks cleaner.

class Day13(input: List<String>) {

    private val paper: Set<Point2d> = parsePoints(input)
    private val instructions: List<Point2d> = parseInstructions(input)

    private fun parsePoints(input: List<String>): Set<Point2d> =
        input.takeWhile { it.isNotEmpty() }
            .map { it.split(",") }
            .map { Point2d(it.first().toInt(), it.last().toInt()) }
            .toSet()

    private fun parseInstructions(input: List<String>): List<Point2d> =
        input.takeLastWhile { it.isNotEmpty() }
            .map { it.split("=") }
            .map {
                if (it.first().endsWith("y")) {
                    Point2d(0, it.last().toInt())
                } else {
                    Point2d(it.last().toInt(), 0)
                }
            }
}

The first function, parsePoints reads input until it finds an empty line and then stops using takeWhile. For every line it finds, we split the coordinates on a comma and then map the results into a Point2d, which we wrote a few days ago and have used a few times now.

The second parseInstructions will skip lines in the input until it finds an empty line, and then lets us have every one after that. I haven’t used the takeLastWhile function too many times, but it’s very handy for situations like this. Every instruction row gets split on the equals sign, so we end up with a list like this: ["fold along x", "655"]. We could parse the first part a bit further to isolate the x or y from the instruction, but since it is always at the end of the String, we can just use endsWith to figure it out. Depending on whether we are parsing an x instruction or a y instruction, we’ll once again use Point2d to represent this. There is a catch - we can’t represent an instruction that deals with 0. But since I don’t have any in my input, I assume that’s generally safe.

⭐ Day 13, Part 1

The puzzle text can be found here.

Before we start in on our solution, I want to talk about terminology. As we’ve seen from days past, Kotlin has a function called fold that allows us to operate on a sequence of values and carry some state along. And the puzzle for today involves folding. Because we’ll legitimately use the fold function in part two, and I don’t want there to be any confusion (this is as much for my sanity as it is for yours, I assure you), anything involving the puzzle’s concept of folding paper will be called crease. So if you see fold, think function. If you see crease, think bending paper.

Let’s start with the math we’ll have to do when calculating where a Point2d ends up when we crease the paper. We’ll implement it as an extension function in Int:

// In Day13

private fun Int.creaseAt(crease: Int): Int =
    if (this < crease) this else (crease * 2) - this

Another way to think of that calculation would be crease - (this - crease). Calculate the different between the point being moved and its crease point, and then subtract that amount from the crease point to mirror the point on the other side of the crease.

Now that we have this, we can actually perform the crease of an entire Set<Point2d>. We’ll do this with another extension function called crease.

// In Day13

private fun Set<Point2d>.crease(instruction: Point2d): Set<Point2d> =
    if (instruction.x != 0) this.map { it.copy(x = it.x.creaseAt(instruction.x)) }.toSet()
    else this.map { it.copy(y = it.y.creaseAt(instruction.y)) }.toSet()

This function takes an instruction and figures out if we’re talking about the x or y axis to crease on. Depending on which one it is, we make a copy of each Point2d, specifying the new x or y value as needed. This calls the creaseAt extension function we wrote above.

Now we can solve part one:

// In Day13

fun solvePart1(): Int =
    paper.crease(instructions.first()).size

Because part one only calls for the number of points (the size of our paper!) after applying the first crease instruction, we can use instructions.first() to grab that and pass it to our crease function.

Star earned! Onward!

⭐ Day 13, Part 2

The puzzle text can be found here.

There have been a couple of puzzles in the past where the instructions ask for us to interpret some printed data. If we really wanted to, we could write some code to interpret our data and output the text instead of printing it and interpreting ourselves, but I’m not going to do that. Not every problem we solve as programmers involves programming.

In order to print our Set<Point2d>, we’ll need to know both the maximum x and y values. Similar to how we’ve parsed data in the past, we’ll set up two ranges along y and then x, and print an “on” character if we find the point in the set, and an “off” character otherwise. I picked hash and space respectively, but you can pick whatever you think looks best.

// In Day13 

private fun Set<Point2d>.printout() {
    (0..this.maxOf { it.y }).forEach { y ->
        (0..this.maxOf { it.x }).forEach { x ->
            print(if (Point2d(x, y) in this) "#" else " ")
        }
        println()
    }
}

I found that backing up from my computer and reading the results from about a meter away made them more clear.

Now we can actually use the fold function (read the note above, we’re using a “functional fold” here, not creasing a piece of paper. I mean, we are, but still…). Anyway.

// In Day13

fun solvePart2(): Unit =
    instructions.fold(paper) { paper, instruction -> paper.crease(instruction) }.printout()

We fold over our instructions and seed our calculation with the start state of the paper. For each new instruction, we crease the paper. At the end, we have a fully creased Set<Point2d>, so we call printout on it.

Star (very confusingly) earned!

Congratulations for making it this far, there are now fewer 2021 Advent of Code puzzles ahead of us than there are behind us.

Further Reading

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