Powered by AppSignal & Oban Pro

Advent of Code - Day 11

day_11.livemd

Advent of Code - Day 11

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

Section

{:ok, puzzle_input} = KinoAOC.download_puzzle("2022", "11", System.fetch_env!("LB_SESSION"))
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
"""
defmodule Monkey do
  defstruct id: nil,
            nb_inspected: 0,
            items: [],
            divisor: nil,
            operation: nil,
            throw_to_true: nil,
            throw_to_false: nil

  def parse(str),
    do:
      str
      |> String.split("\n", trim: true)
      |> Enum.reduce(%__MODULE__{}, &parse/2)

  def parse("Monkey " <> monkey_id, monkey),
    do: Map.put(monkey, :id, monkey_id |> String.slice(0..-2) |> String.to_integer())

  def parse("  Starting items: " <> items, monkey),
    do:
      Map.put(
        monkey,
        :items,
        items
        |> String.split(", ")
        |> Enum.map(&amp;String.to_integer/1)
      )

  def parse("  Operation: new = old " <> <> <> " " <> str_modifier, monkey) do
    operator = if str_operation == 42, do: &amp;Kernel.*/2, else: &amp;Kernel.+/2

    operation =
      case Regex.scan(~r/[0-9]+/, str_modifier) do
        [] -> fn x -> operator.(x, x) end
        _ -> fn x -> operator.(x, String.to_integer(str_modifier)) end
      end

    monkey
    |> Map.put(:operation, operation)
  end

  def parse("  Test: divisible by " <> divisor, monkey),
    do: Map.put(monkey, :divisor, String.to_integer(divisor))

  def parse("    If true: throw to monkey " <> str_monkey, monkey),
    do: Map.put(monkey, :throw_to_true, String.to_integer(str_monkey))

  def parse("    If false: throw to monkey " <> str_monkey, monkey),
    do: Map.put(monkey, :throw_to_false, String.to_integer(str_monkey))

  def worry_level(monkey_id, monkeys, worry_adjuster) do
    monkey = Enum.at(monkeys, monkey_id)

    Enum.reduce(monkey.items, monkeys, fn item, monkeys ->
      new_worry_level =
        item
        |> monkey.operation.()
        |> worry_adjuster.()

      monkey_to_throw_to =
        if rem(new_worry_level, monkey.divisor) == 0,
          do: monkey.throw_to_true,
          else: monkey.throw_to_false

      monkeys
      |> update_in([Access.at(monkey.id), Access.key(:nb_inspected)], &amp;(&amp;1 + 1))
      |> update_in([Access.at(monkey.id), Access.key(:items)], &amp;tl(&amp;1))
      |> update_in(
        [Access.at(monkey_to_throw_to), Access.key(:items)],
        &amp;([new_worry_level] ++ &amp;1)
      )
    end)
  end
end

defmodule Monkeys do
  def parse(str) do
    str
    |> String.split("\n\n", trim: true)
    |> Enum.map(&amp;Monkey.parse/1)
  end

  def round(monkeys, worry_adjuster) do
    Enum.reduce(monkeys, monkeys, fn monkey, monkeys ->
      Monkey.worry_level(monkey.id, monkeys, worry_adjuster)
    end)
  end

  def worry(monkeys, round, worry_adjuster) do
    1..round
    |> Enum.reduce(monkeys, fn _, monkeys -> Monkeys.round(monkeys, worry_adjuster) end)
    |> Enum.map(&amp; &amp;1.nb_inspected)
    |> Enum.sort(:desc)
    |> Enum.take(2)
    |> Enum.product()
  end

  def part1(str) do
    monkeys = parse(str)
    worry(monkeys, 20, &amp;div(&amp;1, 3))
  end

  def part2(str) do
    monkeys = parse(str)

    lcm = Enum.reduce(monkeys, 1, &amp;(&amp;1.divisor * &amp;2))
    worry(monkeys, 10_000, &amp;rem(&amp;1, lcm))
  end
end
Monkeys.part2(puzzle_input)