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

Day 17: Pyroclastic Flow

2022/day-17.livemd

Day 17: Pyroclastic Flow

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

Day 17

sample_input = Kino.Input.textarea("Paste Sample Input")
real_input = Kino.Input.textarea("Paste Real Input")
defmodule Rock do
  # position is the bottom-left corner of the rock
  defstruct position: {2, 3}, points: :dash

  @shapes %{
    0 => [{0, 0}, {1, 0}, {2, 0}, {3, 0}],
    1 => [{1, 0}, {0, 1}, {1, 1}, {2, 1}, {1, 2}],
    2 => [{0, 0}, {1, 0}, {2, 0}, {2, 1}, {2, 2}],
    3 => [{0, 0}, {0, 1}, {0, 2}, {0, 3}],
    4 => [{0, 0}, {1, 0}, {0, 1}, {1, 1}]
  }

  def new(index, max_height) do
    %__MODULE__{
      points: Map.get(@shapes, rem(index, 5)),
      position: {2, max_height + 3}
    }
  end

  def translate(%{position: {x, y}} = rock, :down), do: %{rock | position: {x, y - 1}}
  def translate(%{position: {x, y}} = rock, "<"), do: %{rock | position: {x - 1, y}}
  def translate(%{position: {x, y}} = rock, ">"), do: %{rock | position: {x + 1, y}}

  def points(%{points: points, position: {x, y}}) do
    Enum.map(points, fn {pointX, pointY} -> {pointX + x, pointY + y} end)
  end
end
defmodule Wind do
  defstruct instructions: "", working: ""

  def new(instructions) do
    %__MODULE__{
      working: instructions,
      instructions: instructions
    }
  end

  def next(%{working: "", instructions: instructions} = wind) do
    next(%{wind | working: instructions})
  end

  def next(%{working: <> <> rest} = wind) do
    {head, %{wind | working: rest}}
  end
end
defmodule ForgetfulSet do
  defstruct a: MapSet.new(), b: MapSet.new(), active: :a, count: 0, memory: 100

  def new(memory \\ 100) do
    %__MODULE__{memory: memory}
  end

  def put(set, value) do
    set
    |> Map.update!(set.active, fn active -> MapSet.put(active, value) end)
    |> maybe_swap_active()
  end

  def member?(set, value) do
    MapSet.member?(set.a, value) or MapSet.member?(set.b, value)
  end

  defp maybe_swap_active(%{count: count, memory: memory} = set) when count >= memory,
    do: swap_active(set)

  defp maybe_swap_active(set), do: set

  defp swap_active(%{active: :a} = set), do: %{set | active: :b, b: MapSet.new()}
end
defmodule Chimney do
  defstruct rocks: ForgetfulSet.new(), max_height: 0, rock_count: 0

  def process_rocks(wind_input, count) do
    wind =
      wind_input
      |> Kino.Input.read()
      |> Wind.new()

    0..4
    |> Stream.cycle()
    |> Enum.take(count)
    |> Enum.reduce({%__MODULE__{}, wind}, fn index, {chimney, wind} ->
      index
      |> Rock.new(chimney.max_height)
      |> then(&amp;process_rock(chimney, wind, &amp;1))
    end)
    |> then(&amp;elem(&amp;1, 0))
  end

  def process_rock(chimney, wind, rock) do
    [nil]
    |> Stream.cycle()
    |> Enum.reduce_while({chimney, wind, rock}, fn _index, {chimney, wind, rock} ->
      case move_rock(chimney, wind, rock) do
        {:at_rest, chimney, wind, _rock} -> {:halt, {chimney, wind}}
        {:falling, chimney, wind, rock} -> {:cont, {chimney, wind, rock}}
      end
    end)
  end

  def move_rock(chimney, wind, rock) do
    {wind_dir, next_wind} = Wind.next(wind)
    blown_rock = Rock.translate(rock, wind_dir)
    blown_rock = if collides?(chimney, blown_rock), do: rock, else: blown_rock
    fallen_rock = Rock.translate(blown_rock, :down)

    {state, next_chimney, next_rock} =
      if collides?(chimney, fallen_rock) do
        {:at_rest, add_rock(chimney, blown_rock), blown_rock}
      else
        {:falling, chimney, fallen_rock}
      end

    {state, next_chimney, next_wind, next_rock}
  end

  def collides?(chimney, %Rock{} = rock) do
    rock
    |> Rock.points()
    |> Enum.any?(&amp;collides?(chimney, &amp;1))
  end

  def collides?(chimney, {x, y}) do
    x < 0 or y < 0 or x > 6 or ForgetfulSet.member?(chimney.rocks, {x, y})
  end

  def add_rock(chimney, %Rock{} = rock) do
    Enum.reduce(
      Rock.points(rock),
      %{chimney | rock_count: chimney.rock_count + 1},
      fn point, chimney -> add_rock(chimney, point) end
    )
  end

  def add_rock(chimney, {x, y}) do
    %{
      chimney
      | rocks: ForgetfulSet.put(chimney.rocks, {x, y}),
        max_height: max(chimney.max_height, y + 1)
    }
  end

  def inspect(%{rocks: rocks, max_height: max_height}) do
    for y <- max_height..0 do
      for x <- 0..6 do
        if ForgetfulSet.member?(rocks, {x, y}), do: "#", else: "."
      end
      |> Enum.join()
    end
    |> Enum.join("\n")
    |> IO.puts()
  end
end
sample_input |> Chimney.process_rocks(2022)
real_input |> Chimney.process_rocks(2022)
wind = real_input |> Kino.Input.read() |> Wind.new()

0..1_000_000_000_000
|> Enum.reduce(wind, fn index, wind ->
  {_next, new_wind} = Wind.next(wind)
  new_wind
end)