Powered by AppSignal & Oban Pro

Day 19: Not Enough Minerals

Day 19: Not Enough Minerals.livemd

Day 19: Not Enough Minerals

Mix.install([:nimble_parsec])

text = File.read!("Code/aoc.ex/input/2022/19.txt")

defmodule Parser do
  import NimbleParsec

  blueprint = times(eventually(integer(min: 1)), 7)

  defparsec(:input, repeat(wrap(blueprint)))

  def parse(text) do
    for [i, a, b, c, d, e, f] <- input(text) |> elem(1) do
      %{
        index: i,
        cost: %{
          ore: %{ore: a},
          clay: %{ore: b},
          obsidian: %{ore: c, clay: d},
          geode: %{ore: e, obsidian: f}
        }
      }
    end
  end
end

data = Parser.parse(text)

example = """
Blueprint 1:
  Each ore robot costs 4 ore.
  Each clay robot costs 2 ore.
  Each obsidian robot costs 3 ore and 14 clay.
  Each geode robot costs 2 ore and 7 obsidian.

Blueprint 2:
  Each ore robot costs 2 ore.
  Each clay robot costs 3 ore.
  Each obsidian robot costs 3 ore and 8 clay.
  Each geode robot costs 3 ore and 12 obsidian.
"""

example_data = Parser.parse(example)

Not Enough Minerals

