Advent of Code 2022, Day 9: Rope Bridge

#ruby #advent of code 2022

Part A

Day 9 is about simulating snake-like game. We have a rope with two knots tied at both ends. We can call them head and tail. There is a restriction that head and tail should always be touching, either in horizontal, vertical or diagonal direction. If the head moves, the tail should follow. Tail can also overlap with head. Here are some examples:

....
.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...    .....
.....    .....    .....

Our task is to simulate head and tail movements and then output how many positions tail visited at least once. The input for this task looks like this:

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

First column is the direction and second is number of steps. Head and tail both start at the same position (0, 0). We can parse the input:

data = File.readlines("9.txt", chomp: true).map do |row|
  parts = row.split(" ")
  [parts[0], parts[1].to_i]
end

head = [0, 0]
tail = [0, 0]

And build some helper methods:

def add(position, move)
  [
    position[0] + move[0],
    position[1] + move[1]
  ]
end

def distance(head, tail)
  [(head[0] - tail[0]).abs, (head[1] - tail[1]).abs]
end

def adj(head, tail)
  distance(head, tail).all? { |elem| elem < 2 }
end

def approach(head, tail)
  if head[0] == tail[0]
    [
      0,
      (head[1] - tail[1]) / (head[1] - tail[1]).abs
    ]
  elsif head[1] == tail[1]
    [
      (head[0] - tail[0]) / (head[0] - tail[0]).abs,
      0
    ]
  else
    [
      (head[0] - tail[0]) / (head[0] - tail[0]).abs,
      (head[1] - tail[1]) / (head[1] - tail[1]).abs
    ]
  end
end

add method updates position by the given move. Position is array with two elements (x and y) and move is an offset represented also by array with two elements (offset x and y).

distance method calculates the distance in the x and y directions and returns it as array.

adj checks if head is adjacent to tail, either vertical, horizontal or diagonal. For that to be true both x and y distances must be 0 or 1.

approach method is performing the tail move so it is always adjacent to the head. If head and tail x position is the same we need to move by one step in the y direction. If y position is the same, we need to move in x direction. And in other case tail needs to move in the diagonal direction.

Having above helper methods we can write our solution:

visited = {}
moves = {
  "U" => [0, 1],
  "R" => [1, 0],
  "D" => [0, -1],
  "L" => [-1, 0]
}

visited[tail] = true

data.each do |(move, steps)|
  steps.times do
    head = add(head, moves[move])
    if !adj(head, tail)
      tail = add(tail, approach(head, tail))
      visited[tail] = true
    end
  end
end

puts visited.keys.size

Here we are keeping the list of visited positions in a hash and a little hash with mapped moves. Then we iterate over input data, we update position of the head according to the move and then if tail is no longer adjacent to head, we update it’s position as well. Along the way we are tracking positions visited by the tail.

Here is the full code:

data = File.readlines("9.txt", chomp: true).map do |row|
  parts = row.split(" ")
  [parts[0], parts[1].to_i]
end

head = [0, 0]
tail = [0, 0]

def add(position, move)
  [
    position[0] + move[0],
    position[1] + move[1]
  ]
end

def distance(head, tail)
  [(head[0] - tail[0]).abs, (head[1] - tail[1]).abs]
end

def adj(head, tail)
  distance(head, tail).all? { |elem| elem < 2 }
end

def approach(head, tail)
  if head[0] == tail[0]
    [
      0,
      (head[1] - tail[1]) / (head[1] - tail[1]).abs
    ]
  elsif head[1] == tail[1]
    [
      (head[0] - tail[0]) / (head[0] - tail[0]).abs,
      0
    ]
  else
    [
      (head[0] - tail[0]) / (head[0] - tail[0]).abs,
      (head[1] - tail[1]) / (head[1] - tail[1]).abs
    ]
  end
end

visited = {}
moves = {
  "U" => [0, 1],
  "R" => [1, 0],
  "D" => [0, -1],
  "L" => [-1, 0]
}

visited[tail] = true

data.each do |(move, steps)|
  steps.times do
    head = add(head, moves[move])
    if !adj(head, tail)
      tail = add(tail, approach(head, tail))
      visited[tail] = true
    end
  end
end

puts visited.keys.size

Part B

In Part B instead of head and tail we have 10 knots and they need to follow the same rules as before and be adjacent. So if the first note moves the second needs to follow to be adjacent, and then the third and so on.

Most of the code stays the same. We just need to extend the final loop to simulate all the knots. Here is the full code:

data = File.readlines("9.txt", chomp: true).map do |row|
  parts = row.split(" ")
  [parts[0], parts[1].to_i]
end

knots = [
  [0, 0],
  [0, 0],
  [0, 0],
  [0, 0],
  [0, 0],
  [0, 0],
  [0, 0],
  [0, 0],
  [0, 0],
  [0, 0]
]

def add(position, move)
  [
    position[0] + move[0],
    position[1] + move[1]
  ]
end

def distance(head, tail)
  [(head[0] - tail[0]).abs, (head[1] - tail[1]).abs]
end

def adj(head, tail)
  distance(head, tail).all? { |elem| elem < 2 }
end

def approach(head, tail)
  if head[0] == tail[0]
    [
      0,
      (head[1] - tail[1]) / (head[1] - tail[1]).abs
    ]
  elsif head[1] == tail[1]
    [
      (head[0] - tail[0]) / (head[0] - tail[0]).abs,
      0
    ]
  else
    [
      (head[0] - tail[0]) / (head[0] - tail[0]).abs,
      (head[1] - tail[1]) / (head[1] - tail[1]).abs
    ]
  end
end

visited = {}
moves = {
  "U" => [0, 1],
  "R" => [1, 0],
  "D" => [0, -1],
  "L" => [-1, 0]
}

visited[[0, 0]] = true

data.each do |(move, steps)|
  steps.times do
    knots[0] = add(knots[0], moves[move])
    1.upto(9) do |index|
      if !adj(knots[index - 1], knots[index])
        knots[index] = add(knots[index], approach(knots[index - 1], knots[index]))

        if index == 9
          visited[knots[index]] = true
        end
      end
    end
  end
end

puts visited.keys.size