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

Day 6

2024/elixir/day_06.livemd

Day 6

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

Section

content =
  Kino.FS.file_path("input_06.txt")
  |> File.read!()
input = """
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
"""
bitstring = input |> String.split() |> IO.iodata_to_binary()
side = bitstring |> byte_size() |> :math.sqrt() |> floor()
to_rc = fn i -> {div(i, side), rem(i, side)} end
nodes = for {i, _} <- :binary.matches(bitstring, "#"), do: to_rc.(i)
guard = {:up, bitstring |> :binary.match("^") |> elem(0) |> then(to_rc)}
defmodule Field do
  alias __MODULE__
  
  defstruct [:columns, :rows]
  
  def new(nodes) do
    %Field{
      columns: Enum.group_by(nodes, &amp;elem(&amp;1, 1), &amp;elem(&amp;1, 0)), 
      rows: Enum.group_by(nodes, &amp;elem(&amp;1, 0), &amp;elem(&amp;1, 1))
    }
  end

  def raycast(%Field{} = field, dir, {r, c}) do
    %Field{columns: columns, rows: rows} = field
    
    case dir do
      :up ->
        row =
          columns
          |> Map.get(c, [])
          |> Enum.reverse()
          |> Enum.drop_while(&amp;(&amp;1 > r))
          |> List.first()

        if row, do: {row, c}

      :down ->
        row = columns |> Map.get(c, []) |> Enum.drop_while(&amp;(&amp;1 < r)) |> List.first()
        if row, do: {row, c}

      :left ->
        column =
          rows |> Map.get(r, []) |> Enum.reverse() |> Enum.drop_while(&amp;(&amp;1 > c)) |> List.first()

        if column, do: {r, column}

      :right ->
        column = rows |> Map.get(r, []) |> Enum.drop_while(&amp;(&amp;1 < c)) |> List.first()
        if column, do: {r, column}
    end
  end
end
field = Field.new(nodes)
turn = fn dir ->
  case dir do
    :up -> :right
    :right -> :down
    :down -> :left
    :left -> :up
  end
end
approach = fn from_dir, {r, c} ->
  case from_dir do
    :up -> {r + 1, c}
    :down -> {r - 1, c}
    :left -> {r, c + 1}
    :right -> {r, c - 1}
  end
end
guard_path =
  guard
  |> Stream.iterate(fn {dir, point} ->
    case Field.raycast(field, dir, point) do
      nil ->
        nil

      point ->
        {turn.(dir), approach.(dir, point)}
    end
  end)
  |> Enum.take_while(&amp;(&amp;1 != nil))
exit_point =
  guard_path
  |> Enum.reverse()
  |> hd()
  |> then(fn {dir, {r, c}} ->
    case dir do
      :down -> {side - 1, c}
      :up -> {0, c}
      :left -> {r, 0}
      :right -> {r, side - 1}
    end
  end)
guard_points = guard_path |> Keyword.values()
segments = Enum.zip(guard_points, tl(guard_points) ++ [exit_point])
segment_points = fn {{from_r, from_c}, {to_r, to_c}} ->
  if from_r == to_r do
    for c <- from_c..to_c, do: {to_r, c}
  else
    for r <- from_r..to_r, do: {r, to_c}
  end
end
segments
|> Enum.flat_map(segment_points)
|> Enum.uniq()
|> length()

Part 2

directed_segments = Enum.zip(guard_path, (guard_path |> Keyword.values() |> tl()) ++ [exit_point])
obstacle_points =
  directed_segments
  |> Enum.flat_map(fn {{dir, from_point}, to_point} ->
    clear_points =
      segment_points.({from_point, to_point})
      |> tl()
      |> List.delete(-1)

    possible_dir = turn.(dir)

    clear_points
    |> Enum.filter(&amp;(Field.raycast(field, possible_dir, &amp;1) != nil))
  end)
  |> Enum.uniq()
  |> Enum.reject( &amp; &amp;1 == elem(guard, 1))
for obstacle <- obstacle_points do
  field = [obstacle | nodes] |> Enum.sort() |> Field.new()

  guard
  |> Stream.unfold(fn
    nil ->
      nil

    {dir, point} = guard ->
      new_guard =
        case Field.raycast(field, dir, point) do
          nil ->
            nil

          point ->
            {turn.(dir), approach.(dir, point)}
        end

      {guard, new_guard}
  end)
  |> Enum.reduce_while([], fn guard, guards ->
    if guard in guards do
      {:halt, guard}
    else
      {:cont, [guard | guards]}
    end
  end)
end