Advent of Code 2024 - Day 15
Mix.install([
  :kino_aoc
])
Introduction
2024 - Day 15
Puzzle
{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2024", "15", System.fetch_env!("LB_AOC_SESSION"))
Parser
Code - Parser
defmodule Parser do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.chunk_by(&String.contains?(&1, "<"))
    |> then(fn [map = [head | _], steps] ->
      max_x = String.length(head) - 1
      max_y = Enum.count(map) - 1
      {
        map
        |> Enum.with_index()
        |> Enum.flat_map(fn {line, y} ->
          line
          |> String.graphemes()
          |> Enum.with_index()
          |> Enum.filter(fn {char, _} -> char != "." end)
          |> Enum.map(fn {char, x} -> {{x, y}, char} end)
        end)
        |> Map.new(),
        steps
        |> Enum.flat_map(&String.graphemes/1)
        |> Enum.map(fn
          "<" -> {-1, 0}
          ">" -> {1, 0}
          "^" -> {0, -1}
          "v" -> {0, 1}
        end),
        max_x,
        max_y
      }
    end)
  end
end
Tests - Parser
ExUnit.start(autorun: false)
defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser
  @input """
  ####
  #.O#
  #@##
  ####
  ^v<>
  """
  @expected {%{
               {1, 2} => "@",
               {2, 1} => "O",
               {2, 2} => "#",
               {0, 2} => "#",
               {0, 3} => "#",
               {1, 0} => "#",
               {1, 3} => "#",
               {2, 0} => "#",
               {2, 3} => "#",
               {3, 0} => "#",
               {3, 1} => "#",
               {3, 2} => "#",
               {3, 3} => "#",
               {0, 0} => "#",
               {0, 1} => "#"
             }, [{0, -1}, {0, 1}, {-1, 0}, {1, 0}], 3, 3}
  test "parse test" do
    actual = parse(@input)
    assert actual == @expected
  end
end
ExUnit.run()
Shared
defmodule Shared do
  def print_map(map, max_x, max_y) do
    0..max_y
    |> Enum.each(fn y ->
      line =
        0..max_x
        |> Enum.map(fn x ->
          Map.get(map, {x, y}, ".")
        end)
        |> Enum.join()
      IO.inspect(line)
    end)
  end
end
Part One
Code - Part 1
defmodule PartOne do
  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end
  def run(input) do
    {map, steps, max_x, max_y} = Parser.parse(input)
    {start, _} =
      map
      |> Enum.find(fn {_, char} -> char == "@" end)
    map
    |> move(max_x, max_y, start, steps)
    |> Enum.map(fn
      {{x, y}, "O"} -> x + 100 * y
      _ -> 0
    end)
    |> Enum.sum()
  end
  def move(map, _, _, _, []), do: map
  def move(map, max_x, max_y, curr = {x, y}, [{dx, dy} | steps]) do
    next = {x + dx, y + dy}
    case Map.get(map, next) do
      "#" ->
        move(map, max_x, max_y, curr, steps)
      nil ->
        map
        |> Map.delete(curr)
        |> Map.put(next, "@")
        |> move(max_x, max_y, next, steps)
      "O" ->
        {max, d, dir} = if dx == 0, do: {max_y, dy, :y}, else: {max_x, dx, :x}
        case to_push(map, max, next, d, dir) do
          [] ->
            move(map, max_x, max_y, curr, steps)
          pushes ->
            map
            |> Map.delete(curr)
            |> then(fn map ->
              pushes
              |> Enum.reduce(map, fn {pos, char}, map ->
                Map.put(map, pos, char)
              end)
            end)
            |> move(max_x, max_y, next, steps)
        end
    end
  end
  defp to_push(map, max_y, start = {x, start_y}, d, :y) do
    Stream.iterate(start_y + d, &(&1 + d))
    |> Stream.take_while(&(&1 >= 0 and &1 <= max_y))
    |> Enum.reduce_while([{start, "@"}], fn y, pushes ->
      curr = {x, y}
      case Map.get(map, curr) do
        "O" -> {:cont, [{curr, "O"} | pushes]}
        "#" -> {:halt, []}
        nil -> {:halt, [{curr, "O"} | pushes]}
      end
    end)
  end
  defp to_push(map, max_x, start = {start_x, y}, d, :x) do
    Stream.iterate(start_x + d, &(&1 + d))
    |> Stream.take_while(&(&1 >= 0 and &1 <= max_x))
    |> Enum.reduce_while([{start, "@"}], fn x, pushes ->
      curr = {x, y}
      case Map.get(map, curr) do
        "O" -> {:cont, [{curr, "O"} | pushes]}
        "#" -> {:halt, []}
        nil -> {:halt, [{curr, "O"} | pushes]}
      end
    end)
  end
end
Tests - Part 1
ExUnit.start(autorun: false)
defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne
  @input """
  ##########
  #..O..O.O#
  #......O.#
  #.OO..O.O#
  #..O@..O.#
  #O#..O...#
  #O..O..O.#
  #.OO.O.OO#
  #....O...#
  ##########
  ^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^
  vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv<
  <>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
  ^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^><
  ^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
  <><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
  ^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><
  v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^
  """
  @expected 10092
  test "part one" do
    actual = run(@input)
    assert actual == @expected
  end
end
ExUnit.run()
Solution - Part 1
PartOne.solve(puzzle_input)
Part Two
Code - Part 2
defmodule PartTwo do
  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end
  def run(input) do
    {map, steps, max_x, max_y} = Parser.parse(input)
    map = x2(map)
    max_x = max_x * 2 + 1
    {start, _} =
      map
      |> Enum.find(fn {_, char} -> char == "@" end)
    map
    |> move(max_x, max_y, start, steps)
    |> Enum.map(fn
      {{x, y}, "["} -> x + 100 * y
      _ -> 0
    end)
    |> Enum.sum()
  end
  def move(map, _, _, _, []), do: map
  def move(map, max_x, max_y, curr = {x, y}, [{dx, 0} | steps]) do
    next = {x + dx, y}
    case Map.get(map, next) do
      "#" ->
        move(map, max_x, max_y, curr, steps)
      nil ->
        map
        |> Map.delete(curr)
        |> Map.put(next, "@")
        |> move(max_x, max_y, next, steps)
      char when char in ["[", "]"] ->
        case push_left_right(map, max_x, next, dx) do
          [] ->
            move(map, max_x, max_y, curr, steps)
          pushes ->
            map
            |> Map.delete(curr)
            |> then(fn map ->
              pushes
              |> Enum.reduce(map, fn {pos, char}, map ->
                Map.put(map, pos, char)
              end)
            end)
            |> move(max_x, max_y, next, steps)
        end
    end
  end
  def move(map, max_x, max_y, curr = {x, y}, [{0, dy} | steps]) do
    next = {x, y + dy}
    case Map.get(map, next) do
      "#" ->
        move(map, max_x, max_y, curr, steps)
      nil ->
        map
        |> Map.delete(curr)
        |> Map.put(next, "@")
        |> move(max_x, max_y, next, steps)
      char when char in ["[", "]"] ->
        case push_up_down(map, next, dy, MapSet.new(), []) do
          {[], _} ->
            move(map, max_x, max_y, curr, steps)
          {pushes, _} ->
            {back, sort_modifier} = if dy == 1, do: {-1, -1}, else: {1, 1}
            pushes
            |> List.flatten()
            |> Enum.sort_by(fn {_, y} -> y * sort_modifier end)
            |> Enum.reduce(map, fn curr = {x, y}, map ->
              prev = {x, y + back}
              prev_char = Map.get(map, prev)
              map
              |> Map.put(curr, prev_char)
              |> Map.delete(prev)
            end)
            |> Map.delete(curr)
            |> Map.put(next, "@")
            |> move(max_x, max_y, next, steps)
        end
    end
  end
  defp push_left_right(map, max_x, start = {start_x, y}, dx) do
    Stream.iterate(start_x + dx, &(&1 + dx))
    |> Stream.take_while(&(&1 >= 1 and &1 <= max_x))
    |> Enum.reduce_while([{start, "@"}], fn x, pushes ->
      curr = {x, y}
      case Map.get(map, curr) do
        "[" ->
          {:cont, [{curr, "]"} | pushes]}
        "]" ->
          {:cont, [{curr, "["} | pushes]}
        "#" ->
          {:halt, []}
        nil ->
          case dx do
            1 -> {:halt, [{curr, "]"} | pushes]}
            -1 -> {:halt, [{curr, "["} | pushes]}
          end
      end
    end)
  end
  defp push_up_down(map, start = {x, y}, dy, visited, pushes) do
    box =
      [left, right] =
      case Map.get(map, start) do
        "[" -> [start, {x + 1, y}]
        "]" -> [{x - 1, y}, start]
      end
    if MapSet.member?(visited, left) or MapSet.member?(visited, right) do
      {pushes, visited}
    else
      visited = visited |> MapSet.put(left) |> MapSet.put(right)
      case box
           |> Enum.map(fn {x, y} ->
             next = {x, y + dy}
             {next, Map.get(map, next)}
           end) do
        [{_, "#"}, _] ->
          {[], visited}
        [_, {_, "#"}] ->
          {[], visited}
        [{left, nil}, {right, nil}] ->
          {[left, right | pushes], visited}
        [{left, nil}, {right, _}] ->
          push_up_down(map, right, dy, visited, [left, right | pushes])
        [{left, _}, {right, nil}] ->
          push_up_down(map, left, dy, visited, [left, right | pushes])
        [{left, _}, {right, _}] ->
          case push_up_down(map, left, dy, visited, [left, right | pushes]) do
            {[], visited} -> {[], visited}
            {pushes, visited} -> push_up_down(map, right, dy, visited, pushes)
          end
      end
    end
  end
  defp x2(map) do
    map
    |> Enum.flat_map(fn
      {{x, y}, "@"} -> [{{2 * x, y}, "@"}]
      {{x, y}, "O"} -> [{{2 * x, y}, "["}, {{2 * x + 1, y}, "]"}]
      {{x, y}, "#"} -> [{{2 * x, y}, "#"}, {{2 * x + 1, y}, "#"}]
    end)
    |> Map.new()
  end
end
Tests - Part 2
ExUnit.start(autorun: false)
defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo
  @input """
  ##########
  #..O..O.O#
  #......O.#
  #.OO..O.O#
  #..O@..O.#
  #O#..O...#
  #O..O..O.#
  #.OO.O.OO#
  #....O...#
  ##########
  ^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^
  vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv<
  <>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
  ^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^><
  ^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
  <><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
  ^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><
  v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^
  """
  @expected 9021
  test "part two" do
    actual = run(@input)
    assert actual == @expected
  end
end
ExUnit.run()
Solution - Part 2
PartTwo.solve(puzzle_input)