[Advent Of Code 2022] Day 9: Rope Bridge [SPOILERS]

I had fun refactoring this.

Ruby Solution
require 'set'

def num_visited(num_knots)
  knots = Array.new(num_knots) { [0, 0] }
  visited = Set.new([knots.last.dup])

  moves = File.readlines('inputs/09.txt', chomp: true).map { |line| line.split(' ').then { [_1, _2.to_i] } }
  moves.each do |move, distance|
    distance.times do
      case move
      when 'R' then knots[0][0] += 1
      when 'L' then knots[0][0] -= 1
      when 'U' then knots[0][1] += 1
      when 'D' then knots[0][1] -= 1
      end

      knots.each_cons(2) do |leader, follower|
        if (leader[1] - follower[1]).abs > 1 || (leader[0] - follower[0]).abs > 1
          follower[0] += leader[0] <=> follower[0]
          follower[1] += leader[1] <=> follower[1]
        end
      end

      visited << knots.last.dup
    end
  end

  visited.size
end

a = num_visited(2)
b = num_visited(10)

require 'minitest/autorun'

describe 'day 09' do
  it 'part a' do assert_equal 6_023, a end
  it 'part b' do assert_equal 2_533, b end
end

I could use my Pos and Delta classes for the positions and movements of the knots.

Python Solution
directions = dict(zip('ULRD', dirs4))


def read_file(file: TextIO) -> list[tuple[Delta, int]]:
    """Read motions from the `file`."""
    lines = (line.split() for line in file)
    return [(directions[direction], int(steps)) for direction, steps in lines]


def move(knot: Pos, previous: Pos) -> Pos:
    """Calculate the new position of the knot, based on the previous knot."""
    delta = previous - knot
    assert delta.chessboard_length() <= 2
    if delta.chessboard_length() <= 1:
        return knot
    return knot + Delta(dx=sign0(delta.dx), dy=sign0(delta.dy))


def count_nth_knot_positions(
        motions: list[tuple[Delta, int]], length: int) -> int:
    """Count the different positions of the last knot during the `motions`."""
    positions = set((origin, ))
    knots = [origin] * length
    for direction, count in motions:
        for _ in range(count):
            knots[0] = knots[0] + direction
            for i in range(1, length):
                knots[i] = move(knots[i], knots[i - 1])
            positions.add(knots[-1])
    return len(positions)


def part1(file: TextIO) -> int:
    """Solve the first part of the puzzle."""
    motions = read_file(file)
    return count_nth_knot_positions(motions, 2)


def part2(file: TextIO) -> int:
    """Solve the second part of the puzzle."""
    motions = read_file(file)
    return count_nth_knot_positions(motions, 10)

Once again, using complex values for my points made my life so much simpler!

I did need to add a cmp(a, b) function to my library for this (returns -1 | 0 | 1 based on a < b | a == b | a > b). Many languages have this built in.

Code: FinalJust Make It Work

Python Solution
    def solver(self, lines: list[tuple[str, int]], knot_count: int) -> int:
        """Track the movement of knots on a rope. Return the number of positions the tail visits."""
        points: set[complex] = set([0])
        knots: list[complex] = [0] * knot_count
        movement = {"R": 1, "L": -1, "U": 1j, "D": -1j}
        for direction, unit in lines:
            for _ in range(unit):
                knots[0] += movement[direction]
                for i, (a, b) in enumerate(zip(knots, knots[1:])):
                    if a - b in aoc.NEIGHBORS:
                        continue
                    x = self.cmp(a.real, b.real)
                    y = self.cmp(a.imag, b.imag)
                    knots[i + 1] += complex(x, y)
                points.add(knots[-1])
        return len(points)