Day 15. That was hard

master
Dustin Swan 3 weeks ago
parent 9d8e3f05a6
commit 19597d95b0
Signed by: dustinswan
GPG Key ID: AB49BD6B2B3A6377

@ -0,0 +1,225 @@
defmodule Day15 do
def run do
input = File.read!("data.txt") |> String.trim() |> String.split("\n\n")
map_list = hd(input) |> String.split("\n") |> Enum.map(&String.graphemes/1)
height = length(map_list)
width = length(hd(map_list))
# convert the map List to a map Map
map =
Enum.concat(map_list)
|> Enum.with_index()
|> Enum.reduce(%{}, fn {el, idx}, acc ->
Map.put(acc, {div(idx, height), rem(idx, width)}, el)
end)
moves = Enum.at(input, 1) |> String.replace("\n", "") |> String.graphemes()
# step over each move, reducing to create a map
part1 =
Enum.reduce(moves, map, fn move, acc ->
step(move, acc)
end)
# grab all the Os, calculate the GPS value for each, and sum them
|> Enum.filter(fn {_k, v} -> v == "O" end)
|> Enum.map(fn {{x, y}, _} -> 100 * x + y end)
|> Enum.sum()
dbg(part1)
# expand the map
map2 =
Enum.reduce(map, %{}, fn {{x, y}, el}, acc ->
{el1, el2} =
case el do
"O" -> {"[", "]"}
"@" -> {"@", "."}
x -> {x, x}
end
Map.put(acc, {x, y * 2}, el1)
|> Map.put({x, y * 2 + 1}, el2)
end)
render(map2, {width * 2, height})
part2 =
Enum.reduce(moves, map2, fn move, acc ->
step(move, acc)
end)
|> render({width * 2, height})
|> Enum.filter(fn {_k, v} -> v == "[" end)
|> Enum.map(fn {{x, y}, _} -> 100 * x + y end)
|> Enum.sum()
dbg(part2)
end
defp step(move, map) do
dir =
case move do
"^" -> {-1, 0}
">" -> {0, 1}
"v" -> {1, 0}
"<" -> {0, -1}
end
{r_row, r_col} = robot_location(map)
next_loc = {r_row + elem(dir, 0), r_col + elem(dir, 1)}
next_el = Map.get(map, next_loc, ".")
case next_el do
# if next el is empty, just move the robot there
"." -> Map.put(map, {r_row, r_col}, ".") |> Map.put(next_loc, "@")
# if it's a wall, don't change the map
"#" -> map
# if it's a block, push on it
"O" -> push(dir, next_loc, map)
# if it's a big block, push on it
"[" -> push_big(dir, {r_row, r_col}, map)
"]" -> push_big(dir, {r_row, r_col}, map)
end
end
# pushing big blocks
defp push_big(dir, {x, y}, map) do
case dir do
# pushing left or right
{0, dy} ->
case next_empty(dir, {x, y}, map) do
nil ->
map
{free_x, free_y} ->
Enum.reduce(free_y..y, map, fn col, acc ->
el = Map.get(acc, {free_x, col}, ".")
next_el = if el == "@", do: ".", else: Map.get(acc, {free_x, col - dy}, ".")
Map.put(acc, {free_x, col}, next_el)
end)
end
# pushing up or down
{dx, 0} ->
{bx, by} = {x + dx, y}
el = Map.get(map, {bx, by})
box_location = if el == "[", do: {bx, by}, else: {bx, by - 1}
# get list of boxes that can move
boxes = move_big_boxes(dir, box_location, map)
if is_nil(boxes) do
map
else
{rx, ry} = robot_location(map)
# sort it so we don't overwrite stuff
Enum.sort(boxes, fn {x1, _}, {x2, _} ->
if dx == -1, do: x1 < x2, else: x1 > x2
end)
# now actually move all the boxes up or down
|> Enum.reduce(map, fn {x, y}, acc ->
Map.put(acc, {x, y}, ".")
|> Map.put({x, y + 1}, ".")
|> Map.put({x + dx, y}, "[")
|> Map.put({x + dx, y + 1}, "]")
end)
|> Map.put({rx, ry}, ".")
|> Map.put({rx + dx, ry}, "@")
end
end
end
# we're trying to push a big box in this direction
# check all boxes in its path, recursively, until we find an open spot
# we only do this up and down so we can ignore dy
# if we've found a spot, return all boxes that can move
# if there is no spot, return nil
defp move_big_boxes({dx, dy}, {x, y}, map) do
left_el = Map.get(map, {x + dx, y}, ".")
right_el = Map.get(map, {x + dx, y + 1}, ".")
case {left_el, right_el} do
# if the 2 spots above/below are free, we've found a spot
{".", "."} ->
[{x, y}]
# if either are walls, this box can't move
{l, r} when l == "#" or r == "#" ->
nil
# if it's just 1 block above
{"[", "]"} ->
res = move_big_boxes({dx, dy}, {x + dx, y}, map)
if is_nil(res), do: nil, else: [{x, y} | res]
{"]", "."} ->
res = move_big_boxes({dx, dy}, {x + dx, y - 1}, map)
if is_nil(res), do: nil, else: [{x, y} | res]
{".", "["} ->
res = move_big_boxes({dx, dy}, {x + dx, y + 1}, map)
if is_nil(res), do: nil, else: [{x, y} | res]
{"]", "["} ->
res = move_big_boxes({dx, dy}, {x + dx, y - 1}, map)
if is_nil(res) do
nil
else
res2 = move_big_boxes({dx, dy}, {x + dx, y + 1}, map)
if is_nil(res2) do
nil
else
Enum.concat([{x, y} | res], res2)
end
end
end
end
# if the block is pushable in that direction, push it and move the robot
defp push(dir, loc, map) do
case next_empty(dir, loc, map) do
# if there isn't a next empty spot, do nothing
nil ->
map
# if there is a next empty spot, put a O there, move the robot to the next spot, replace robot's position with a .
{x, y} ->
Map.put(map, loc, "@") |> Map.put({x, y}, "O") |> Map.put(robot_location(map), ".")
end
end
# finds the next empty spot in a given direction starting at the given location
defp next_empty({dir_x, dir_y}, {loc_x, loc_y}, map) do
case Map.get(map, {loc_x, loc_y}, ".") do
"#" -> nil
"." -> {loc_x, loc_y}
_ -> next_empty({dir_x, dir_y}, {loc_x + dir_x, loc_y + dir_y}, map)
end
end
# return the {row, col} of the robot
defp robot_location(map) do
Enum.find(map, fn {_key, val} -> val == "@" end) |> elem(0)
end
defp render(map, {width, height}) do
Enum.each(0..(height - 1), fn row ->
Enum.each(0..(width - 1), fn col ->
el = Map.get(map, {row, col}, ".")
color = if el == "@", do: :red_background, else: :default_background
formatted = IO.ANSI.format([color, el])
IO.write(formatted)
end)
IO.write("\n")
end)
IO.write("\n")
map
end
end
Day15.run()
Loading…
Cancel
Save