Advent of Code 2020 Day 12
[2020-12-12]
#development #adventofcode #julia
Previous: Advent of Code 2020 Day 11
Next: Advent of Code 2020 Day 13
Part 1
This sounds familiar, although as I recall it was “R” or “L” to turn then “F” and “B” to move forward or backwards.
We’ll start with an immutable struct to store the ship data:
struct Ship
x::Int
y::Int
dir::Int
end
Ship(x,y) = Ship(x,y,90)
Ship() = Ship(0,0)
A simple regex to parse the input:
m = match(r"([NSEWLRF])(\d+)", s)
Surprisingly Julia lacks a native switch or match expression. Match.jl looks nice for simple usage, but gets a bit ugly once you add alternatives and guards. I started to use it:
@match dir begin
"N" => Ship(b.x, b.y+dist, b.dir)
"E" => Ship(b.x+dist, b.y, b.dir)
"S" => Ship(b.x, b.y-dist, b.dir)
"W" => Ship(b.x-dist, b.y, b.dir)
"L" => Ship(b.x, b.y, (b.dir+270)%360)
"R" => Ship(b.x, b.y, (b.dir+90)%360)
but realized it would be a bit ugly handling “F”, since I either have to repeat
myself (since “F” is really the same as one of the directions, depending on
which direction the ship is pointing) or add some ungainly guards. Instead I
just went back to a bog-standard if/elseif
chain.
if dir == "N" || (dir == "F" && b.dir == 0)
Ship(b.x, b.y+dist, b.dir)
elseif dir == "E" || (dir == "F" && b.dir == 90)
Ship(b.x+dist, b.y, b.dir)
elseif dir == "S" || (dir == "F" && b.dir == 180)
Ship(b.x, b.y-dist, b.dir)
elseif dir == "W" || (dir == "F" && b.dir == 270)
Ship(b.x-dist, b.y, b.dir)
elseif dir == "L"
Ship(b.x, b.y, (b.dir+360-(dist%360))%360)
elseif dir == "R"
Ship(b.x, b.y, (b.dir+dist)%360)
end
Finally a couple utility functions and we’re done:
manhattan(b::Ship)::Int = abs(b.x)+abs(b.y)
move(b::Ship, in)::Ship = reduce(translate, eachline(in); init=b)
Part 2
So now we need to track both the ship location and the relative waypoint. Start by modifying the Ship struct, with backwards-compatible construtors:
struct Ship
x::Int
y::Int
dir::Int
wx::Int
wy::Int
end
Ship(x,y,dir) = Ship(x,y,dir,0,0)
Ship(x,y) = Ship(x,y,90)
Ship() = Ship(0,0)
Now that “F” does something completely different I’ll go back to using Match
to clean up the translation logic.
@match dir begin
"N" => Ship(b.x, b.y, b.dir, b.wx, b.wy+dist)
"E" => Ship(b.x, b.y, b.dir, b.wx+dist, b.wy)
"S" => Ship(b.x, b.y, b.dir, b.wx, b.wy-dist)
"W" => Ship(b.x, b.y, b.dir, b.wx-dist, b.wy)
"F" => Ship(b.x+dist*b.wx, b.y+dist*b.wy, b.dir, b.wx, b.wy)
Rotation is rather trickier now. We could use trig and rotation matrices, but I’d rather not get floating-point arithmetic involved. All rotations are units of 90°, which we can do analytically. For instance, starting with a point at (-1,2), we get the following points rotating 90° left/counter-clockwise repeatedly:
(-1,2) -> (-2,-1) -> (1,-2) -> (2,1) -> (-1,2)
we observe that (x,y) -> (-y,x). Doing the same but 90° right/clockwise:
(-1,2) -> (2,1) -> (1,-2) -> (-2,-1) -> (-1,2)
observe that (x,y) -> (y,-x). For larger rotations we can just repeat the 90° rotation, which I’ll do recursively.
rotateleft(b::Ship,deg)::Ship = deg <= 0 ? b :
rotateleft(Ship(b.x, b.y, b.dir, -b.wy, b.wx), deg-90)
rotateright(b::Ship,deg)::Ship = deg <= 0 ? b :
rotateright(Ship(b.x, b.y, b.dir, b.wy, -b.wx), deg-90)
Next: Advent of Code 2020 Day 13