Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 9

2022/day_9.livemd

Day 9

Setup

Mix.install([:kino])
input =
  Kino.Input.textarea("Paste input here",
    default: """
    R 4
    U 4
    L 3
    D 1
    R 4
    D 1
    L 5
    R 2
    """
  )
instructions =
  Kino.Input.read(input)
  |> String.split("\n", trim: true)
  |> Stream.map(&String.split(&1, " ", trim: true))
  |> Stream.map(&List.to_tuple/1)
  |> Enum.map(fn {direction, distance} ->
    {direction |> String.downcase() |> String.to_atom(), String.to_integer(distance)}
  end)
defmodule Printer do
  def print_rope(rope) do
    {min_x, min_y, max_x, max_y} =
      Enum.reduce(rope, {1, 1, 1, 1}, fn {x, y}, {left, down, right, up} ->
        {min(left, x - 1), min(down, y - 1), max(right, x + 1), max(up, y + 1)}
      end)

    [{hx, hy} | tail] = rope

    min_y..max_y
    |> Enum.map(fn y ->
      min_x..max_x
      |> Enum.map(fn x ->
        cond do
          x == hx and y == hy -> "H"
          Enum.any?(tail, fn {tx, ty} -> x == tx and y == ty end) -> "#"
          true -> "•"
        end
      end)
      |> Enum.join("")
    end)
    |> Enum.reverse()
    |> Enum.join("\n")
    |> Kernel.<>("\n\n")
    |> IO.puts()

    rope
  end
end
defmodule Rope do
  def move(rope, {_direction, 0}, tail_positions) do
    Printer.print_rope(rope)
    {rope, tail_positions}
  end

  def move([head | tail], {direction, steps}, tail_positions) do
    new_rope =
      Enum.scan([step(head, direction) | tail], fn el, acc ->
        follow(acc, el)
      end)

    [new_tail | _] = Enum.reverse(new_rope)

    # IO.inspect({direction, steps})
    # Printer.print_rope(new_rope)
    move(new_rope, {direction, steps - 1}, MapSet.put(tail_positions, new_tail))
  end

  # overlap, no move
  def follow({x, y}, {x, y}), do: {x, y}
  # same row, move tail left or right
  def follow({hx, y}, {tx, y}) when abs(hx - tx) == 1, do: {tx, y}
  def follow({hx, y}, {tx, y}) when hx > tx, do: {tx + 1, y}
  def follow({hx, y}, {tx, y}) when hx < tx, do: {tx - 1, y}
  # same col, move tail up or down
  def follow({x, hy}, {x, ty}) when abs(hy - ty) == 1, do: {x, ty}
  def follow({x, hy}, {x, ty}) when hy > ty, do: {x, ty + 1}
  def follow({x, hy}, {x, ty}) when hy < ty, do: {x, ty - 1}

  # diagonal neighbour, no move
  def follow({hx, hy}, {tx, ty}) when abs(hx - tx) + abs(hy - ty) == 2, do: {tx, ty}

  # move tail closer diagonally
  def follow({hx, hy}, {tx, ty}) do
    {dx, dy} = {hx - tx, hy - ty}

    {tx + div(dx, abs(dx)), ty + div(dy, abs(dy))}
  end

  defp step({x, y}, :u), do: {x, y + 1}
  defp step({x, y}, :d), do: {x, y - 1}
  defp step({x, y}, :l), do: {x - 1, y}
  defp step({x, y}, :r), do: {x + 1, y}
end

Part 1

rope = [{0, 0}, {0, 0}]

{_, visited} =
  instructions
  |> Enum.reduce({rope, MapSet.new()}, fn ins, {rope, visited} ->
    Rope.move(rope, ins, visited)
  end)

MapSet.size(visited)

Part 2

rope = 1..10 |> Enum.map(fn _ -> {0, 0} end)

{final_rope, visited} =
  instructions
  |> Enum.reduce({rope, MapSet.new()}, fn instruction, {rope, visited} ->
    Rope.move(rope, instruction, visited)
  end)

Enum.count(visited)