@@ -98,3 +98,149 @@ Finally, the `part1` function uses the `reduce` function on the starting state,
98
98
process each line. Then when we're done, we grab the ` [x y] ` coordinates out of the state using ` first ` ,
99
99
and calculate the Manhattan distance by taking the absolute value of ` x ` and ` y ` and adding them.
100
100
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
+ ```
0 commit comments