DEV Community

Cover image for Advent of Code 2019 - Day 2
bretthancox
bretthancox

Posted on • Updated on

Advent of Code 2019 - Day 2

Introduction

Day 2 (posting on day 5...sigh...) was an increase in complexity over day 1. Parsing "Intcode" to generate an instruction set for Santa's rocket. Sounds legit to me!

Advent of Code

Day 2.1

The Intcode provides instructions on how to further edit the Intcode to generate a final version of the code. For this part, the goal is to complete the instruction set and submit the value of the 0-indexed item in the code set after it has been processed.

(ns advent.core
  (:gen-class)
  (:require [advent.inputs :refer [day2_intcode]]))
Enter fullscreen mode Exit fullscreen mode

Once again, I moved the inputs into my advent.inputs file

If anyone is curious, it looks like this:

(ns advent.inputs
  (:gen-class))

(def day2_intcode [1 0 0 3 1 1 2 3 1 3 4 3 1 5 0 3 2 1 9 19 1 19 5 23 2 6 23 27 1 6 27 31 2 31 9 35 1 35 6 39 1 10 39 43 2 9 43 47 1 5 47 51 2 51 6 55 1 5 55 59 2 13 59 63 1 63 5 67 2 67 13 71 1 71 9 75 1 75 6 79 2 79 6 83 1 83 5 87 2 87 9 91 2 9 91 95 1 5 95 99 2 99 13 103 1 103 5 107 1 2 107 111 1 111 5 0 99 2 14 0 0
])
Enter fullscreen mode Exit fullscreen mode

Then, the Intcode repeatedly requires adding or multiplying two values. To make my code more readable I moved these concepts into separate functions. Not necessary, but sometimes parentheses hell is worth avoiding!

(defn day2_add
  "Extracts repeated additions from other code"
  [int1 int2]
  (+ int1 int2))

(defn day2_multiply
  "Extracts repeated multiplications from other code"
  [int1 int2]
  (* int1 int2))
Enter fullscreen mode Exit fullscreen mode

EDIT: Reading this again, I noticed I could have written a day2_operator function instead. Functions are first class citizens, so instead of writing two functions to do essentially the same thing with a different operator, I could have done the following:

(defn day2_operator
  "Extracts repeated operations from other code"
    [int1 int2 operator]
    (operator int1 int2))

(day2_operator 5 5 +) ;; Result: 10
(day2_operator 5 5 *) ;; Result: 25
Enter fullscreen mode Exit fullscreen mode

C'est la vie. END EDIT

Then comes day 2.1 in action. The key is to work in groups of 4: [opcode noun verb insert_at].
Opcode defines whether to add, multiply, or end the program.
Noun and verb are (seemingly) just names for the integers at index 1 and 2.
Insert_at defines where the result of the add/multiply is placed in the Intcode.

For Clojure, this is loop territory. I explicitly define the four indices I need, then on each successful loop I add 4 to each. That moves each value to the next sequence of 4 integers.
I maintain the Intcode throughout, updating it for each loop.
I extract the Opcode each time through and use it to determine what action to take, with a result of 99 returning the revised Intcode.

(defn day2_1
  "Checks the opcode and performs 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)]
      (if (= opcode 99)
        intcode
        (if (= opcode 1)
          (recur
           (+ posa 4)
           (+ posb 4)
           (+ posc 4)
           (+ posd 4)
           ;; This horrible line does the following: 
             ;; for index 1 and 2, get the value at index 1 and 2, then use those values as indices to get the values for calculation
             ;; then, get the target_index value at index 3, take the calculation result, and write the calculation result to the target_index
           (assoc intcode (get intcode posd) (day2_add (get intcode (get intcode posb)) (get intcode (get intcode posc)))) 
           )
          (if (= opcode 2)
            (recur
             (+ posa 4)
             (+ posb 4)
             (+ posc 4)
             (+ posd 4)
             (assoc intcode (get intcode posd) (day2_multiply (get intcode (get intcode posb)) (get intcode (get intcode posc))))
             )
            (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4) intcode
                   )))))))
Enter fullscreen mode Exit fullscreen mode

NOTE: Thinking about it as I write this, a cond could have worked here. Oh well.

The problem then throws one final curveball: substitute the values at index 1 and 2 of the Intcode before running, a process that apparently produces a 1202 program alarm in the story. A quick day2_prep function accepting the Intcode with no alarm and that's done.
Running this from main, I print the result and get that nice star.


(defn day2_prep
  "Performs the preparation task of replacing the 'noun' and 'verb' (aka index 1 and index 2) with new values"
  [no_alarm noun verb]
  (assoc (assoc no_alarm 1 noun) 2 verb))

(defn -main
  "I call the functions for the Advent of Code"
  []
  (println "Day 2.1 - Intcode output: " (day2_1 (day2_prep day2_intcode 12 2))))
Enter fullscreen mode Exit fullscreen mode

Day 2.2

As with day 1, time to complicate the previous problem.
This time, it's working out what combination of Intcode produces a specified answer at index 0. Time to brute force this!

