advent_of_code/elixir/lib/Y2023/day16.ex
2024-11-15 21:03:49 -05:00

187 lines
4.3 KiB
Elixir

defmodule Lightmap do
defstruct grid: %{}, height: 0, width: 0
@char_map %{
?. => :space,
?/ => :fw_mirror,
?\\ => :bk_mirror,
?| => :vert_split,
?- => :horz_split
}
def out_of_bounds?(%Lightmap{height: height, width: width}, {x, y}) do
x < 0 || y < 0 || y >= height || x >= width
end
def parse(input) do
grid =
for ent <-
input
|> String.split("\n")
|> Stream.with_index()
|> Stream.flat_map(fn {line, row_index} ->
line
|> String.to_charlist()
|> Stream.with_index()
|> Enum.map(fn {char, col_index} ->
{{col_index, row_index}, @char_map[char]}
end)
end),
into: %{} do
ent
end
%Lightmap{
grid: grid,
height: input |> String.trim() |> String.split("\n") |> Enum.count(),
width: input |> String.trim() |> String.split("\n") |> Enum.at(0) |> String.length()
}
end
end
defmodule Lightbeam do
def trace([], _, positions) do
positions
end
def trace(heads, map, positions) do
new_heads =
heads
|> Stream.flat_map(fn {pos, dir} ->
next_position(pos, dir, Map.get(map.grid, pos))
end)
|> Stream.reject(fn {new_pos, _} ->
Lightmap.out_of_bounds?(map, new_pos)
end)
|> Enum.reject(fn {new_pos, new_dir} ->
MapSet.member?(positions, {new_pos, new_dir})
end)
positions =
for {pos, dir} <- new_heads,
reduce: positions do
acc -> MapSet.put(acc, {pos, dir})
end
trace(new_heads, map, positions)
end
def solve_for_pos_and_direction(pos, direction, map) do
initial_pos_dir = {pos, direction}
Lightbeam.trace([initial_pos_dir], map, MapSet.new([initial_pos_dir]))
|> Stream.map(&elem(&1, 0))
|> Stream.uniq()
|> Enum.count()
end
def solve_for_max_along_edges(puzzle_input) do
map = Lightmap.parse(puzzle_input)
rows = puzzle_input |> String.trim() |> String.split("\n") |> Enum.with_index()
{first_row, _} = rows |> Enum.at(0)
{_, last_row_idx} = rows |> Enum.at(-1)
last_col_idx = (first_row |> String.length()) - 1
for row <- 0..last_row_idx, reduce: 0 do
acc -> max(acc, Lightbeam.solve_for_pos_and_direction({0, row}, :right, map))
end
|> max(
for row <- 0..last_row_idx, reduce: 0 do
acc -> max(acc, Lightbeam.solve_for_pos_and_direction({last_col_idx, row}, :left, map))
end
)
|> max(
for col <- 0..last_col_idx, reduce: 0 do
acc -> max(acc, Lightbeam.solve_for_pos_and_direction({col, 0}, :down, map))
end
)
|> max(
for col <- 0..last_col_idx, reduce: 0 do
acc -> max(acc, Lightbeam.solve_for_pos_and_direction({col, last_row_idx}, :up, map))
end
)
end
@direction_tuple %{
:up => {0, -1},
:down => {0, 1},
:left => {-1, 0},
:right => {1, 0}
}
@direction_map %{
:vert_split => %{
:up => [:up],
:down => [:down],
:left => [:up, :down],
:right => [:up, :down]
},
:horz_split => %{
:left => [:left],
:right => [:right],
:up => [:left, :right],
:down => [:left, :right]
},
:fw_mirror => %{
:left => [:down],
:up => [:right],
:right => [:up],
:down => [:left]
},
:bk_mirror => %{
:left => [:up],
:up => [:left],
:right => [:down],
:down => [:right]
},
:space => %{
:left => [:left],
:right => [:right],
:up => [:up],
:down => [:down]
}
}
def next_position(current_position, direction, tile) do
@direction_map[tile][direction]
|> Enum.map(fn dir ->
{
AoC.Util.Tuple.pairwise_sum(current_position, @direction_tuple[dir]),
dir
}
end)
end
def visualize_trace(trace, input) do
for {row, r_idx} <- input |> String.trim() |> String.split("\n") |> Stream.with_index(),
{_, col} <- row |> String.to_charlist() |> Stream.with_index(),
max_col = row |> String.length() do
if trace |> Enum.any?(&(elem(&1, 0) === {col, r_idx})) do
IO.write("#")
else
IO.write(".")
end
if col === max_col - 1 do
IO.write("\n")
end
end
end
end