Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

Advent of Code - 2022

This is a solution to Day 9 of Advent of Code 2022.

Day 9 - Rope Bridge

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.

Read input

Let's start by reading the input which today is a namedtuple of directions and distances separated by a whitespace. And the distance is an integer.

from utils import read_input
from collections import namedtuple

Step = namedtuple('Step', ['direction', 'distance'])

def transformer(line):
    direction, distance = line.split(' ')
    return Step(direction, int(distance))

steps = read_input(9, transformer)
example = read_input(9, transformer, True)

Part 1

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

Ah, this was super fun. I'm getting more and more excited about these "moving in the grid" puzzles in Advent of Code. Partly because I'm confident with 2D grids, partly because there no tree structures/recursion (unless there's some pathfinding) and partly because it usually means I get to use pattern matching.

This time, I wrote two helper functions in addition to the main traversing function to keep the code easier to read, hiding all the maths behind nice sounding names.

is_adjacent checks whether two knots are adjacent to each other. If they are, then in move, we don't need to move the tail.

The move function takes care of calculating the new positions based on the direction of the step. Using pattern matching we move either the x or y coordinate to the right direction of both the head and the tail if necessary. This grid is infinite, so there's never need for any kind of edge detection.

The main logic is in traverse which simulates all the steps, collects the spots where tail has already gone and returns the result.

from collections import namedtuple

Position = namedtuple('Position', ['x', 'y'])

def is_adjacent(head, tail):
    return abs(head.x - tail.x) <= 1 and abs(head.y - tail.y) <= 1

def move(head, tail, step):
    match step.direction:
        case 'U':
            new_head = Position(head.x, head.y + 1)
            if(is_adjacent(new_head, tail)):
                new_tail = tail
            else:
                new_tail = Position(new_head.x, new_head.y - 1)
        case 'D':
            new_head = Position(head.x, head.y - 1)
            if(is_adjacent(new_head, tail)):
                new_tail = tail
            else:
                new_tail = Position(new_head.x, new_head.y + 1)
        case 'L':
            new_head = Position(head.x-1, head.y)
            if(is_adjacent(new_head, tail)):
                new_tail = tail
            else:
                new_tail = Position(new_head.x + 1, new_head.y)
        case 'R':
            new_head = Position(head.x + 1, head.y)
            if(is_adjacent(new_head, tail)):
                new_tail = tail
            else:
                new_tail = Position(new_head.x - 1, new_head.y)
    return new_head, new_tail

def traverse(steps, _debug=False):
    head = Position(0, 0)
    tail = Position(0, 0)
    tail_spots = set()

    for step in steps:
        for i in range(step.distance):
            if _debug:
                debug((head, tail))
            head, tail = move(head, tail, step)
            tail_spots.add(tail)

    return tail_spots
def debug(points):
    xs = [p.x for p in points]
    ys = [p.y for p in points]
    min_x, max_x = min(0, min(xs)), max(5, max(xs))
    min_y, max_y = min(0, min(ys)), max(5, max(ys))

    locations = {}
    for point in points:
        locations[(point.x, point.y)] = True

    for y in range(min_y, max_y+1):
        for x in range(min_x, max_x+1):
            try:
                locations[(x, y)]
                print('#', end="")
            except KeyError:
                print('.', end="")
        print()
    print()
visited_spots = traverse(steps)
print(f'Part 1: {len(visited_spots)}')
assert len(visited_spots) == 6357

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?

When I first saw the start of the second part, I was so excited because I thought I already had a pretty generalized solution. And I quite have.

But I learned I had made an incorrect assumption and skipped some reading. When the tail "catches up", in part 1 I assumed it was based on the direction of the movement of the head but in this second part we learn that it in fact is not. For the sake of preserving my original solution, I'm not gonna refactor the part 1 solution to a generic one but will solve this one as a separate code, even if it means I'll repeat most of it.

So first I read about how it should move and wrote a solution. It was incorrect but every now and then, the example code worked (to my amusement, often for the wrong reasons).

What had happened is, when I tried to implement the new movement, I forgot to cover cases where the knot and its following knot were on the same axis (a.x == b.x or a.y == b.y) so my rope turned into something you could find in Dr. Strange comics or movies rather than our Christmasland.

You can see from my increased unit tests and examples that it took a bit of visualizing to get it right.

from collections import namedtuple
from itertools import pairwise

def move_head(head, step):
    """
    >>> move_head(Position(0, 0), Step('U', 1))
    Position(x=0, y=1)
    >>> move_head(Position(0, 0), Step('D', 1))
    Position(x=0, y=-1)
    >>> move_head(Position(0, 0), Step('L', 1))
    Position(x=-1, y=0)
    >>> move_head(Position(0, 0), Step('R', 1))
    Position(x=1, y=0)
    """
    match step.direction:
        case 'U':
            return Position(head.x, head.y + 1)
        case 'D':
            return Position(head.x, head.y - 1)
        case 'L':
            return Position(head.x - 1, head.y)
        case 'R':
            return Position(head.x + 1, head.y)

def adjust(a, b):
    """
    >>> adjust(Position(2, 3), Position(1, 1))
    Position(x=2, y=2)
    >>> adjust(Position(1, 1), Position(2, 3))
    Position(x=1, y=2)
    >>> adjust(Position(3, 2), Position(1, 1))
    Position(x=2, y=2)
    >>> adjust(Position(1, 1), Position(3, 2))
    Position(x=2, y=1)
    >>> adjust(Position(0, 0), Position(1, 1))
    Position(x=1, y=1)
    >>> adjust(Position(0, 2), Position(0, 0))
    Position(x=1, y=0)
    """
    """
    Origin is bottom-left
    Case 1: Position(2, 3), Position(1, 1)
    . . . . .
    . . H . .
    . . . . .
    . T . . .
    . . . . .
    =>
    . . . . .
    . . H . .
    . . T . .
    . . . . .
    . . . . .

    Case 2: Position(1, 1), Position(2, 3)
    . . . . .
    . . T . .
    . . . . .
    . H . . .
    . . . . .
    =>
    . . . . .
    . . . . .
    . T . . .
    . H . . .
    . . . . .

    Case 3: Position(3, 2), Position(1, 1)
    . . . . .
    . . . . .
    . . . H .
    . T . . .
    . . . . .
    =>
    . . . . .
    . . . . .
    . . T H .
    . . . . .
    . . . . .

    Case 4: Position(1, 1), Position(3, 2)
    . . . . .
    . . . . .
    . . . T .
    . H . . .
    . . . . .
    =>
    . . . . .
    . . . . .
    . . . . .
    . H T . .
    . . . . .

    Case 5: Position(0, 2), Position(0, 0)
    . . . . .
    . . . . .
    . . . . .
    . . . . .
    T . H . .
    =>
    . . . . .
    . . . . .
    . . . . .
    . . . . .
    . T H . .

    """
    assert abs(a.x - b.x) <= 3 and abs(a.y - b.y) <= 3
    if(is_adjacent(a, b)):
        return b

    # Move up
    if a.y > b.y:
        # Move up-left
        if a.x < b.x:
            return Position(b.x-1, b.y+1)
        # Move up-right
        elif a.x > b.x:
            return Position(b.x+1, b.y+1)
        # Move up
        else:
            return Position(b.x, b.y + 1)
    # Move down
    elif a.y < b.y:
        # Move down-left
        if a.x < b.x:
            return Position(b.x-1, b.y-1)
        # Move down-right
        elif a.x > b.x:
            return Position(b.x+1, b.y-1)
        # Move down
        else:
            return Position(b.x, b.y-1)
    # Move left or right
    else:
        if a.x < b.x:
            return Position(b.x-1, b.y)
        else:
            return Position(b.x+1, b.y)

def traverse2(steps, knots_=10):
    """
    >>> steps = [Step('R', 4), Step('U', 4), Step('L', 3), Step('D', 1), Step('R', 4), Step('D', 1), Step('L', 5), Step('R', 2)]
    >>> #traverse2(steps, 10)
    #{Position(x=0, y=0)}
    >>> steps = [Step('R', 5), Step('U', 8)]
    >>> expected = {Position(x=0, y=0) }
    >>> steps = [Step('R', 5), Step('U', 8), Step('L', 8), Step('D', 3), Step('R', 17), Step('D', 10), Step('L', 25), Step('U', 20)]
    >>> expected = { Position(0, 0), Position(1, 1), Position(2, 2), Position(1, 3), Position(2, 4), Position(3, 5), Position(4, 5), Position(5, 5), Position(6, 4), Position(7, 3), Position(8, 2), Position(9, 0), Position(8, -1), Position(7, -2), Position(6, -3), Position(5, -4), Position(4, -5), Position(3, -5), Position(2, -5), Position(1, -5), Position(0, -5), Position(-1, -5), Position(-2, -5), Position(-3, -4), Position(-4, -3), Position(-5, -2), Position(-6, -1), Position(-7, 0), Position(-8, 1), Position(-9, 2), Position(-10, 3), Position(-11, 4), Position(-11, 5), Position(-11, 6) }
    >>> traverse2(steps, 10)
    """
    knots = [Position(0, 0) for _ in range(knots_)]
    assert len(knots) == knots_
    tail_spots = set()

    for step in steps:
        for i in range(step.distance):
            knots[0] = move_head(knots[0], step)
            for idx in range(1, len(knots)):
                in_position = knots[idx-1]
                to_move = knots[idx]
                new_location = adjust(in_position, to_move)
                knots[idx] = new_location
            tail_spots.add(knots[-1])
    return tail_spots
ten_knot_visited = len(traverse2(steps))
print(f'Part 2: {ten_knot_visited}')
assert ten_knot_visited == 2627

Appendix A - Part 2 is generalized

And just to prove that the second part also solves the first:

visited_spots = traverse2(steps, 2)
print(f'Part 1: {len(visited_spots)}')
assert len(visited_spots) == 6357