Powered by AppSignal & Oban Pro

Day 14

2022/day14.livemd

Day 14

Mix.install(
  [
    {:kino_aoc, git: "https://github.com/ljgago/kino_aoc"},
    :image
  ],
  consolidate_protocols: false
)

# defimpl Kino.Render, for: Vix.Vips.Image do
#   alias Vix.Vips.Image, as: VImg

#   def to_livebook(image) do
#     format = "png"

#     {:ok, image_bin} = VImg.write_to_buffer(image, ".#{format}")
#     Kino.Output.image(image_bin, "image/#{format}")
#   end
# end
:ok

Setup

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2022", "14", System.fetch_env!("LB_ADVENT_OF_CODE_SESSION"))
{:ok,
 "492,26 -> 492,17 -> 492,26 -> 494,26 -> 494,16 -> 494,26 -> 496,26 -> 496,22 -> 496,26 -> 498,26 -> 498,17 -> 498,26 -> 500,26 -> 500,20 -> 500,26 -> 502,26 -> 502,25 -> 502,26 -> 504,26 -> 504,23 -> 504,26 -> 506,26 -> 506,21 -> 506,26 -> 508,26 -> 508,16 -> 508,26 -> 510,26 -> 510,24 -> 510,26\n481,92 -> 481,96 -> 476,96 -> 476,99 -> 487,99 -> 487,96 -> 485,96 -> 485,92\n460,73 -> 460,70 -> 460,73 -> 462,73 -> 462,69 -> 462,73 -> 464,73 -> 464,65 -> 464,73 -> 466,73 -> 466,65 -> 466,73 -> 468,73 -> 468,64 -> 468,73 -> 470,73 -> 470,68 -> 470,73 -> 472,73 -> 472,68 -> 472,73\n470,76 -> 470,80 -> 466,80 -> 466,84 -> 481,84 -> 481,80 -> 474,80 -> 474,76\n492,26 -> 492,17 -> 492,26 -> 494,26 -> 494,16 -> 494,26 -> 496,26 -> 496,22 -> 496,26 -> 498,26 -> 498,17 -> 498,26 -> 500,26 -> 500,20 -> 500,26 -> 502,26 -> 502,25 -> 502,26 -> 504,26 -> 504,23 -> 504,26 -> 506,26 -> 506,21 -> 506,26 -> 508,26 -> 508,16 -> 508,26 -> 510,26 -> 510,24 -> 510,26\n495,155 -> 499,155\n460,73 -> 460,70 -> 460,73 -> 462,73 -> 462,69 -> 462,73 -> 464,73 -> 464,65 -> 464,73 -> 466,73 -> 466,65 -> 466,73 -> 468,73 -> 468,64 -> 468,73 -> 470,73 -> 470,68 -> 470,73 -> 472,73 -> 472,68 -> 472,73\n500,138 -> 505,138\n484,39 -> 484,38 -> 484,39 -> 486,39 -> 486,33 -> 486,39 -> 488,39 -> 488,30 -> 488,39 -> 490,39 -> 490,35 -> 490,39 -> 492,39 -> 492,38 -> 492,39 -> 494,39 -> 494,30 -> 494,39 -> 496,39 -> 496,36 -> 496,39\n481,92 -> 481,96 -> 476,96 -> 476,99 -> 487,99 -> 487,96 -> 485,96 -> 485,92\n492,26 -> 492,17 -> 492,26 -> 494,26 -> 494,16 -> 494,26 -> 496,26 -> 496,22 -> 496,26 -> 498,26 -> 498,17 -> 498,26 -> 500,26 -> 500,20 -> 500,26 -> 502,26 -> 502,25 -> 502,26 -> 504,26 -> 504,23 -> 504,26 -> 506,26 -> 506,21 -> 506,26 -> 508,26 -> 508,16 -> 508,26 -> 510,26 -> 510,24 -> 510,26\n460,73 -> 460,70 -> 460,73 -> 462,73 -> 462,69 -> 462,73 -> 464,73 -> 464,65 -> 464,73 -> 466,73 -> 466,65 -> 466,73 -> 468,73 -> 468,64 -> 468,73 -> 470,73 -> 470,68 -> 470,73 -> 472,73 -> 472,68 -> 472,73\n492,26 -> 492,17 -> 492,26 -> 494,26 -> 494,16 -> 494,26 -> 496,26 -> 496,22 -> 496,26 -> 498,26 -> 498,17 -> 498,26 -> 500,26 -> 500,20 -> 500,26 -> 502,26 -> 502,25 -> 502,26 -> 504,26 -> 504,23 -> 504,26 -> 506,26 -> 506,21 -> 506,26 -> 508,26 -> 508,16 -> 508,26 -> 510,26 -> 510,24 -> 510,26\n460,73 -> 460,70 -> 460,73 -> 462,73 -> 462,69 -> 462,73 -> 464,73 -> 464,65 -> 464,73 -> 466,73 -> 466,65 -> 466,73 -> 468,73 -> 468,64 -> 468,73 -> 470,73 -> 470,68 -> 470,73 -> 472,73 -> 472,68 -> 472,73\n492,26 -> 492,17 -> 492,26 -> 494,26 -> 494,16 -> 494,26 -> 496,26 -> 496,22 -> 496,26 -> 498,26 -> 498,17 -> 498,26 -> 500,26 -> 500,20 -> 500,26 -> 502,26 -> 502,25 -> 502,26 -> 504,26 -> 504,23 -> 504,26 -> 506,26 -> 506,21 -> 506,26 -> 508,26 -> 508,16 -> 508,26 -> 510,26 -> 510,24 -> 510,26\n489,115 -> 489,119 -> 481,119 -> 481,126 -> 494,126 -> 494,119 -> 493,119 -> 493,115\n492,26 -> 492,17 -> 492,26 -> 494,26 -> 494,16 -> 494,26 -> 496,26 -> 496,22 -> 496,26 -> 498,26 -> 498,17 -> 498,26 -> 500,26 -> 500,20 -> 500,26 -> 502,26 -> 502,25 -> 502,26 -> 504,26 -> 504,23 -> 504,26 -> 506,26 -> 506,21 -> 506,26 -> 508,26 -> 508,16 -> 508,26 -> 510,26 -> 510,24 -> 510,26\n485,112 -> 485,102 -> 485,112 -> 487,112 -> 487,111 -> 487,112 -> 489,112 -> 489,105 -> 489,112\n481,92 -> 481,96 -> 476,96 -> 476,99 -> 487,99 -> 487,96 -> 485,96 -> 485,92\n484,39 -> 484,38 -> 484,39 -> 486,39 -> 486,33 -> 486,39 -> 488,39 -> 488,30 -> 488,39 -> 490,39 -> 490,35 -> 490,39 -> 492,39 -> 492,38 -> 492,39 -> 494,39 -> 494,30 -> 494,39 -> 496,39 -> 496,36 -> 496,39\n470,49 -> 470,53 -> 468,53 -> 468,60 -> 477,60 -> 477,53 -> 476,53 -> 476,49\n483,160 -> 487,160\n492,26 -> 492,17 -> 492,26 -> 494,26 -> 494,16 -> 494,26 -> 496,26 -> 496,22 -> 496,26 -> 498,26 -> 498,17 -> 498,26 -> 500,26 -> 500,20 -> 500,26 -> 502,26 -> 502,25 -> 502,26 -> 504,26 -> 504,23 -> 504,26 -> 506,26 -> 506,21 -> 506,26 -> 508,26 -> 508,16 -> 508,26 -> 510,26 -> 510,24 -> 510,26\n460,73 -> 460,70 -> 460,73 -> 462,73 -> 462,69 -> 462,73 -> 464,73 -> 464,65 -> 464,73 -> 466,73 -> 466,65 -> 466,73 -> 468,73 -> 468,64 -> 468,73 -> 47" <> ...}
