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()