Day 9: Rope Bridge¶

Part 1¶

This rope bridge creaks as you walk along it. You aren't sure how old it is, or whether it can even support your weight.

It seems to support the Elves just fine, though. The bridge spans a gorge which was carved out by the massive river far below you.

You step carefully; as you do, the ropes stretch and twist. You decide to distract yourself by modeling rope physics; maybe you can even figure out where not to step.

Consider a rope with a knot at each end; these knots mark the head and the tail of the rope. If the head moves far enough away from the tail, the tail is pulled toward the head.

Due to nebulous reasoning involving Planck lengths, you should be able to model the positions of the knots on a two-dimensional grid. Then, by following a hypothetical series of motions (your puzzle input) for the head, you can determine how the tail will move.

Due to the aforementioned Planck lengths, the rope must be quite short; in fact, the head (H) and tail (T) must always be touching (diagonally adjacent and even overlapping both count as touching):

....
.TH.
....
....
.H..
..T.
....
...
.H. (H covers T)
...

If the head is ever two steps directly up, down, left, or right from the tail, the tail must also move one step in that direction so it remains close enough:

.....    .....    .....
.TH.. -> .T.H. -> ..TH.
.....    .....    .....
...    ...    ...
.T.    .T.    ...
.H. -> ... -> .T.
...    .H.    .H.
...    ...    ...

Otherwise, if the head and tail aren't touching and aren't in the same row or column, the tail always moves one step diagonally to keep up:

.....    .....    .....
.....    ..H..    ..H..
..H.. -> ..... -> ..T..
.T...    .T...    .....
.....    .....    .....
.....    .....    .....
.....    .....    .....
..H.. -> ...H. -> ..TH.
.T...    .T...    .....
.....    .....    .....

You just need to work out where the tail goes as the head follows a series of motions. Assume the head and the tail both start at the same position, overlapping.

For example:

R 4
U 4
L 3
D 1
R 4
D 1
L 5
R 2

This series of motions moves the head right four steps, then up four steps, then left three steps, then down one step, and so on. After each step, you'll need to update the position of the tail if the step means the head is no longer adjacent to the tail. Visually, these motions occur as follows (s marks the starting position as a reference point):

== Initial State ==

......
......
......
......
H.....  (H covers T, s)

== R 4 ==

......
......
......
......
TH....  (T covers s)

......
......
......
......
sTH...

......
......
......
......
s.TH..

......
......
......
......
s..TH.

== U 4 ==

......
......
......
....H.
s..T..

......
......
....H.
....T.
s.....

......
....H.
....T.
......
s.....

....H.
....T.
......
......
s.....

== L 3 ==

...H..
....T.
......
......
s.....

..HT..
......
......
......
s.....

.HT...
......
......
......
s.....

== D 1 ==

..T...
.H....
......
......
s.....

== R 4 ==

..T...
..H...
......
......
s.....

..T...
...H..
......
......
s.....

......
...TH.
......
......
s.....

......
....TH
......
......
s.....

== D 1 ==

......
....T.
.....H
......
s.....

== L 5 ==

......
....T.
....H.
......
s.....

......
....T.
...H..
......
s.....

......
......
..HT..
......
s.....

......
......
.HT...
......
s.....

......
......
HT....
......
s.....

== R 2 ==

......
......
.H....  (H covers T)
......
s.....

......
......
.TH...
......
s.....

After simulating the rope, you can count up all of the positions the tail visited at least once. In this diagram, s again marks the starting position (which the tail also visited) and # marks other positions the tail visited:

..##..
...##.
.####.
....#.
s###..

So, there are 13 positions the tail visited at least once.

Simulate your complete hypothetical series of motions. How many positions does the tail of the rope visit at least once?

In [1]:
# Python imports
from __future__ import annotations  # aids with typing classes

import numpy as np

from pathlib import Path
In [2]:
test = Path("data/day09_test.txt")  # test data
test2 = Path("data/day09_test2.txt")  # more test data
data = Path("data/day09_data.txt")  # puzzle data
In [3]:
def parse_input(fpath: Path) -> list[tuple[str]]:
    """Parse puzzle input into
    
    :param fpath: path to puzzle input
    """
    with fpath.open() as ifh:
        data = [_.strip().split() for _ in ifh.readlines()]  # list of instruction characters
        return [(_[0], int(_[1])) for _ in data]  # cast number to int
    
