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

Day 15: Warehouse Woes

day15.livemd

Day 15: Warehouse Woes

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

Input

input = Kino.Input.textarea("Please, paste your input:")
defmodule Day15Shared do
  def parse(input) do
    [warehouse_raw, moves_raw] = Kino.Input.read(input) |> String.split("\n\n")

    moves =
      moves_raw
      |> String.replace("\n", "")
      |> String.codepoints()
      |> Enum.map(fn
        "^" -> :up
        ">" -> :right
        "v" -> :down
        "<" -> :left
      end)

    {warehouse_raw, moves}
  end

  def to_string(%{max_x: mx, max_y: my} = warehouse, robot_position, direction) do
    direction_char =
      case direction do
        :up -> "^"
        :right -> ">"
        :down -> "v"
        :left -> "<"
      end

    Enum.map(0..my, fn y ->
      Enum.map(0..mx, fn x ->
        if {x, y} == robot_position do
          direction_char
        else
          Map.get(warehouse, {x, y})
        end
      end)
      |> Enum.join("")
    end)
    |> Enum.join("\n")
  end
end

# Day15Shared.parse(input)

Part 1

defmodule Day15Part1 do
  def process({warehouse_raw, moves}) do
    warehouse = build_warehouse_map(warehouse_raw)
    
    %{start: start_pos, max_x: mx, max_y: my} = warehouse

    moves
    |> move(start_pos, mx, my, warehouse)
    |> Enum.filter(fn {_, v} -> v == "O" end)
    |> Enum.reduce(0, fn {{x, y}, _}, acc ->
      acc + (x + 100 * y)
    end)
  end

  defp build_warehouse_map(warehouse_raw) do
      warehouse_raw
      |> String.split("\n")
      |> Enum.with_index()
      |> Enum.reduce(%{}, fn {line, y}, map ->
        line
        |> String.codepoints()
        |> Enum.with_index()
        |> Enum.reduce(map, fn {value, x}, map ->
          case value do
            v when v in ~w[# . O] -> Map.put(map, {x, y}, value)
            "@" -> map |> Map.put({x, y}, ".") |> Map.put(:start, {x, y})
          end
          |> Map.update(:max_x, x, &amp;max(&amp;1, x))
          |> Map.update(:max_y, y, &amp;max(&amp;1, y))
        end)
      end)
  end

  def move([], _, _, _, warehouse), do: warehouse

  def move([direction | next_moves], pos, mx, my, warehouse) do
    {next_warehouse, next_pos} =
      to_check(pos, direction, mx, my)
      |> Enum.map(&amp;{&amp;1, Map.get(warehouse, &amp;1)})
      |> shift([])
      |> then(fn
        {:wall, _} ->
          {warehouse, pos}

        {:move, to_be_moved} ->
          new_warehouse =
            to_be_moved
            |> Enum.map(&amp;elem(&amp;1, 0))
            |> Enum.reduce(warehouse, fn box_pos, warehouse ->
              new_box_pos = shift_to(box_pos, direction)

              warehouse
              |> Map.put(box_pos, ".")
              |> Map.put(new_box_pos, "O")
            end)

          {new_warehouse, shift_to(pos, direction)}
      end)

    move(next_moves, next_pos, mx, my, next_warehouse)
  end

  defp to_check({x0, y0}, :up, _mx, _my), do: Enum.map((y0 - 1)..1, fn y -> {x0, y} end)
  defp to_check({x0, y0}, :right, mx, _my), do: Enum.map((x0 + 1)..(mx - 1), fn x -> {x, y0} end)
  defp to_check({x0, y0}, :down, _mx, my), do: Enum.map((y0 + 1)..(my - 1), fn y -> {x0, y} end)
  defp to_check({x0, y0}, :left, _mx, _my), do: Enum.map((x0 - 1)..1, fn x -> {x, y0} end)

  def shift([], _), do: {:wall, []}
  def shift([{_, "#"} | _], to_be_moved), do: {:wall, to_be_moved}
  def shift([{_, "."} | _], to_be_moved), do: {:move, to_be_moved}
  def shift([{_, "O"} = to_move | next], to_be_moved), do: shift(next, [to_move | to_be_moved])

  def shift_to({x, y}, :up), do: {x, y - 1}
  def shift_to({x, y}, :right), do: {x + 1, y}
  def shift_to({x, y}, :down), do: {x, y + 1}
  def shift_to({x, y}, :left), do: {x - 1, y}
end

input |> Day15Shared.parse() |> Day15Part1.process()

# 1437174 is the right answer

Part 2

defmodule Day15Part2 do
  import Day15Shared, only: [to_string: 3]

  def process({warehouse_raw, moves}) do
    warehouse = build_warehouse_map(warehouse_raw)

    %{start: start_pos, max_x: mx, max_y: my} = warehouse

    moves
    |> move(start_pos, mx, my, warehouse, 0)
    |> Enum.filter(fn {_, v} -> v == "[" end)
    |> Enum.reduce(0, fn {{x, y}, _}, acc ->
      acc + (x + 100 * y)
    end)

    # debug(wh, {0, 0}, :up, "LAST")
  end

  defp build_warehouse_map(warehouse_raw) do
    warehouse_raw
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {line, y}, map ->
      line
      |> String.codepoints()
      |> Enum.with_index()
      |> Enum.reduce(map, fn {value, x}, map ->
        # every x will become two coordinates
        x0 = x * 2
        x1 = x0 + 1

        case value do
          v when v in ~w[# .] -> map |> Map.put({x0, y}, value) |> Map.put({x1, y}, value)
          "O" -> map |> Map.put({x0, y}, "[") |> Map.put({x1, y}, "]")
          "@" -> map |> Map.put({x0, y}, ".") |> Map.put({x1, y}, ".") |> Map.put(:start, {x0, y})
        end
        |> Map.update(:max_x, x, &amp;max(&amp;1, x1))
        |> Map.update(:max_y, y, &amp;max(&amp;1, y))
      end)
    end)
  end

  def move([], _, _, _, warehouse, _), do: warehouse

  def move([direction | next_moves], pos, mx, my, warehouse, step) do
    # debug(warehouse, pos, direction, step)

    {next_warehouse, next_pos} =
      case to_move(pos, direction, warehouse) do
        {:wall, _} ->
          # IO.inspect(:WALL, label: "to_move_#{direction}@#{step}: WALL")
          {warehouse, pos}

        {:move, to_be_moved} ->
          # IO.inspect(to_be_moved, label: "to_move_#{direction}@#{step}")
          # debug(warehouse, pos, direction, step)

          new_warehouse =
            Enum.reduce(to_be_moved, warehouse, fn field, new_warehouse ->
              swap(new_warehouse, field, direction)
            end)

          {new_warehouse, shift_to(pos, direction)}
      end

    move(next_moves, next_pos, mx, my, next_warehouse, step + 1)
  end

  def debug(warehouse, pos, direction, _step) do
    to_string(warehouse, pos, direction) |> IO.puts()

    # IO.puts("^#{step}")
    IO.puts("")
  end

  def to_move({x0, y0}, :up, warehouse) do
    Enum.reduce_while((y0 - 1)..0, {[], [x0]}, fn y, {acc, x_range} ->
      pressure = Enum.map(x_range, &amp;Map.get(warehouse, {&amp;1, y}))

      # IO.inspect({x_range, pressure}, label: "pressure")

      any_wall? = Enum.any?(pressure, &amp;(&amp;1 == "#"))
      all_free? = Enum.all?(pressure, &amp;(&amp;1 == "."))

      cond do
        any_wall? ->
          {:halt, {:wall, []}}

        all_free? ->
          {:halt, {:move, Enum.uniq(acc)}}

        true ->
          to_move =
            Enum.reduce(x_range, acc, fn x, acc ->
              case Map.get(warehouse, {x, y}) do
                "[" -> [{x, y} | [{x + 1, y} | acc]]
                "]" -> [{x - 1, y} | [{x, y} | acc]]
                "." -> acc
              end
            end)

          new_x_range =
            to_move
            |> Enum.filter(&amp;(elem(&amp;1, 1) == y))
            |> Enum.map(&amp;elem(&amp;1, 0))

          {:cont, {to_move, new_x_range}}
      end
    end)
  end

  def to_move({x0, y0}, :down, %{max_y: my} = warehouse) do
    Enum.reduce_while((y0 + 1)..my, {[], [x0]}, fn y, {acc, x_range} ->
      pressure = Enum.map(x_range, &amp;Map.get(warehouse, {&amp;1, y}))

      # IO.inspect({x_range, pressure}, label: "pressure")

      any_wall? = Enum.any?(pressure, &amp;(&amp;1 == "#"))
      all_free? = Enum.all?(pressure, &amp;(&amp;1 == "."))

      cond do
        any_wall? ->
          {:halt, {:wall, []}}

        all_free? ->
          {:halt, {:move, Enum.uniq(acc)}}

        true ->
          to_move =
            Enum.reduce(x_range, acc, fn x, acc ->
              case Map.get(warehouse, {x, y}) do
                "[" -> [{x, y} | [{x + 1, y} | acc]]
                "]" -> [{x - 1, y} | [{x, y} | acc]]
                "." -> acc
              end
            end)

          new_x_range =
            to_move
            |> Enum.filter(&amp;(elem(&amp;1, 1) == y))
            |> Enum.map(&amp;elem(&amp;1, 0))

          {:cont, {to_move, new_x_range}}
      end
    end)
  end

  def to_move({x0, y0}, :right, %{max_x: mx} = warehouse) do
    Enum.reduce_while((x0 + 1)..(mx - 1), [], fn x, acc ->
      case Map.get(warehouse, {x, y0}) do
        "#" -> {:halt, {:wall, []}}
        "." -> {:halt, {:move, acc}}
        _ -> {:cont, [{x, y0} | acc]}
      end
    end)
  end

  def to_move({x0, y0}, :left, warehouse) do
    Enum.reduce_while((x0 - 1)..1, [], fn x, acc ->
      case Map.get(warehouse, {x, y0}) do
        "#" -> {:halt, {:wall, []}}
        "." -> {:halt, {:move, acc}}
        _ -> {:cont, [{x, y0} | acc]}
      end
    end)
  end

  def swap(warehouse, pos, direction) do
    field = Map.get(warehouse, pos)
    new_pos = shift_to(pos, direction)

    warehouse |> Map.put(pos, ".") |> Map.put(new_pos, field)
  end

  def shift_to({x, y}, :up), do: {x, y - 1}
  def shift_to({x, y}, :right), do: {x + 1, y}
  def shift_to({x, y}, :down), do: {x, y + 1}
  def shift_to({x, y}, :left), do: {x - 1, y}
end

input |> Day15Shared.parse() |> Day15Part2.process()

# 1437468 is the right answer