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

Advent of Code 2022 - Day 17

2022/day17.livemd

Advent of Code 2022 - Day 17

Mix.install([
  :kino,
  {:kino_aoc, git: "https://github.com/ljgago/kino_aoc"}
])

Input

test_input = ">>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>"
{:ok, puzzle_input} = KinoAOC.download_puzzle("2022", "17", System.fetch_env!("LB_AOC_SESSION"))
input_field =
  Kino.Input.select("input", [
    {test_input, "test_input"},
    {puzzle_input, "puzzle_input"}
  ])

Parse & Prep

jets =
  input_field
  |> Kino.Input.read()
  |> String.trim()
  |> String.to_charlist()

rocks = [
  [{2, 3}, {3, 3}, {4, 3}, {5, 3}],
  [{3, 3}, {2, 4}, {3, 4}, {4, 4}, {3, 5}],
  [{2, 3}, {3, 3}, {4, 3}, {4, 4}, {4, 5}],
  [{2, 3}, {2, 4}, {2, 5}, {2, 6}],
  [{2, 3}, {3, 3}, {2, 4}, {3, 4}]
]
defmodule Tetris do
  # defguardp is_in_wall?(board, x, y)
  #          when is_map_key(board, {x, y}) or x not in 0..6

  def is_in_wall?(board, x, y) do
    {x, y} in board or x not in 0..6
  end

  # defguardp is_collision?(board, x, y)
  #          when is_map_key(board, {x, y}) or y < 0

  def is_collision?(board, x, y) do
    {x, y} in board or y < 0
  end

  def next_x(x, ?>), do: x + 1
  def next_x(x, ?<), do: x - 1

  def draw(particles) do
    {miny, maxy} =
      particles
      |> Enum.map(&amp;elem(&amp;1, 1))
      |> Enum.min_max()

    for y <- maxy..miny, x <- 0..6 do
      if {x, y} in particles, do: IO.write("#"), else: IO.write(".")
      if x == 6, do: IO.write("\n")
    end
  end

  def move_up(rock, nr) do
    Enum.map(rock, fn {x, y} -> {x, y + nr} end)
  end

  def move(rock, board, jet) do
    rock
    |> move_horiz(board, jet)
    |> move_down(board)
  end

  def move_horiz(rock, board, jet) do
    moved_rock =
      rock
      |> Enum.map(fn {x, y} -> {next_x(x, jet), y} end)
      |> Enum.reduce_while([], fn
        {x, y}, moved_rock ->
          if is_in_wall?(board, x, y),
            do: {:halt, nil},
            else: {:cont, [{x, y} | moved_rock]}
      end)

    moved_rock || rock
  end

  defp move_down(rock, board) do
    rock
    |> Enum.map(fn {x, y} -> {x, y - 1} end)
    |> Enum.reduce_while([], fn
      {x, y}, moved_rock ->
        if is_collision?(board, x, y),
          do: {:halt, {:stop, rock}},
          else: {:cont, [{x, y} | moved_rock]}
    end)
  end

  def play(jets, rocks, nr_rocks) do
    jets
    |> Stream.with_index()
    |> Stream.cycle()
    |> Enum.reduce_while({hd(rocks), MapSet.new(), 1, -1, []}, fn
      {_, idx}, {_, board, ^nr_rocks, maxy, history} ->
        # draw(board)
        {:halt, {maxy + 1, board, idx, history}}

      {jet, jet_idx}, {rock, board, rock_counter, max_y, history} ->
        case move(rock, board, jet) do
          {:stop, rock} ->
            max_rock_y = rock |> Enum.map(&amp;elem(&amp;1, 1)) |> Enum.max()
            top = max(max_y, max_rock_y)
            new_board = MapSet.union(board, MapSet.new(rock))
            rock_idx = rem(rock_counter, 5)

            next_rock =
              rocks
              |> Enum.at(rock_idx)
              |> move_up(top + 1)

            {:cont,
             {next_rock, new_board, rock_counter + 1, top, [{rock_idx, jet_idx} | history]}}

          moved_rock ->
            {:cont, {moved_rock, board, rock_counter, max_y, history}}
        end
    end)
  end
end

Part 1

result = Tetris.play(jets, rocks, 2023)
elem(result, 0) |> IO.inspect(label: "result")

Part 2

history = Tetris.play(jets, rocks, 10000) |> elem(3)

pattern =
  Enum.reduce_while(history, [], fn
    entry, [] ->
      {:cont, [entry]}

    entry, list ->
      case history |> Enum.chunk_every(length(list)) do
        [a, a, a | _] ->
          {:halt, a}

        _ ->
          {:cont, [entry | list]}
      end
  end)

full_list =
  Enum.reduce_while(history, history, fn
    _, [_ | list] = full_list ->
      case list |> Enum.chunk_every(length(pattern)) do
        [a, a | _] ->
          {:cont, list}

        _ ->
          {:halt, full_list}
      end
  end)

[block | rest] = Enum.chunk_every(full_list, length(pattern))
first = List.last(rest)
{block_length, first_length} = {length(block), length(first)}
first_length = first_length + 1

height_first = Tetris.play(jets, rocks, first_length + 1) |> elem(0)

height_block =
  (Tetris.play(jets, rocks, block_length + first_length + 1) |> elem(0)) - height_first

rocks_after_rest = 1_000_000_000_000 - first_length
whole_blocks = div(rocks_after_rest, block_length)
remaining_rocks = rem(rocks_after_rest, block_length)

height_remaining =
  (Tetris.play(jets, rocks, first_length + remaining_rocks + 1) |> elem(0)) - height_first

height_block * whole_blocks + height_remaining

whole_blocks * height_block + height_remaining + height_first