class Knot:
    
    """Represents a single knot on the rope
    
    It has a location and potentially follows another knot
    """
    
    def __init__(self, xpos: int=0, ypos:int=0):
        """Initialise knot with start position and empty followers
        
        :param xpos:  start x-coordinate
        :param ypos:  start y-coordinate
        """
        self.xpos = xpos
        self.ypos = ypos
        self.next = []  # holds knots that will follow directly from this knot
        
    def add_follower(self, knot: Knot) -> None:
        """Add a Knot to follow the location of this knot"""
        self.next.append(knot)
        
    def update(self, xpos: int, ypos: int) -> None:
        """Move this knot to the new position (not following another knot)
        
        :param xpos:  new x-coordinate
        :param ypos:  new y-coordinate        
        """
        self.xpos = xpos
        self.ypos = ypos
        
        # Update locations of any following knots
        for knot in self.next:
            knot.follow(self.xpos, self.ypos)
            
    def follow(self, xpos: int, ypos: int) -> None:
        """Update knot location to follow that of another linked knot
        
        :param xpos:  knot x-coordinate (being followed)
        :param ypos:  knot y-coordinate (being followed)
        """
        # account for diagonal movement first
        if abs(xpos - self.xpos) > 1 and abs(ypos - self.ypos) > 1:  # diagonal
            if xpos - self.xpos > 1 and ypos - self.ypos > 1:  # move up/right
                self.xpos += 1
                self.ypos += 1
            if xpos - self.xpos > 1 and ypos - self.ypos < 1:  # move down/right
                self.xpos += 1
                self.ypos -= 1
            if xpos - self.xpos < 1 and ypos - self.ypos > 1:  # move up/left
                self.xpos -= 1
                self.ypos += 1
            if xpos - self.xpos < 1 and ypos - self.ypos < 1:  # move down/left
                self.xpos -= 1
                self.ypos -= 1
        else:  # U/D/L/R movement
            if xpos - self.xpos > 1:  # move right
                self.xpos += 1
                self.ypos = ypos
            elif self.xpos - xpos > 1:  # move left
                self.xpos -= 1
                self.ypos = ypos
            elif ypos - self.ypos > 1:  # move up
                self.ypos += 1
                self.xpos = xpos
            elif self.ypos - ypos > 1:  # move down
                self.ypos -= 1
                self.xpos = xpos

        # Update locations of any following knots
        for knot in self.next:
            knot.follow(self.xpos, self.ypos)
            
    @property
    def position(self) -> tuple[int, int]:
        """Return current knot position"""
        return (self.xpos, self.ypos)
        

