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

Regolith Reservoir

livebook/day14.livemd

Regolith Reservoir

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

alias VegaLite, as: Vl
:ok

Input

input = Kino.Input.textarea("Input:")
input =
  Kino.Input.read(input)
  |> String.trim()
  |> String.split("\n")
  |> Enum.map(fn s ->
    String.split(s, " -> ", trim: true)
    |> Enum.map(fn t ->
      String.split(t, ",") |> Enum.map(&String.to_integer/1)
    end)
    |> Enum.map(&List.to_tuple/1)
  end)
create_grid = fn input ->
  Enum.reduce(input, %{{500, 0} => :hole}, fn path, acc ->
    Enum.chunk_every(path, 2, 1, :discard)
    |> Enum.reduce(acc, fn
      [{x0, y}, {x1, y}], acc when x0 <= x1 ->
        Enum.reduce(x0..x1, acc, &amp;Map.put(&amp;2, {&amp;1, y}, :rock))

      [{x0, y}, {x1, y}], acc when x0 > x1 ->
        Enum.reduce(x0..x1//-1, acc, &amp;Map.put(&amp;2, {&amp;1, y}, :rock))

      [{x, y0}, {x, y1}], acc when y0 <= y1 ->
        Enum.reduce(y0..y1, acc, &amp;Map.put(&amp;2, {x, &amp;1}, :rock))

      [{x, y0}, {x, y1}], acc when y0 > y1 ->
        Enum.reduce(y0..y1//-1, acc, &amp;Map.put(&amp;2, {x, &amp;1}, :rock))
    end)
  end)
end
grid = create_grid.(input)
:ok
render = fn grid, {{x_min, x_max}, {y_min, y_max}} ->
  for y <- y_min..y_max do
    line = ""

    line =
      for x <- x_min..x_max do
        case Map.get(grid, {x, y}) do
          nil -> ?.
          :hole -> ?+
          :rock -> ?#
          :sand -> ?o
        end
      end

    IO.puts(line)
  end

  :ok
end
bounds = Physics.bounds(grid)
render.(grid, bounds)
defmodule Physics do
  def drop(sand = {_x, y}, grid, y_max) when y <= y_max do
    case next(sand, grid) do
      {:cont, sand} -> drop(sand, grid, y_max)
      {:halt, sand} -> {:cont, sand}
    end
  end

  def drop(_sand, _grid, _y_max) do
    :halt
  end

  def next(_sand = {x, y}, grid) do
    cond do
      Map.get(grid, {x, y + 1}) == nil -> {:cont, {x, y + 1}}
      Map.get(grid, {x - 1, y + 1}) == nil -> {:cont, {x - 1, y + 1}}
      Map.get(grid, {x + 1, y + 1}) == nil -> {:cont, {x + 1, y + 1}}
      true -> {:halt, {x, y}}
    end
  end

  def bounds(grid) do
    {{{x_min, _}, _}, {{x_max, _}, _}} = Enum.min_max_by(grid, fn {{x, _}, _} -> x end)
    {{{_, y_min}, _}, {{_, y_max}, _}} = Enum.min_max_by(grid, fn {{_, y}, _} -> y end)
    {{x_min, x_max}, {y_min, y_max}}
  end

  def pour(grid) do
    {{_, _}, {_, y_max}} = bounds(grid)

    Stream.iterate(1, &amp;(&amp;1 + 1))
    |> Enum.reduce_while(grid, fn count, grid ->
      sand = {500, 0}

      case drop(sand, grid, y_max) do
        :halt -> {:halt, {count - 1, grid}}
        # part 2
        {:cont, _sand = {500, 0}} -> {:halt, {count, grid}}
        # part 1
        {:cont, sand} -> {:cont, Map.put(grid, sand, :sand)}
      end
    end)
  end
end

Part 1

{count, grid} = Physics.pour(grid)
render.(grid, bounds)
count

Part 2

add_floor = fn grid ->
  {{x_min, x_max}, {y_min, y_max}} = Physics.bounds(grid)
  height = y_max - y_min

  Enum.reduce((x_min - height)..(x_max + height), grid, fn x, grid ->
    Map.put(grid, {x, y_max + 2}, :rock)
  end)
end

grid = create_grid.(input)
grid = add_floor.(grid)

bounds = Physics.bounds(grid)
render.(grid, bounds)
{count, grid} = Physics.pour(grid)
render.(grid, bounds)
count