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

Day 17

day17.livemd

Day 17

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

Section

input = Kino.Input.textarea("Puzzle input")
defmodule Grid do
  @width 6
  @shapes [
    [
      # @@@@
      {0, 2},
      {0, 3},
      {0, 4},
      {0, 5}
    ],
    [
      # .@.
      # @@@
      # .@.
      {0, 3},
      {1, 2},
      {1, 3},
      {1, 4},
      {2, 3}
    ],
    [
      # ..@
      # ..@
      # @@@
      {0, 2},
      {0, 3},
      {0, 4},
      {2, 4},
      {1, 4}
    ],
    [
      # @
      # @
      # @
      # @
      {0, 2},
      {1, 2},
      {2, 2},
      {3, 2}
    ],
    [
      # @@
      # @@
      {0, 2},
      {0, 3},
      {1, 2},
      {1, 3}
    ]
  ]

  def new() do
    0..@width
    |> Enum.reduce(%{}, fn w, acc ->
      Map.put(acc, {0, w}, :floor)
    end)
  end

  def falling_shape(grid, moves, current_move_index, shape_index) do
    highest = highest_position(grid) + 4

    shape = shape_at_starting_pos(highest, shape_index)

    shape = apply_move(grid, shape, moves, current_move_index)
    current_move_index = Integer.mod(current_move_index + 1, length(moves))

    0..highest
    |> Enum.reduce_while({grid, shape, current_move_index}, fn _,
                                                               {grid, shape, current_move_index} ->
      shape = move_one_down(grid, shape)

      shape = apply_move(grid, shape, moves, current_move_index)
      current_move_index = Integer.mod(current_move_index + 1, length(moves))

      if can_continue_down?(grid, shape) do
        {:cont, {grid, shape, current_move_index}}
      else
        {:halt, {place_shape(grid, shape), current_move_index}}
      end
    end)
  end

  def print(highest, grid, shape \\ []) do
    for h <- 0..highest |> Enum.reverse() do
      for w <- 0..6 do
        if Enum.any?(shape, &amp;(&amp;1 == {h, w})) do
          IO.write("@")
        else
          case Grid.at(grid, {h, w}) do
            :air -> IO.write(".")
            :rock -> IO.write("#")
            :floor -> IO.write("-")
          end
        end
      end

      IO.write('\n')
    end
  end

  defp move_one_down(grid, shape) do
    new_shape =
      Enum.map(shape, fn {h, w} ->
        {h - 1, w}
      end)

    if blocked?(grid, new_shape) do
      shape
    else
      new_shape
    end
  end

  defp blocked?(grid, shape) do
    Enum.any?(shape, fn pos -> at(grid, pos) != :air end)
  end

  defp can_continue_down?(grid, shape) do
    new_shape =
      Enum.map(shape, fn {h, w} ->
        {h - 1, w}
      end)

    not blocked?(grid, new_shape)
  end

  defp apply_move(grid, shape, moves, current_move_index) do
    move = Enum.at(moves, current_move_index)

    move_w =
      case move do
        ">" -> 1
        "<" -> -1
      end

    new_shape =
      Enum.map(shape, fn {h, w} ->
        {h, w + move_w}
      end)

    if out_of_range?(grid, new_shape) do
      shape
    else
      new_shape
    end
  end

  defp out_of_range?(grid, shape) do
    Enum.any?(shape, fn {_, w} = pos ->
      w < 0 or w > @width or at(grid, pos) != :air
    end)
  end

  def place_shape(grid, shape) do
    shape
    |> Enum.reduce(grid, fn pos, grid ->
      set(grid, pos, :rock)
    end)
  end

  def highest_position(grid) do
    grid
    |> Enum.filter(fn {_, type} -> type in [:rock, :floor] end)
    |> Enum.max_by(fn {{h, _}, _} -> h end)
    |> elem(0)
    |> elem(0)
  end

  def at(grid, position) do
    Map.get(grid, position, :air)
  end

  def set(grid, position, type) do
    Map.put(grid, position, type)
  end

  defp shape_at_starting_pos(highest, shape_index) do
    shape = Enum.at(@shapes, shape_index)

    Enum.map(shape, fn {h, w} -> {h + highest, w} end)
  end

  def snapshot(grid) do
    snapshot =
      Enum.reduce(grid, [], fn {{h, w}, _}, acc ->
        insert_or_update(acc, w, max(Enum.at(acc, w, -1), h))
      end)

    h = Enum.max(snapshot)

    Enum.map(snapshot, fn v ->
      v - h
    end)
  end

  defp insert_or_update(list, index, value) do
    case Enum.at(list, index) do
      nil -> List.insert_at(list, index, value)
      _ -> List.update_at(list, index, fn _ -> value end)
    end
  end
end

Part 1

grid = Grid.new()

moves =
  input
  |> Kino.Input.read()
  |> String.codepoints()

{grid, _, _} =
  Enum.reduce(1..2022, {grid, 0, 0}, fn _, {grid, shape_index, current_move_index} ->
    {grid, current_move_index} = Grid.falling_shape(grid, moves, current_move_index, shape_index)

    {grid, Integer.mod(shape_index + 1, 5), current_move_index}
  end)

h = Grid.highest_position(grid)
Grid.print(h, grid)

h

Part 2

defmodule State do
  defstruct high: 0, offset: 0, seen: %{}, shape_index: 0, current_move_index: 0
end

defmodule Part2 do
  def run(grid, moves, total) do
    do_run(total, 0, grid, moves, %State{})
  end

  defp do_run(total, rock_count, _, _, %State{high: high, offset: offset})
       when rock_count >= total do
    high + offset
  end

  defp do_run(total, rock_count, grid, moves, %State{} = state) do
    rock_count = rock_count + 1

    {grid, current_move_index} =
      Grid.falling_shape(grid, moves, state.current_move_index, state.shape_index)

    shape_index = Integer.mod(state.shape_index + 1, 5)

    high = Grid.highest_position(grid)

    state = %{
      state
      | current_move_index: current_move_index,
        shape_index: shape_index,
        high: high
    }

    key = {shape_index, current_move_index, Grid.snapshot(grid)}

    if Map.has_key?(state.seen, key) do
      {rock_count, offset} =
        state.seen
        |> Map.get(key)
        |> get_rock_count_and_offset(total, rock_count, high)

      seen = Map.put(%{}, key, {rock_count, high})

      state = %{state | seen: seen, offset: offset}
      do_run(total, rock_count, grid, moves, state)
    else
      seen = Map.put(state.seen, key, {rock_count, high})
      state = %{state | seen: seen}

      do_run(total, rock_count, grid, moves, state)
    end
  end

  defp get_rock_count_and_offset({seen_rock_count, seen_high}, total, rock_count, high) do
    remaining_rocks = total - rock_count
    repetition = Integer.floor_div(remaining_rocks, rock_count - seen_rock_count)

    offset = repetition * (high - seen_high)

    rock_count = rock_count + repetition * (rock_count - seen_rock_count)

    {rock_count, offset}
  end
end
grid = Grid.new()

moves =
  input
  |> Kino.Input.read()
  |> String.codepoints()

total = 1_000_000_000_000

Part2.run(grid, moves, total)