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)

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)