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

--- Day 6: Guard Gallivant ---

2024/day_6.livemd

— Day 6: Guard Gallivant —

Mix.install([
  {:kino_aoc, "~> 0.1"}
])

Setup

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2024", "6", System.fetch_env!("LB_AOC_SESSION_COOKIE"))
input = Kino.Input.textarea("input")
test_input = Kino.Input.read(input)
defmodule GuardGallivant do
  def parse(input) do
    input
    |> String.split("\n")
    |> Enum.map(&String.graphemes/1)
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {row, row_i}, acc ->
      for {point, col_i} <- Enum.with_index(row), reduce: acc do
        acc -> Map.put(acc, {row_i, col_i}, point)
      end
    end)
  end

  def starting_pos(grid) do
    grid
    |> Enum.find(fn {_coords, point} -> point == "^" end)
    |> elem(0)
  end

  def max_dimension(grid) do
    grid
    |> Enum.max_by(fn {{row_a, col_a}, _} -> row_a + col_a end)
    |> elem(0)
    |> elem(0)
  end

  def move("^", pos, func), do: move(".", pos, func)

  def move(".", %{dir: :up} = pos, func),
    do: {pos, pos |> Map.put(:row, pos.row - 1) |> Map.put(:visited, func.(pos))}

  def move(".", %{dir: :right} = pos, func),
    do: {pos, pos |> Map.put(:col, pos.col + 1) |> Map.put(:visited, func.(pos))}

  def move(".", %{dir: :down} = pos, func),
    do: {pos, pos |> Map.put(:row, pos.row + 1) |> Map.put(:visited, func.(pos))}

  def move(".", %{dir: :left} = pos, func),
    do: {pos, pos |> Map.put(:col, pos.col - 1) |> Map.put(:visited, func.(pos))}

  # obstacle
  def move("#", %{dir: :up} = pos, _func),
    do: {pos, Map.merge(pos, %{row: pos.row + 1, dir: :right})}

  def move("#", %{dir: :right} = pos, _func),
    do: {pos, Map.merge(pos, %{col: pos.col - 1, dir: :down})}

  def move("#", %{dir: :down} = pos, _func),
    do: {pos, Map.merge(pos, %{row: pos.row - 1, dir: :left})}

  def move("#", %{dir: :left} = pos, _func),
    do: {pos, Map.merge(pos, %{col: pos.col + 1, dir: :up})}

  def move(nil, _pos, _func), do: nil

  def visit(pos), do: MapSet.put(pos.visited, {pos.row, pos.col})
  def visit_dir(pos), do: MapSet.put(pos.visited, {pos.row, pos.col, pos.dir})

  def loop_or_exit(%{row: 0, dir: :up}, _max, acc), do: acc
  def loop_or_exit(%{col: 0, dir: :left}, _max, acc), do: acc
  def loop_or_exit(%{row: row, dir: :down}, max, acc) when row == max, do: acc
  def loop_or_exit(%{col: col, dir: :right}, max, acc) when col == max, do: acc
  def loop_or_exit(_pos, _max, acc), do: acc + 1
end

Part 1

import GuardGallivant

grid = parse(puzzle_input)
{row, col} = starting_pos(grid)

%{row: row, col: col, dir: :up, visited: MapSet.new([{row, col}])}
|> Stream.unfold(fn pos -> move(grid[{pos.row, pos.col}], pos, &amp;visit/1) end)
|> Enum.to_list()
|> List.last()
|> visit()
|> MapSet.size()

Part 2

# infinite loop means the guard returns to the same coordinates, going the same direction.
frame = Kino.Frame.new() |> Kino.render()

grid = parse(puzzle_input)
max_dimension = max_dimension(grid)
{row, col} = starting_pos = starting_pos(grid)

guard_path =
  %{row: row, col: col, dir: :up, visited: MapSet.new([{row, col}])}
  |> Stream.unfold(fn pos -> move(grid[{pos.row, pos.col}], pos, &amp;visit/1) end)
  |> Enum.to_list()
  |> List.last()
  |> visit()

for pos <- guard_path, pos != starting_pos, reduce: 0 do
  acc ->
    Kino.Frame.render(frame, acc)
    
    grid = Map.put(grid, pos, "#")

    %{row: row, col: col, dir: :up, visited: MapSet.new([{row, col}])}
    |> Stream.unfold(fn pos ->
      if MapSet.member?(pos.visited, {pos.row, pos.col, pos.dir}) do
        nil
      else
        move(grid[{pos.row, pos.col}], pos, &amp;visit_dir/1)
      end
    end)
    |> Enum.to_list()
    |> List.last()
    |> loop_or_exit(max_dimension, acc)
end