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)