December 10, 2019

Advent of Code 2019 - Days 8 & 9

Since my last post ended up being so long I should probably start breaking these up more. And with that, here's days 8 and 9!

Day 8

Before we do anything else we need to get our image layers. Each image is 25x6, so each layer will be (* 25 6) characters. Let's split our layers using partition:

(defn parse-layers [img-width img-height]
  (-> (get-input)
      (clojure.string/split #"")
      (->> rest ;; ignore the newline
           (map js/parseInt)
           (partition (* img-width img-height)))))

Once we have our layers we can sort them by the number of 0s that they contain to find our target layer. Then we just need to count the 1s and 2s and multiply the results.

(defn part-one []
  (let [layers          (parse-layers 25 6)
        min-zeros-layer (->> layers
                             (sort-by #(count (filter zero? %)))
                             first)]
    (-> (* (count (filter #(= 1 %) min-zeros-layer))
           (count (filter #(= 2 %) min-zeros-layer)))
        println)))

Part 2

In part 1 we parsed the data into a list of lists, with each inner list representing a layer. We now want to transform this data so that, instead, each inner list represents all of the layers for a specific pixel. This is easy to do with (apply map vector layers). For example,

(apply map vector [[1 2 3] [:a :b :c]])
=> ([1 :a] [2 :b] [3 :c])

;; Using example data from the aoc site
(apply map vector [[0 2 2 2] [1 1 2 2] [2 2 1 2] [0 0 0 0]])
=> ([0 1 2 0] [2 1 2 0] [2 2 1 0] [2 2 2 0])

If you haven't realized, this is just a matrix transpose. 😊 Now just remove all of the transparent pixels and the first value in each inner list will be the colour of the pixel. Let's replace our 0s and 1s with something more readable too:

(defn flatten-layers [layers]
  (->> (apply map vector layers)
       (map (comp #(if (= 1 %) \█ \space)
                  first
                  #(remove (partial = 2) %)))))

(flatten-layers [[0 2 2 2] [1 1 2 2] [2 2 1 2] [0 0 0 0]])
=> (0 1 1 0) ;; reverted back to `1`s and `0`s for readability

Finally, we need to see what our image actually looks like:

(defn print-img [pixels width]
  (->> (partition width pixels)
       ;; mapv to force the println
       (mapv (comp println #(string/join "" %)))))

(defn part-two []
  (-> (parse-layers 25 6)
      flatten-layers
      (print-img 25)))

And here's what my image looked like!

█   ██    ████ ███    ██
█   ██    █    █  █    █
 █ █ █    ███  █  █    █
  █  █    █    ███     █
  █  █    █    █    █  █
  █  ████ █    █     ██

Day 9

I'm starting to get sick of this intcode computer... With how often it's come back into play, I'm glad I rewrote it in a more maintainable way though. Honestly, this has been a good exercise in writing "proper" code rather than just solving cute puzzles.

Part 1 of this problem asks us to implement a new parameter mode: relative mode. Parameters in relative mode behave similarly to parameters in position mode in the sense that they both represent locations in the program's memory. However, parameters in relative mode count from a relative base rather than 0.

Let's update our computer (reusing code from Day 7) to reflect this. The computer has to store the releative-base and I'll also add a new helper for accessing the memory using an offset from the relative base. normalize-param should be updated as well.

(defn ->computer [input]
  {:ip     0
   :mem    program
   :input  input
   :output []
   :rb     0})

(defn read-rb-offset-word [computer offset]
  (get-in computer [:mem (+ offset (:rb computer))]))

(defn normalize-param [computer modes offset]
  (case (modes (dec offset)) ;; dec because no mode for the instruction
    0 (read-word computer (read-offset-word computer offset))
    1 (read-offset-word computer offset)
    2 (read-rb-offset-word computer (read-offset-word computer offset))))

Addresses used for writing will also need to be normalized now. They weren't previously, as they could only be in position mode.

(defn normalize-write-address [computer modes offset]
  (if (zero? (modes (dec offset))) ;; dec because no mode for the instruction
    (read-offset-word computer offset)
    (+ (:rb computer) (read-offset-word computer offset))))

The next thing we need to add is instruction 9, which will update the relative-base with the value of its parameter.

(defn update-rb-op [computer {:keys [modes]}]
  (let [param (normalize-param computer modes 1)]
    (-> computer
        (update :rb + param)
        (move-ip 2))))

(defn run-program [computer]
  (let [opcode (parse-opcode computer)]
    (case (:op opcode)
      99 computer
      (1 2 7 8) (recur (binary-op computer opcode))
      3 (recur (input-op computer opcode))
      ;; I added recur back because I wanted it to keep printing
      4 (recur (output-op computer opcode))
      ;;
      (5 6) (recur (jump-op computer opcode))
      9 (recur (update-rb-op computer opcode)))))

Finally, the computer's memory needs to be "increased" – we need to be able to read/write to/from beyond the initial size of the memory.

If we try to read anything beyond the "limit", we should get 0:

(defn read-word [computer address]
  (get-in computer [:mem address] 0))

(defn read-offset-word [computer offset]
  (get-in computer [:mem (+ offset (:ip computer))] 0))

(defn read-rb-offset-word [computer offset]
  (get-in computer [:mem (+ offset (:rb computer))] 0))

And when we try to write beyond the limit, we'll have to pad our existing memory:

(defn write-word [computer address value]
  (let [padding-amount (- address (count (:mem computer)))]
    (if (pos? padding-amount)
      (update computer :mem into (conj (vec (repeat padding-amount 0)) value))
      (assoc-in computer [:mem address] value))))

(write-word {:mem [0 1 2]} 7 7)
=> {:mem [0 1 2 0 0 0 0 7]}

And let's run our program for parts 1 and 2

(run-program (->computer [1]))
(run-program (->computer [2]))

Tags: clojure aoc