Advent of Code 2023 - Day 22
Mix.install([
  {:req, "~> 0.4.0"},
  {:libgraph, "~> 0.16.0"}
])
Input
opts = [headers: [{"cookie", "session=#{System.fetch_env!("LB_AOC_SESSION")}"}]]
puzzle_input = Req.get!("https://adventofcode.com/2023/day/22/input", opts).body
input =
  for row <- String.split(puzzle_input, "\n", trim: true) do
    String.split(row, [",", "~"])
    |> Enum.map(&String.to_integer/1)
  end
  |> Enum.sort_by(fn [_, _, z1, _, _, z2] -> min(z1, z2) end)
  |> Enum.with_index()
defmodule SandSlabs do
  def fall(falling, fallen, downs \\ %{}) do
    case tick(falling, fallen, downs) do
      {[], all_fallen, downs} -> {Enum.reverse(all_fallen), downs}
      {still_falling, already_fallen, downs} -> fall(still_falling, already_fallen, downs)
    end
  end
  defp tick([first | rest], fallen, downs) do
    if is_fallen?(first, fallen) do
      {rest, [first | fallen], downs}
    else
      {[down(first) | rest], fallen, inc(downs, first)}
    end
  end
  defp inc(downs, {_, i}) do
    Map.update(downs, i, 1, &(&1 + 1))
  end
  defp down({[x1, y1, z1, x2, y2, z2], i}) do
    {[x1, y1, z1 - 1, x2, y2, z2 - 1], i}
  end
  defp is_fallen?({[_, _, z1, _, _, z2], _i}, _fallen) when z1 == 0 or z2 == 0 do
    true
  end
  defp is_fallen?(a, fallen) do
    Enum.any?(fallen, fn b -> touches?(a, b) end)
  end
  def touches?({[xa1, ya1, za1, xa2, ya2, za2], _}, {[xb1, yb1, zb1, xb2, yb2, zb2], _i}) do
    (za1 == zb1 + 1 or za2 == zb2 + 1 or za1 == zb2 + 1 or za2 == zb1 + 1) and
      (xa1 in xb1..xb2 or xa2 in xb1..xb2 or xb1 in xa1..xa2 or xb2 in xa1..xa2) and
      (ya1 in yb1..yb2 or ya2 in yb1..yb2 or yb1 in ya1..ya2 or yb2 in ya1..ya2)
  end
end
{fallen_bricks, _downs} = SandSlabs.fall(input, [])
Part 1
init_graph =
  for {v, i} <- fallen_bricks, reduce: Graph.new() do
    g -> Graph.add_vertex(g, {v, i})
  end
graph =
  for {a, i} <- Graph.vertices(init_graph),
      {b, j} <- Graph.vertices(init_graph),
      a != b,
      SandSlabs.touches?({b, j}, {a, i}),
      reduce: init_graph do
    g -> Graph.add_edge(g, {a, i}, {b, j})
  end
graph
|> Graph.vertices()
|> Stream.filter(fn lower ->
  to_upper = Graph.out_edges(graph, lower)
  if length(to_upper) == 0 do
    true
  else
    to_upper
    |> Stream.map(fn %{v2: v2} -> Graph.in_edges(graph, v2) end)
    |> Stream.map(&length/1)
    |> Enum.all?(&(&1 > 1))
  end
end)
|> Enum.count()
Part 2
0..length(fallen_bricks)
|> Stream.map(fn i ->
  fallen_bricks
  |> List.pop_at(i)
  |> then(fn {_, rest} -> rest end)
  |> SandSlabs.fall([])
  |> then(fn {_bricks, downs} -> Enum.count(downs) end)
end)
|> Enum.sum()
  