Skip to content

Commit 134e0d2

Browse files
committed
Day 12 refactoring and docs
1 parent 5fe810e commit 134e0d2

File tree

2 files changed

+181
-61
lines changed

2 files changed

+181
-61
lines changed

docs/day12.md

+146
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,149 @@ Finally, the `part1` function uses the `reduce` function on the starting state,
9898
process each line. Then when we're done, we grab the `[x y]` coordinates out of the state using `first`,
9999
and calculate the Manhattan distance by taking the absolute value of `x` and `y` and adding them.
100100

101+
```clojure
102+
(defn part1 [input]
103+
(->> (reduce #(next-state %1 %2)
104+
[[0 0] :east]
105+
(str/split-lines input))
106+
first
107+
(mapv utils/abs)
108+
(apply +)))
109+
```
110+
111+
---
112+
113+
## An Interlude before Part 2
114+
115+
Part 2 says that the instructions we took for most of the operations in part 1 were wrong, and now we
116+
need to change how our program works. (Helpful tip - when the problem statement says "you work out
117+
what [the commands] probably mean," that means they're going to change in part 2!) We learn that our
118+
ship has a waypoint that does most of the movement. The waypoint is what moves north, south, east,
119+
and west on `N`, `S`, `E`, and `W`. And instead of the ship turning left or right, the waypoint revolves
120+
around the ship by a number of degrees. Finally, when the ship goes forward on an `F` command, it moves
121+
`n` times to the relative position of the waypoint to the ship.
122+
123+
Here's the fun realization - we can implement the ship's direction from part 1 as the waypoint's position
124+
from part 2. In part 1, we know the ship starts off facing east. If the first command were to be `F10`,
125+
then it would end up moving from `[0 0]` to `[10 0]`. Now another way to think of "facing East" as "has
126+
a waypoint of `[1 0]`." If the waypoint were at `[1 0]` and we moved toward it 10 times because of `F10`,
127+
we would still end up at position `[10 0]`. Likewise, if the ship faces east and rotates 90 degrees to
128+
the right, it would be facing south. Or stated different, if a waypoint at `[1 0]` rotates 90 degrees,
129+
remembering that North is negative and South is positive, then the waypoint would end up at `[0 1]`.
130+
And from that position, moving forward 10 positions would have the same effect whether going 10 South or
131+
moving 10 steps toward `[0 1]`.
132+
133+
So let's figure out what's really different between parts 1 and 2. In part 1, `N`, `S`, `E`, and `W`
134+
moves the ship in that direction, while in part 2 it moves the waypoint. In both parts, `L` and `R`
135+
can be seen as rotating the waypoint around the ship, and `F` can be seen as moving toward the waypoint,
136+
so long as we initialize the waypoint correctly.
137+
138+
Therefore, the two differences between parts 1 and 2 are
139+
1. The initial state of the waypoint (`[1 0]` for part 1 and `[10 -1]` for part 2), and
140+
2. What moves during a directional movement.
141+
142+
Ok, let's refactor!
143+
144+
---
145+
146+
## Part 2
147+
148+
First of all, let's change the shape of our data. Instead of holding on to an `[x y]` point and a direction,
149+
a ship's state will be defined as the ship's position and the waypoint's position. This time, I'll represent
150+
it as a map, so our state will be of shape `{:ship [x y], :waypoint [x y]}`. We can initialize the state
151+
with the initial position of the waypoint.
152+
153+
```clojure
154+
(defn new-ship [initial-waypoint]
155+
{:ship [0 0] :waypoint initial-waypoint})
156+
```
157+
158+
We'll keep the old var `dir-amounts` from the initial implementation, and that still maps each direction
159+
to the `[dx dy]` amount we move in that direction.
160+
161+
Next, let's get two functions that deal with movement -- `slide` and `follow-waypoint`. I named the first one
162+
`slide` because in part 1, a ship facing East and traveling North is somehow magically travelling laterally,
163+
so it feels like it's slipping. In part 1, the ship will slide, and in part 2 the waypoint will. To do this,
164+
we'll `update` the state, multiplying the instruction's amount by the `dir-amount`, and adding that to the
165+
current value in the state. For `follow-waypoint`, we do the same thing, except we multiply the instruction's
166+
amount by the waypoint's data instead. Since these two commands are very similar, both `slide` and
167+
`follow-waypoint` delegate their work to a common function called `move`. `move` accepts the current state;
168+
the `mover`, either `:ship` or `:waypoint`, to determine what's going to change; the `target` as the `[dx dy]`
169+
point to work with; and the `amt` to multiply the `target`. Since we can `slide` either the ship or the
170+
waypoint, we feed that value in to the function, but `follow-waypoint` always moves the ship, so we can
171+
hard-code that value.
172+
173+
```clojure
174+
(defn move [state mover target amt]
175+
(let [move-by (mapv * target [amt amt])]
176+
(update state mover (partial mapv + move-by))))
177+
178+
(defn slide [state mover dir amt]
179+
(move state mover (dir-amounts dir) amt))
180+
181+
(defn follow-waypoint [state amt]
182+
(move state :ship (state :waypoint) amt))
183+
```
184+
185+
Now we have to work on rotating the waypoint, and again we remember that we only receive multiples of 90.
186+
The easiest way to handle rotations is to do so iteratively, since the math can be a little tedious to
187+
work with if we try to do it in one step. To build out `rotate-waypoint`, I first bind `times` to the
188+
number of 90-degree clockwise rotations we need to do. The function will accept counter-clockwise
189+
rotations as negative numbers, since turning left 90 degrees is the same as turning right -90 degrees.
190+
We `mod` the degrees by 360 to get a value in `#{0, 90, 180, 270}`, and then take the quotient from 90
191+
to get the number of rotations between 0 and 3. Then for clarity, I define a helperful function `rotate90`,
192+
which takes in a point and rotates it once by setting `[x' y'] = [-y x]`. Hooray for paper math. Then
193+
instead of a `loop`, I combined `iterate` with `nth` to complete the function.
194+
195+
```clojure
196+
(defn rotate-waypoint [state degrees]
197+
(let [times (-> degrees (mod 360) (quot 90))
198+
rotate90 (fn [[x y]] [(- y) x])]
199+
(-> (iterate
200+
(fn [s] (update s :waypoint rotate90))
201+
state)
202+
(nth times))))
203+
```
204+
205+
We're almost done. The `next-state` function looks very similar to what we saw before, except the
206+
`case` expression leverages the new helper functions. Again, `mover` is a parameter that's set to
207+
`:ship` for part 1 and `:waypoint` for part 2. So `N`, `S`, `E`, and `W` slides the `state` based on
208+
the `mover`; `L` and `R` rotates the waypoint; and `F` moves the ship toward the waypoint. Everything
209+
is reused.
210+
211+
```clojure
212+
(defn next-state [state mover line]
213+
(let [op (first line)
214+
amt (-> (subs line 1) Integer/parseInt)]
215+
(case op
216+
\N (slide state mover :north amt)
217+
\S (slide state mover :south amt)
218+
\E (slide state mover :east amt)
219+
\W (slide state mover :west amt)
220+
\L (rotate-waypoint state (- amt))
221+
\R (rotate-waypoint state amt)
222+
\F (follow-waypoint state amt))))
223+
```
224+
225+
All that's left is the `solve` function and the tiny `part1` and `part2` functions. `solve` looks
226+
almost identical to what we used to do in `part1` - split the input data, initialize the ship with
227+
the starting waypoint, reduce the data using the `next-state` function. Then from the resulting
228+
state, retrieve the point that's associated to `:ship`, and calculate the Manhattan Distance using
229+
absolute value and addition. Then we call this function using `[1 0]` (due East) and `:ship` for
230+
part 1, and using `[10 -1]` (10 East, 1 North) and `:waypoint` for part 2.
231+
232+
```clojure
233+
(defn solve [initial-waypoint mover input]
234+
(->> (reduce #(next-state %1 mover %2)
235+
(new-ship initial-waypoint)
236+
(str/split-lines input))
237+
:ship
238+
(mapv utils/abs)
239+
(apply +)))
240+
241+
(defn part1 [input]
242+
(solve [1 0] :ship input))
243+
244+
(defn part2 [input]
245+
(solve [10 -1] :waypoint input))
246+
```

src/advent_2020_clojure/day12.clj

+35-61
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,54 @@
22
(:require [clojure.string :as str]
33
[advent-2020-clojure.utils :as utils]))
44

