diff --git a/day15/main.exs b/day15/main.exs new file mode 100644 index 0000000..0560cb5 --- /dev/null +++ b/day15/main.exs @@ -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()