Powered by AppSignal & Oban Pro

Advent of Code

solutions.livemd

Advent of Code

Mix.install([
  {:req, "~> 0.3.2"}
])

stream_for_day! = fn day ->
  unless File.exists?("AdventOfCode/input/#{day}.txt") do
    content =
      Req.get!("https://adventofcode.com/2022/day/#{day}/input",
        headers: [{"cookie", "session=#{System.fetch_env!("LB_SESSION")}"}]
      ).body

    File.write!("AdventOfCode/input/#{day}.txt", content)
  end

  File.stream!("AdventOfCode/input/#{day}.txt")
end

file_for_day! = fn day ->
  stream_for_day!.(day) |> Enum.join("")
end

Day 1

# Part 1
stream_for_day!.(1)
|> Enum.reduce({0, 0}, fn
  "\n", {current_max, current} ->
    {max(current_max, current), 0}

  value, {current_max, current} ->
    {current_max, current + (String.trim(value) |> String.to_integer())}
end)
|> then(fn {current_max, current} -> max(current_max, current) end)
# Part 2
stream_for_day!.(1)
|> Enum.reduce({[0, 0, 0], 0}, fn
  "\n", {current_max, current} ->
    {[current | current_max] |> Enum.sort(:desc) |> Enum.take(3), 0}

  value, {current_max, current} ->
    {current_max, current + (String.trim(value) |> String.to_integer())}
end)
|> then(fn {current_max, current} ->
  [current | current_max] |> Enum.sort(:desc) |> Enum.take(3)
end)
|> Enum.sum()

Day 2

# Part 1
opponent_mapping = %{
  "A" => :rock,
  "B" => :paper,
  "C" => :scissors
}

my_mapping = %{
  "X" => :rock,
  "Y" => :paper,
  "Z" => :scissors
}

points = %{
  rock: 1,
  paper: 2,
  scissors: 3,
  draw: 3,
  opponent_win: 0,
  my_win: 6
}

result = fn
  same_move, same_move -> :draw
  :rock, :scissors -> :opponent_win
  :paper, :rock -> :opponent_win
  :scissors, :paper -> :opponent_win
  _, _ -> :my_win
end

stream_for_day!.(2)
|> Enum.reduce(0, fn line, acc ->
  [opponent_move, my_move] = String.trim(line) |> String.split(" ")

  acc + points[result.(opponent_mapping[opponent_move], my_mapping[my_move])] +
    points[my_mapping[my_move]]
end)
# Part 2
opponent_mapping = %{
  "A" => :rock,
  "B" => :paper,
  "C" => :scissors
}

my_mapping = %{
  "X" => :lose,
  "Y" => :draw,
  "Z" => :win
}

points = %{
  rock: 1,
  paper: 2,
  scissors: 3,
  draw: 3,
  lose: 0,
  win: 6
}

move_mapping = %{
  rock: %{win: :paper, lose: :scissors},
  paper: %{win: :scissors, lose: :rock},
  scissors: %{win: :rock, lose: :paper}
}

stream_for_day!.(2)
|> Enum.reduce(0, fn line, acc ->
  [opponent_move_raw, my_result_raw] = String.trim(line) |> String.split(" ")
  my_result = my_mapping[my_result_raw]
  opponent_move = opponent_mapping[opponent_move_raw]
  acc + points[my_result] + points[move_mapping[opponent_move][my_result] || opponent_move]
end)

Day 3

# Part 1
stream_for_day!.(3)
|> Enum.reduce(0, fn line, acc ->
  line = String.trim(line)

  {part_1, part_2} = String.split_at(line, div(String.length(line), 2))
  part_1 = String.splitter(part_1, "", trim: true)
  part_2 = String.splitter(part_2, "", trim: true)

  intersection = MapSet.intersection(MapSet.new(part_1), MapSet.new(part_2))

  [element] = MapSet.to_list(intersection)
  [element] = String.to_charlist(element)

  if element >= ?a do
    element - ?a + 1
  else
    element - ?A + 27
  end
  |> Kernel.+(acc)
end)
# Part 2
stream_for_day!.(3)
|> Stream.map(&String.trim/1)
|> Stream.chunk_every(3)
|> Enum.reduce(0, fn [elf1, elf2, elf3], acc ->
  elf1_items = elf1 |> String.splitter("", trim: true) |> MapSet.new()
  elf2_items = elf2 |> String.splitter("", trim: true) |> MapSet.new()
  elf3_items = elf3 |> String.splitter("", trim: true) |> MapSet.new()

  intersection =
    MapSet.intersection(MapSet.new(elf1_items), MapSet.new(elf2_items))
    |> MapSet.intersection(elf3_items)

  [element] = MapSet.to_list(intersection)
  [element] = String.to_charlist(element)

  if element >= ?a do
    element - ?a + 1
  else
    element - ?A + 27
  end
  |> Kernel.+(acc)
end)