nounverb accepts the Intcode and the specified max value either the noun or verb can be (99 for the challenge). It then just iterates the verb until it reaches 99. At 99, it resets the verb to 0 and increments the noun by 1.
Repeat until the result (calculated using the same code as in Day 2.1) matches the desired outcome. Having the outcome hardcoded is bad practice, but for this limited scope I'll take the risk. My test cases are set up to succeed at finding this value if the rest of the code runs correctly.
The end result is a vector of the noun and verb that produce the result.

The result function does the final calculation as defined in the problem.

(defn day2_2_nounverb
  "I find the noun and verb that produce the desired outcome of 19690720 at index 0 and return them as a vector"
  [intcode max] 
  (let [number_of_items (if (< (count intcode) max) max (count intcode))]
    (loop [noun 0 verb 0]
      (let [result (day2_1 (day2_prep intcode noun verb))]
        (if (= (get result 0) 19690720)
          [(get result 1) (get result 2)]
          (recur 
           (if (= verb (- number_of_items 1)) 
             (inc noun) 
             noun)
           (if (= verb (- number_of_items 1)) 
             0 
             (inc verb)))
          )))))


(defn day2_2_result
  "I use the noun and verb vector to perform the calculation that produces the final answer for day 2.2"
  [nounverb]
  (+ (* 100 (get nounverb 0)) (get nounverb 1)))
Enter fullscreen mode Exit fullscreen mode

Testing

I didn't really need test cases for Day 1, but Day 2 had me chasing my tail at one point and I was breaking previously working code. To help me out, I wrote test cases.

Where results are not intuitive, I manually produced the answer and then used it in the test.

I continue this into Day 3 as it is a nice check that I haven't broken logic. This is especially true as you go from part 1 to part 2 of a day, when you might be tempted to edit existing functions

(ns advent.core-test
  (:require [clojure.test :refer :all]
            [advent.core :refer :all]))

(deftest day2_1_computer_test
  (testing "Day 2.1"
    (is (= (day2_1 [10 0 0 99]) [2 0 0 0 99]))
    (is (= (day2_1 [2 3 0 3 99]) [2 3 0 6 99]))
    (is (= (day2_1 [2 4 4 5 99 0]) [2 4 4 5 99 9801]))
    (is (= (day2_1 [1 1 1 4 99 5 6 0 99]) [30 1 1 4 2 5 6 0 99]))
    ))

(deftest day2_1_alarm_prep_test
  (testing "Day 2.1 alarm prep"
    (is (= (day2_prep [1 0 0 0 99] 12 2) [1 12 2 0 99]))
    (is (= (day2_prep [1 1 1 4 99 5 6 0 99] 12 2) [1 12 2 4 99 5 6 0 99]))
    ))

(deftest day2_2_test
  (testing "Day 2.1 noun and verb finder"
    (is (= (day2_2_nounverb [1 5 6 0 99 19690719 1] 99) [0 5]))
    (is (= (day2_2_result (day2_2_nounverb [1 5 6 0 99 19690719 1] 99)) 5))
    ))
Enter fullscreen mode Exit fullscreen mode

So there we go. Day 2 in the bag. Day 3 still to finish...only two days late.


UPDATE

I did refactor the day2_1 function to use cond. Not much shorter, but much clearer about what the code is doing:

(defn day2_1
"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)]
      (cond
        (= opcode 99) intcode
        (= opcode 1) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
                            (assoc intcode (get intcode posd) (day2_operator 
                                                               (get intcode (get intcode posb)) 
                                                               (get intcode (get intcode posc)) 
                                                               +)))
        (= opcode 2) (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4)
                            (assoc intcode (get intcode posd) (day2_operator 
                                                               (get intcode (get intcode posb)) 
                                                               (get intcode (get intcode posc)) 
                                                               *)))
        :else (recur (+ posa 4) (+ posb 4) (+ posc 4) (+ posd 4) intcode)))))

Enter fullscreen mode Exit fullscreen mode

I also noticed some strange, late night decisions in my day2_2_nounverb function, so I took a stab at fixing it up. I dropped the slightly weird number_of_items and instead opted to use the max value being passed in. This simplifies things a little. I also added an argument, desired_value, to replace the hardcoded value in the original:

(defn day2_2_nounverb
  "I find the noun and verb that produce the desired outcome of 19690720 at index 0 and return them as a vector"
  [intcode max desired_result] 
    (loop [noun 0 verb 0]
      (let [result (day2_1 (day2_prep intcode noun verb))]
        (if (= (get result 0) desired_result)
          [(get result 1) (get result 2)]
          (recur 
           (if (= verb (- max 1)) 
             (inc noun) ;Noun only iterates every full cycle of verb, otherwise it loops without change
             noun)
           (if (= verb (- max 1)) 
             0 ;If a full verb cycle has occurred, reset to 0, otherwise increment
             (inc verb)))
          ))))
Enter fullscreen mode Exit fullscreen mode

Oldest comments (2)

Collapse
 
ballpointcarrot profile image
Christopher Kruse

You'll get to have more fun with this, as day 5 builds on the solution from day 2. :)

Collapse
 
bretthancox profile image
bretthancox

Oh no...I'm going to be punished for my lazy coding 😖