5-
; State: [[x y] dir]
6-
(def dirs [:north :east :south :west])
7-
(defn rotate [dir amt]
8-
(-> (.indexOf dirs dir)
9-
(+ (quot amt 90))
10-
(mod (count dirs))
11-
dirs))
5+
(defn new-ship [initial-waypoint]
6+
{:ship [0 0] :waypoint initial-waypoint})
127

138
(def dir-amounts {:north [0 -1]
149
:south [0 1]
1510
:east [1 0]
1611
:west [-1 0]})
1712

18-
(defn move [[[x y] dir] amt]
19-
(->> (dir-amounts dir)
20-
(mapv * [amt amt])
21-
(mapv + [x y])))
22-
23-
(defn next-state [[[x y] dir :as state] line]
24-
(let [op (first line)
25-
amt (-> (subs line 1) Integer/parseInt)]
26-
(case op
27-
\N [[x (- y amt)] dir]
28-
\S [[x (+ y amt)] dir]
29-
\E [[(+ x amt) y] dir]
30-
\W [[(- x amt) y] dir]
31-
\L [[x y] (rotate dir (- amt))]
32-
\R [[x y] (rotate dir amt)]
33-
\F [(move state amt) dir])))
34-
35-
(defn part1 [input]
36-
(->> (reduce #(next-state %1 %2)
37-
[[0 0] :east]
38-
(str/split-lines input))
39-
first
40-
(mapv utils/abs)
41-
(apply +)))
13+
(defn move [state mover target amt]
14+
(let [move-by (mapv * target [amt amt])]
15+
(update state mover (partial mapv + move-by))))
4216