Day 4

# Part 1
stream_for_day!.(4)
|> Stream.map(&String.trim/1)
|> Enum.count(fn pair ->
  [elf1, elf2] = String.split(pair, ",")
  [elf1_start, elf1_end] = String.split(elf1, "-") |> Enum.map(&String.to_integer/1)
  [elf2_start, elf2_end] = String.split(elf2, "-") |> Enum.map(&String.to_integer/1)

  (elf1_start <= elf2_start and elf1_end >= elf2_end) or
    (elf2_start <= elf1_start and elf2_end >= elf1_end)
end)
# Part 2
stream_for_day!.(4)
|> Stream.map(&amp;String.trim/1)
|> Enum.count(fn pair ->
  [elf1, elf2] = String.split(pair, ",")
  [elf1_start, elf1_end] = String.split(elf1, "-") |> Enum.map(&amp;String.to_integer/1)
  [elf2_start, elf2_end] = String.split(elf2, "-") |> Enum.map(&amp;String.to_integer/1)

  not Range.disjoint?(elf1_start..elf1_end, elf2_start..elf2_end)
end)

Day 5

# Part 1

{tower, moves} =
  stream_for_day!.(5)
  |> Stream.map(&amp;String.trim(&amp;1, "\n"))
  |> Stream.reject(&amp;(&amp;1 == ""))
  |> Enum.split_while(&amp;(not String.starts_with?(&amp;1, "move")))

columns =
  tower
  |> Enum.drop(-1)
  |> Enum.reduce(%{}, fn line, acc ->
    line
    |> String.to_charlist()
    |> Enum.chunk_every(4)
    |> Enum.map(&amp;to_string/1)
    |> IO.inspect()
    |> Enum.map(&amp;String.replace(&amp;1, ~r{\[|\]| }, "", global: true))
    |> Enum.with_index(1)
    |> Enum.reduce(acc, fn
      {"", _index}, acc ->
        acc

      {column, index}, acc ->
        Map.update(acc, index, [column], &amp;[column | &amp;1])
    end)
  end)

moves
|> Enum.map(fn line ->
  [_, number_of_crates, _, from, _, to] = line |> String.split(" ")
  [number_of_crates, from, to] |> Enum.map(&amp;String.to_integer/1)
end)
|> Enum.reduce(columns, fn [number_of_crates, from, to], columns ->
  crates_to_move = Enum.take(columns[from], -number_of_crates)

  columns
  |> Map.update!(from, fn column ->
    column |> Enum.drop(-number_of_crates)
  end)
  |> Map.update!(to, fn column ->
    column ++ Enum.reverse(crates_to_move)
  end)
end)
|> Enum.map(fn {_, column} ->
  List.last(column)
end)
|> Enum.join()
# Part 2

{tower, moves} =
  stream_for_day!.(5)
  |> Stream.map(&amp;String.trim(&amp;1, "\n"))
  |> Stream.reject(&amp;(&amp;1 == ""))
  |> Enum.split_while(&amp;(not String.starts_with?(&amp;1, "move")))

columns =
  tower
  |> Enum.drop(-1)
  |> Enum.reduce(%{}, fn line, acc ->
    line
    |> String.to_charlist()
    |> Enum.chunk_every(4)
    |> Enum.map(&amp;to_string/1)
    |> Enum.map(&amp;String.replace(&amp;1, ~r{\[|\]| }, "", global: true))
    |> Enum.with_index(1)
    |> Enum.reduce(acc, fn
      {"", _index}, acc ->
        acc

      {column, index}, acc ->
        Map.update(acc, index, [column], &amp;[column | &amp;1])
    end)
  end)

