Advent of Code 2022 - Day 9

Took a while, but another use case for “move vectors” today and pattern matching. :slight_smile:

The trick was to first generate a list of position for the head with the help of the instructions.
Then you you use that list to move the tail, and repeat that process for as many knots there are… a perfect job for Enum.scan :slight_smile:

Quite happy with this solution.

You’re not supposed to share the problem text or input according to the AoC ToS. Although that’s probably a lost cause by now :see_no_evil:

Enjoyed this one. Part 2 took me by surprise, but it was fun. I managed to reuse the functions from part 1, just updated motion function to keep track of the intermediary knots in a list, with map_reduce to simultaneously enumerate the knots + track the previous one.

  defp step_head({x, y}, "R"), do: {x + 1, y}
  defp step_head({x, y}, "L"), do: {x - 1, y}
  defp step_head({x, y}, "U"), do: {x, y + 1}
  defp step_head({x, y}, "D"), do: {x, y - 1}

  defp step_tail({hx, hy}, {tx, ty}) when abs(hx - tx) <= 1 and abs(hy - ty) <= 1, do: {tx, ty}
  defp step_tail({tx, hy}, {tx, ty}) when hy - ty > 0, do: {tx, ty + 1}
  defp step_tail({tx, hy}, {tx, ty}) when hy - ty < 0, do: {tx, ty - 1}
  defp step_tail({hx, ty}, {tx, ty}) when hx - tx > 0, do: {tx + 1, ty}
  defp step_tail({hx, ty}, {tx, ty}) when hx - tx < 0, do: {tx - 1, ty}
  defp step_tail({hx, hy}, {tx, ty}) do
    dx = if hx - tx > 0, do: 1, else: -1
    dy = if hy - ty > 0, do: 1, else: -1
    {tx + dx, ty + dy}
  end

1 Like

@kwando That’s a nice use of Enum.scan. As you said, perfect for the task :slight_smile:

It was really fun coding this ̶s̶n̶a̶k̶e̶ rope-thing simulator (code below is part 2)

defmodule Day9 do

  def move_head({x, y}, "L"), do: {x - 1, y}
  def move_head({x, y}, "R"), do: {x + 1, y}
  def move_head({x, y}, "U"), do: {x, y + 1}
  def move_head({x, y}, "D"), do: {x, y - 1}

  def move_toward(prev, current) do
    if prev - current > 0, do: current + 1, else: current - 1
  end

  def move_knot({x_prev, y_prev}, {x, y}) do
    cond do
      abs(x_prev - x) <= 1 and abs(y_prev - y) <= 1 -> {x, y}  # adjacent
      x == x_prev -> {x, move_toward(y_prev, y)}               # same line
      y == y_prev -> {move_toward(x_prev, x), y}               # same column
      true -> {move_toward(x_prev, x), move_toward(y_prev, y)} # not touching
    end
  end

  def start() do

    moves = File.stream!("input09")
    |> Stream.map(&String.trim/1)
    |> Enum.map(&String.split/1)
    |> Enum.flat_map(fn [dir, step] -> for _ <-1..String.to_integer(step), do: dir end)

    knots = for _ <- 1..9, do: {0, 0}

    {_, _, tail_history} = Enum.reduce(moves , {{0, 0}, knots, []}, fn dir, {head, knots, tail_history} ->
      head = move_head(head, dir)
      {knots, tail} = Enum.map_reduce(knots, head, fn knot, prev_knot ->
        knot = move_knot(prev_knot, knot)
        {knot, knot}
      end)

      {head, knots, [tail | tail_history]}
    end)

    tail_history |> Enum.uniq |> Enum.count
  end
end
2 Likes

Oh, didnt know that. Gonna stop adding that to my livebooks :slight_smile: thanks for the heads up!

1 Like

This one was the most fun I’ve had in a long time :slight_smile:

After a lot of refactoring I’m solving both parts using the same function and I’m happy with my code :slight_smile:

  defp move([dir | rest], [head | knots], tail_path) do
    new_head = move_head(dir, head)
    new_rope = Enum.scan([new_head | knots], fn tail, head -> follow(head, tail) end)
    move(rest, new_rope, MapSet.put(tail_path, Enum.at(new_rope, -1)))
  end

Full solution on GitHub

2 Likes

You are expanding out the directions, right? So if the head moves up 10k in one movement, you will have 10k up instructions in your list. And if another rope snaps and there are more knots, that’s a lot of work to traverse the whole rope to find the last one each time. Some points to consider if you want to spend more time refactoring :smile:

I spent a lot of time refactoring my initial part 1 solution after reading the part 2 prompt and realizing I should be able to reuse the solution for both with the rope length as a parameter. Mostly had to change my data structure from %{head: {x,y}, tail: {m,n}, visited: MapSet<>} to %{rope: [], visited: MapSet<>}. I’m still not pleased about reversing the list of knots on every move. Curious to see how others managed to avoid that.

This was actually more fun than I initially expected having read part 1 yesterday and opting to skip it till the weekend. I did need to refactor much for part 2. Just needed to replace reducing over {0, 0} and a single move with List.duplicate({0, 0}, 9) and doing the move over the list as well. List.duplicate together with Enum.flat_map also turned out to be useful for the instruction list.

Solution
defmodule Day9 do
  # {0, 0} is in left/bottom corner
  def parse(text) do
    ~r/^([RULD]) (\d+?)$/m
    |> Regex.scan(text)
    |> Enum.flat_map(fn [_full, direction, amount] ->
      amount = String.to_integer(amount)

      dir =
        case direction do
          "R" -> List.duplicate({1, 0}, amount)
          "L" -> List.duplicate({-1, 0}, amount)
          "U" -> List.duplicate({0, 1}, amount)
          "D" -> List.duplicate({0, -1}, amount)
        end
    end)
  end

  def count_followed_places(text, tail_segments \\ 1)
      when tail_segments >= 1 do
    movements = parse(text)

    movements
    # Add start
    |> List.insert_at(0, {0, 0})
    |> Enum.map_reduce({0, 0}, fn {delta_x, delta_y}, {x, y} ->
      next = {x + delta_x, y + delta_y}
      {next, next}
    end)
    |> elem(0)
    |> Enum.map_reduce(List.duplicate({0, 0}, tail_segments), fn head, tails ->
      tails =
        Enum.map_reduce(tails, head, fn tail, head ->
          next = calculate_next(head, tail)
          {next, next}
        end)
        |> elem(0)

      {List.last(tails), tails}
    end)
    |> elem(0)
    |> Enum.uniq()
    |> Enum.count()
  end

  def calculate_next(head, tail) do
    case {head, tail} do
      # Touching or on top of each other
      {{head_x, head_y}, {tail_x, tail_y}}
      when abs(head_x - tail_x) <= 1 and abs(head_y - tail_y) <= 1 ->
        {tail_x, tail_y}

      # Move into direction of head
      {{head_x, head_y}, {tail_x, tail_y}} ->
        {
          tail_x + direction(head_x, tail_x),
          tail_y + direction(head_y, tail_y)
        }
    end
  end

  defp direction(a, b) when a == b, do: 0
  defp direction(a, b) when a > b, do: 1
  defp direction(a, b) when a < b, do: -1
end

I forgot to mention I was refactoring mainly for beauty :wink: It looks like we’ll get to enjoy enough of 10k optimizing in Day 11, though :smile:

3 Likes

Have to admit I was stuck for a minute on the math and then traveling so now I’m really behind :grimacing:

But I agree with others this was a fun one, very satisfying to solve: