Skip to Content

Advent of Code 2018 - Day 16, in Kotlin

Kotlin solutions to parts 1 and 2 of Advent of Code 2018, Day 16: 'Chronal Classification'

Posted on

Day 16 reminded me of several problems from Advent of Code 2017 where we had to make a virtual machine over the course of a few days. This was a nice problem, I had fun with this one.

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

Problem Input

The input given to us is one file with two sections. To make things easier, I broke it up into two separate files and parse them wholly and individually. We’ll do them inline below.

Day 16, Part 1

As you see the Elves defend their hot chocolate successfully, you go back to falling through time. This is going to become a problem.

If you’re ever going to return to your own time, you need to understand how this device on your wrist works. You have a little while before you reach your next destination, and with a bit of trial and error, you manage to pull up a programming manual on the device’s tiny screen.

According to the manual, the device has four registers (numbered 0 through 3) that can be manipulated by instructions containing one of 16 opcodes. The registers start with the value 0.

Every instruction consists of four values: an opcode, two inputs (named A and B), and an output (named C), in that order. The opcode specifies the behavior of the instruction and how the inputs are interpreted. The output, C, is always treated as a register.

In the opcode descriptions below, if something says “value A”, it means to take the number given as A literally. (This is also called an “immediate” value.) If something says “register A”, it means to use the number given as A to read from (or write to) the register with that number. So, if the opcode addi adds register A and value B, storing the result in register C, and the instruction addi 0 7 3 is encountered, it would add 7 to the value contained by register 0 and store the sum in register 3, never modifying registers 0, 1, or 2 in the process.

Many opcodes are similar except for how they interpret their arguments. The opcodes fall into seven general categories:

Addition:

  • addr (add register) stores into register C the result of adding register A and register B.
  • addi (add immediate) stores into register C the result of adding register A and value B.

Multiplication:

  • mulr (multiply register) stores into register C the result of multiplying register A and register B.
  • muli (multiply immediate) stores into register C the result of multiplying register A and value B.

Bitwise AND:

  • banr (bitwise AND register) stores into register C the result of the bitwise AND of register A and register B.
  • bani (bitwise AND immediate) stores into register C the result of the bitwise AND of register A and value B.

Bitwise OR:

  • borr (bitwise OR register) stores into register C the result of the bitwise OR of register A and register B.
  • bori (bitwise OR immediate) stores into register C the result of the bitwise OR of register A and value B.

Assignment:

  • setr (set register) copies the contents of register A into register C. (Input B is ignored.)
  • seti (set immediate) stores value A into register C. (Input B is ignored.)

Greater-than testing:

  • gtir (greater-than immediate/register) sets register C to 1 if value A is greater than register B. Otherwise, register C is set to 0.
  • gtri (greater-than register/immediate) sets register C to 1 if register A is greater than value B. Otherwise, register C is set to 0.
  • gtrr (greater-than register/register) sets register C to 1 if register A is greater than register B. Otherwise, register C is set to 0.

Equality testing:

  • eqir (equal immediate/register) sets register C to 1 if value A is equal to register B. Otherwise, register C is set to 0.
  • eqri (equal register/immediate) sets register C to 1 if register A is equal to value B. Otherwise, register C is set to 0.
  • eqrr (equal register/register) sets register C to 1 if register A is equal to register B. Otherwise, register C is set to 0.

Unfortunately, while the manual gives the name of each opcode, it doesn’t seem to indicate the number. However, you can monitor the CPU to see the contents of the registers before and after instructions are executed to try to work them out. Each opcode has a number from 0 through 15, but the manual doesn’t say which is which. For example, suppose you capture the following sample:

Before: [3, 2, 1, 1]
9 2 1 2
After:  [3, 2, 2, 1]

This sample shows the effect of the instruction 9 2 1 2 on the registers. Before the instruction is executed, register 0 has value 3, register 1 has value 2, and registers 2 and 3 have value 1. After the instruction is executed, register 2’s value becomes 2.

