Skip to content

Latest commit

 

History

History
172 lines (139 loc) · 4.24 KB

day09.livemd

File metadata and controls

172 lines (139 loc) · 4.24 KB

Rope Bridge

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

alias VegaLite, as: Vl

Instructions

input = Kino.Input.textarea("Input:")
instructions =
  Kino.Input.read(input)
  |> String.trim()
  |> String.split("\n")
  |> Enum.map(fn i ->
    [direction, distance] = String.split(i, " ")
    {direction, String.to_integer(distance)}
  end)

Expand Instructions

If we want to run an animation, we need to see the individual steps. The easiest way to do that is to convert the instructions into single steps, so that "D 3" becomes "D 1, D 1, D 1" and so on.

steps =
  Enum.flat_map(instructions, fn
    {direction, distance} -> for _ <- 1..distance, do: direction
  end)

Rope

defmodule Rope do
  defmodule Knot do
    defstruct x: 0, y: 0, visited: MapSet.new(), text: "?"
  end

  def new(_num_tails = 1) do
    {%Knot{text: "H"}, [%Knot{text: "T"}]}
  end

  def new(num_tails) do
    head = %Knot{text: "H"}
    tails = for t <- 1..num_tails, do: %Knot{text: Integer.to_string(t)}
    {head, tails}
  end

  def bounds(steps) do
    # TODO: Calculate this as we go along; saves the second pass.
    {visited, _final} =
      Enum.map_reduce(steps, {0, 0}, fn
        "D", {x, y} -> {{x, y - 1}, {x, y - 1}}
        "U", {x, y} -> {{x, y + 1}, {x, y + 1}}
        "L", {x, y} -> {{x - 1, y}, {x - 1, y}}
        "R", {x, y} -> {{x + 1, y}, {x + 1, y}}
      end)

    {{min_x, _}, {max_x, _}} = visited |> Enum.min_max_by(fn {x, _y} -> x end)
    {{_, min_y}, {_, max_y}} = visited |> Enum.min_max_by(fn {_x, y} -> y end)
    bounds = {min_x..max_x, min_y..max_y}
    bounds
  end

  def run(state, steps) do
    Enum.reduce(steps, state, &step/2)
  end

  def step(step, _state = {head, tails}) do
    head = move(head, step) |> mark_visited()

    {tails, _} =
      Enum.map_reduce(tails, head, fn tail, head ->
        tail = chase(tail, head) |> mark_visited()
        {tail, tail}
      end)

    {head, tails}
  end

  def mark_visited(knot = %Knot{x: x, y: y, visited: visited}) do
    %Knot{knot | visited: MapSet.put(visited, {x, y})}
  end

  def move(knot = %Knot{x: x}, "R"), do: %Knot{knot | x: x + 1}
  def move(knot = %Knot{y: y}, "U"), do: %Knot{knot | y: y + 1}
  def move(knot = %Knot{x: x}, "L"), do: %Knot{knot | x: x - 1}
  def move(knot = %Knot{y: y}, "D"), do: %Knot{knot | y: y - 1}

  # The head and tail must always be touching.
  def chase(tail = %Knot{x: tx, y: ty}, _head = %Knot{x: hx, y: hy})
      when hx - 1 <= tx and tx <= hx + 1 and hy - 1 <= ty and ty <= hy + 1,
      do: tail

  def chase(tail = %Knot{x: tx, y: ty}, _head = %Knot{x: hx, y: hy}),
    do: %Knot{tail | x: tx + sign(hx - tx), y: ty + sign(hy - ty)}

  def sign(value) when value < 0, do: -1
  def sign(value) when value == 0, do: 0
  def sign(value) when value > 0, do: 1
end

Part 1

rope = Rope.new(1)
final = Rope.run(rope, steps)
{_h, [_t = %Rope.Knot{visited: visited}]} = rope |> Rope.run(steps)
Enum.count(visited)

Part 2

rope = Rope.new(9)
final = Rope.run(rope, steps)
{_h, tails} = rope |> Rope.run(steps)
%Rope.Knot{visited: visited} = List.last(tails)
Enum.count(visited)

Heatmap

defmodule Render do
  def heatmap(_bounds = {x_scale, y_scale}, _state = {head, tails}) do
    visited_layers =
      for %Rope.Knot{visited: visited} <- [head | tails] do
        visited_points =
          Enum.map(visited, fn {x, y} ->
            %{x: x, y: y}
          end)

        Vl.new()
        |> Vl.mark(:rect)
        |> Vl.data(values: visited_points)
        |> Vl.encode_field(:x, "x", axis: false, scale: [domain: Enum.to_list(x_scale)])
        |> Vl.encode_field(:y, "y",
          axis: false,
          scale: [domain: Enum.to_list(y_scale) |> Enum.reverse()]
        )
      end

    text_layer =
      Vl.new()
      |> Vl.mark(:text)
      |> Vl.data(values: [head | tails] |> Enum.map(&Map.take(&1, [:x, :y, :text])))
      |> Vl.encode_field(:x, "x", axis: false)
      |> Vl.encode_field(:y, "y", axis: false)
      |> Vl.encode_field(:text, "text")

    Vl.new(width: 600, height: 600)
    |> Vl.layers(visited_layers ++ [text_layer])
  end
end
bounds = Rope.bounds(steps)
Render.heatmap(bounds, final)