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

Advent of Code - 2024 - Day 6

advent-of-code/2024/day06.livemd

Advent of Code - 2024 - Day 6

Mix.install([
  {:kino, "~> 0.14.2"},
  {:kino_vega_lite, "~> 0.1.13"},
  {:kino_explorer, "~> 0.1.23"}
])

Puzzle Input

puzzle_input = Kino.Input.textarea("Please paste the puzzle input:")
defmodule SituationMap.Tile.Guard do
  defstruct visited: MapSet.new(),
            facing: nil,
            position: {},
            looping: false

  def next_tile(%__MODULE__{position: {row, column}, facing: :north}), do: {row - 1, column}
  def next_tile(%__MODULE__{position: {row, column}, facing: :east}), do: {row, column + 1}
  def next_tile(%__MODULE__{position: {row, column}, facing: :south}), do: {row + 1, column}
  def next_tile(%__MODULE__{position: {row, column}, facing: :west}), do: {row, column - 1}

  def pivot(guard = %__MODULE__{facing: direction, position: position}) do
    guard
    |> visit()
    |> turn()
    |> Map.put(:facing, next_direction(direction))
    |> Map.update!(:visited, fn visited ->
      MapSet.put(visited, {position, direction})
    end)
  end

  def step(guard = %__MODULE__{}) do
    guard
    |> visit()
    |> move()
  end

  def visit(guard = %__MODULE__{position: position, facing: direction}) do
    if MapSet.member?(guard.visited, {position, direction}) do
      %{guard | looping: true}
    else
      %{guard | visited: MapSet.put(guard.visited, {position, direction})}
    end
  end

  def turn(guard = %__MODULE__{facing: direction}) do
    %{guard | facing: next_direction(direction)}
  end

  def move(guard = %__MODULE__{}) do
    %{guard | position: next_tile(guard)}
  end

  def unicode(%__MODULE__{looping: true}) do
    "🌀"
  end

  def unicode(%__MODULE__{}) do
    "💂‍♀️"
  end

  defp next_direction(:north), do: :east
  defp next_direction(:east), do: :south
  defp next_direction(:south), do: :west
  defp next_direction(:west), do: :north
end
defmodule SituationMap.Tile do
  alias SituationMap.Tile.Guard

  defstruct obstacle: false

  def new(_string = "^", position) do
    %Guard{facing: :north, position: position}
  end

  def new(_string = ">", position) do
    %Guard{facing: :east, position: position}
  end

  def new(_string = "<", position) do
    %Guard{facing: :west, position: position}
  end

  def new(_string = "v", position) do
    %Guard{facing: :south, position: position}
  end

  def new("#", _position) do
    %__MODULE__{obstacle: true}
  end

  def new(_string, _position) do
    %__MODULE__{}
  end
