Advent of Code 2019 - Day 2, in Kotlin
Kotlin solutions to parts 1 and 2 of Advent of Code 2019, Day 2: '1202 Program Alarm'
Today’s puzzle has us rebuilding a small subset of a computer. We’ve seen puzzles like this in years past, and I always enjoy them. The name for today’s puzzle (“1202 Program Alarm”) comes from a recurring error that the Apollo 11 astronauts encountered while landing on the moon. It’s an interesting (if not terrifying) story and you can read all about the 1202 alarm that Apollo 11 experienced .
If you’d rather just view code, the GitHub Repository is here .
On Day 5 we end up reimplementing our IntCode computer to something more advanced. A version of this solution that uses the Day 5 IntCodeComputer
can be found over on GitHub !
Problem Input
We receive a single file with a comma separated list of integers. In order to turn that into something more useful, we will pass that to today’s class as a String
and parse it into an IntArray
as a private variable.
class Day02(input: String) {
private val memory: IntArray = input.split(",").map { it.toInt() }.toIntArray()
}
⭐ Day 2, Part 1
The puzzle text can be found here.
Let’s start by jumping ahead a bit, just so we properly name some things. I won’t give away too much about part 2, just some naming that is made explicit once we finish part 1. In the instructions for part 1, we’re asked to manually alter the program memory so that position 1 is 12 and position 2 is 2. We learn after completing part 1 that these are called noun
and verb
respectively (another nod to the Apollo Program Computer). So we will do the same here in this part and call the variable parts of our computer memory noun
and verb
.
Since our memory is an IntArray
, and the instructions call for us to look up references to various parts of it, let’s write some helpful extension functions first. One of our tasks is to follow and set references. By that I mean we will have to look up a value from a place in memory and go to the position it specifies. For example, if we read position 1 and it has the value 12, we would need to get our real value from position 12.
private fun IntArray.setRef(at: Int, value: Int) {
this[this[at]] = value
}
private fun IntArray.getRef(at: Int): Int =
this[this[at]]
Both our extension functions retrieve a value and then the value that that value specifies. I’m calling them getRef
and setRef
because this seemed a lot like chasing a pointer reference to me. In the case of setRef
we are given a value to set at the ultimate reference point. I am intentionally ignoring the possibility that this might return a reference that does not exist, mostly because the instructions seem to imply that it is not possible.
Believe it or not, we have enough to write the logic to run the program (as specified in the instructions):
private fun runProgram(memory: IntArray, noun: Int, verb: Int): Int {
val memoryCopy = memory.copyOf().apply {
this[1] = noun
this[2] = verb
}
(memoryCopy.indices step 4).forEach { ip ->
when (memoryCopy[ip]) {
1 -> memoryCopy.setRef(ip + 3, memoryCopy.getRef(ip + 1) + memoryCopy.getRef(ip + 2))
2 -> memoryCopy.setRef(ip + 3, memoryCopy.getRef(ip + 1) * memoryCopy.getRef(ip + 2))
99 -> return memoryCopy[0]
}
}
throw IllegalStateException("Program ran out of instructions")
}
I realize that there is a lot going on in our runProgram
function, so let’s break it down. Let’s go over the function arguments. We take in the IntArray
memory that represents the program to run, a noun
, and a verb
. We take noun
and verb
in here to set the two variables discussed above. In part 1, these will be 12
and 2
respectively (see below).
First we make a copy of the memory (because we mutate it by running the program) and use the apply
extension function to set our noun
and verb
in one single expression.
Next comes the actual running of the program. I could have gone with a for
loop or a while
loop here to represent moving the instruction pointer, but I think using a range (memoryCopy.indices
) that skips (step 4
) by four is cleaner. This construct will forward our instruction pointer through our memory until we hit the end, and then it will stop. Every time we get a new ip
(instruction pointer), we will use a when
statement to examine the op code. Remember from the instructions that 1
means addition, 2
means multiplication, and 99
means halt.
In the case of addition or multiplication, we use our helpful extension functions to get and set our pointer-referenced values. In the case if a halt command, we return the first value of the memory, which is what the instructions ask for in the end. Some people don’t like early returns, but I honestly don’t mind them. If we had gone and made this a loop instead of using a range we could have broken out, but I don’t really like break
statements because I personally find them harder to follow than a return. That’s just me.
Finally, if we got this far, we’ve done something really wrong, so we throw an exception. Our input shouldn’t get us to this state, so if we end up here, it’s a bug.
We can call this by invoking our solvePart1()
function:
fun solvePart1(noun: Int = memory[1], verb: Int = memory[2]): Int =
runProgram(memory, noun, verb)
The interesting part here is how we specify noun
and verb
. When testing our code (you are writing tests, aren’t you?), we don’t always want to change the noun
and verb
, so these default to their existing values. That lets us specify them if we want to, like we will be doing when we calculate our actual answer.
Running this with the correct noun and verb (solvePart1(12, 2)
) gives us our first star for the day!
⭐ Day 2, Part 2
The puzzle text can be found here.
I know the instructions hint at more to come with IntCode, but I’m not going to refactor this into its own class just yet. If we find ourselves having to reimplement large parts of this in the future, maybe we’ll consider refactoring. But not today.
Part 2 asks us to find the starting noun
and verb
that cause our program to hit a magic number: 19690720. Thankfully, the instructions tell us that the noun
and verb
will both be between 0 and 99, so we can just brute force this - try every combination until we find one that works. We already have our runProgram
function, which will not need any modification. The only thing we need to do is to create two nested loops of 0 to 99 and figure out which combination hits our target.
fun solvePart2(target: Int = 19_690_720): Int {
(0..99).forEach { noun ->
(0..99).forEach { verb ->
if (runProgram(memory, noun, verb) == target) {
return (noun * 100) + verb
}
}
}
throw IllegalStateException("Cannot find starting noun/verb that end up with $target")
}
I made the target configurable, but I could have just hard coded it I suppose. It just felt cleaner this way.
Our code here sets up two ranges: noun
and verb
and re-runs the program over and over again for every combination until we hit our target. Once a set of inputs results in the target being hit, we return the value the instructions asked for. Again, like yesterday, we could do without the parenthesis, but I like them to avoid ambiguity. And like above, I don’t mind early return types. And similar to part 1, if we get to the end without hitting the target, that’s a bug and we should throw an exception.
Running this code is actually pretty quick, and earns us a second star for the day!
Further Reading
- Index of All Solutions - All posts and solutions for 2019, in Kotlin.
- My Github repo - Solutions and tests for each day.
- Solution - Full code for day 2
- Advent of Code - Come join in and do these challenges yourself!