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

Day 14: Regolith Reservoir

2022/day-14.livemd

Day 14: Regolith Reservoir

Mix.install([{:kino, "~> 0.7.0"}])

Day 14

sample_input = Kino.Input.textarea("Paste Sample Input")
real_input = Kino.Input.textarea("Paste Real Input")
defmodule Sand do
  defstruct position: {500, 0}, at_rest?: false, falling_forever?: false
end
defmodule Cavern do
  defstruct walls: MapSet.new(),
            sand: MapSet.new(),
            bottom: 0,
            entry_blocked?: false,
            with_floor?: false

  def is_blocked?(cavern, position),
    do: is_rock?(cavern, position) or is_sand?(cavern, position) or is_floor?(cavern, position)

  def is_rock?(%{walls: walls}, position), do: MapSet.member?(walls, position)

  def is_sand?(%{sand: sand}, position), do: MapSet.member?(sand, position)

  def is_floor?(%{with_floor?: with_floor?, bottom: bottom}, {_x, y}),
    do: with_floor? and y == bottom + 2

  def simulate(%{bottom: bottom, with_floor?: false} = cavern, %{position: {_, y}} = sand)
      when y > bottom do
    {cavern, %{sand | falling_forever?: true}}
  end

  def simulate(cavern, %{position: {x, y}} = sand) do
    cond do
      !is_blocked?(cavern, {x, y + 1}) ->
        simulate(cavern, %{sand | position: {x, y + 1}})

      !is_blocked?(cavern, {x - 1, y + 1}) ->
        simulate(cavern, %{sand | position: {x - 1, y + 1}})

      !is_blocked?(cavern, {x + 1, y + 1}) ->
        simulate(cavern, %{sand | position: {x + 1, y + 1}})

      true ->
        {add_sand(cavern, {x, y}), %{sand | at_rest?: true}}
    end
  end

  def add_sand(cavern, {x, y}) do
    %{cavern | sand: MapSet.put(cavern.sand, {x, y}), entry_blocked?: x == 500 && y == 0}
  end

  def add_cell(cavern, {x, y}) do
    %{cavern | walls: MapSet.put(cavern.walls, {x, y}), bottom: max(cavern.bottom, y)}
  end

  # single-segment path
  def add_path(cavern, [start_cell, end_cell]), do: add_segment(cavern, start_cell, end_cell)

  # multi-segment path
  def add_path(cavern, [start_cell, next_cell | rest]) do
    cavern
    |> add_segment(start_cell, next_cell)
    |> add_path([next_cell | rest])
  end

  # single-cell wall
  def add_segment(cavern, only_cell, only_cell), do: add_cell(cavern, only_cell)

  # vertical wall
  def add_segment(cavern, {x, startY} = start_cell, {x, endY} = end_cell) do
    increment = if startY < endY, do: 1, else: -1
    next_cell = {x, startY + increment}

    cavern
    |> add_cell(start_cell)
    |> add_segment(next_cell, end_cell)
  end

  # horizontal wall
  def add_segment(cavern, {startX, y} = start_cell, {endX, y} = end_cell) do
    increment = if startX < endX, do: 1, else: -1
    next_cell = {startX + increment, y}

    cavern
    |> add_cell(start_cell)
    |> add_segment(next_cell, end_cell)
  end
end
parse_cell = fn cell_string ->
  cell_string |> String.split(",") |> Enum.map(&amp;String.to_integer/1) |> List.to_tuple()
end

parse_input = fn input, with_floor? ->
  input
  |> Kino.Input.read()
  |> String.split("\n")
  |> Enum.map(fn line -> line |> String.split(" -> ") |> Enum.map(parse_cell) end)
  |> Enum.reduce(%Cavern{with_floor?: with_floor?}, fn path, cavern ->
    Cavern.add_path(cavern, path)
  end)
end

simulate = fn input, with_floor? ->
  cavern = parse_input.(input, with_floor?)

  [nil]
  |> Stream.cycle()
  |> Enum.reduce_while({cavern, %Sand{}}, fn _index, {cavern, sand} ->
    case Cavern.simulate(cavern, sand) do
      {cavern, %{falling_forever?: true}} -> {:halt, cavern}
      {%{entry_blocked?: true} = cavern, _sand} -> {:halt, cavern}
      {cavern, _sand} -> {:cont, {cavern, %Sand{}}}
    end
  end)
end
sample_input |> simulate.(false) |> Map.get(:sand) |> Enum.count()
real_input |> simulate.(false) |> Map.get(:sand) |> Enum.count()
sample_input |> simulate.(true) |> Map.get(:sand) |> Enum.count()
real_input |> simulate.(true) |> Map.get(:sand) |> Enum.count()