Skip to Content

Advent of Code 2024 - Day 24, in Kotlin - Crossed Wires

Kotlin solutions to parts 1 and 2 of Advent of Code 2024, Day 24: 'Crossed Wires'

Posted on

Puzzles like this remind me of the computer architecture classes I took in college. While I don’t really have much call to know the inner workings of processors, gates, and transistors these days, I did enjoy it at the time. Today, we’ll solve part 1 with code and part 2 with our eyes and brains. I had help with the visualization of part 2 from this comment by /u/burnt_heatshield on Reddit.

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

Puzzle Input

Today we’ll take our input in as a List<String> and parse them into wires and gates. Since the system we’re simulating changes over time, we’ll make both of these structures mutable.

class Day24(input: List<String>) {

    private val wires: MutableMap<String, Int> = parseWires(input)
    private val gates: MutableList<Gate> = parseGates(input)

}

Parsing the wires is done by taking input while it isNotEmpty. Meaning, once we hit the empty row separating wires an gates, we stop. We associate the two parts of the wire together, which gives us a Map<String,Int>, and we make that mutable via toMutableMap.

// In Day24

private fun parseWires(input: List<String>): MutableMap<String, Int> =
    input
        .takeWhile { it.isNotEmpty() }
        .associate { it.substringBefore(":") to it.last().digitToInt() }
        .toMutableMap()

Our gate information is stored in a class called Gate. It takes four arguments, the left and right input wires, the op (operation) to perform, and the out wire name. We’ll parse it via an of function in the companion object, as usual.

// In Day24

private data class Gate(
    val left: String, 
    val right: String, 
    val op: String, 
    val out: String
) {
    companion object {
        fun of(input: String): Gate =
            input.split(" ").let { Gate(it[0], it[2], it[1], it[4]) }
    }
}

Parsing the gates is similar to the wires except we’re skipping over non-empty lines and then dropping the first empty line, in order to get to the gates part of the input.

// In Day24

private fun parseGates(input: List<String>): MutableList<Gate> =
    input
        .dropWhile { it.isNotEmpty() }
        .drop(1)
        .map { Gate.of(it) }
        .toMutableList()

⭐ Day 24, Part 1

The puzzle text can be found here.

Since not all of the gates will be ready to perform an action at the start of the puzzle, we need a way of figuring out which ones are ready. We could have made gates immutable and kept track of gates we’ve already used in a set, but I went with this because it seemed more fun.

Basically, we call findAndRemoveReady on our list of gates and it finds and removes any gate whose input wires both have values present in the wires map.

// In Day24

private fun MutableList<Gate>.findAndRemoveReady(): List<Gate> =
    filter {
        it.left in wires && it.right in wires
    }.also { removeAll(it) }

Next, we can simulate the entire circuit.

// In Day24

private fun simulate() {
    while (gates.isNotEmpty()) {
        gates
            .findAndRemoveReady()
            .forEach { (left, right, op, out) ->
                wires[out] = when (op) {
                    "AND" -> wires.getValue(left) and wires.getValue(right)
                    "OR" -> wires.getValue(left) or wires.getValue(right)
                    "XOR" -> wires.getValue(left) xor wires.getValue(right)
                    else -> throw IllegalArgumentException("Invalid op: $op")
                }
            }
    }
}

We loop through our logic until we run out of gates to execute. For each gate we find, we look at the op and perform the action required against the left and right input wires, storing the result in the out wire.

Now we can solve part 1.

// In Day24

    fun solvePart1(): Long {
        simulate()
        return wires
            .filter { it.key.startsWith("z") }
            .entries
            .sortedByDescending { it.key }
            .map { it.value }
            .joinToString("")
            .toLong(2)
    }

We simulate the circuit, and then find all of the z wires, sort them highest to lowest, join all of the values together and then convert the String representing a binary number of 1’s and 0’s to a Long, specifying the string we have is in binary.

Star earned! Onward!

⭐ Day 24, Part 2

The puzzle text can be found here.

While I’m sure there is an algorithmic solution to Part 2, I solved this like I solved Advent of Code 2024 Day 14 - by visual inspection. This time using GraphViz and manually working out which wires need to move where. Thankfully we know there are four pair of wires, so working this out came down to a matter of time, coffee, and focus. And a lot of mumbling to myself.

I found this tedious but effective for two reasons:

  1. I had trouble creating the graph until I stumbled across this comment by /u/burnt_heatshield on Reddit who suggested linking the x, y, and z nodes together to get a nice ladder effect. Otherwise, the graph that GraphViz creates is unruly and hard to follow. This made most of the errors stick out immediately when visualized. I also learned about subgraphs and this great online visualizer

  2. Even though I could spot that an error was present visually, fixing it required a fair deal of pointing at my screen with multiple fingers and talking to myself. But again, that worked.

Here is the code I used to generate output that GraphViz can use.

// In Day24

fun solvePart2() {
    val z = gates.filter { it.out.startsWith("z") }.map { it.out }.sorted().joinToString("->")
    val x = z.replace('z', 'x')
    val y = z.replace('z', 'y')

    println(
        """
        digraph G {
            subgraph {
               node [style=filled,color=green]
                $z
            }
            subgraph {
                node [style=filled,color=gray]
                $x
            }
            subgraph {
                node [style=filled,color=gray]
                $y
            }
            subgraph {
                node [style=filled,color=pink]
                ${gates.filter { gate -> gate.op == "AND" }.joinToString(" ") { gate -> gate.out }}
            }
            subgraph {
                node [style=filled,color=yellow];
                ${gates.filter { gate -> gate.op == "OR" }.joinToString(" ") { gate -> gate.out }}
            }
            subgraph {
                node [style=filled,color=lightblue];
                ${gates.filter { gate -> gate.op == "XOR" }.joinToString(" ") { gate -> gate.out }}
            }
            """.trimIndent()
    )
    gates.forEach { (left, right, _, out) ->
        println("    $left -> $out")
        println("    $right -> $out")
    }
    println("}")
}

When visualized, you can clearly make out that something isn’t right because the pattern suddenly changes:

A break in the pattern

Or it might be more subtle, which is why the types of operations have different colors:

AND->AND

Sorry these pictures aren’t larger - they have labels from my input and I don’t want to run afoul of the “no uploading your input” rule. Running your input through the code above and copying the resulting output through Graphviz will generate a graph specific to your output! I’ll leave it to you to work out the alterations that need to be made! :)

Star Earned! See you tomorrow for the last day of Advent of Code 2024!

Further Reading

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