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(&(&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>>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...#
##########

<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>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()