Advent of Code 2019 - Day 5
bretthancox
Posted on December 13, 2019
Introduction
Wow...day 5 really confused the heck out of me. Implementation was no more complicated than any other day, but the explanation was tough to follow.
That being said, once I did understand it, this was a fun problem. It's hard to show Day 5.1 and Day 5.2 separately as the "Intcode computer" was updated during both days. I can speak to the different implementations, but the normal split between the two is harder to show.
EDIT: To be fair to Jon Bristow, there is a lot going on in this day of the advent. I have no doubt that whatever method was used to convey the instructions, someone would have struggled to understand. I was just that person today.
Day 5.1
Part 1 of Day 5 requires that the Intcode computer from Day 2 be updated to accommodate some new features:
- The Opcode from Day 2 now comprises 5 digits. This is true even if only one digit is provided - you must infer the rest to be 0.
- The Opcodes from Day 2 continue to apply, but with some additions that break from the [instruction, parameter1, parameter2, parameter3] setup:
- Opcode 3 means take a value from human input (or simulate it) and write it to the address provided in the parameter x, where (as we will see in a moment) x is either the address to write it, or a pointer to the address to write it: [3, x]
- Opcode 4 means read the value from an address and print it to the screen. It simulates "diagnostic codes" being printed out. This is like the reverse of Opcode 3.
- The Opcode will provide parameter "modes". The modes are binary. For each Opcode, there are between 1 and 3 parameters, thus there are between 1 and 3 modes. In Day 2 the values in the parameters are always pointers to somewhere else in the Intcode. If a parameter mode is 0, that behavior is unchanged. If the mode is 1, use the literal content of the parameter.
- For Opcode of 1 or 2, you either sum or multiply the literal content of par 1 and par 2
- For Opcode of 3 or 4 (or any other write to/read from operation), a 0 means read/write to the location defined in the parameter (i.e. a pointer), while a 1 means read/write to the parameter itself. Specifically:
- [4, 4, 99, 2, 3] results in printing 3
- [104, 4, 99, 2, 3] results in printing 4
- The Opcode will be provided in reverse. Example: 101103 - in this case we can break it into ABCDE:
- A is the mode for parameter 3
- B is the mode for parameter 2
- C is the mode for parameter 1
- DE is the Opcode, now provided as two digits
- The new two-digit Opcode is really a single digit Opcode with an unnecessary prefix, so in 01, only the 1 has value.
Phew! I'll just use this tinsel to wipe my brow.
With that laid out, I started with my mental step 1: determining the Opcode and the associated modes. This function takes in the new, (possibly) longer Opcode, reverses it, splits it, and outputs a map comprising the Opcode and each individual mode. The map is created with the default modes in place, ensuring it always contains all modes. If the Opcode doesn't actually have three parameters, subsequent code simply ignores the modes it doesn't need, so having three modes in every map regardless of Opcode is not a problem.
I prevent null pointer errors by counting the length of the full Opcode and building the map based on the information actually provided in the Opcode.
(defn opcode_reverse_and_dissect
"I take the new, longer opcode and reverse it, then dissect it into the meaningful components"
[opcode]
(let [opcode_details {:opcode nil :par1_mode 0 :par2_mode 0 :par3_mode 0}
opcode_length (count (str opcode))
opcode (apply str (reverse (str opcode)))]
(if (= opcode "99")
{:opcode 99}
(case opcode_length
1 (assoc opcode_details
:opcode (Integer/parseInt (str (nth opcode 0))))
2 (assoc opcode_details
:opcode (Integer/parseInt (str (nth opcode 0))))
3 (assoc opcode_details
:opcode (Integer/parseInt (str (nth opcode 0)))
:par1_mode (Integer/parseInt (str (nth opcode 2))))
4 (assoc opcode_details
:opcode (Integer/parseInt (str (nth opcode 0)))
:par1_mode (Integer/parseInt (str (nth opcode 2)))
:par2_mode (Integer/parseInt (str (nth opcode 3))))
5 (assoc opcode_details
:opcode (Integer/parseInt (str (nth opcode 0)))
:par1_mode (Integer/parseInt (str (nth opcode 2)))
:par2_mode (Integer/parseInt (str (nth opcode 3)))
:par3_mode (Integer/parseInt (str (nth opcode 4))))))))
So then the new Intcode computer is needed. I take the Day 2 version and update it.
For Opcode 1 and 2, the new modes made the code more complicated to house within this primary function, so I broke it into a new, very literally-named function.
The new logic for Opcode 3 and 4 basically checks the mode and either writes to or reads from the location defined by posb (i.e. the first parameter). Not complicated enough to need breaking out.
One note, the code (Integer/parseInt (do (print "Input code: ") (flush) (read-line)))))
helps prompt the user for their input and then clears the output stream before accepting the read-line
from the user and turning it into an integer. There is no error-checking, so those elves better not enter non-numeric characters!
(defn revised_one_and_two_behavior
"I check the modes in the full Opcode and adjust the values of par1 and par2 appropriately"
[intcode posb posc posd opcode_details operator]
(let [par1 (if (= (:par1_mode opcode_details) 0)
(get intcode (get intcode posb))
(get intcode posb))
par2 (if (= (:par2_mode opcode_details) 0)
(get intcode (get intcode posc))
(get intcode posc))
par3 (if (= (:par3_mode opcode_details) 0)
(get intcode posd)
posd)]
(assoc intcode par3
(day2_operator
par1
par2
operator))))
(defn day5_opcode
"I check the opcode and perform, or cause to be performed, the appropriate actions"
[intcode]
(loop [posa 0
posb 1
posc 2
posd 3
intcode intcode]
(let [opcode (get intcode posa)
opcode_details (opcode_reverse_and_dissect opcode)]
(cond
(= (:opcode opcode_details) 99) intcode
(= (:opcode opcode_details) 1) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
(revised_one_and_two_behavior intcode posb posc posd opcode_details +))
(= (:opcode opcode_details) 2) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
(revised_one_and_two_behavior intcode posb posc posd opcode_details *))
(= (:opcode opcode_details) 3) (recur (+ posa 2) (+ posb 2) (+ posc 2) (+ posd 2)
(assoc intcode
(if (= (:par1_mode opcode_details) 0)
(get intcode posb)
posb)
(Integer/parseInt (do (print "Input code: ") (flush) (read-line)))))
(= (:opcode opcode_details) 4) (do
(println "Diagnostic code:" (if (= (:par1_mode opcode_details) 0)
(get intcode (get intcode posb))
(get intcode posb)))
(recur (+ posa 2) (+ posb 2) (+ posc 2) (+ posd 2) intcode))
;; Hiding part 2 for now...
:else (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4) intcode)))))
Day 5 depends on user input to vary the output. The Day 5.2 code is the same as Day 5.1. Each part can be completed by entering a different user input value with the same inputs. Specifially, enter "1" for part 1 and "5" for part 2. Hence my -main
function for Day 5 only has a single line with no println
. That's because the Intcode computer does the required printing:
(defn -main
"I call the functions for the Advent of Code on the basis of which day(s) are added as arguments to the command line call"
[& days]
(loop [days days
day (first days)]
(if (empty? days)
nil
(do
(cond
;; *** Snip ***
(= day "5") (day5_opcode day5_intcode)
:else (println "Day not completed yet"))
(recur (rest days) (first (rest days)))))))
Day 5.2
So Day 5.1 passes with the above code. Now we go to Day 5.2. New Opcode instructions arrive, now with three numbers. This brings us to a situation where sequences can be 1, 2, 3, or 4 elements in length:
- Opcode of 5 means that if parameter 1 is non-zero, skip to a new location defined by parameter 2, otherwise move to the next set in the sequence
- Opcode of 6 is the same as 5, but checking for zero, not non-zero
- Opcode of 7 writes a 1 or a 0 to the location defined in parameter 3 if parameter 1 is less than parameter 2
- Opcode of 8 writes a 1 or a 0 to the location defined in parameter 3 if parameter 1 is equal to parameter 2
- For all of the above, modes still apply, switching the behavior from looking at the parameter value as a pointer, to literally looking at the parameter value. Examples:
- [5, 2, 4, 99, 12 .....] would see that parameter 1 is non-zero. It would read parameter 2 as a pointer. Pointer is 4, so the value of index 4 is 12. Thus the code would skip to the next sequence starting at index 12 (not shown).
- [1105, 2, 4, 99, 12 .....] would see that parameter 1 is non-zero. It would read parameter 2 literally. The value is 4. Thus the code would skip to the next sequence starting at index 4.
Since this logic was complicated, I broke each new Opcode logic into a function. Again, very literal naming.
The Intcode Computer is also updated to include the new checks and behavior:
(defn opcode_5
"I check for non-zero at par1 and move return the starting integer for the next Opcode/parameter set depending on finding"
[intcode posa posb posc opcode_details]
(let [par1 (if (= (:par1_mode opcode_details) 0)
(get intcode (get intcode posb))
(get intcode posb))
par2 (if (= (:par2_mode opcode_details) 0)
(get intcode (get intcode posc))
(get intcode posc))]
(if (zero? par1)
(+ posa 3)
par2)))
(defn opcode_6
"I check for zero at par1 and move return the starting integer for the next Opcode/parameter set depending on finding"
[intcode posa posb posc opcode_details]
(let [par1 (if (= (:par1_mode opcode_details) 0)
(get intcode (get intcode posb))
(get intcode posb))
par2 (if (= (:par2_mode opcode_details) 0)
(get intcode (get intcode posc))
(get intcode posc))]
(if (zero? par1)
par2
(+ posa 3))))
(defn opcode_7
"I compare par1 and par2 to see if par1 < par2. If true, write 1 to par3/par3 pointer, otherwise write 0"
[intcode posa posb posc posd opcode_details]
(let [par1 (if (= (:par1_mode opcode_details) 0)
(get intcode (get intcode posb))
(get intcode posb))
par2 (if (= (:par2_mode opcode_details) 0)
(get intcode (get intcode posc))
(get intcode posc))
par3 (if (= (:par3_mode opcode_details) 0)
(get intcode posd)
posd)]
(if (< par1 par2)
(assoc intcode par3 1)
(assoc intcode par3 0))))
(defn opcode_8
"I compare par1 and par2 to see if par1 == par2. If true, write 1 to par3/par3 pointer, otherwise write 0"
[intcode posa posb posc posd opcode_details]
(let [par1 (if (= (:par1_mode opcode_details) 0)
(get intcode (get intcode posb))
(get intcode posb))
par2 (if (= (:par2_mode opcode_details) 0)
(get intcode (get intcode posc))
(get intcode posc))
par3 (if (= (:par3_mode opcode_details) 0)
(get intcode posd)
posd)]
(if (= par1 par2)
(assoc intcode par3 1)
(assoc intcode par3 0))))
(defn day5_opcode
"I check the opcode and perform the appropriate replacements of items based on the primary rules. Opcode = index 0; Noun = index 1; Verb = index 2; Insert_at = index 3"
[intcode]
(loop [posa 0
posb 1
posc 2
posd 3
intcode intcode]
(let [opcode (get intcode posa)
opcode_details (opcode_reverse_and_dissect opcode)]
(cond
(= (:opcode opcode_details) 99) intcode
(= (:opcode opcode_details) 1) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
(revised_one_and_two_behavior intcode posb posc posd opcode_details +))
(= (:opcode opcode_details) 2) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
(revised_one_and_two_behavior intcode posb posc posd opcode_details *))
(= (:opcode opcode_details) 3) (recur (+ posa 2) (+ posb 2) (+ posc 2) (+ posd 2)
(assoc intcode
(if (= (:par1_mode opcode_details) 0)
(get intcode posb)
posb)
(Integer/parseInt (do (print "Input code: ") (flush) (read-line)))))
(= (:opcode opcode_details) 4) (do
(println "Diagnostic code:" (if (= (:par1_mode opcode_details) 0)
(get intcode (get intcode posb))
(get intcode posb)))
(recur (+ posa 2) (+ posb 2) (+ posc 2) (+ posd 2) intcode))
(= (:opcode opcode_details) 5) (let [new_posa (opcode_5 intcode posa posb posc opcode_details)]
(recur new_posa (+ new_posa 1) (+ new_posa 2) (+ new_posa 3) intcode))
(= (:opcode opcode_details) 6) (let [new_posa (opcode_6 intcode posa posb posc opcode_details)]
(recur new_posa (+ new_posa 1) (+ new_posa 2) (+ new_posa 3) intcode))
(= (:opcode opcode_details) 7) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
(opcode_7 intcode posa posb posc posd opcode_details)) ;; intcode posa posb posc posd opcode_details
(= (:opcode opcode_details) 8) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
(opcode_8 intcode posa posb posc posd opcode_details))
:else (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4) intcode)))))
Finally, -main
is exactly the same. Just enter "5" instead of "1" and the result is produced.
Testing
Since I struggled with the logic I didn't know how to write tests until I was done. At that point I didn't see the value, so today is a bad day for testing...
Posted on December 13, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.