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

Day 09

day09.livemd

Day 09

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

IEx.Helpers.c("/Users/johnb/dev/2022adventOfCode/advent_of_code.ex")
alias AdventOfCode, as: AOC
alias Kino.Input

# Note: when making the next template, something like this works well:
#   `cat day04.livemd | sed 's/03/04/' > day04.livemd`
#

Installation and Data

input_p1example = Kino.Input.textarea("Example Data")
input_p1puzzleInput = Kino.Input.textarea("Puzzle Input")
input_source_select =
  Kino.Input.select("Source", [{:example, "example"}, {:puzzle_input, "puzzle input"}])
p1data = fn ->
  (Kino.Input.read(input_source_select) == :example &&
     Kino.Input.read(input_p1example)) ||
    Kino.Input.read(input_p1puzzleInput)
end

Part 1

Rope Bridge

This rope bridge creaks as you walk along it. You aren’t sure how old it is, or whether it can even support your weight.

It seems to support the Elves just fine, though. The bridge spans a gorge which was carved out by the massive river far below you.

You step carefully; as you do, the ropes stretch and twist. You decide to distract yourself by modeling rope physics; maybe you can even figure out where not to step.

Consider a rope with a knot at each end; these knots mark the head and the tail of the rope. If the head moves far enough away from the tail, the tail is pulled toward the head.

Due to nebulous reasoning involving Planck lengths, you should be able to model the positions of the knots on a two-dimensional grid. Then, by following a hypothetical series of motions (your puzzle input) for the head, you can determine how the tail will move.

Due to the aforementioned Planck lengths, the rope must be quite short; in fact, the head (H) and tail (T) must always be touching (diagonally adjacent and even overlapping both count as touching):

###

defmodule Day09 do
  # conceptual board is 1000x1000, and we start at 500x500
  # track the head position and tail position, adding the
  # tail position to a MapSet every time it moves, then
  # count the number of positions.
  def solve(text) do
    center = 500 * 500
    deltas = %{"U" => -1000, "D" => 1000, "L" => -1, "R" => 1}

    rope =
      text
      |> AOC.as_single_lines()
      |> Enum.reduce(%{h: center, t: center, set: MapSet.new([center])}, fn line, acc ->
        [direction, distance] = String.split(line, " ")
        delta = deltas[direction]
        distance = String.to_integer(distance)

        # IO.inspect([acc, delta, distance], label: "\nline: #{line}")
        move(acc, delta, distance)
      end)

    Enum.count(rope.set)
  end

  def move(map, _delta, 0), do: map

  def move(%{h: h, t: t, set: set} = _map, delta, distance) when delta in [1, -1] do
    # IO.inspect(map)
    h2 = h + delta
    new_dist = abs(h2 - t)

    cond do
      new_dist == 2 ->
        move(%{h: h2, t: h, set: MapSet.put(set, h)}, delta, distance - 1)

      new_dist < 2 ->
        move(%{h: h2, t: t, set: set}, delta, distance - 1)

      new_dist > 1001 || new_dist < 999 ->
        move(%{h: h2, t: h, set: MapSet.put(set, h)}, delta, distance - 1)

      true ->
        move(%{h: h2, t: t, set: set}, delta, distance - 1)
    end
  end

  def move(%{h: h, t: t, set: set} = _map, delta, distance) do
    # IO.inspect(map)
    h2 = h + delta
    new_dist = abs(h2 - t)

    cond do
      new_dist == 2 ->
        move(%{h: h2, t: h, set: MapSet.put(set, h)}, delta, distance - 1)

      new_dist < 2 ->
        move(%{h: h2, t: t, set: set}, delta, distance - 1)

      new_dist > 1001 || new_dist < 999 ->
        move(%{h: h2, t: h, set: MapSet.put(set, h)}, delta, distance - 1)

      true ->
        move(%{h: h2, t: t, set: set}, delta, distance - 1)
    end
  end

  def solve2(text) do
    center = 5000 * 5000
    deltas = %{"U" => -1000, "D" => 1000, "L" => -1, "R" => 1}
    rope = [center, center, center, center, center, center, center, center, center, center]

    rope =
      text
      |> AOC.as_single_lines()
      |> Enum.reduce(%{rope: rope, set: MapSet.new([center])}, fn line, acc ->
        [direction, distance] = String.split(line, " ")
        delta = deltas[direction]
        distance = String.to_integer(distance)

        # IO.puts(line)
        move2(acc, delta, distance)
      end)

    Enum.count(rope.set)
  end

  def nearby(), do: [-1001, -1000, -999, -1, 0, 1, 999, 1000, 1001]
  def two_away(), do: [-2002, -2000, -1998, -2, 2, 1998, 2000, 2002]
  def knights(), do: [-2001, -1999, -1002, -998, 998, 1002, 1999, 2001]

  def knight_fix() do
    # what we add when they're a knight's move away
    %{
      -2001 => -1001,
      -1999 => -999,
      -1002 => -1001,
      -998 => -999,
      998 => 999,
      1002 => 1001,
      1999 => 999,
      2001 => 1001
    }
  end

  def keep_together([head, tail]) do
    dist = head - tail
    # IO.inspect([head, tail, dist])
    cond do
      dist in nearby() ->
        [head, tail]

      dist in two_away() ->
        [head, div(tail + head, 2)]

      dist in knights() ->
        [head, tail + knight_fix()[dist]]
        # true ->
        #   IO.inspect([head, tail, dist])
    end
  end

  def keep_together([head, next | knots] = _list) do
    [head, next] = keep_together([head, next])
    # |> IO.inspect()
    [head | keep_together([next | knots])]
  end

  def move2(map, delta, distance) do
    Enum.reduce(1..distance, map, fn _dist, %{rope: [head | knots], set: set} = _acc ->
      # IO.inspect(acc, label: "delta #{delta}")
      h2 = head + delta
      rope = keep_together([h2 | knots])
      %{rope: rope, set: MapSet.put(set, List.last(rope))}
    end)
  end
end

p1data.()
|> Day09.solve()
|> IO.inspect(label: "\n*** Part 1 solution (example: 13)")

# 6212

p1data.()
|> Day09.solve2()
|> IO.inspect(label: "\n*** Part 2 solution (example: 36)")

# 2522

Part Two