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

Day 6: Guard Gallivant

day6.livemd

Day 6: Guard Gallivant

Mix.install([:kino])

Section

input = Kino.Input.textarea("input")
input = Kino.Input.read(input)

Part 1

defmodule GuardGallivantPart1 do
  defp parse_input(input) do
    String.split(input, "\n") |> Enum.map(&String.split(&1, "", trim: true))
  end

  defp get_pos(map, x, y) do
    Enum.at(map, y) |> Enum.at(x)
  end

  defp get_start_point(map) do
    cols = length(hd(map)) - 1
    rows = length(map) - 1

    {_start_x, _start_y} =
      for x <- 0..cols, y <- 0..rows, get_pos(map, x, y) == "^", reduce: {0, 0} do
        _acc ->
          {x, y}
      end
  end

  defp out_of_bound?(map, x, y) do
    x < 0 || y < 0 || y > length(map) - 1 || x > length(hd(map)) - 1
  end

  defp next_point(x, y, {dir_x, dir_y}), do: {x + 1 * dir_x, y + 1 * dir_y}

  # up
  defp next_direction({0, -1}), do: {1, 0}
  # right
  defp next_direction({1, 0}), do: {0, 1}
  # down
  defp next_direction({0, 1}), do: {-1, 0}
  # left
  defp next_direction({-1, 0}), do: {0, -1}

  defp run_guard(map, {x, y} = _position, {_dir_x, _dir_y} = dir, visited_points = %MapSet{}) do
    visited_points = MapSet.put(visited_points, {x, y})

    with {next_x, next_y} <- next_point(x, y, dir),
         false <- out_of_bound?(map, next_x, next_y),
         point <- get_pos(map, next_x, next_y) do
      case point do
        "#" ->
          run_guard(
            map,
            next_point(x, y, next_direction(dir)),
            next_direction(dir),
            visited_points
          )

        s when s in [".", "^", "O"] ->
          run_guard(map, {next_x, next_y}, dir, visited_points)
      end
    else
      _ -> visited_points
    end
  end

  def solve(input) do
    map = parse_input(input)

    map
    |> run_guard(get_start_point(map), {0, -1}, MapSet.new())
    |> MapSet.size()
  end
end

GuardGallivantPart1.solve(input)

Part 2

defmodule GuardGallivantPart2 do
  defp parse_input(input) do
    String.split(input, "\n") |> Enum.map(&amp;String.split(&amp;1, "", trim: true))
  end

  defp get_pos(map, {x, y}) do
    get_pos(map, x, y)
  end

  defp get_pos(map, x, y) do
    Enum.at(map, y) |> Enum.at(x)
  end

  defp get_start_point(map) do
    cols = length(hd(map)) - 1
    rows = length(map) - 1

    {_start_x, _start_y} =
      for x <- 0..cols, y <- 0..rows, get_pos(map, x, y) == "^", reduce: {0, 0} do
        _acc ->
          {x, y}
      end
  end

  defp out_of_bound?(map, {x, y}) do
    out_of_bound?(map, x, y)
  end

  defp out_of_bound?(map, x, y) do
    x < 0 || y < 0 || y > length(map) - 1 || x > length(hd(map)) - 1
  end

  defp next_point(x, y, {dir_x, dir_y}), do: {x + 1 * dir_x, y + 1 * dir_y}

  # up
  defp next_direction({0, -1}), do: {1, 0}
  # right
  defp next_direction({1, 0}), do: {0, 1}
  # down
  defp next_direction({0, 1}), do: {-1, 0}
  # left
  defp next_direction({-1, 0}), do: {0, -1}

  defp is_loop?(visited_points, position, dir) do
    MapSet.member?(visited_points, {position, dir})
  end

  defp make_next_turn(map, {x, y} = cur_pos, {_dx, _dy} = cur_dir) do
    next_dir = next_direction(cur_dir)
    next_pos = next_point(x, y, next_dir)

    cond do
      !out_of_bound?(map, next_pos) &amp;&amp; get_pos(map, next_pos) == "#" ->
        {cur_pos, next_dir}

      out_of_bound?(map, next_pos) ->
        {cur_pos, next_dir}

      true ->
        {next_pos, next_dir}
    end
  end

  defp run_guard(map, {x, y} = _position, {_dir_x, _dir_y} = dir, visited_points = %MapSet{}) do
    with {next_x, next_y} <- next_point(x, y, dir),
         :ok <- if(is_loop?(visited_points, {x, y}, dir), do: :is_loop, else: :ok),
         :ok <- if(out_of_bound?(map, next_x, next_y), do: :out_of_bound, else: :ok),
         point <- get_pos(map, next_x, next_y) do
      case point do
        "#" ->
          {next_pos, next_dir} = make_next_turn(map, {x, y}, dir)

          run_guard(
            map,
            next_pos,
            next_dir,
            MapSet.put(visited_points, {{x, y}, dir})
          )

        s when s in [".", "^"] ->
          run_guard(map, {next_x, next_y}, dir, MapSet.put(visited_points, {{x, y}, dir}))
      end
    else
      :is_loop ->
        {:loop, visited_points}

      :out_of_bound ->
        {:ok, visited_points}
    end
  end

  def solve(input) do
    map = parse_input(input)
    start = get_start_point(map)

    points =
      for x <- 0..(length(hd(map)) - 1),
          y <- 0..(length(map) - 1),
          get_pos(map, x, y) == ".",
          do: {x, y}

    Task.async_stream(
      points,
      fn {x, y} ->
        Enum.at(map, y)
        |> List.replace_at(x, "#")
        |> then(&amp;List.replace_at(map, y, &amp;1))
        |> run_guard(start, {0, -1}, MapSet.new())
      end,
      ordered: false,
      timeout: 5000
    )
    |> Enum.map(&amp;elem(&amp;1, 1))
    |> Enum.reject(&amp;(elem(&amp;1, 0) == :ok))
    |> Enum.count()
  end
end

GuardGallivantPart2.solve(input)