defmodule N1 do
  @initial_bots {1, 0, 0, 0}
  @initial_reserve {0, 0, 0, 0}

  def start(costs, t) do
    bots = @initial_bots
    reserve = @initial_reserve
    work(0, bots, reserve, costs, t)
  end

  def work(mx, bots, reserve, costs, 0) do
    do_work(mx, nil, bots, reserve, costs, 0)
  end

  def work(mx, bots, reserve, costs, t) do
    mx
    |> do_work(:geode, bots, reserve, costs, t)
    |> do_work(:obsidian, bots, reserve, costs, t)
    |> do_work(:clay, bots, reserve, costs, t)
    |> do_work(:ore, bots, reserve, costs, t)
    |> do_work(nil, bots, reserve, costs, t)
  end

  # do nothing
  def do_work(mx, nil, {_, _, _, bots}, {_, _, _, reserve}, _, t), do: max(mx, reserve + bots * t)

  # not enough obsidian production to get a geode bot in time
  def do_work(mx, :geode, {_, _, b, _}, {_, _, r, _}, %{geode: %{obsidian: c}}, t)
      when b * (t - 2) + r < c,
      do: mx

  # not enough ore production to get a geode bot in time
  def do_work(mx, :geode, {b, _, _, _}, {r, _, _, _}, %{geode: %{ore: c}}, t)
      when b * (t - 2) + r < c,
      do: mx

  # not enough clay production to get an obsidian bot in time
  def do_work(mx, :obsidian, {_, b, _, _}, {_, r, _, _}, %{obsidian: %{clay: c}}, t)
      when b * (t - 2) + r < c,
      do: mx

  # not enough ore production to get an obsidian bot in time
  def do_work(mx, :obsidian, {b, _, _, _}, {r, _, _, _}, %{obsidian: %{ore: c}}, t)
      when b * (t - 2) + r < c,
      do: mx

  # not enough ore production to get a clay bot in time
  def do_work(mx, :clay, {b, _, _, _}, {r, _, _, _}, %{clay: %{ore: c}}, t)
      when b * (t - 2) + r < c,
      do: mx

  # not enough ore production to get an ore bot in time
  def do_work(mx, :ore, {b, _, _, _}, {r, _, _, _}, %{ore: %{ore: c}}, t)
      when b * (t - 2) + r < c,
      do: mx

  # no more obsidian bots needed
  def do_work(mx, :obsidian, {_, _, b, _}, _, %{geode: %{obsidian: c}}, _)
      when b >= c,
      do: mx

  # no more clay bots needed
  def do_work(mx, :clay, {_, b, _, _}, _, %{obsidian: %{clay: c}}, _)
      when b >= c,
      do: mx

  # no more ore bots needed
  def do_work(mx, :ore, {b, _, _, _}, _, c, _)
      when b >= c.clay.ore and b >= c.obsidian.ore and b >= c.geode.ore,
      do: mx

  def do_work(mx, :geode, bots, reserve, costs, t) do
    {ore_bots, clay_bots, obsidian_bots, geode_bots} = bots
    {ore_reserve, clay_reserve, obsidian_reserve, geode_reserve} = reserve
    %{geode: %{ore: ore_cost, obsidian: obsidian_cost}} = costs

    t_needed =
      0
      |> max(div(ore_cost - ore_reserve + ore_bots - 1, ore_bots))
      |> max(div(obsidian_cost - obsidian_reserve + obsidian_bots - 1, obsidian_bots))
      |> Kernel.+(1)

    reserve = {
      ore_reserve + t_needed * ore_bots - ore_cost,
      clay_reserve + t_needed * clay_bots,
      obsidian_reserve + t_needed * obsidian_bots - obsidian_cost,
      geode_reserve + t_needed * geode_bots
    }

    bots = {ore_bots, clay_bots, obsidian_bots, geode_bots + 1}

    work(mx, bots, reserve, costs, t - t_needed)
  end

  def do_work(mx, :obsidian, bots, reserve, costs, t) do
    {ore_bots, clay_bots, obsidian_bots, geode_bots} = bots
    {ore_reserve, clay_reserve, obsidian_reserve, geode_reserve} = reserve
    %{obsidian: %{ore: ore_cost, clay: clay_cost}} = costs

    t_needed =
      0
      |> max(div(ore_cost - ore_reserve + ore_bots - 1, ore_bots))
      |> max(div(clay_cost - clay_reserve + clay_bots - 1, clay_bots))
      |> Kernel.+(1)

    reserve = {
      ore_reserve + t_needed * ore_bots - ore_cost,
      clay_reserve + t_needed * clay_bots - clay_cost,
      obsidian_reserve + t_needed * obsidian_bots,
      geode_reserve + t_needed * geode_bots
    }

    bots = {ore_bots, clay_bots, obsidian_bots + 1, geode_bots}

    work(mx, bots, reserve, costs, t - t_needed)
  end

  def do_work(mx, :clay, bots, reserve, costs, t) do
    {ore_bots, clay_bots, obsidian_bots, geode_bots} = bots
    {ore_reserve, clay_reserve, obsidian_reserve, geode_reserve} = reserve
    %{clay: %{ore: ore_cost}} = costs

    t_needed =
      0
      |> max(div(ore_cost - ore_reserve + ore_bots - 1, ore_bots))
      |> Kernel.+(1)

    reserve = {
      ore_reserve + t_needed * ore_bots - ore_cost,
      clay_reserve + t_needed * clay_bots,
      obsidian_reserve + t_needed * obsidian_bots,
      geode_reserve + t_needed * geode_bots
    }

    bots = {ore_bots, clay_bots + 1, obsidian_bots, geode_bots}

    work(mx, bots, reserve, costs, t - t_needed)
  end

  def do_work(mx, :ore, bots, reserve, costs, t) do
    {ore_bots, clay_bots, obsidian_bots, geode_bots} = bots
    {ore_reserve, clay_reserve, obsidian_reserve, geode_reserve} = reserve
    %{ore: %{ore: ore_cost}} = costs

    t_needed =
      0
      |> max(div(ore_cost - ore_reserve + ore_bots - 1, ore_bots))
      |> Kernel.+(1)

    reserve = {
      ore_reserve + t_needed * ore_bots - ore_cost,
      clay_reserve + t_needed * clay_bots,
      obsidian_reserve + t_needed * obsidian_bots,
      geode_reserve + t_needed * geode_bots
    }

    bots = {ore_bots + 1, clay_bots, obsidian_bots, geode_bots}

    work(mx, bots, reserve, costs, t - t_needed)
  end
end
Enum.sum(for %{index: i, cost: c} <- data, do: N1.start(c, 24) * i)
for %{index: i, cost: c} <- data, i < 4 do
  Task.async(fn -> N1.start(c, 32) end)
end
|> Task.await_many(40_000)
|> Enum.reduce(&amp;*/2)

# using concurrent tasks saves 20% of time