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

day-9

advent_of_code_2022/day9.livemd

day-9

Mix.install([
  {:kino, "~> 0.7.0"}
])

Input

input = Kino.Input.textarea("Input")

Data

moves =
  input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.map(fn move ->
    [dir, steps] = String.split(move, " ", trim: true)
    {dir, String.to_integer(steps)}
  end)

Logic

All logic is contained within a module. We can call Traveler.travel for a normal run, or Traveler.debug for a run where the board is printed after every move.

The way this was done was, for part 1, an implementation of just a head and tail was written.

Once part 2 was introduced, the implementation was generalized to support a list of knots.

defmodule Traveler do
  @doc "Run all the moves and return the end result"
  def travel(moves, knots) when is_list(moves) and knots > 1 do
    Enum.reduce(moves, initial(knots - 1), &perform_move/2)
  end

  @doc """
  Run all the moves, return the end result 
  and print the board after every step
  """
  def debug(moves, knots) when is_list(moves) and knots > 1 do
    Enum.reduce(moves, initial(knots - 1), &perform_move(&1, &2, true))
  end

  defp perform_move(move, acc, debug \\ false)

  # move by one step
  defp perform_move({dir, 1}, acc, debug) do
    head = move_head(acc.head, dir)

    {knots, _acc} =
      Enum.map_reduce(acc.knots, head, fn
        curr, prev ->
          knot = move_knot(prev, curr)
          {knot, knot}
      end)

    tail = Enum.at(knots, -1)
    visited = MapSet.put(acc.visited, {tail.x, tail.y})

    if debug, do: print(%{head: head, knots: knots, visited: visited})

    %{head: head, knots: knots, visited: visited}
  end

  # move by multiple steps by running the 1 step clause repeatedly
  defp perform_move({dir, s}, acc, debug) when s > 1 do
    1..s
    |> Enum.map(fn _ -> {dir, 1} end)
    |> Enum.reduce(acc, &perform_move(&1, &2, debug))
  end

  # Moving the head is completely straightforward. 
  # Just modify the appropriate coordinate by 1.
  defp move_head(%{x: x, y: y}, "U"), do: %{x: x, y: y + 1}
  defp move_head(%{x: x, y: y}, "D"), do: %{x: x, y: y - 1}
  defp move_head(%{x: x, y: y}, "L"), do: %{x: x - 1, y: y}
  defp move_head(%{x: x, y: y}, "R"), do: %{x: x + 1, y: y}

  # Moving the knot is more complicated. We look at distance
  # from the knot that comes before and perform an appropriate move.
  defp move_knot(h, t) do
    x_diff = h.x - t.x
    y_diff = h.y - t.y

    cond do
      # we are too far on both the x and y axis
      abs(x_diff) > 1 && abs(y_diff) > 1 ->
        x_move = floor(abs(x_diff) / x_diff)
        y_move = floor(abs(y_diff) / y_diff)
        %{x: t.x + x_move, y: t.y + y_move}

      # we are too far on just the x axis
      abs(x_diff) > 1 ->
        x_move = floor(abs(x_diff) / x_diff)
        %{x: t.x + x_move, y: h.y}

      # we are too far on just the y axis
      abs(y_diff) > 1 ->
        y_move = floor(abs(y_diff) / y_diff)
        %{x: h.x, y: t.y + y_move}

      # we are adjacent to the next knot
      true ->
        t
    end
  end

  # Setup initial state of the board.
  # The head and all the knots are at {0,0} and
  # this coordinate is considered visited.
  defp initial(knot_count) do
    %{
      visited: MapSet.new([{0, 0}]),
      head: %{x: 0, y: 0},
      knots: Enum.map(1..knot_count, fn _ -> %{x: 0, y: 0} end)
    }
  end

  @doc """
  Print the contents of the board in a 2d grid
  """
  def print(%{head: head, knots: knots, visited: visited}) do
    points =
      [{head.x, head.y}]
      |> Enum.concat(Enum.map(knots, &{&1.x, &1.y}))
      |> Enum.concat(visited)

    xs = Enum.map(points, &Kernel.elem(&1, 0))
    ys = Enum.map(points, &Kernel.elem(&1, 1))

    Enum.map(Enum.min(ys)..Enum.max(ys), fn y ->
      Enum.min(xs)..Enum.max(xs)
      |> Enum.map(fn x ->
        knot_index = Enum.find_index(knots, fn k -> k.x == x && k.y == y end)
        visited_index = Enum.find_index(visited, fn {v_x, v_y} -> v_x == x && v_y == y end)

        cond do
          x == head.x && y == head.y -> "H"
          knot_index != nil -> "#{knot_index + 1}"
          visited_index != nil -> "#"
          true -> "."
        end
      end)
      |> Enum.join("")
    end)
    |> Enum.reverse()
    |> Enum.join("\n")
    |> IO.puts()

    IO.puts("\n")
  end
end

Part 1

data = Traveler.travel(moves, 2)
Enum.count(data.visited)

Part 2

data = Traveler.travel(moves, 10)
Enum.count(data.visited)