Powered by AppSignal & Oban Pro

d15

d15/d15.livemd

d15

Section

defmodule D15 do
  defstruct grid: %{}, robot: nil, moves: []

  def widen(map) do
    for line <- String.split(map) do
      for char <- String.to_charlist(line) do
        case char do
          ?# -> "##"
          ?O -> "[]"
          ?@ -> "@."
          ?. -> ".."
        end
      end
      |> Enum.join()
    end
    |> Enum.join("\n")
  end

  def parse(input, part \\ :part1) do
    [map, moves] = String.split(input, "\n\n")

    map = if part == :part2, do: D15.widen(map), else: map

    {grid, robot} =
      for {line, row} <- Enum.with_index(String.split(map)),
          {char, col} <- Enum.with_index(String.to_charlist(line)),
          reduce: {%{}, nil} do
        {grid, robot} ->
          case char do
            ?# -> {Map.put(grid, {row, col}, :wall), robot}
            ?O -> {Map.put(grid, {row, col}, :box), robot}
            ?[ -> {Map.put(grid, {row, col}, :left), robot}
            ?] -> {Map.put(grid, {row, col}, :right), robot}
            ?@ -> {grid, {row, col}}
            ?. -> {grid, robot}
          end
      end

    %D15{grid: grid, robot: robot, moves: String.to_charlist(moves) |> Enum.filter(&amp;(&amp;1 != ?\n))}
  end

  defp vec_add({r, c}, {dr, dc}), do: {r + dr, c + dc}

  defp move(map, src, dest) do
    {value, map} = Map.pop!(map, src)
    Map.put(map, dest, value)
  end

  def displace(grid, pos, delta) do
    case Map.get(grid, pos) do
      :wall ->
        nil

      nil ->
        grid

      side when side != :box and elem(delta, 1) == 0 ->
        dest = vec_add(pos, delta)

        case displace(grid, dest, delta) do
          nil ->
            nil

          grid ->
            cdelta =
              case side do
                :left -> {0, 1}
                :right -> {0, -1}
              end
            cpos = vec_add(pos, cdelta)
            cdest = vec_add(cpos, delta)

            case displace(grid, cdest, delta) do
              nil ->
                nil

              grid ->
                grid
                |> move(pos, dest)
                |> move(cpos, cdest)
            end
        end

      box ->
        dest = vec_add(pos, delta)

        case displace(grid, dest, delta) do
          nil ->
            nil

          grid ->
            grid
            |> Map.delete(pos)
            |> Map.put(dest, box)
        end
    end
  end

  def step(%D15{grid: grid, robot: robot, moves: [move | moves]} = world) do
    delta =
      case move do
        ?< -> {0, -1}
        ?> -> {0, 1}
        ?^ -> {-1, 0}
        ?v -> {1, 0}
      end

    dest = vec_add(robot, delta)

    case displace(grid, dest, delta) do
      nil -> %D15{world | moves: moves}
      grid -> %D15{world | grid: grid, robot: dest, moves: moves}
    end
  end

  def run(%D15{moves: []} = world), do: world
  def run(world), do: D15.run(D15.step(world))

  def gps(%D15{grid: grid}) do
    for {{row, col}, object} <- grid do
      if object == :box or object == :left, do: row * 100 + col, else: 0
    end
    |> Enum.sum()
  end
end
ExUnit.start()

defmodule D15.Test do
  use ExUnit.Case

  test "small" do
    input = "########
#..O.O.#
##@.O..#
#...O..#
#.#.O..#
#...O..#
#......#
########

<^^>>>vv>v<<
"
    assert 2028 == D15.parse(input) |> D15.run() |> D15.gps()
  end

  test "large" do
    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^<<^
"
    assert 10092 == D15.parse(input) |> D15.run() |> D15.gps()
    assert 9021 == D15.parse(input, :part2) |> D15.run() |> D15.gps()
  end
end

ExUnit.run()
input = File.read!(__DIR__ <> "/input")
D15.parse(input) |> D15.run() |> D15.gps()
D15.parse(input, :part2) |> D15.run() |> D15.gps()