Skip to Content

Advent of Code 2023 - Day 13, in Kotlin - Point of Incidence

Kotlin solutions to parts 1 and 2 of Advent of Code 2023, Day 13: 'Point of Incidence'

Posted on

I enjoyed this puzzle a lot. I was really worried part 2 would be something more challenging, but was surprised that I had very little refactoring to do once I read it.

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

Puzzle Input

Today we’ll take our input as a List<String> and convert it to a List<List<String>> where the inner List<String> represents the map of mirrors as text. I was originally tempted to convert them to a Set<Point2D> but felt in the end that working with strings would be simpler.

class Day13(input: List<String>) {

    private val patterns: List<List<String>> = parseInput(input)

    private fun parseInput(input: List<String>): List<List<String>> =
        input.joinToString("\n").split("\n\n").map { it.lines() }
}

In order to break up our input whenever we see two newlines, we need to join the List<String> together into a single String and then split it back apart whenever we see a pair of newlines. Maybe there’s an easier way to do this?

⭐ Day 13, Part 1

The puzzle text can be found here.

The main driver of our solution to part 1 is going to be the findMirror function. It takes in a pattern and a goalTotal value and returns the score that that pattern would generate. We assume each pattern has at least one mirror.

What’s up with the goalTotal? It tells our to-be-written findHorizontalMirror and findVerticalMirror functions how many differences we’re looking for. In part 1, we’re looking for a mirror where neither side of the reflected images has any differences from the other. In part 2, that assumption changes. So we’ll build it in from here instead of refactoring.

// In Day13

private fun findMirror(pattern: List<String>, goalTotal: Int): Int =
    findHorizontalMirror(pattern, goalTotal) ?:
    findVerticalMirror(pattern, goalTotal) ?:
    throw IllegalStateException("Pattern does not mirror")

Let’s start with findVerticalMirror since it is more complicated. When given a pattern we want to go through each row and its neighbor from the start to the end. To do this, we’ll set up a range over all of the columns of the pattern except for the last one (because it has no right-hand neighbor). This will let us look at each column and its neighbor to see if we can find a pair of these that form a mirror.

// In Day13

private fun findVerticalMirror(pattern: List<String>, goalTotal: Int): Int? =
    (0 until pattern.first().lastIndex).firstNotNullOfOrNull { start ->
        if (createMirrorRanges(start, pattern.first().lastIndex)
                .sumOf { (left, right) ->
                    pattern.columnToString(left) diff pattern.columnToString(right)
                } == goalTotal
        ) start + 1
        else null
    }

Within that loop, we need to know which columns form the mirror image and what order. For example, if we’re looking at columns [4, 5] we want to check [4,5], [3, 6], [2, 7], [1, 8], [0, 9] (assuming there are even 10 columns). To do this, we’ll write createMirrorRanges to help us.

// In Day13

private fun createMirrorRanges(start: Int, max: Int): List<Pair<Int, Int>> =
    (start downTo 0).zip(start + 1..max)

To get the desired List<Pair<Int,Int>>, we set up a downward-oriented range to get from the mirror point to the left edge of the input, and zip it with another range to get the right side of the pattern. We specify the start point, assume start+1 is the second column we’re testing against initially, and specify the max so we don’t overflow.

One interesting aspect of the findVerticalMirror is that it is column-oriented. So let’s write a function to grab a single column out of a List<String> and convert it into a String. We’ll use the clever name columnToString.

// In Day13

private fun List<String>.columnToString(column: Int): String =
    this.map { it[column] }.joinToString("")

And since we’re measuring how different the strings are, let’s write a diff function to compare them. Yes, if we’re just worried about part 1 we could skip this and do an equality check on two String objects.

// In Day13

private infix fun String.diff(other: String): Int =
    indices.count { this[it] != other[it] } + (length - other.length).absoluteValue

Although this puzzle doesn’t have input that would trigger this condition, we write diff to take into account strings that do not have identical lengths.

Our findHorizontalMirror function is almost the same but sets up the outer loop differently and specifies the max value for the inner pairs differently. The other thing to note is the score is calculated differently as well.

// In Day13

private fun findHorizontalMirror(pattern: List<String>, goalTotal: Int): Int? =
    (0 until pattern.lastIndex).firstNotNullOfOrNull { start ->
        if (createMirrorRanges(start, pattern.lastIndex)
                .sumOf { (up, down) ->
                    pattern[up] diff pattern[down]
                } == goalTotal
        ) (start + 1) * 100
        else null
    }

Now that we have this, we can solve part 1 of our puzzle. We call findMirror for each of the patterns, setting the goalTarget to 0 because we don’t want to tolerate any differences at all. Sum the results and we have our answer.

// In Day13

fun solvePart1(): Int =
    patterns.sumOf { findMirror(it, 0) }

Star earned! Onward!

⭐ Day 13, Part 2

The puzzle text can be found here.

By writing part 1 with a goalTotal in mind, we’re done except to specify our new goalTotal, which is 1 because we want to detect the one single flaw, not a perfectly mirrored pattern.

// In Day13

fun solvePart2(): Int =
    patterns.sumOf { findMirror(it, 1) }

Star earned! See you tomorrow!

Further Reading

  1. Index of All Solutions - All posts and solutions for 2023, 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!