— 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, &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, &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, &visit_dir/1)
end
end)
|> Enum.to_list()
|> List.last()
|> loop_or_exit(max_dimension, acc)
end