/wakamoleguy

Advent of Code 2022 - Day 9: Rope Bridge

I'm still running a couple days behind, and so this grey Sunday I am tackling Day 9: Rope Bridge. We're asked to simulate the movement of a rope, where the tail of the rope follows along with the movements of the head.

The Ways Ropes Move

Unusually, today I didn't start with parsing! Instead, I jumped right into modeling the rules of motion. There are two that we care about: the head following steps directly from our input file, and the tail being lazily tugged along behind it.

-- Tugs the first argument towards the second argument
tug :: (Int, Int) -> (Int, Int) -> (Int, Int)
tug (x, y) (a, b)
  | isTouching = (x, y)
  | x < a && y < b = (x + 1, y + 1)
  | x > a && y > b = (x - 1, y - 1)
  | x < a && y > b = (x + 1, y - 1)
  | x > a && y < b = (x - 1, y + 1)
  | x < a = (x + 1, y)
  | x > a = (x - 1, y)
  | y < b = (x, y + 1)
  | y > b = (x, y - 1)
  | otherwise = (x, y)
  where
    isTouching = abs (x - a) <= 1 && abs (y - b) <= 1

data Direction = North | South | East | West deriving (Show, Eq)

step :: (Int, Int) -> Direction -> (Int, Int)
step (x, y) North = (x, y + 1)
step (x, y) South = (x, y - 1)
step (x, y) East = (x + 1, y)
step (x, y) West = (x - 1, y)

The tug function isn't pretty, but it works!

Parsing The Input

We have a list of commands in our input. Each line can represent several individual motions, but we really want to simulate them one by one. Let's expand them out.

strToDir :: Char -> Direction
strToDir 'U' = North
strToDir 'D' = South
strToDir 'R' = East
strToDir 'L' = West
strToDir c = error $ "Invalid direction: " ++ show c

input :: IO [Direction]
input = concatMap parseMotions . lines <$> readDay 2022 9
  where
    parseMotions (d : ' ' : n) = replicate (read n) (strToDir d)
    parseMotions _ = error "Invalid motion"

Simulating the Rope

How do we simulate the rope? Well, the head of the rope moves according to the direction, and the tail of the rope follows. I had a hunch (read: saw on Reddit) that Part 2 may involve a longer rope, so I structured it as a recursive function:

stepRope :: [(Int, Int)] -> Direction -> [(Int, Int)]
stepRope (h : t) dir = step h dir : follow (step h dir) t
  where
    follow prev (next : rest) = tug next prev : follow (tug next prev) rest
    follow _ [] = []

This pretty closely matches how we'd describe it in English, which is one thing I love about the expressiveness of Haskell.

Now we want to fold our list of steps over our rope to simulate its motion. But wait! We care about each state along the way, so instead of fold we use scan. Tracking the tail locations is as simple as getting the last item from each scan result, filtering to only unique coordinates, and counting them up.

part1 :: [Direction] -> Int
part1 = length . nub . fmap last . scanl stepRope [(0, 0), (0, 0)]

Part 2 is a longer rope, with 10 knots, but looks precisely the same:

part2 :: [Direction] -> Int
part2 = length . nub . fmap last . scanl stepRope (replicate 10 (0, 0))

Full Code

module AOC2022.Day09 (spec) where

import Data.List
import Input
import Test.Hspec

data Direction = North | South | East | West deriving (Show, Eq)

strToDir :: Char -> Direction
strToDir 'U' = North
strToDir 'D' = South
strToDir 'R' = East
strToDir 'L' = West
strToDir c = error $ "Invalid direction: " ++ show c

input :: IO [Direction]
input = concatMap parseMotions . lines <$> readDay 2022 9
  where
    parseMotions (d : ' ' : n) = replicate (read n) (strToDir d)
    parseMotions _ = error "Invalid motion"

spec :: IO ()
spec = hspec $ do
  describe "Day 9" $ do
    describe "Part 1" $ do
      it "runs on custom input" $ do
        myInput <- input
        stepRope [(1, 1), (0, 0)] North `shouldBe` [(1, 2), (1, 1)]
        part1 myInput `shouldBe` 0 -- redacted
    describe "Part 2" $ do
      it "runs on custom input" $ do
        myInput <- input
        part2 myInput `shouldBe` 0 -- redacted

-- Tugs the first argument towards the second argument
tug :: (Int, Int) -> (Int, Int) -> (Int, Int)
tug (x, y) (a, b)
  | isTouching = (x, y)
  | x < a && y < b = (x + 1, y + 1)
  | x > a && y > b = (x - 1, y - 1)
  | x < a && y > b = (x + 1, y - 1)
  | x > a && y < b = (x - 1, y + 1)
  | x < a = (x + 1, y)
  | x > a = (x - 1, y)
  | y < b = (x, y + 1)
  | y > b = (x, y - 1)
  | otherwise = (x, y)
  where
    isTouching = abs (x - a) <= 1 && abs (y - b) <= 1

step :: (Int, Int) -> Direction -> (Int, Int)
step (x, y) North = (x, y + 1)
step (x, y) South = (x, y - 1)
step (x, y) East = (x + 1, y)
step (x, y) West = (x - 1, y)

stepRope :: [(Int, Int)] -> Direction -> [(Int, Int)]
stepRope (h : t) dir = step h dir : follow (step h dir) t
  where
    follow prev (next : rest) = tug next prev : follow (tug next prev) rest
    follow _ [] = []
stepRope [] _ = []

part1 :: [Direction] -> Int
part1 = length . nub . fmap last . scanl stepRope [(0, 0), (0, 0)]

part2 :: [Direction] -> Int
part2 = length . nub . fmap last . scanl stepRope (replicate 10 (0, 0))

Advent of Code 2022 Series

This post is part of a series describing my Haskell solutions to Advent of Code 2022.

Next: Day 10: Cathode-Ray Tube Previous: Day 8: Treetop Tree House

Cheers!