17+
(defn slide [state mover dir amt]
18+
(move state mover (dir-amounts dir) amt))
4319

20+
(defn follow-waypoint [state amt]
21+
(move state :ship (state :waypoint) amt))
4422

45-
; New state: [[x y] [wx wy]
23+
(defn rotate-waypoint [state degrees]
24+
(let [times (-> degrees (mod 360) (quot 90))
25+
rotate90 (fn [[x y]] [(- y) x])]
26+
(-> (iterate
27+
(fn [s] (update s :waypoint rotate90))
28+
state)
29+
(nth times))))
4630

47-
(defn rotate-2 [point amt]
48-
(loop [[x y] point n (-> amt (mod 360) (quot 90))]
49-
(if (zero? n)
50-
[x y]
51-
(recur [(- y) x] (dec n))))
52-
53-
#_(case (mod amt 360)
54-
0 [x y]
55-
90 [(- y) (- x)]
56-
180 [(- x) (- y)]
57-
270 [y x]))
58-
59-
(defn move2 [[ship waypoint] amt]
60-
(->> (map * waypoint [amt amt])
61-
(map + ship)))
62-
63-
(defn next-state2 [[[x y] [wx wy] :as state] line]
31+
(defn next-state [state mover line]
6432
(let [op (first line)
6533
amt (-> (subs line 1) Integer/parseInt)]
6634
(case op
67-
\N [[x y] [wx (- wy amt)]]
68-
\S [[x y] [wx (+ wy amt)]]
69-
\E [[x y] [(+ wx amt) wy]]
70-
\W [[x y] [(- wx amt) wy]]
71-
\L [[x y] (rotate-2 [wx wy] (- amt))]
72-
\R [[x y] (rotate-2 [wx wy] amt)]
73-
\F [(move2 state amt) [wx wy]])))
74-
75-
(defn part2 [input]
76-
(->> (reduce #(next-state2 %1 %2)
77-
[[0 0] [10 -1]]
35+
\N (slide state mover :north amt)
36+
\S (slide state mover :south amt)
37+
\E (slide state mover :east amt)
38+
\W (slide state mover :west amt)
39+
\L (rotate-waypoint state (- amt))
40+
\R (rotate-waypoint state amt)
41+
\F (follow-waypoint state amt))))
42+
43+
(defn solve [initial-waypoint mover input]
44+
(->> (reduce #(next-state %1 mover %2)
45+
(new-ship initial-waypoint)
7846
(str/split-lines input))
79-
first
47+
:ship
8048
(mapv utils/abs)
8149
(apply +)))
50+
51+
(defn part1 [input]
52+
(solve [1 0] :ship input))
53+
54+
(defn part2 [input]
55+
(solve [10 -1] :waypoint input))

0 commit comments

Comments
 (0)