The instruction itself, 9 2 1 2, means that opcode 9 was executed with A=2, B=1, and C=2. Opcode 9 could be any of the 16 opcodes listed above, but only three of them behave in a way that would cause the result shown in the sample:

  • Opcode 9 could be mulr: register 2 (which has a value of 1) times register 1 (which has a value of 2) produces 2, which matches the value stored in the output register, register 2.
  • Opcode 9 could be addi: register 2 (which has a value of 1) plus value 1 produces 2, which matches the value stored in the output register, register 2.
  • Opcode 9 could be seti: value 2 matches the value stored in the output register, register 2; the number given for B is irrelevant.

None of the other opcodes produce the result captured in the sample. Because of this, the sample above behaves like three opcodes.

You collect many of these samples (the first section of your puzzle input). The manual also includes a small test program (the second section of your puzzle input) - you can ignore it for now.

Ignoring the opcode numbers, how many samples in your puzzle input behave like three or more opcodes?

Yay, another computer! These are fun.

Parsing Input

I decided to go low-complexity on this by chunking every 4 lines of our part 1 input int one map operation using the chunked function. I also defined a regular expression that removes everything except digits and spaces from our input so everything will be uniform. This allows us to trim and split our input, mapping each String to an Int, and then to an IntArray. We could have done this with a more complicated regular expression and matchers, but I feel this is easy to follow.

class Day16(
    part1RawInput: List<String>,
    part2RawInput: List<String>
) {

    private val part1Input: List<Input> = parsePart1Input(part1RawInput)

    private companion object {
        val digitsRegex = """[^0-9 ]""".toRegex()

        fun parsePart1Input(rawInput: List<String>): List<Input> =
            rawInput.chunked(4) { chunk ->
                Input(
                    chunk[0].toIntArray(),
                    chunk[1].toIntArray(),
                    chunk[2].toIntArray()
                )
            }

        private fun String.toIntArray(): IntArray =
            this.replace(digitsRegex, "").trim().split(" ").map { it.toInt() }.toIntArray()

    }

Types

I don’t usually use typealias but I feel like the complexity of the types in this solution warrant it, or at least are helped by it. So let’s define those:

typealias Registers = IntArray
typealias Instruction = IntArray
typealias Operation = (Registers, Instruction) -> Registers

As you can see, most everything is an IntArray. The only exception is the Operation, which is a function that takes a set of Registers, an Instruction, and emits a new set of Registers. This is a pure function because it has no side effects, which I always prefer if I can.

Operations

There are a few ways we could have gone with defining the actual operations, but I decided to created a private object to house them all. Each is defined a a function and each function is references in a List. We could probably also have defined an enum for this, but my gut told me that would be more verbose. Read the instructions carefully for each operation, there are some twists and turns in there.

private object Operations {
    val operations: List<Operation> = listOf(
        ::addr,
        ::addi,
        ::mulr,
        ::muli,
        ::banr,
        ::bani,
        ::borr,
        ::bori,
        ::setr,
        ::seti,
        ::gtir,
        ::gtri,
        ::gtrr,
        ::eqir,
        ::eqri,
        ::eqrr
    )

    fun addr(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] + registers[instruction[2]] }

    fun addi(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] + instruction[2] }

    fun mulr(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] * registers[instruction[2]] }

    fun muli(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] * instruction[2] }

    fun banr(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] and registers[instruction[2]] }

    fun bani(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] and instruction[2] }

    fun borr(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] or registers[instruction[2]] }

    fun bori(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] or instruction[2] }

    fun setr(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = registers[instruction[1]] }

    fun seti(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = instruction[1] }

    fun gtir(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = if (instruction[1] > registers[instruction[2]]) 1 else 0 }

    fun gtri(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = if (registers[instruction[1]] > instruction[2]) 1 else 0 }

    fun gtrr(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = if (registers[instruction[1]] > registers[instruction[2]]) 1 else 0 }

    fun eqir(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = if (instruction[1] == registers[instruction[2]]) 1 else 0 }

    fun eqri(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = if (registers[instruction[1]] == instruction[2]) 1 else 0 }

    fun eqrr(registers: Registers, instruction: Instruction): Registers =
        registers.copyOf().apply { this[instruction[3]] = if (registers[instruction[1]] == registers[instruction[2]]) 1 else 0 }
}