end
defmodule SituationMap do
  alias SituationMap.Tile
  alias SituationMap.Tile.Guard

  defstruct max_row: 0,
            max_column: 0,
            obstacles: MapSet.new(),
            guard_left: false,
            guard_looping: false,
            guard: nil,
            added_obstacle: nil

  def from_string(string) do
    rows =
      string
      |> String.split("\n")

    for {line, row} <- Enum.with_index(rows),
        {marker, column} <- Enum.with_index(String.graphemes(line)),
        reduce: %__MODULE__{} do
      acc ->
        position = {row, column}

        map =
          case Tile.new(marker, position) do
            guard = %Guard{} ->
              %{acc | guard: guard}

            %Tile{obstacle: true} ->
              %{acc | obstacles: MapSet.put(acc.obstacles, {row, column})}

            _empty ->
              acc
          end

        %{
          map
          | max_row: Enum.max([map.max_row, row]),
            max_column: Enum.max([map.max_column, column])
        }
    end
  end

  def step(map = %__MODULE__{guard_left: true}), do: map

  def step(map = %__MODULE__{guard: guard}) do
    {row, column} = Guard.next_tile(guard)

    cond do
      row < 0 or column < 0 or row > map.max_row or column > map.max_column ->
        new_guard = Guard.visit(guard)
        %{map | guard: new_guard, guard_left: true}

      obstacle?(map, {row, column}) ->
        new_guard = Guard.pivot(guard)
        %{map | guard: new_guard}

      true ->
        new_guard = Guard.step(guard)
        %{map | guard: new_guard, guard_looping: new_guard.looping}
    end
  end

  def step_until_determined(map = %__MODULE__{guard_left: true}), do: map

  def step_until_determined(map = %__MODULE__{guard_looping: true}), do: map

  def step_until_determined(map = %__MODULE__{}) do
    map
    |> step()
    |> step_until_determined()
  end

  def step_until_guard_leaves(map = %__MODULE__{}) do
    determined = step_until_determined(map)

    if determined.guard_looping do
      {:error, :guard_did_not_leave, determined}
    else
      {:ok, determined}
    end
  end

  def step_until_guard_leaves!(map = %__MODULE__{}) do
    determined = step_until_determined(map)

    if determined.guard_looping do
      throw(GuardStuckLooping)
    else
      determined
    end
  end

  def count_visited_tiles(%__MODULE__{guard: guard}) do
    guard.visited
    |> Enum.uniq_by(fn {position, _direction} -> position end)
    |> Enum.count()
  end

  def obstacle?(%__MODULE__{obstacles: obstacles}, position) do
    MapSet.member?(obstacles, position)
  end

  def render(map = %__MODULE__{}) do
    map
    |> unicode()
    |> IO.puts()
  end

  def unicode(map = %__MODULE__{}) do
    for row <- 0..map.max_row, column <- 0..map.max_column, into: %{} do
      {{row, column}, "⬜️"}
    end
    |> then(fn grid ->
      Enum.reduce(map.obstacles, grid, fn position, acc ->
        Map.put(acc, position, "🟥")
      end)
    end)
    |> then(fn grid ->
      Enum.reduce(map.guard.visited, grid, fn {position, direction}, acc ->
        case direction do
          :north ->
            Map.put(acc, position, "⬆️")

          :east ->
            Map.put(acc, position, "➡️")

          :south ->
            Map.put(acc, position, "⬇️")

          :west ->
            Map.put(acc, position, "⬅️")
        end
      end)
    end)
    |> then(fn grid ->
      if map.guard_left do
        grid
      else
        Map.put(grid, map.guard.position, Guard.unicode(map.guard))
      end
    end)
    |> Enum.sort()
    |> Enum.map(fn {_position, tile} ->
      tile
    end)
    |> Enum.chunk_every(map.max_column + 1)
    |> Enum.map(fn row ->
      Enum.intersperse(row, " ")
    end)
    |> Enum.intersperse("\n")
  end
end
defmodule SituationMap.GuardPathAnalyzer do
  def determine_single_changes_that_create_guard_loops(map = %SituationMap{}) do
    initial_guard_position = map.guard.position

    potential_changes =
      map
      |> SituationMap.step_until_determined()
      |> then(fn analyzed ->
        analyzed.guard.visited
        |> Enum.map(fn {position, _direction} ->
          position
        end)
      end)
      |> Enum.reject(fn position ->
        position == initial_guard_position
      end)
      |> MapSet.new()

    potential_maps =
      potential_changes
      |> Enum.map(fn position ->
        new_obstacles = MapSet.put(map.obstacles, position)
        %{map | obstacles: new_obstacles, added_obstacle: position}
      end)

    potential_maps
    |> Task.async_stream(fn map ->
      SituationMap.step_until_determined(map)
    end)
    |> Stream.filter(fn {:ok, map} ->
      map.guard_looping
    end)
    |> Enum.map(fn {:ok, map} ->
      map.added_obstacle
    end)
  end
end

Part 1

part_1_test_input = Kino.Input.textarea("Please paste the test input for part 1:")
part_1_test_input
|> Kino.Input.read()
|> SituationMap.from_string()
|> SituationMap.step_until_guard_leaves!()
|> then(fn map ->
  SituationMap.render(map)
  map
end)
|> SituationMap.count_visited_tiles()
button = Kino.Control.button("Step")
map = part_1_test_input
|> Kino.Input.read()
|> SituationMap.from_string()

button
|> Kino.Control.stream()
|> Kino.animate(map, fn _event, map ->
  new_map = SituationMap.step(map)
  md = Kino.Markdown.new("
#{SituationMap.unicode(new_map)}
"
) {:cont, md, new_map} end)
puzzle_input
|> Kino.Input.read()
|> SituationMap.from_string()
|> SituationMap.step_until_guard_leaves!()
|> SituationMap.count_visited_tiles()

Part 2

part_2_test_input = Kino.Input.textarea("Please paste the test input for part 2:")
part_2_test_input
|> Kino.Input.read()
|> SituationMap.from_string()
|> SituationMap.GuardPathAnalyzer.determine_single_changes_that_create_guard_loops()
|> Enum.count()
puzzle_input
|> Kino.Input.read()
|> SituationMap.from_string()
|> SituationMap.GuardPathAnalyzer.determine_single_changes_that_create_guard_loops()
|> Enum.count()