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 Direction
s, the Action
s 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.