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

AoC 2022 Day 11

2022/day11.livemd

AoC 2022 Day 11

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

Input

input_field = Kino.Input.textarea("Puzzle input")
input = Kino.Input.read(input_field)

Part 1

defmodule MonkeyParser do
  import NimbleParsec

  newline = ignore(string("\n"))
  whitespace = ignore(repeat(string(" ")))
  break = newline |> concat(whitespace)

  identifier =
    ignore(string("Monkey "))
    |> integer(1)
    |> tag(:id)
    |> ignore(string(":"))

  items =
    ignore(string("Starting items:"))
    |> repeat(
      optional(whitespace)
      |> integer(2)
      |> optional(ignore(string(",")))
    )
    |> tag(:starting_items)

  operation =
    ignore(string("Operation: new = old "))
    |> choice([
      ignore(string("* old")) |> tag(:square),
      ignore(string("* ")) |> integer(min: 1) |> tag(:mult),
      ignore(string("+ ")) |> integer(min: 1) |> tag(:add)
    ])
    |> tag(:operation)

  test =
    ignore(string("Test: divisible by "))
    |> integer(min: 1)
    |> tag(:test)

  if_true =
    ignore(string("If true: throw to monkey "))
    |> integer(1)
    |> tag(true)

  if_false =
    ignore(string("If false: throw to monkey "))
    |> integer(1)
    |> tag(false)

  decisions =
    if_true
    |> concat(break)
    |> concat(if_false)
    |> tag(:decisions)

  monkey =
    identifier
    |> concat(break)
    |> concat(items)
    |> concat(break)
    |> concat(operation)
    |> concat(break)
    |> concat(test)
    |> concat(break)
    |> concat(decisions)
    |> optional(times(newline, 2))
    |> tag(:monkey)

  defparsec(:parse, repeat(monkey))
end
{:ok, monkeys, "", _, _, _} = MonkeyParser.parse(input)
defmodule Monkey do
  defstruct [:id, :items, :operation, :test, :decisions, :inspected]

  def new(
        id: [id],
        starting_items: items,
        operation: [operation],
        test: [test],
        decisions: decisions
      ) do
    %Monkey{
      id: id,
      items: items,
      operation: operation,
      test: test,
      decisions: decisions,
      inspected: 0
    }
  end

  def apply_op(item, {:mult, [amt]}), do: amt * item
  def apply_op(item, {:add, [amt]}), do: amt + item
  def apply_op(item, {:square, []}), do: item * item

  def apply_test(level, test), do: Integer.mod(level, test) == 0

  def incr_inspected(%Monkey{inspected: i} = monkey), do: %{monkey | inspected: i + 1}

  def pop_item(%Monkey{items: [item | items]} = monkey),
    do: {
      %{monkey | items: items},
      item
    }

  def add_item(%Monkey{items: items} = monkey, item), do: %{monkey | items: items ++ [item]}

  def move(%Monkey{items: []}), do: :done

  def move(%Monkey{operation: operation, test: test, decisions: decisions} = monkey) do
    {monkey, item} =
      monkey
      |> Monkey.incr_inspected()
      |> Monkey.pop_item()

    item =
      item
      |> Monkey.apply_op(operation)
      |> Integer.floor_div(3)

    [target] =
      item
      |> Monkey.apply_test(test)
      |> then(&Keyword.fetch!(decisions, &1))

    {monkey, item, target}
  end

  def take_turn(monkeys, id) do
    monkeys[id]
    |> Monkey.move()
    |> case do
      :done ->
        monkeys

      {monkey, item, target} ->
        monkeys
        |> Map.update!(target, &add_item(&1, item))
        |> Map.replace!(id, monkey)
        |> then(&Monkey.take_turn(&1, id))
    end
  end

  def run_round(monkeys) do
    ids = Map.keys(monkeys)

    Enum.reduce(ids, monkeys, &Monkey.take_turn(&2, &1))
  end
end
monkeys =
  monkeys
  |> Keyword.values()
  |> Enum.map(&Monkey.new/1)
  |> Enum.map(&{&1.id, &1})
  |> Enum.into(%{})

1..20
|> Enum.reduce(monkeys, fn _, acc -> Monkey.run_round(acc) end)
|> Map.values()
|> Enum.map(& &1.inspected)
|> Enum.sort()
|> Enum.reverse()
|> Enum.take(2)
|> Enum.product()

Part 2

defmodule Monkey2 do
  def move(%Monkey{items: []}, _), do: :done

  def move(%Monkey{operation: operation, test: test, decisions: decisions} = monkey, divisor) do
    {monkey, item} =
      monkey
      |> Monkey.incr_inspected()
      |> Monkey.pop_item()

    item =
      item
      |> Monkey.apply_op(operation)
      |> Integer.mod(divisor)

    [target] =
      item
      |> Monkey.apply_test(test)
      |> then(&Keyword.fetch!(decisions, &1))

    {monkey, item, target}
  end

  def take_turn(monkeys, id, divisor) do
    monkeys[id]
    |> Monkey2.move(divisor)
    |> case do
      :done ->
        monkeys

      {monkey, item, target} ->
        monkeys
        |> Map.update!(target, &Monkey.add_item(&1, item))
        |> Map.replace!(id, monkey)
        |> then(&Monkey2.take_turn(&1, id, divisor))
    end
  end

  def run_round(monkeys, divisor) do
    ids = Map.keys(monkeys)

    Enum.reduce(ids, monkeys, &Monkey2.take_turn(&2, &1, divisor))
  end
end
divisor =
  monkeys
  |> Map.values()
  |> Enum.map(& &1.test)
  |> Enum.product()
1..10000
|> Enum.reduce(monkeys, fn round, acc ->
  Monkey2.run_round(acc, divisor)
end)
|> Map.values()
|> Enum.map(& &1.inspected)
|> Enum.sort()
|> Enum.reverse()
|> Enum.take(2)
|> Enum.product()