moves
|> Enum.map(fn line ->
  [_, number_of_crates, _, from, _, to] = line |> String.split(" ")
  [number_of_crates, from, to] |> Enum.map(&amp;String.to_integer/1)
end)
|> Enum.reduce(columns, fn [number_of_crates, from, to], columns ->
  crates_to_move = Enum.take(columns[from], -number_of_crates)

  columns
  |> Map.update!(from, fn column ->
    column |> Enum.drop(-number_of_crates)
  end)
  |> Map.update!(to, fn column ->
    column ++ crates_to_move
  end)
end)
|> Enum.map(fn {_, column} ->
  List.last(column)
end)
|> Enum.join()

Day 6

# Part 1
check_if_continue = fn list, index ->
  if list == Enum.uniq(list) do
    {:halt, index}
  else
    {:cont, list}
  end
end

first_n_different_characters_at = fn n ->
  stream_for_day!.(6)
  |> Enum.fetch!(0)
  |> String.split("", trim: true)
  |> Enum.with_index(1)
  |> Enum.reduce_while([], fn
    {elem, _index}, acc when length(acc) < n - 1 ->
      acc = acc ++ [elem]

      {:cont, acc}

    {elem, index}, acc when length(acc) == n - 1 ->
      acc = acc ++ [elem]
      check_if_continue.(acc, index)

    {elem, index}, acc ->
      acc = Enum.drop(acc, 1) ++ [elem]
      check_if_continue.(acc, index)
  end)
end

first_n_different_characters_at.(4)
# Part 2
first_n_different_characters_at.(14)

Day 7

defmodule Day7 do
  def generate_tree(input, directory_content \\ %{}) do
    case List.pop_at(input, 0) do
      {"$ ls", input} ->
        {ls_result, input} = Enum.split_while(input, &amp;(not String.starts_with?(&amp;1, "$")))

        directory_content =
          Map.new(ls_result, fn
            "dir " <> dir_name ->
              {dir_name, %{type: :dir, size: nil, children: %{}}}

            file_line ->
              [file_size, file_name] = String.split(file_line, " ")
              {file_name, %{type: :file, size: String.to_integer(file_size)}}
          end)

        generate_tree(input, directory_content)

      {"$ cd ..", input} ->
        {input, directory_content}

      {"$ cd " <> directory_to_change, input} ->
        {input, children_content} = Day7.generate_tree(input)

        generate_tree(
          input,
          put_in(directory_content, [directory_to_change, :children], children_content)
        )

      _ ->
        {input, directory_content}
    end
  end

  def calculate_dir_sizes(tree) do
    Enum.reduce(tree, {0, []}, fn
      {name, %{type: :dir} = dir}, {size, acc} ->
        {add_size, add_acc} = calculate_dir_sizes(dir.children)

        if add_size <= 100_000 do
          {add_size + size, [{name, add_size}] ++ acc ++ add_acc}
        else
          {add_size + size, acc ++ add_acc}
        end

      {_name, %{size: add_size}}, {size, acc} ->
        {add_size + size, acc}
    end)
  end

  def calculate_all_dir_sizes(tree) do
    Enum.reduce(tree, {0, []}, fn
      {name, %{type: :dir} = dir}, {size, acc} ->
        {add_size, add_acc} = calculate_all_dir_sizes(dir.children)
        {add_size + size, [{name, add_size}] ++ acc ++ add_acc}

      {_name, %{size: add_size}}, {size, acc} ->
        {add_size + size, acc}
    end)
  end
end

input = stream_for_day!.(7) |> Stream.map(&amp;String.trim/1) |> Enum.to_list()
{"$ cd /", input} = List.pop_at(input, 0)

{_, tree} = Day7.generate_tree(input)
{space_taken, to_sum} = Day7.calculate_dir_sizes(tree)

to_sum |> Enum.map(fn {_name, size} -> size end) |> Enum.sum()
free_space = 70_000_000 - space_taken
need_to_delete = 30_000_000 - free_space

{_, to_sort} = Day7.calculate_all_dir_sizes(tree)