class Rope:
    
    """Represents a rope, comprising a finite number of knots"""
    
    def __init__(self, knots: int=2):
        """Instantiate a rope with a stated number of knots
        
        :param knots:  the number of knots in the rope
        """
        self.knots = []  # holds all knots in rope
        self.knots.append(Knot())  # add a head knot
        
        # append any following knots to head knot
        for _ in range(knots - 1):  
            knot = Knot()  # new knot
            self.knots[-1].add_follower(knot)  # knot follows last current knot in rope
            self.knots.append(knot)  # add knot to current list
                    
        # tail visited locations
        self.tvis = set()
        self.tvis.add(self.tail.position)
            
        
    def update_head(self, drn: str, val: int) -> None:
        """Update the position of the head knot in the rope
        
        :param drn:  movement direction
        :param val:  number of steps of movement
        
        Each step updates the head position and cascades down the rope.
        Because each step may also update the tail position, we update our
        record of this on each step.
        """
        if drn == "R":  # move head right
            for _ in range(val):
                xpos, ypos = self.head.position
                self.head.update(xpos + 1, ypos)
                self.tvis.add(self.tail.position)
        elif drn == "L":  # move head left
            for _ in range(val):
                xpos, ypos = self.head.position
                self.head.update(xpos - 1, ypos)
                self.tvis.add(self.tail.position)
        elif drn == "U":  # move head up
            for _ in range(val):
                xpos, ypos = self.head.position
                self.head.update(xpos, ypos + 1)
                self.tvis.add(self.tail.position)
        else:  #  "D" - move head down
            for _ in range(val):
                xpos, ypos = self.head.position
                self.head.update(xpos, ypos - 1)
                self.tvis.add(self.tail.position)

    @property
    def head(self) -> Knot:
        """Return head knot in rope"""
        return self.knots[0]

    @property
    def tail(self) -> Knot:
        """Return tail knot in rope"""
        return self.knots[-1]
    
    def __str__(self) -> str:
        """Return string visualising current rope position"""
        # get bounds on positions
        xmin, xmax, ymin, ymax = 0, 0, 0, 0
        for xpos, ypos in [_.position for _ in self.knots]:
            if xpos < xmin:
                xmin = xpos
            elif xpos > xmax:
                xmax = xpos
            if ypos < ymin:
                ymin = ypos
            elif ypos > ymax:
                ymax = ypos
                
        # create square numpy array bounded as above
        arrsize = max(ymax - ymin + 1, xmax - xmin + 1)
        arrshape = (arrsize, arrsize)
        arr = np.zeros(arrshape, dtype=str)
        
        # populate array with rope locations
        for idx in range(len(self.knots)-1, -1, -1):
            xpos, ypos = self.knots[idx].position
            arr[ypos, xpos] = idx
            
        # make string representation that matches puzzle
        # (clunky, but hey...)
        outstr = []
        for row in arr[::-1]:
            rstr = ""
            for elem in row:
                if elem == "":
                    rstr += "."
                elif elem == "0":
                    rstr += "H"
                else:
                    rstr += elem
            outstr.append(rstr)
                
        return '\n'.join(outstr) + "\n"
            
    
def simulate(knots: int, instructions: list[tuple[str, int]]) -> Rope:
    """Return rope after simulation
    
    :param knots:  number of knots in the rope
    :param instructions:  instructions for moving the head of the rope
    """
    # initialise start position for the rope, and empty set of
    # positions visited by the tail
    rope = Rope(knots)
    
    # move the head with each instruction
    for drn, val in instructions:
        rope.update_head(drn, val)
        
    return rope

   
    
def part1(fpath: Path):
    """Solve the puzzle
    
    :param fpath:  path to puzzle input
    """
    instructions = parse_input(fpath)
    rope = simulate(2, instructions)
    return len(rope.tvis)
In [4]:
part1(test)
Out[4]:
13
In [5]:
part1(data)
Out[5]:
6026

Part 2¶

A rope snaps! Suddenly, the river is getting a lot closer than you remember. The bridge is still there, but some of the ropes that broke are now whipping toward you as you fall through the air!

The ropes are moving too quickly to grab; you only have a few seconds to choose how to arch your body to avoid being hit. Fortunately, your simulation can be extended to support longer ropes.

Rather than two knots, you now must simulate a rope consisting of ten knots. One knot is still the head of the rope and moves according to the series of motions. Each knot further down the rope follows the knot in front of it using the same rules as before.

Using the same series of motions as the above example, but with the knots marked H, 1, 2, ..., 9, the motions now occur as follows:

== Initial State ==

......
......
......
......
H.....  (H covers 1, 2, 3, 4, 5, 6, 7, 8, 9, s)

== R 4 ==

......
......
......
......
1H....  (1 covers 2, 3, 4, 5, 6, 7, 8, 9, s)

......
......
......
......
21H...  (2 covers 3, 4, 5, 6, 7, 8, 9, s)

......
......
......
......
321H..  (3 covers 4, 5, 6, 7, 8, 9, s)

......
......
......
......
4321H.  (4 covers 5, 6, 7, 8, 9, s)

== U 4 ==

......
......
......
....H.
4321..  (4 covers 5, 6, 7, 8, 9, s)

......
......
....H.
.4321.
5.....  (5 covers 6, 7, 8, 9, s)

......
....H.
....1.
.432..
5.....  (5 covers 6, 7, 8, 9, s)

....H.
....1.
..432.
.5....
6.....  (6 covers 7, 8, 9, s)

== L 3 ==

...H..
....1.
..432.
.5....
6.....  (6 covers 7, 8, 9, s)

..H1..
...2..
..43..
.5....
6.....  (6 covers 7, 8, 9, s)

