Powered by AppSignal & Oban Pro

Day 14

2022/day14.livemd

Day 14

Mix.install([
  {:kino_vega_lite, "~> 0.1.7"}
])

alias VegaLite, as: Vl

Section

defmodule Day14 do
  def part1(input) do
    bottom = input |> Map.keys() |> Enum.max_by(&elem(&1, 1)) |> elem(1)

    input
    |> Stream.unfold(&fall(&1, bottom, {500, 0}))
    |> Enum.reduce({nil, 0}, fn state, {_, count} ->
      {state, count + 1}
    end)
  end

  def part2(input) do
    bottom = input |> Map.keys() |> Enum.max_by(&elem(&1, 1)) |> elem(1) |> Kernel.+(2)

    state =
      (-bottom - 1)..(bottom + 1)
      |> Stream.map(&{500 + &1, bottom})
      |> Stream.map(&{&1, :Rock})
      |> Enum.into(input)

    state
    |> Stream.unfold(&fall(&1, bottom, {500, 0}))
    |> Enum.reduce({nil, 0}, fn state, {_, count} ->
      {state, count + 1}
    end)
  end

  defp fall(state, bottom, {x, y} = sand) do
    if state[{500, 0}] do
      nil
    else
      air = Enum.find([{x, y + 1}, {x - 1, y + 1}, {x + 1, y + 1}], &(!state[&1]))

      case air do
        nil -> {state, Map.put(state, sand, :Sand)}
        {_, y} when y > bottom -> nil
        _ -> fall(state, bottom, air)
      end
    end
  end

  def parse_input(path) do
    path
    |> File.stream!()
    |> Stream.flat_map(&parse_line/1)
    |> Map.new(fn coord -> {coord, :Rock} end)
  end

  defp parse_line(line) do
    line
    |> String.split(~r/\D+/, trim: true)
    |> Stream.map(&String.to_integer/1)
    |> Stream.chunk_every(2)
    |> Stream.chunk_every(2, 1, :discard)
    |> Stream.flat_map(fn [[a, b], [c, d]] ->
      for x <- a..c, y <- b..d, do: {x, y}
    end)
  end

  def visualize(state, opts \\ []) do
    Vl.new(opts)
    |> Vl.data_from_values(for {{x, y}, type} <- state, do: %{"x" => x, "y" => y, "type" => type})
    |> Vl.mark(:rect, tooltip: true)
    |> Vl.encode_field(:x, "x", axis: nil)
    |> Vl.encode_field(:y, "y", axis: nil)
    |> Vl.encode(:color, field: "type", type: :nominal)
  end
end
input = Day14.parse_input("#{__DIR__}/day14.txt")
{state, count} = Day14.part1(input)
count
Day14.visualize(state, width: 500, height: 1000)
{state, count} = Day14.part2(input)
count
Day14.visualize(state, width: 600, height: 400)