to_sort
|> Enum.sort_by(fn {_name, size} -> size end)
|> Enum.find(fn {_name, size} -> size >= need_to_delete end)
|> elem(1)

Day 8

# Part 1
rows = []
columns = []

{rows, columns} =
  stream_for_day!.(8)
  |> Stream.map(&amp;String.trim/1)
  |> Enum.reduce({rows, columns}, fn line, {rows, columns} ->
    row = String.split(line, "", trim: true) |> Enum.map(&amp;String.to_integer/1)
    number_of_rows = length(rows)
    row_length = length(row)
    row = row |> Enum.with_index(number_of_rows * row_length + 1)

    columns =
      if columns == [] do
        row |> Enum.map(&amp;List.wrap/1)
      else
        Enum.zip_reduce([columns, row], [], fn [column, tree], acc ->
          [[tree | column] | acc]
        end)
        |> Enum.reverse()
      end

    {[row | rows], columns}
  end)

reduce_list = fn {height, id}, {max_height, visible_trees} ->
  if height > max_height do
    {height, MapSet.put(visible_trees, id)}
  else
    {max_height, visible_trees}
  end
end

set1 =
  Enum.reduce(rows, MapSet.new(), fn row, acc ->
    set1 = row |> Enum.reduce({-1, MapSet.new()}, reduce_list) |> elem(1)
    set2 = row |> Enum.reverse() |> Enum.reduce({-1, MapSet.new()}, reduce_list) |> elem(1)
    MapSet.union(set1, set2) |> MapSet.union(acc)
  end)

set2 =
  Enum.reduce(columns, MapSet.new(), fn column, acc ->
    set1 = column |> Enum.reduce({-1, MapSet.new()}, reduce_list) |> elem(1)
    set2 = column |> Enum.reverse() |> Enum.reduce({-1, MapSet.new()}, reduce_list) |> elem(1)
    MapSet.union(set1, set2) |> MapSet.union(acc)
  end)

[List.first(rows), List.last(rows), List.first(columns), List.last(columns)]
|> List.flatten()
|> Enum.map(&amp;elem(&amp;1, 1))
|> MapSet.new()
|> MapSet.union(set1)
|> MapSet.union(set2)
|> MapSet.size()
# Part 2
map =
  stream_for_day!.(8)
  |> Stream.map(&amp;String.trim/1)
  |> Enum.reduce(%{}, fn line, map ->
    row =
      String.split(line, "", trim: true)
      |> Enum.map(&amp;String.to_integer/1)
      |> Enum.with_index(fn element, index -> {index, element} end)
      |> Map.new()

    number_of_rows = map_size(map)

    Map.put(map, number_of_rows, row)
  end)

for x <- 0..98, y <- 0..98 do
  house_height = map[x][y]

  left =
    Enum.reduce_while((x - 1)..0//-1, 0, fn x, acc ->
      if map[x][y] < house_height, do: {:cont, acc + 1}, else: {:halt, acc + 1}
    end)

  right =
    Enum.reduce_while((x + 1)..98, 0, fn x, acc ->
      if map[x][y] < house_height, do: {:cont, acc + 1}, else: {:halt, acc + 1}
    end)

  top =
    Enum.reduce_while((y - 1)..0//-1, 0, fn y, acc ->
      if map[x][y] < house_height, do: {:cont, acc + 1}, else: {:halt, acc + 1}
    end)

  bottom =
    Enum.reduce_while((y + 1)..98, 0, fn y, acc ->
      if map[x][y] < house_height, do: {:cont, acc + 1}, else: {:halt, acc + 1}
    end)

  left * right * top * bottom
end
|> Enum.max()

Day 9