defmodule Cave do
  defstruct map: %{}, width: nil, height: 0, start: {500, 0}

  def parse(input, start \\ {500, 0}) do
    map =
      input
      |> String.split("\n", trim: true)
      |> Enum.map(&amp;Cave.parse_line/1)
      |> Enum.reduce(&amp;Map.merge/2)

    {width, height} =
      for {{x, y}, _} <- map,
          reduce: {nil, nil} do
        {nil, nil} ->
          {x..x, y}

        {min_x..max_x, max_y} ->
          {min(min_x, x)..max(max_x, x), max(max_y, y)}
      end

    %__MODULE__{map: map, start: start, width: width, height: height}
  end

  def parse_point(str) do
    [x, y] = String.split(str, ",")

    {String.to_integer(x), String.to_integer(y)}
  end

  def parse_line(line) do
    points =
      line
      |> String.split(" -> ")
      |> Enum.map(&amp;parse_point/1)
      |> Enum.chunk_every(2, 1, :discard)
      |> Enum.flat_map(fn
        [{x, y1}, {x, y2}] ->
          for y <- y1..y2, do: {x, y}

        [{x1, y}, {x2, y}] ->
          for x <- x1..x2, do: {x, y}
      end)
      |> Map.new(&amp;{&amp;1, :rock})

    points
  end

  def drop_sand(%__MODULE__{} = cave), do: drop_sand(cave, cave.start)

  def drop_sand(%__MODULE__{height: h} = cave, {_, sy} = p)
      when sy >= h,
      do: {p, struct(cave, map: Map.put(cave.map, p, :sand))}

  def drop_sand(%__MODULE__{map: map} = cave, {sx, sy} = p) do
    cond do
      not Map.has_key?(map, {sx, sy + 1}) -> drop_sand(cave, {sx, sy + 1})
      not Map.has_key?(map, {sx - 1, sy + 1}) -> drop_sand(cave, {sx - 1, sy + 1})
      not Map.has_key?(map, {sx + 1, sy + 1}) -> drop_sand(cave, {sx + 1, sy + 1})
      true -> {p, struct(cave, map: Map.put(cave.map, p, :sand))}
    end
  end

  def to_image(%__MODULE__{} = cave) do
    {{{x0, _}, _}, {{x1, _}, _}} = Enum.min_max_by(cave.map, fn {{x, _}, _} -> x end)
    {sx, sy} = cave.start
    image = Image.new!(x1 - x0 + 3, cave.height + 3)

    {:ok, image} =
      Image.mutate(image, fn im ->
        Image.Draw.point(im, sx - x0 + 1, sy + 1, color: :red)

        cave.map
        |> Enum.sort()
        |> Enum.each(fn {{x, y}, type} ->
          case type do
            :rock -> Image.Draw.point(im, x - x0 + 1, y + 1, color: :white)
            :sand -> Image.Draw.point(im, x - x0 + 1, y + 1, color: :tan)
          end
        end)

        :ok
      end)

    image
  end
