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

Advent of Code 2024 - Day 15

2024/day-15.livemd

Advent of Code 2024 - Day 15

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

kino_input = Kino.Input.textarea("Please paste your input file: ")

Part 1

# didn't have enough time today, gave up. From: https://github.com/bjorng/advent-of-code/blob/main/2024/day15/lib/day15.ex
defmodule Part1 do
  def part1(input) do
    {grid, moves} = parse(input)
    {grid, robot} = parse_grid(grid)

    solve(grid, robot, moves)
  end

  defp push_boxes_part1(grid, position, move) do
    next = add(position, move)
    case grid do
      %{^next => ?\#} ->
        grid
      %{^next => ?.} ->
        %{grid | position => ?., next => ?O}
      %{^next => ?O} ->
        case find_empty(grid, next, move) do
          nil ->
            grid
          empty ->
            %{grid | position => ?., empty => ?O}
        end
    end
  end

  defp find_empty(grid, pos, move) do
    case Map.fetch!(grid, pos) do
      ?. -> pos
      ?\# -> nil
      ?O -> find_empty(grid, add(pos, move), move)
    end
  end

  def solve(grid, robot, moves) do
    Enum.reduce(moves, {grid, robot}, fn move, {grid, robot} ->
      next = add(robot, move)
      case Map.fetch!(grid, next) do
        ?. ->
          {grid, next}
        ?\# ->
          {grid, robot}
        ?O ->
          # Part 1
          grid = push_boxes_part1(grid, next, move)
          if Map.fetch!(grid, next) === ?. do
            {grid, next}
          else
            {grid, robot}
          end
        char when char in [?\[, ?\]] ->
          # Part 2
          case push_boxes_part2(grid, next, move) do
            nil -> {grid, robot}
            grid -> {grid, next}
          end
      end
    end)
    |> elem(0)
    |> Enum.map(fn {{row, col}, what} ->
      if what in [?O, ?\[] do
        100 * row + col
      else
        0
      end
    end)
    |> Enum.sum
  end

  defp push_boxes_part2(grid, position, {0, _} = move) do
    next1 = add(position, move)
    next2 = add(next1, move)

    move_box = fn grid ->
      %{grid | next2 => grid[next1],
        next1 => grid[position],
        position => ?.}
    end

    case Map.fetch!(grid, next2) do
      ?\# ->
        nil
      ?. ->
        move_box.(grid)
      char when char in [?\[, ?\]] ->
        case push_boxes_part2(grid, next2, move) do
          nil -> nil
          grid -> move_box.(grid)
        end
    end
  end
  defp push_boxes_part2(grid, position, {_, 0} = move) do
    {position1, position2} =
      case Map.fetch!(grid, position) do
        ?\[ ->
          {position, add(position, {0, 1})}
        ?\] ->
          {add(position, {0, -1}), position}
      end
    next1 = add(position1, move)
    next2 = add(position2, move)

    move_box = fn grid ->
      %{grid | position1 => ?., position2 => ?.,
        next1 => ?\[, next2 => ?\]}
    end

    push = fn position ->
      case push_boxes_part2(grid, position, move) do
        nil -> nil
        grid -> move_box.(grid)
      end
    end

    case [Map.fetch!(grid, next1), Map.fetch!(grid, next2)] do
      ~c".." ->
        move_box.(grid)
      ~c"[]" ->
        push.(next1)
      ~c".\[" ->
        push.(next2)
      ~c"\]." ->
        push.(next1)
      ~c"\]\[" ->
        case push_boxes_part2(grid, next1, move) do
          nil ->
            nil
          grid ->
            case push_boxes_part2(grid, next2, move) do
              nil -> nil
              grid -> move_box.(grid)
            end
        end
      [a, b] when a === ?\# or b === ?\# ->
        nil
    end
  end

  defp parse(input) do
    [grid, moves] = String.split(input, "\n\n", trim: true)
    |> Enum.map(&String.trim/1)

    grid = String.split(grid, "\n", trim: true)

    moves = moves
    |> String.to_charlist
    |> Enum.flat_map(fn c ->
      case c do
        ?^ -> [{-1, 0}]
        ?v -> [{1, 0}]
        ?< -> [{0, -1}]
        ?> -> [{0, 1}]
        ?\n -> []
      end
    end)

    {grid, moves}
  end

  defp add({a, b}, {c, d}), do: {a + c, b + d}

  defp parse_grid(grid) do
    grid = grid
    |> Enum.with_index
    |> Enum.flat_map(fn {line, row} ->
      String.to_charlist(line)
      |> Enum.with_index
      |> Enum.flat_map(fn {char, col} ->
        position = {row, col}
        [{position, char}]
      end)
    end)
    |> Map.new

    {robot, ?@} = Enum.find(grid, fn {_, what} ->
      what === ?@
    end)

    grid = Map.put(grid, robot, ?.)

    {grid, robot}
  end

  def print_grid({map, robot}) do
    :io.nl
    {{min_row, _}, {max_row, _}} = Enum.min_max_by(Map.keys(map), &amp;elem(&amp;1, 0))
    {{_, min_col}, {_, max_col}} = Enum.min_max_by(Map.keys(map), &amp;elem(&amp;1, 1))
    Enum.each(min_row..max_row, fn row ->
      Enum.each(min_col..max_col, fn col ->
        position = {row, col}
        if position === robot do
          :io.put_chars("@")
        else
          :io.put_chars([Map.fetch!(map, position)])
        end
      end)
      :io.nl
    end)
    :io.nl
    {map, robot}
  end
end

input = Kino.Input.read(kino_input)

Part1.part1(input)

Part 2

defmodule Part2 do
  def part2(input) do
    {grid, moves} = parse(input)
    grid = widen(grid)
    {grid, robot} = parse_grid(grid)

    solve(grid, robot, moves)
  end

  def solve(grid, robot, moves) do
    Enum.reduce(moves, {grid, robot}, fn move, {grid, robot} ->
      next = add(robot, move)
      case Map.fetch!(grid, next) do
        ?. ->
          {grid, next}
        ?\# ->
          {grid, robot}
        ?O ->
          # Part 1
          # grid = push_boxes_part1(grid, next, move)
          if Map.fetch!(grid, next) === ?. do
            {grid, next}
          else
            {grid, robot}
          end
        char when char in [?\[, ?\]] ->
          # Part 2
          case push_boxes_part2(grid, next, move) do
            nil -> {grid, robot}
            grid -> {grid, next}
          end
      end
    end)
    |> elem(0)
    |> Enum.map(fn {{row, col}, what} ->
      if what in [?O, ?\[] do
        100 * row + col
      else
        0
      end
    end)
    |> Enum.sum
  end

  defp push_boxes_part2(grid, position, {0, _} = move) do
    next1 = add(position, move)
    next2 = add(next1, move)

    move_box = fn grid ->
      %{grid | next2 => grid[next1],
        next1 => grid[position],
        position => ?.}
    end

    case Map.fetch!(grid, next2) do
      ?\# ->
        nil
      ?. ->
        move_box.(grid)
      char when char in [?\[, ?\]] ->
        case push_boxes_part2(grid, next2, move) do
          nil -> nil
          grid -> move_box.(grid)
        end
    end
  end
  defp push_boxes_part2(grid, position, {_, 0} = move) do
    {position1, position2} =
      case Map.fetch!(grid, position) do
        ?\[ ->
          {position, add(position, {0, 1})}
        ?\] ->
          {add(position, {0, -1}), position}
      end
    next1 = add(position1, move)
    next2 = add(position2, move)

    move_box = fn grid ->
      %{grid | position1 => ?., position2 => ?.,
        next1 => ?\[, next2 => ?\]}
    end

    push = fn position ->
      case push_boxes_part2(grid, position, move) do
        nil -> nil
        grid -> move_box.(grid)
      end
    end

    case [Map.fetch!(grid, next1), Map.fetch!(grid, next2)] do
      ~c".." ->
        move_box.(grid)
      ~c"[]" ->
        push.(next1)
      ~c".\[" ->
        push.(next2)
      ~c"\]." ->
        push.(next1)
      ~c"\]\[" ->
        case push_boxes_part2(grid, next1, move) do
          nil ->
            nil
          grid ->
            case push_boxes_part2(grid, next2, move) do
              nil -> nil
              grid -> move_box.(grid)
            end
        end
      [a, b] when a === ?\# or b === ?\# ->
        nil
    end
  end

  defp widen(grid) do
    Enum.map(grid, fn line ->
      :binary.replace(line, "#", "##", [:global])
      |> :binary.replace("O", "[]", [:global])
      |> :binary.replace(".", "..", [:global])
      |> :binary.replace("@", "@.", [:global])
    end)
  end

  defp parse(input) do
    [grid, moves] = String.split(input, "\n\n", trim: true)
    |> Enum.map(&amp;String.trim/1)

    grid = String.split(grid, "\n", trim: true)

    moves = moves
    |> String.to_charlist
    |> Enum.flat_map(fn c ->
      case c do
        ?^ -> [{-1, 0}]
        ?v -> [{1, 0}]
        ?< -> [{0, -1}]
        ?> -> [{0, 1}]
        ?\n -> []
      end
    end)

    {grid, moves}
  end

  defp add({a, b}, {c, d}), do: {a + c, b + d}

  defp parse_grid(grid) do
    grid = grid
    |> Enum.with_index
    |> Enum.flat_map(fn {line, row} ->
      String.to_charlist(line)
      |> Enum.with_index
      |> Enum.flat_map(fn {char, col} ->
        position = {row, col}
        [{position, char}]
      end)
    end)
    |> Map.new

    {robot, ?@} = Enum.find(grid, fn {_, what} ->
      what === ?@
    end)

    grid = Map.put(grid, robot, ?.)

    {grid, robot}
  end

  def print_grid({map, robot}) do
    :io.nl
    {{min_row, _}, {max_row, _}} = Enum.min_max_by(Map.keys(map), &amp;elem(&amp;1, 0))
    {{_, min_col}, {_, max_col}} = Enum.min_max_by(Map.keys(map), &amp;elem(&amp;1, 1))
    Enum.each(min_row..max_row, fn row ->
      Enum.each(min_col..max_col, fn col ->
        position = {row, col}
        if position === robot do
          :io.put_chars("@")
        else
          :io.put_chars([Map.fetch!(map, position)])
        end
      end)
      :io.nl
    end)
    :io.nl
    {map, robot}
  end
end

input = Kino.Input.read(kino_input)

Part2.part2(input)