# Part 1
defmodule Day9 do
  def step([line | rest_lines], head_position, tail_position, all_tail_positions) do
    [direction, steps] = String.split(line, " ")

    move_shift = %{
      "U" => {0, 1},
      "D" => {0, -1},
      "R" => {1, 0},
      "L" => {-1, 0}
    }

    {head_position, tail_position, all_tail_positions} =
      move(
        String.to_integer(steps),
        move_shift[direction],
        head_position,
        tail_position,
        all_tail_positions
      )

    step(rest_lines, head_position, tail_position, all_tail_positions)
  end

  def step([], _, _, all_tail_positions), do: all_tail_positions

  defp move(0, _move_shift, head_position, tail_position, all_tail_positions) do
    {head_position, tail_position, all_tail_positions}
  end

  defp move(steps, move_shift, head_position, tail_position, all_tail_positions) do
    head_position = move_head(head_position, move_shift)
    tail_position = move_tail(tail_position, head_position)

    move(
      steps - 1,
      move_shift,
      head_position,
      tail_position,
      MapSet.put(all_tail_positions, tail_position)
    )
  end

  defp move_head({head_x_position, head_y_position}, {shift_x, shift_y}),
    do: {head_x_position + shift_x, head_y_position + shift_y}

  defp move_tail({tail_x, tail_y}, {head_x, tail_y}) when head_x - tail_x >= 2,
    do: {head_x - 1, tail_y}

  defp move_tail({tail_x, tail_y}, {head_x, tail_y}) when head_x - tail_x <= -2,
    do: {head_x + 1, tail_y}

  defp move_tail({tail_x, tail_y}, {tail_x, head_y}) when head_y - tail_y >= 2,
    do: {tail_x, head_y - 1}

  defp move_tail({tail_x, tail_y}, {tail_x, head_y}) when head_y - tail_y <= -2,
    do: {tail_x, head_y + 1}

  defp move_tail({tail_x, tail_y}, {head_x, head_y})
       when abs(tail_x - head_x) > 1 or abs(tail_y - head_y) > 1 do
    cond do
      tail_y - head_y == 2 -> {head_x, head_y + 1}
      tail_y - head_y == -2 -> {head_x, head_y - 1}
      tail_x - head_x == 2 -> {head_x + 1, head_y}
      tail_x - head_x == -2 -> {head_x - 1, head_y}
    end
  end

  defp move_tail(tail_position, _head_position), do: tail_position
end

stream_for_day!.(9)
|> Stream.map(&amp;String.trim/1)
|> Enum.to_list()
|> Day9.step({0, 0}, {0, 0}, MapSet.new([{0, 0}]))
|> MapSet.size()
# Part 2
defmodule Day9 do
  def step([line | rest_lines], positions, all_9_positions) do
    [direction, steps] = String.split(line, " ")

    move_shift = %{
      "U" => {0, 1},
      "D" => {0, -1},
      "R" => {1, 0},
      "L" => {-1, 0}
    }

    {positions, all_9_positions} =
      move(String.to_integer(steps), move_shift[direction], positions, all_9_positions)

    step(rest_lines, positions, all_9_positions)
  end

  def step([], _positions, all_9_positions), do: all_9_positions

  defp move(0, _move_shift, positions, all_9_positions) do
    {positions, all_9_positions}
  end

  defp move(steps, move_shift, positions, all_9_positions) do
    positions = Map.put(positions, 0, move_head(positions[0], move_shift))

    positions =
      Enum.reduce(1..9, positions, fn i, positions ->
        Map.put(positions, i, move_tail(positions[i], positions[i - 1]))
      end)

    all_9_positions = MapSet.put(all_9_positions, positions[9])
    move(steps - 1, move_shift, positions, all_9_positions)
  end

  defp move_head({head_x_position, head_y_position}, {shift_x, shift_y}),
    do: {head_x_position + shift_x, head_y_position + shift_y}

  defp move_tail({tail_x, tail_y}, {head_x, tail_y}) when head_x - tail_x >= 2,
    do: {tail_x + 1, tail_y}

  defp move_tail({tail_x, tail_y}, {head_x, tail_y}) when head_x - tail_x <= -2,
    do: {tail_x - 1, tail_y}

  defp move_tail({tail_x, tail_y}, {tail_x, head_y}) when head_y - tail_y >= 2,
    do: {tail_x, tail_y + 1}

  defp move_tail({tail_x, tail_y}, {tail_x, head_y}) when head_y - tail_y <= -2,
    do: {tail_x, tail_y - 1}

  defp move_tail({tail_x, tail_y}, {head_x, head_y})
       when abs(tail_x - head_x) + abs(tail_y - head_y) >= 3 do
    y = sign(head_y - tail_y)
    x = sign(head_x - tail_x)

    {tail_x + x, tail_y + y}
  end

  defp move_tail(tail_position, _head_position), do: tail_position

  defp sign(number) when number >= 0, do: 1
  defp sign(_number), do: -1