end

defimpl Kino.Render, for: Cave do
  def to_livebook(%@for{} = cave) do
    cave
    |> @for.to_image()
    |> Image.resize!(4, interpolate: :nearest)
    |> Kino.Render.to_livebook()
  end
end
{:module, Kino.Render.Cave, <<70, 79, 82, 49, 0, 0, 9, ...>>, {:to_livebook, 1}}
cave = Cave.parse(puzzle_input)

floor = cave.height + 1

cave = struct(cave, height: floor)

Task 1

frame = Kino.Frame.new() |> Kino.render()

image = Cave.to_image(cave)

cave
|> Stream.unfold(fn cave ->
  {_, new_cave} = ret = Cave.drop_sand(cave)

  {ret, new_cave}
end)
|> Enum.reduce_while(0, fn {{_x, y}, cave}, acc ->
  if y < cave.height do
    {:cont, acc + 1}
  else
    Kino.Frame.render(frame, Image.resize!(Cave.to_image(cave), 4, interpolate: :nearest))
    {:halt, acc}
  end
end)
768

Task 2

{grains, pyramid} =
  Stream.unfold(cave, fn cave ->
    {_, new_cave} = ret = Cave.drop_sand(cave)

    {ret, new_cave}
  end)
  |> Enum.reduce_while(0, fn {p, cave}, acc ->
    if p != {500, 0} do
      {:cont, acc + 1}
    else
      {:halt, {acc + 1, cave}}
    end
  end)

grains
26686
Kino.render(pyramid)

:ok
:ok