Day15
Mix.install([
{:kino_aoc, git: "https://github.com/ljgago/kino_aoc"}
])
Setup
{:ok, data} = KinoAOC.download_puzzle("2024", "15", System.fetch_env!("LB_AOC_SECRET"))
Helpers
defmodule Aoc.Grid do
@type aoc_grid :: %{grid: map(), mx: non_neg_integer(), my: non_neg_integer()}
@type data :: String.t()
@up {-1, 0}
@down {1, 0}
@left {0, -1}
@right {0, 1}
@moves [@up, @down, @left, @right]
@dirs %{"^" => @up, "v" => @down, "<" => @left, ">" => @right}
@spec parse(data()) :: aoc_grid()
def parse(data), do: parse(data, &(&1))
@spec parse(data(), fun()) :: aoc_grid()
def parse(data, fun) do
raw =
data
|> String.split("\n", trim: true)
|> Enum.map(&String.graphemes/1)
my = length(raw)
mx = raw |> hd() |> length()
grid =
for {row, y} <- Enum.with_index(raw),
{sym, x} <- Enum.with_index(row),
into: %{} do
{{y, x}, fun.(sym)}
end
%{grid: grid, mx: mx - 1, my: my - 1}
end
def in_grid?({y, x}, aoc) do
y >= 0 and y <= aoc.my and x >= 0 and x <= aoc.mx
end
def can_move?({y, x}, aoc) do
in_grid?({y, x}, aoc) and aoc.grid[{y, x}] != "#"
end
def next_moves({y, x}, aoc) do
@moves
|> Enum.filter(fn {dy, dx} -> in_grid?({y + dy, x + dx}, aoc) end)
|> Enum.map(fn {dy, dx} -> {y + dy, x + dx} end)
end
def all_moves({y, x}) do
Enum.map(@moves, fn {dy, dx} -> {y + dy, x + dx} end)
end
def moves, do: @moves
def dirs, do: @dirs
def cw({r, c}), do: {c, -r}
def ccw({r, c}), do: {-c, r}
def fwd({y, x}, {dy, dx}), do: {y + dy, x + dx}
def plot(aoc) do
Enum.map(0..aoc.my, fn y ->
Enum.reduce(0..aoc.mx, "", fn x, acc -> acc <> aoc.grid[{y, x}] end)
end)
|> Enum.join("\n")
|> IO.puts()
end
end
Solve
defmodule Day15 do
alias Aoc.Grid, as: G
@dup %{"#" => "##", "O" => "[]", "." => "..", "@" => "@."}
def parse(data, dup \\ false) do
[grid, cmds] = data |> String.trim() |> String.split("\n\n", trim: true)
grid = if dup do
String.replace(grid, ~w(# O . @), fn char -> @dup[char] end)
else
grid
end
do_parse(grid, cmds)
end
def do_parse(grid, cmds) do
aoc = G.parse(grid)
cmds = cmds
|> String.split("\n", trim: true)
|> Enum.map(fn row -> String.split(row, "", trim: true) end)
|> List.flatten()
{aoc, cmds}
end
def solve(data, opts \\ []) do
verbose = Keyword.get(opts, :verbose, false)
dup = Keyword.get(opts, :dup, false)
{aoc, cmds} = parse(data, dup)
verbose && G.plot(aoc)
{r, "@"} = find(aoc, "@")
mfun = if dup, do: &move2/3, else: &move1/3
{aoc, _r} = mfun.(cmds, r, aoc)
verbose && G.plot(aoc)
calc_coord(aoc, dup)
end
def calc_coord(aoc, dup) do
calc_sym = dup && "[" || "O"
Enum.reduce(aoc.grid, 0, fn
{{y, x}, ^calc_sym}, acc ->
(100 * y + x) + acc
_, acc ->
acc
end)
end
def move2([], r, aoc), do: {aoc, r}
def move2([cmd | t], r, aoc) do
dir = G.dirs[cmd]
case can_move2(MapSet.new([r]), dir, aoc, true) do
{_, false} ->
move2(t, r, aoc)
{items, true} ->
old_g = aoc.grid
nr = G.fwd(r, dir)
# clean old
clean = Enum.reduce(items, aoc, fn pos, aoc ->
put_in(aoc, [:grid, pos], ".")
end)
# write new
new = Enum.reduce(items, clean, fn pos, aoc ->
nxt = G.fwd(pos, dir)
put_in(aoc, [:grid, nxt], old_g[pos])
end)
move2(t, nr, new)
end
end
def can_move2(items, dir, aoc, check) do
{acc, check} =
Enum.reduce(items, {MapSet.new(), check}, fn pos, {acc, check} ->
nxt = G.fwd(pos, dir)
case aoc.grid[nxt] do
"#" -> {MapSet.put(acc, pos), false}
"." -> {MapSet.put(acc, pos), check}
"]" -> # + [
l = G.fwd(nxt, {0, -1})
acc = acc |> MapSet.put(pos) |> MapSet.put(nxt) |> MapSet.put(l)
{acc, check}
"[" -> # + ]
r = G.fwd(nxt, {0, 1})
acc = acc |> MapSet.put(pos) |> MapSet.put(nxt) |> MapSet.put(r)
{acc, check}
end
end)
if MapSet.size(items) == MapSet.size(acc) or !check do
{acc, check}
else
can_move2(acc, dir, aoc, check)
end
end
def move1([], r, aoc), do: {aoc, r}
def move1([cmd | t], r, aoc) do
dir = G.dirs[cmd]
case can_move([r], dir, aoc) do
{false, _} -> move1(t, r, aoc)
{true, acc} ->
[r | boxes] = Enum.reverse(acc)
nr = G.fwd(r, dir)
aoc =
Enum.reduce(boxes, aoc, fn pos, aoc ->
nxt = G.fwd(pos, dir)
put_in(aoc, [:grid, nxt], "O")
end)
|> put_in([:grid, nr], "@")
|> put_in([:grid, r], ".")
move1(t, nr, aoc)
end
end
def can_move(acc, dir, aoc) do
[h | _t] = acc
nxt = G.fwd(h, dir)
case aoc.grid[nxt] do
"." -> {true, acc}
"#" -> {false, acc}
"O" -> can_move([nxt | acc], dir, aoc)
end
end
def find(%{grid: g}, el) do
Enum.find(g, fn {_p, k} -> k == el end)
end
end
t2 = """
##########
#..O..O.O#
#......O.#
#.OO..O.O#
#..O@..O.#
#O#..O...#
#O..O..O.#
#.OO.O.OO#
#....O...#
##########
^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv<
<>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^><
^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
<><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><
v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^
"""
t1 = """
########
#..O.O.#
##@.O..#
#...O..#
#.#.O..#
#...O..#
#......#
########
<^^>>>vv>v<<
"""
t3 = """
#######
#...#.#
#.....#
#..OO@#
#..O..#
#.....#
#######
IO.inspect(label: "t1-1")
Day15.solve(t2) |> IO.inspect(label: "t2-1")
Day15.solve(t3, verbose: true, dup: true) |> IO.inspect(label: "t3-2")
Day15.solve(t2, dup: true) |> IO.inspect(label: "t2-2")
Day15.solve(data) |> IO.inspect(label: "r1") #1490942
Day15.solve(data, dup: true) |> IO.inspect(label: "r2") # 1519202
:ok