aoc2022/day9.livemd
2023-01-02 19:53:13 +01:00

3.9 KiB

AOC 2022 - Day 9

Mix.install([
  {:req, "~> 0.3.3"},
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.7"}
])

alias VegaLite, as: Vl

Puzzle description

Day 9: Rope Bridge.

Input

defmodule Load do
  def input do
    aoc_session = System.fetch_env!("LB_AOC_SESSION")
    input_url = "https://adventofcode.com/2022/day/9/input"
    Req.get!(input_url, headers: [cookie: "session=#{aoc_session}"]).body
  end
end

Solution

defmodule Util do
  def process(input) do
    input
    |> String.split()
    |> Enum.chunk_every(2)
    |> Enum.map(&parse_motion/1)
  end

  defp parse_motion(["U", amount]), do: {{0, 1}, String.to_integer(amount)}
  defp parse_motion(["D", amount]), do: {{0, -1}, String.to_integer(amount)}
  defp parse_motion(["L", amount]), do: {{-1, 0}, String.to_integer(amount)}
  defp parse_motion(["R", amount]), do: {{1, 0}, String.to_integer(amount)}

  def plot_locations(positions) do
    data =
      positions
      |> Enum.map(fn {x, y} -> %{x: x, y: y} end)

    chart =
      Vl.new(width: 500, height: 400)
      |> Vl.mark(:square)
      |> Vl.encode_field(:x, "x", type: :ordinal, title: "X-coordinate", axis: [label_angle: 0])
      |> Vl.encode_field(:y, "y", type: :ordinal, title: "Y-coordinate", sort: :descending)
      |> Vl.config(view: [stroke: nil])
      |> Kino.VegaLite.new()
      |> Kino.render()

    for position <- data do
      Kino.VegaLite.push(chart, position)
      Process.sleep(1)
    end

    positions
  end
end
defmodule RopeSimulator do
  def simulate(instructions, knots \\ 2) do
    {_final_pos, trail} =
      for i <- instructions, reduce: {{0, 0}, []} do
        {pos, trail} ->
          new_trail = move(pos, i, [])
          new_pos = hd(new_trail)
          {new_pos, new_trail ++ trail}
      end

    trail = Enum.reverse(trail)

    for _ <- 2..knots, reduce: trail do
      trail -> Enum.scan(trail, {0, 0}, fn head, tail -> move_tail(head, tail) end)
    end
    |> MapSet.new()
  end

  def move({x, y}, {_dir, 0}, trail), do: [{x, y} | trail]

  def move(pos, {dir, n}, trail) do
    new_pos = translate(pos, dir)
    move(new_pos, {dir, n - 1}, [new_pos | trail])
  end

  # overlap, do nothing
  def move_tail({x, y}, {x, y}), do: {x, y}
  # on same row
  def move_tail({hx, y}, {tx, y}) when abs(hx - tx) == 1, do: {tx, y}
  def move_tail({hx, y}, {tx, y}) when hx > tx, do: {tx + 1, y}
  def move_tail({hx, y}, {tx, y}) when hx < tx, do: {tx - 1, y}
  # on same column
  def move_tail({x, hy}, {x, ty}) when abs(hy - ty) == 1, do: {x, ty}
  def move_tail({x, hy}, {x, ty}) when hy > ty, do: {x, ty + 1}
  def move_tail({x, hy}, {x, ty}) when hy < ty, do: {x, ty - 1}
  # somewhere diagonally => do nothing
  def move_tail({hx, hy}, {tx, ty}) when abs(hx - tx) + abs(hy - ty) == 2, do: {tx, ty}

  # move tail closer diagonally
  def move_tail({hx, hy}, {tx, ty}) do
    {dx, dy} = {hx - tx, hy - ty}
    translate({tx, ty}, {round(dx / abs(dx)), round(dy / abs(dy))})
  end

  defp translate({x, y}, {dx, dy}), do: {x + dx, y + dy}
end
defmodule Part1 do
  def run(input) do
    input
    |> Util.process()
    |> RopeSimulator.simulate()
    |> Enum.count()
  end
end

defmodule Part2 do
  def run(input) do
    input
    |> Util.process()
    |> RopeSimulator.simulate(10)
    |> Enum.count()
  end
end

ExUnit.start(autorun: false)

defmodule Test do
  use ExUnit.Case, async: true
  @example_input ~s(R 5
U 8
L 8
D 3
R 17
D 10
L 25
U 20)
  @input Load.input()

  test "it loads the input" do
    assert String.length(@input) > 0
  end

  test "part 1 example" do
    assert Part1.run(@example_input) === 88
  end

  test "part 1" do
    assert Part1.run(@input) === 5683
  end

  test "part 2 example" do
    assert Part2.run(@example_input) === 36
  end

  test "part 2" do
    assert Part2.run(@input) === 2372
  end
end

ExUnit.run()