end

start_positions =
  for i <- 0..9, into: %{} do
    {i, {0, 0}}
  end

stream_for_day!.(9)
|> Stream.map(&amp;String.trim/1)
|> Enum.to_list()
|> Day9.step(start_positions, MapSet.new([{0, 0}]))
|> MapSet.size()

Day 10

# Part 1
stream_for_day!.(10)
|> Stream.map(&amp;String.trim/1)
|> Enum.reduce({1, 0, 0}, fn instruction, {register_value, cycle, sum} ->
  {cycle_start, cycle_end, end_value} =
    case instruction do
      "noop" ->
        {cycle + 1, cycle + 1, register_value}

      "addx " <> value ->
        {cycle + 1, cycle + 2, register_value + String.to_integer(value)}
    end

  sum =
    [20, 60, 100, 140, 180, 220]
    |> Enum.find(fn x ->
      x in cycle_start..cycle_end
    end)
    |> case do
      nil ->
        sum

      x ->
        sum + x * register_value
    end

  {end_value, cycle_end, sum}
end)
# Part 2
{lit_pixels, _, _} =
  stream_for_day!.(10)
  |> Stream.map(&amp;String.trim/1)
  |> Enum.reduce({%{}, 1, 0}, fn instruction, {lit_pixels, register_value, cycle} ->
    {cycle_start, cycle_end, end_value} =
      case instruction do
        "noop" ->
          {cycle, cycle + 1, register_value}

        "addx " <> value ->
          {cycle + 1, cycle + 2, register_value + String.to_integer(value)}
      end

    lit_pixels =
      cycle_start..cycle_end
      |> Enum.reduce(lit_pixels, fn cycle, lit_pixels ->
        if Integer.mod(cycle - 1, 40) in (register_value - 1)..(register_value + 1) do
          Map.put(lit_pixels, cycle, "#")
        else
          lit_pixels
        end
      end)

    {lit_pixels, end_value, cycle_end}
  end)

1..240
|> Enum.map(fn i ->
  if Map.has_key?(lit_pixels, i) do
    "#"
  else
    "."
  end
end)
|> Enum.chunk_every(40)
|> Enum.map(&amp;Enum.join(&amp;1, ""))
|> Enum.join("\n")
|> IO.puts()

Day 11

# Part 1
monkeys =
  file_for_day!.(11)
  |> String.split("\n\n")
  |> Map.new(fn monkey_input ->
    [monkey_name_line, starting_items_line, operration_line, test_line, true_line, false_line] =
      monkey_input |> String.split("\n", trim: true) |> Enum.map(&amp;String.trim(&amp;1))

    monkey_id =
      monkey_name_line
      |> String.trim_trailing(":")
      |> String.trim_leading("Monkey ")
      |> String.to_integer()

    starting_items =
      starting_items_line
      |> String.trim_leading("Starting items: ")
      |> String.split(", ")
      |> Enum.map(&amp;String.to_integer/1)

    operation_string = operration_line |> String.trim_leading("Operation: new = ")
    operation = fn old -> Code.eval_string(operation_string, old: old) |> elem(0) end

    divisor = test_line |> String.trim_leading("Test: divisible by ") |> String.to_integer()

    true_monkey =
      true_line |> String.trim_leading("If true: throw to monkey ") |> String.to_integer()

    false_monkey =
      false_line |> String.trim_leading("If false: throw to monkey ") |> String.to_integer()

    {monkey_id,
     %{
       items: starting_items,
       operation: operation,
       divisor: divisor,
       true_monkey: true_monkey,
       false_monkey: false_monkey,
       counter: 0
     }}
  end)