Executing Operations

Now that we have our input, types, and operations, we can move directly to solving the problem at hand. Let’s write a function that for a given input tells us how many of the Operations match the before and after of the registers. We need to be careful not to use == to compare our IntArray, we need to use .contentEquals here instead or we’ll get the wrong answer.

private fun countMatchingOperations(input: Input): Int =
    Operations.operations.count { it(input.registersBefore, input.instruction).contentEquals(input.expectedRegisters) }

And then, count how many of the input groupings have at least three matching operations:

fun solvePart1(): Int =
    part1Input.count { countMatchingOperations(it) >= 3 }

Star earned!

Day 16, Part 2

Using the samples you collected, work out the number of each opcode and execute the test program (the second section of your puzzle input).

What value is contained in register 0 after executing the test program?

Parsing

Like part 1, we’ll define our parse function in our companion:

private val part2Input: List<Instruction> = parsePart2Input(part2RawInput)

private companion object {

    fun parsePart2Input(rawInput: List<String>): List<Instruction> =
        rawInput.map { it.toIntArray() }
}

Finding What Each Code Means

Let’s look at the code, and then go over it. Essentially we will need to run all of part 1 again, and find the one pair of operation/id which is unambiguous. Using that, we can gradually figure out which ids and operations match up.

fun solvePart2(): Int {
    // Create all possible matches.
    val functionToOpCodes: MutableMap<Operation, MutableSet<Int>> = part1Input.flatMap { input ->
        Operations.operations.mapNotNull { operation ->
            if (operation(input.registersBefore, input.instruction).contentEquals(input.expectedRegisters)) {
                input.id to operation
            } else {
                null
            }
        }
    }
        .groupBy({ it.second }, { it.first })
        .mapValues { (_, list) -> list.toMutableSet() }
        .toMutableMap()

    val operations = mutableMapOf<Int, Operation>()
    while (functionToOpCodes.isNotEmpty()) {
        // Find all that have only one outcome, map them into the operations map and remove them from contention,
        functionToOpCodes
            .filter { (_, codes) -> codes.size == 1 }
            .map { Pair(it.key, it.value.first()) }
            .forEach { (op, code) ->
                operations[code] = op
                functionToOpCodes.remove(op)
                functionToOpCodes.forEach { (_, thoseFuncs) -> thoseFuncs.remove(code) }
            }
        functionToOpCodes.entries.removeIf { (_, value) -> value.isEmpty() }
    }


    // Run the code and return register 0
    return part2Input.fold(intArrayOf(0, 0, 0, 0)) { registers, instruction ->
        operations[instruction[0]]!!.invoke(registers, instruction)
    }.first()
}

I know that’s a lot more code than I usually put in a single function, so let’s go through it. In the first part, we run all of our actual/expected pairs from part 1 again, but this time when one matches we create a Pair<Int, Operation>, where the Int is the instruction code. Then we group all of those so we end up with a MutableMap<Int, Set<Operation>>. For each code, what are the operations that match it?

Next, we iteratively go through that map and find keys (ids) that have one value (operations). Bingo! We know one part of the answer. If we remove that operation from all of the other sets that contain it, we will reduce another set to just having one entry. Keep repeating this process and we have a full set of code to operations!

Finally, we run our part 2 input again using a fold. The fold is handy because we can use our pure operation functions and end up with a set of registers. The only difference here is that instead of running all operations, we peek at the first digit in each instruction to find the operation id, look up that operation, and execute that operation with our registers and input.

Ending with a set of registers, we get the first element and return it as our answer!

Star earned!

Further Reading

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