.H1...
...2..
..43..
.5....
6.....  (6 covers 7, 8, 9, s)

== D 1 ==

..1...
.H.2..
..43..
.5....
6.....  (6 covers 7, 8, 9, s)

== R 4 ==

..1...
..H2..
..43..
.5....
6.....  (6 covers 7, 8, 9, s)

..1...
...H..  (H covers 2)
..43..
.5....
6.....  (6 covers 7, 8, 9, s)

......
...1H.  (1 covers 2)
..43..
.5....
6.....  (6 covers 7, 8, 9, s)

......
...21H
..43..
.5....
6.....  (6 covers 7, 8, 9, s)

== D 1 ==

......
...21.
..43.H
.5....
6.....  (6 covers 7, 8, 9, s)

== L 5 ==

......
...21.
..43H.
.5....
6.....  (6 covers 7, 8, 9, s)

......
...21.
..4H..  (H covers 3)
.5....
6.....  (6 covers 7, 8, 9, s)

......
...2..
..H1..  (H covers 4; 1 covers 3)
.5....
6.....  (6 covers 7, 8, 9, s)

......
...2..
.H13..  (1 covers 4)
.5....
6.....  (6 covers 7, 8, 9, s)

......
......
H123..  (2 covers 4)
.5....
6.....  (6 covers 7, 8, 9, s)

== R 2 ==

......
......
.H23..  (H covers 1; 2 covers 4)
.5....
6.....  (6 covers 7, 8, 9, s)

......
......
.1H3..  (H covers 2, 4)
.5....
6.....  (6 covers 7, 8, 9, s)

Now, you need to keep track of the positions the new tail, 9, visits. In this example, the tail never moves, and so it only visits 1 position. However, be careful: more types of motion are possible than before, so you might want to visually compare your simulated rope to the one above.

Here's a larger example:

R 5
U 8
L 8
D 3
R 17
D 10
L 25
U 20

These motions occur as follows (individual steps are not shown):

== Initial State ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
...........H..............  (H covers 1, 2, 3, 4, 5, 6, 7, 8, 9, s)
..........................
..........................
..........................
..........................
..........................

== R 5 ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
...........54321H.........  (5 covers 6, 7, 8, 9, s)
..........................
..........................
..........................
..........................
..........................

== U 8 ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
................H.........
................1.........
................2.........
................3.........
...............54.........
..............6...........
.............7............
............8.............
...........9..............  (9 covers s)
..........................
..........................
..........................
..........................
..........................

== L 8 ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
........H1234.............
............5.............
............6.............
............7.............
............8.............
............9.............
..........................
..........................
...........s..............
..........................
..........................
..........................
..........................
..........................

== D 3 ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
.........2345.............
........1...6.............
........H...7.............
............8.............
............9.............
..........................
..........................
...........s..............
..........................
..........................
..........................
..........................
..........................

== R 17 ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
................987654321H
..........................
..........................
..........................
..........................
...........s..............
..........................
..........................
..........................
..........................
..........................

== D 10 ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
...........s.........98765
.........................4
.........................3
.........................2
.........................1
.........................H

== L 25 ==

..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
...........s..............
..........................
..........................
..........................
..........................
H123456789................

== U 20 ==

H.........................
1.........................
2.........................
3.........................
4.........................
5.........................
6.........................
7.........................
8.........................
9.........................
..........................
..........................
..........................
..........................
..........................
...........s..............
..........................
..........................
..........................
..........................
..........................

Now, the tail (9) visits 36 positions (including s) at least once:

..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
..........................
#.........................
#.............###.........
#............#...#........
.#..........#.....#.......
..#..........#.....#......
...#........#.......#.....
....#......s.........#....
.....#..............#.....
......#............#......
.......#..........#.......
........#........#........
.........########.........

Simulate your complete series of motions on a larger rope with ten knots. How many positions does the tail of the rope visit at least once?

In [6]:
def part2(fpath: Path) -> int:
    """Solve the puzzle
    
    :param fpath:  path to puzzle input
    """
    instructions = parse_input(fpath)
    rope = simulate(10, instructions)
    return len(rope.tvis)
In [7]:
part2(test)
Out[7]:
1
In [8]:
part2(test2)
Out[8]:
36
In [9]:
part2(data)
Out[9]:
2273
In [ ]: