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

Advent of Code 2022 - Day 14

livebooks/day_14.livemd

Advent of Code 2022 - Day 14

Mix.install([
  {:kino, github: "livebook-dev/kino"}
])

Setup

example_input = """
498,4 -> 498,6 -> 496,6
503,4 -> 502,4 -> 502,9 -> 494,9
"""

input = Kino.Input.textarea("Puzzle Input", default: example_input, monospace: true)
rock_walls =
  input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.flat_map(fn line ->
    line
    |> String.split(" -> ", trim: true)
    |> Enum.map(fn coordinates ->
      [x, y] = String.split(coordinates, ",")

      {String.to_integer(x), String.to_integer(y)}
    end)
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.flat_map(fn
      [{x, y1}, {x, y2}] -> Enum.map(y1..y2, &{x, &1})
      [{x1, y}, {x2, y}] -> Enum.map(x1..x2, &{&1, y})
    end)
  end)
  |> MapSet.new()

lowest_y =
  rock_walls
  |> Enum.map(fn {_x, y} -> y end)
  |> Enum.max()

cave_map = %{rock_walls: rock_walls, lowest_y: lowest_y}

Section

defmodule Part1 do
  @sand_source {500, 0}

  def solve(cave_map) do
    initial_resting_sand = MapSet.new()

    cave_map
    |> fill_cave(initial_resting_sand)
    |> Enum.count()
  end

  defp fill_cave(cave_map, resting_sand) do
    case simulate_falling_sand(cave_map, resting_sand, @sand_source) do
      {:ok, new_resting_sand} -> fill_cave(cave_map, new_resting_sand)
      :error -> resting_sand
    end
  end

  defp simulate_falling_sand(cave_map, resting_sand, sand_coordinates) do
    sand_coordinates
    |> next_coordinates()
    |> Enum.find(&(&1 not in cave_map.rock_walls and &1 not in resting_sand))
    |> then(fn
      nil -> {:ok, MapSet.put(resting_sand, sand_coordinates)}
      {_, new_y} when new_y >= cave_map.lowest_y -> :error
      {_, _} = new_coordinates -> simulate_falling_sand(cave_map, resting_sand, new_coordinates)
    end)
  end

  defp next_coordinates({x, y}), do: [{x, y + 1}, {x - 1, y + 1}, {x + 1, y + 1}]
end

Part1.solve(cave_map)

Part 2

defmodule Part2 do
  @sand_source {500, 0}

  def solve(cave_map) do
    initial_resting_sand = MapSet.new()

    cave_map
    |> fill_cave(initial_resting_sand)
    |> Enum.count()
  end

  defp fill_cave(cave_map, resting_sand) do
    if @sand_source in resting_sand do
      resting_sand
    else
      new_resting_sand = simulate_falling_sand(cave_map, resting_sand, @sand_source)

      fill_cave(cave_map, new_resting_sand)
    end
  end

  defp simulate_falling_sand(cave_map, resting_sand, sand_coordinates) do
    sand_coordinates
    |> next_coordinates()
    |> Enum.find(&(&1 not in cave_map.rock_walls and &1 not in resting_sand))
    |> then(fn
      nil ->
        MapSet.put(resting_sand, sand_coordinates)

      {_, new_y} when new_y == cave_map.lowest_y + 2 ->
        MapSet.put(resting_sand, sand_coordinates)

      {_, _} = new_coordinates ->
        simulate_falling_sand(cave_map, resting_sand, new_coordinates)
    end)
  end

  defp next_coordinates({x, y}), do: [{x, y + 1}, {x - 1, y + 1}, {x + 1, y + 1}]
end

Part2.solve(cave_map)