|
| 1 | +# Day Twelve: Rain Risk |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2020/day/12) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day12.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +I liked today's puzzle, because it became clear after implementing Part 1 that there's a logical |
| 9 | +rewrite to do to make both parts look the same. This time around, I'm going to provide my solution |
| 10 | +to part 1 without knowing anything about part 2, and then we'll do a refactoring. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +# Part 1 |
| 15 | + |
| 16 | +In this part, we are given a list of instructions and are given instructions on how to steer the |
| 17 | +ship. Each instruction has a single-character operation, following by a numeric argument. |
| 18 | +We move north, south, east, or west _without changing direction_ if the operation is `N`, `S`, `E`, |
| 19 | +or `W`. We turn the ship without moving it counter-clockwise or clockwise, `L` or `R`, by the number |
| 20 | +of degrees; note that we only get multiples of 90 to keep the math simple. And finally, we can move |
| 21 | +the ship forward, `F`, in the direction it's currently facing. |
| 22 | + |
| 23 | +The first part I want to solve is how to rotate the ship with the `L` and `R` instructions. I |
| 24 | +define a vector of all possible directions going in a clockwise motion. Then in the `rotate` |
| 25 | +function, the goal is to find the index of the current direction, and add `(quot amt 90)` to the |
| 26 | +index, since each 90 degrees will move us to the next direction in the circle. We use |
| 27 | +`(mod (count dirs))` to wrap around the edges, and then we grab the direction at the new index. |
| 28 | + |
| 29 | +Note that there's another way to write this function. After finding the index and the number of steps |
| 30 | +to move, instead of using `mod`, we can just find the `nth` location in an infinite cycle of the |
| 31 | +original direction list. It's less efficient if for some reason we had to rotate a 9 million degrees, |
| 32 | +but it's a neat option. |
| 33 | + |
| 34 | +```clojure |
| 35 | +(def dirs [:north :east :south :west]) |
| 36 | +(defn rotate [dir amt] |
| 37 | + (-> (.indexOf dirs dir) |
| 38 | + (+ (quot amt 90)) |
| 39 | + (mod (count dirs)) |
| 40 | + dirs)) |
| 41 | + |
| 42 | +; inefficient alternative |
| 43 | +(defn rotate [dir amt] |
| 44 | + (->> (.indexOf dirs dir) |
| 45 | + (+ (quot amt 90)) |
| 46 | + (nth (cycle dirs)))) |
| 47 | +``` |
| 48 | + |
| 49 | +Let's take a quick moment to think about the state we're going to use. A ship is represented by two |
| 50 | +pieces of data - the current `[x y]` coordinates and the direction we're facing. I could use a map |
| 51 | +of these values, or a 3-element vector, but a two element vector of the `[x y]` pair and the direction |
| 52 | +seemed simple enough. So the starting position of facing east will be `[[0 0] :east]`. |
| 53 | + |
| 54 | +Now, let's figure out how to move the ship without rotating it. We'll write a function called `move`, |
| 55 | +which takes in the current position, direction to move, and the amount to move. Just as we made a |
| 56 | +vector of all directions, we'll make a map of each direction to the vector we'll add to move once in |
| 57 | +that direction. Most AoC functions assume that the origin is in the top-left corner of the input and, |
| 58 | +`x` moves to the right while `y` moves _down_. It's strange but I've become used to it, but it does |
| 59 | +mean that north is `[0 -1]` instead of a more traditional `[0 1]`. |
| 60 | + |
| 61 | +To move, we find the amount to move from the `dir-amounts` map, multiply the `[dx dy]` pair by the |
| 62 | +`amt` value to know how far we need to move in total, and then use `(mapv + [x y]`) to add it to the |
| 63 | +current position of the ship. |
| 64 | + |
| 65 | +```clojure |
| 66 | +(def dir-amounts {:north [0 -1] |
| 67 | + :south [0 1] |
| 68 | + :east [1 0] |
| 69 | + :west [-1 0]}) |
| 70 | + |
| 71 | +(defn move [[[x y] dir] amt] |
| 72 | + (->> (dir-amounts dir) |
| 73 | + (mapv * [amt amt]) |
| 74 | + (mapv + [x y]))) |
| 75 | +``` |
| 76 | + |
| 77 | +Then we create the `next-state` function to move the state based on the current line in the instructions. |
| 78 | +We'll destructure the heck ouf of the input again, reading in `state` as `[[x y] dir :as state]` to pull |
| 79 | +apart the values and keep the state intact at the same time. Then we parse the `op` and `amt` out of the |
| 80 | +instruction, and use `case` to switch based on the `op`. For each value, I just manually reconstruct the |
| 81 | +vector by manually changing the point. It's not clean, but again we're about to rewrite this. |
| 82 | + |
| 83 | +```clojure |
| 84 | +(defn next-state [[[x y] dir :as state] line] |
| 85 | + (let [op (first line) |
| 86 | + amt (-> (subs line 1) Integer/parseInt)] |
| 87 | + (case op |
| 88 | + \N [[x (- y amt)] dir] |
| 89 | + \S [[x (+ y amt)] dir] |
| 90 | + \E [[(+ x amt) y] dir] |
| 91 | + \W [[(- x amt) y] dir] |
| 92 | + \L [[x y] (rotate dir (- amt))] |
| 93 | + \R [[x y] (rotate dir amt)] |
| 94 | + \F [(move state amt) dir]))) |
| 95 | +``` |
| 96 | + |
| 97 | +Finally, the `part1` function uses the `reduce` function on the starting state, calling `next-state` to |
| 98 | +process each line. Then when we're done, we grab the `[x y]` coordinates out of the state using `first`, |
| 99 | +and calculate the Manhattan distance by taking the absolute value of `x` and `y` and adding them. |
| 100 | + |
0 commit comments