[a, b] =
  Enum.reduce(1..20, monkeys, fn _round, monkeys ->
    Enum.reduce(0..(map_size(monkeys) - 1), monkeys, fn monkey_id, monkeys ->
      Enum.reduce(monkeys[monkey_id].items, monkeys, fn _item, monkeys ->
        current_monkey = monkeys[monkey_id]
        {current_item, rest_of_items} = List.pop_at(current_monkey.items, 0)
        monkeys = put_in(monkeys, [monkey_id, :items], rest_of_items)
        monkeys = update_in(monkeys, [monkey_id, :counter], &amp;(&amp;1 + 1))

        worry_level = floor(current_monkey.operation.(current_item) / 3)

        pass_to_monkey_id =
          if Integer.mod(worry_level, current_monkey.divisor) == 0 do
            current_monkey.true_monkey
          else
            current_monkey.false_monkey
          end

        update_in(monkeys, [pass_to_monkey_id, :items], &amp;(&amp;1 ++ [worry_level]))
      end)
    end)
  end)
  |> Enum.map(&amp;elem(&amp;1, 1).counter)
  |> Enum.sort(:desc)
  |> Enum.take(2)

a * b
# Part 2
test_input = """
Monkey 0:
Starting items: 79, 98
Operation: new = old * 19
Test: divisible by 23
  If true: throw to monkey 2
  If false: throw to monkey 3

Monkey 1:
Starting items: 54, 65, 75, 74
Operation: new = old + 6
Test: divisible by 19
  If true: throw to monkey 2
  If false: throw to monkey 0

Monkey 2:
Starting items: 79, 60, 97
Operation: new = old * old
Test: divisible by 13
  If true: throw to monkey 1
  If false: throw to monkey 3

Monkey 3:
Starting items: 74
Operation: new = old + 3
Test: divisible by 17
  If true: throw to monkey 0
  If false: throw to monkey 1
"""

monkeys =
  file_for_day!.(11)
  |> String.split("\n\n")
  |> Map.new(fn monkey_input ->
    [monkey_name_line, starting_items_line, operration_line, test_line, true_line, false_line] =
      monkey_input |> String.split("\n", trim: true) |> Enum.map(&amp;String.trim(&amp;1))

    monkey_id =
      monkey_name_line
      |> String.trim_trailing(":")
      |> String.trim_leading("Monkey ")
      |> String.to_integer()

    starting_items =
      starting_items_line
      |> String.trim_leading("Starting items: ")
      |> String.split(", ")
      |> Enum.map(&amp;String.to_integer(&amp;1))

    operation_string = operration_line |> String.trim_leading("Operation: new = ")
    operation = fn old -> Code.eval_string(operation_string, old: old) |> elem(0) end

    divisor = test_line |> String.trim_leading("Test: divisible by ") |> String.to_integer()

    true_monkey =
      true_line |> String.trim_leading("If true: throw to monkey ") |> String.to_integer()

    false_monkey =
      false_line |> String.trim_leading("If false: throw to monkey ") |> String.to_integer()

    {monkey_id,
     %{
       items: starting_items,
       operation: operation,
       divisor: divisor,
       true_monkey: true_monkey,
       false_monkey: false_monkey,
       counter: 0
     }}
  end)

lcm =
  Enum.reduce(monkeys, 1, fn {_id, monkey}, lcm ->
    lcm * monkey.divisor
  end)

[a, b] =
  Enum.reduce(1..10000, monkeys, fn _round, monkeys ->
    Enum.reduce(0..(map_size(monkeys) - 1), monkeys, fn monkey_id, monkeys ->
      Enum.reduce(monkeys[monkey_id].items, monkeys, fn _item, monkeys ->
        current_monkey = monkeys[monkey_id]
        {current_item, rest_of_items} = List.pop_at(current_monkey.items, 0)
        monkeys = put_in(monkeys, [monkey_id, :items], rest_of_items)
        monkeys = update_in(monkeys, [monkey_id, :counter], &amp;(&amp;1 + 1))

        worry_level = current_monkey.operation.(current_item)

        pass_to_monkey_id =
          if Integer.mod(worry_level, current_monkey.divisor) == 0 do
            current_monkey.true_monkey
          else
            current_monkey.false_monkey
          end

        update_in(monkeys, [pass_to_monkey_id, :items], &amp;(&amp;1 ++ [Integer.mod(worry_level, lcm)]))
      end)
    end)
  end)
  |> Enum.map(&amp;elem(&amp;1, 1).counter)
  |> Enum.sort(:desc)
  |> Enum.take(2)

a * b