Advent of Code 2020 day 12

    Day 12 was fairly straighforward: apply a bunch of rules to update a state. The two parts are so similar, I'll treat them together here.

    I started with some data definitions, for Directions, the Actions and the Ship.

    data Direction = North | East | South | West 
      deriving (Show, Eq, Ord, Enum, Bounded)
    
    type Position = (Int, Int) -- (x, y)
    
    data Action a = N a | S a | E a | W a | L a | R a | F a
      deriving (Show, Eq, Ord)
    
    data Ship = Ship { position :: Position 
                     , direction :: Direction
                     , waypoint :: Position
                     }
      deriving (Show, Eq, Ord)
    

    I thought about using complex numbers for the position, but decided that the benefit wasn't worth it: I'd be spending most of my time looking at the components of the position, so there was no real benefit in bundling them together.

    Direction is a Bounded Enum so that I can increase and decrease it. It's time to reuse my "wrapping enum" definitions, so "increasing" the direction turns clockwise, and "increasing" from West correctly turns North.

    -- | a `succ` that wraps 
    succW :: (Bounded a, Enum a, Eq a) => a -> a 
    succW dir | dir == maxBound = minBound
              | otherwise = succ dir
    
    -- | a `pred` that wraps
    predW :: (Bounded a, Enum a, Eq a) => a -> a
    predW dir | dir == minBound = maxBound
              | otherwise = pred dir
    

    Parsing the inputs is explicit: one rule for each action, a general action is a choice of the specific rules.

    actionsP = actionP `sepBy` endOfLine
    
    actionP = choice [nP, sP, eP, wP, lP, rP, fP]
    
    nP = N <$> ("N" *> decimal)
    sP = S <$> ("S" *> decimal)
    eP = E <$> ("E" *> decimal)
    wP = W <$> ("W" *> decimal)
    lP = L <$> ("L" *> decimal)
    rP = R <$> ("R" *> decimal)
    fP = F <$> ("F" *> decimal)
    

    Applying the actions is a fold, where each step in the fold has type act :: Ship -> Action Int -> Ship.

    part1 actions = manhattan (position ship1) start
      where start = (0, 0)
            ship0 = Ship {position = start, direction = East, waypoint = (10, 1)}
            ship1 = foldl act ship0 actions
    
    part2 actions = manhattan (position ship1) start
      where start = (0, 0)
            ship0 = Ship {position = start, direction = East, waypoint = (10, 1)}
            ship1 = foldl actW ship0 actions
    

    All that's needed is to apply each action to a ship. Note the use of record wildcards to help packing and unpacking the record. For part 1, it looks like this:

    act :: Ship -> Action Int -> Ship
    act Ship{..} (N d) = Ship { position = dDelta d North position, ..}
    act Ship{..} (S d) = Ship { position = dDelta d South position, ..}
    act Ship{..} (W d) = Ship { position = dDelta d West  position, ..}
    act Ship{..} (E d) = Ship { position = dDelta d East  position, ..}
    act Ship{..} (L a) = Ship { direction = d, ..} where d = (iterate predW direction) !! (a `div` 90)
    act Ship{..} (R a) = Ship { direction = d, ..} where d = (iterate succW direction) !! (a `div` 90)
    act Ship{..} (F d) = Ship { position = dDelta d direction position, ..} 
    

    For part 2, the actW (for "waypoint") looks like this:

    actW :: Ship -> Action Int -> Ship
    actW Ship{..} (N d) = Ship { waypoint = dDelta d North waypoint, ..}
    actW Ship{..} (S d) = Ship { waypoint = dDelta d South waypoint, ..}
    actW Ship{..} (W d) = Ship { waypoint = dDelta d West  waypoint, ..}
    actW Ship{..} (E d) = Ship { waypoint = dDelta d East  waypoint, ..}
    actW Ship{..} (L a) = Ship { waypoint = d, ..} where d = (iterate rotL waypoint) !! (a `div` 90)
    actW Ship{..} (R a) = Ship { waypoint = d, ..} where d = (iterate rotR waypoint) !! (a `div` 90)
    actW Ship{..} (F d) = Ship { position = p', ..} 
      where (x, y) = position
            (dx, dy) = waypoint
            p' = (x + (d * dx), y + (d * dy))
    

    Most of the actions involve moving a position by a particular distance in a particular direction, so I wrote a utility function to do that.

    delta North = ( 0,  1)
    delta South = ( 0, -1)
    delta East  = ( 1,  0)
    delta West  = (-1,  0)
    
    dDelta dist dir (x, y) = (x + (dist * dx), y + (dist * dy))
      where (dx, dy) = delta dir
    

    Code

    You can find the code here or on Gitlab.

    Neil Smith

    Read more posts by this author.