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

Day 22

day22.livemd

Day 22

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

Section

input = """
        ...#
        .#..
        #...
        ....
...#.......#
........#...
..#....#....
..........#.
        ...#....
        .....#..
        .#......
        ......#.

10R5L5R10L4R5L5
"""
input = Kino.Input.textarea("paste input here:")
input = Kino.Input.read(input)
defmodule Board do
  def min_max_row(board, at_col \\ :any) do
    for {{row, col}, _} <- board,
        at_col == :any or col == at_col do
      row
    end
    |> Enum.min_max()
  end

  def min_max_col(board, at_row \\ :any) do
    for {{row, col}, _} <- board,
        at_row == :any or row == at_row do
      col
    end
    |> Enum.min_max()
  end

  def pprint(board, {cur_row, cur_col} \\ {-1, -1}, dir \\ ?>) do
    {min_row, max_row} = min_max_row(board)
    {min_col, max_col} = min_max_col(board)

    IO.puts("")

    min_row..max_row
    |> Enum.each(fn row ->
      min_col..max_col
      |> Enum.map(fn col ->
        if row == cur_row and col == cur_col do
          dir
        else
          Map.get(board, {row, col}, ?\s)
        end
      end)
      |> IO.puts()
    end)
  end

  def navigate(board, moves) do
    {min_row, _} = min_max_row(board)

    min_open_col =
      for {{^min_row, col}, ?.} <- board do
        col
      end
      |> Enum.min()

    navigate(board, moves, {min_row, min_open_col}, ?>)
  end

  def rotate(dir, l_or_r) do
    case l_or_r do
      ?L ->
        case dir do
          ?> -> ?^
          ?^ -> ?<
          ?< -> ?v
          ?v -> ?>
        end

      ?R ->
        case dir do
          ?> -> ?v
          ?v -> ?<
          ?< -> ?^
          ?^ -> ?>
        end
    end
  end

  def move(board, {row, col}, dir) do
    {new_row, new_col} =
      case dir do
        ?^ -> {row - 1, col}
        ?> -> {row, col + 1}
        ?v -> {row + 1, col}
        ?< -> {row, col - 1}
      end

    if Map.has_key?(board, {new_row, new_col}) do
      {{new_row, new_col}, dir}
    else
      {min, max} =
        if dir == ?^ or dir == ?v do
          min_max_row(board, col)
        else
          min_max_col(board, row)
        end

      case dir do
        ?^ -> {{max, new_col}, dir}
        ?v -> {{min, new_col}, dir}
        ?> -> {{new_row, min}, dir}
        ?< -> {{new_row, max}, dir}
      end
    end
  end

  def offset_on_line({row, col}, {line_r1, line_c1}, {line_r2, line_c2}) do
    cond do
      line_c1 == line_c2 ->
        {min_r, max_r} = Enum.min_max([line_r1, line_r2])

        if col == line_c1 and row >= min_r and row < max_r do
          if min_r == line_r1 do
            row - min_r
          else
            max_r - 1 - row
          end
        end

      line_r1 == line_r2 ->
        {min_c, max_c} = Enum.min_max([line_c1, line_c2])

        if row == line_r1 and col >= min_c and col < max_c do
          if min_c == line_c1 do
            col - min_c
          else
            max_c - 1 - col
          end
        end
    end
  end

  def reverse_dir(d) do
    case d do
      ?^ -> ?v
      ?> -> ?<
      ?v -> ?^
      ?< -> ?>
    end
  end

  def move_cubed(board, {row, col}, dir) do
    {new_row, new_col} =
      case dir do
        ?^ -> {row - 1, col}
        ?> -> {row, col + 1}
        ?v -> {row + 1, col}
        ?< -> {row, col - 1}
      end

    if Map.has_key?(board, {new_row, new_col}) do
      {{new_row, new_col}, dir}
    else
      size = 50

      IO.puts("before:")
      pprint(board, {row, col}, dir)

      {new_coord, new_dir} =
        [
          # the line coord1 -> coord2 shares edge on cube with coord3 -> coord4, offset
          # and changes from first direction to second
          # coord on line if in coord1={row*size, col*size} < coord2
          {{0, size}, {0, 2 * size}, {3 * size, 0}, {4 * size, 0}, ?^, ?>},
          {{0, 2 * size}, {0, 3 * size}, {4 * size - 1, 0}, {4 * size - 1, size}, ?^, ?^},
          {{0, size}, {size, size}, {3 * size, 0}, {2 * size, 0}, ?<, ?>},
          {{0, 3 * size - 1}, {size, 3 * size - 1}, {3 * size, 2 * size - 1},
           {2 * size, 2 * size - 1}, ?>, ?<},
          {{size - 1, 2 * size}, {size - 1, 3 * size}, {size, 2 * size - 1},
           {2 * size, 2 * size - 1}, ?v, ?<},
          {{size, size}, {2 * size, size}, {2 * size, 0}, {2 * size, size}, ?<, ?v},
          {{3 * size - 1, size}, {3 * size - 1, 2 * size}, {3 * size, size - 1},
           {4 * size, size - 1}, ?v, ?<}
        ]
        # add the reverse edge mapping also
        |> Enum.flat_map(fn {p1, p2, p3, p4, d1, d2} = m ->
          [m, {p3, p4, p1, p2, reverse_dir(d2), reverse_dir(d1)}]
        end)
        |> Enum.find_value(fn {l1_start, l1_end, {l2_r1, l2_c1}, {l2_r2, l2_c2}, d1, d2} ->
          offset = offset_on_line({row, col}, l1_start, l1_end)

          if dir == d1 and offset do
            cond do
              l2_c1 == l2_c2 ->
                if l2_r1 < l2_r2 do
                  {{l2_r1 + offset, l2_c1}, d2}
                else
                  {{l2_r1 - 1 - offset, l2_c1}, d2}
                end

              l2_r1 == l2_r2 ->
                if l2_c1 < l2_c2 do
                  {{l2_r1, l2_c1 + offset}, d2}
                else
                  {{l2_r1, l2_c1 - 1 - offset}, d2}
                end
            end
          end
        end)

      IO.puts("after:")
      pprint(board, new_coord, new_dir)
      {new_coord, new_dir}
    end
  end

  def open?(board, coord), do: board[coord] == ?.

  def navigate(board, moves, {row, col} = coord, dir) do
    # pprint(board, coord, dir)

    case moves do
      "" ->
        # pprint(board, coord, dir)
        1000 * (row + 1) + 4 * (col + 1) + Enum.find_index('>v<^', &amp;(&amp;1 == dir))

      "L" <> rest ->
        navigate(board, rest, coord, rotate(dir, ?L))

      "R" <> rest ->
        navigate(board, rest, coord, rotate(dir, ?R))

      _ ->
        {amount, rest} = Integer.parse(moves)

        {new_coord, new_dir} =
          1..amount
          |> Enum.reduce_while({coord, dir}, fn _, {coord, dir} ->
            # part 1
            # {new_coord, new_dir} = move(board, coord, dir)
            # part 2
            {new_coord, new_dir} = move_cubed(board, coord, dir)

            if open?(board, new_coord) do
              {:cont, {new_coord, new_dir}}
            else
              {:halt, {coord, dir}}
            end
          end)

        navigate(board, rest, new_coord, new_dir)
    end
  end
end
[board, moves] = String.split(input, "\n\n")
moves = String.trim(moves)

board
|> String.split("\n", trim: true)
|> Enum.with_index()
|> Enum.flat_map(fn {line, row} ->
  Enum.with_index(to_charlist(line))
  |> Enum.map(fn {c, col} ->
    {{row, col}, c}
  end)
  |> Enum.reject(fn {_, c} -> c == ?\s end)
end)
|> Map.new()
# |> Map.has_key?({8, 7})
# |> Board.pprint({5, 3}, ?v)
# |> Board.move({7, 3}, ?v)
# |> Board.open?({7, 3})
|> Board.navigate(moves)