#!/usr/bin/env elixir defmodule Day15 do def part1({warehouse, robot, moves}) do attempt_moves(moves, robot, warehouse) |> Enum.filter(&match?({_, :box}, &1)) |> Enum.map(fn {{x, y}, :box} -> x + y * 100 end) |> Enum.sum() end def attempt_moves([], _robot, warehouse), do: warehouse def attempt_moves([move | next_moves], robot, warehouse) do next = move.(robot) case Map.fetch(warehouse, next) do :error -> attempt_moves(next_moves, next, warehouse) {:ok, :wall} -> attempt_moves(next_moves, robot, warehouse) {:ok, :box} -> case get_pushable_boxes(next, move, warehouse, [next]) do :wall -> attempt_moves(next_moves, robot, warehouse) boxes -> warehouse = Enum.reduce(boxes, warehouse, fn box, warehouse -> warehouse |> Map.delete(box) |> Map.put(move.(box), :box) end) attempt_moves(next_moves, next, warehouse) end end end def get_pushable_boxes(box, move, warehouse, pushable_boxes) do next = move.(box) case Map.fetch(warehouse, next) do :error -> pushable_boxes {:ok, :wall} -> :wall {:ok, :box} -> get_pushable_boxes(next, move, warehouse, [next | pushable_boxes]) end end def part2({warehouse, {x, y}, moves}) do bigger_warehouse = Enum.reduce(warehouse, %{}, fn {{x, y}, v}, bigger -> {left, right} = case v do :box -> {{:box, :left}, {:box, :right}} :wall -> {:wall, :wall} end bigger |> Map.put({x * 2, y}, left) |> Map.put({x * 2 + 1, y}, right) end) robot = {x * 2, y} attempt_moves2(moves, robot, bigger_warehouse) |> Enum.filter(&match?({_, {:box, :left}}, &1)) |> Enum.map(fn {{x, y}, {:box, :left}} -> x + y * 100 end) |> Enum.sum() end def attempt_moves2([], _robot, warehouse), do: warehouse def attempt_moves2([move | next_moves], robot, warehouse) do next = move.(robot) case Map.fetch(warehouse, next) do :error -> attempt_moves2(next_moves, next, warehouse) {:ok, :wall} -> attempt_moves2(next_moves, robot, warehouse) {:ok, {:box, side}} -> case get_pushable_boxes2(next, side, move, warehouse, [{next, side}]) do :wall -> attempt_moves2(next_moves, robot, warehouse) boxes -> new_boxes = Map.new(boxes, fn {box, side} -> {move.(box), {:box, side}} end) warehouse = warehouse |> Map.drop(Enum.map(boxes, &elem(&1, 0))) |> Map.merge(new_boxes) attempt_moves2(next_moves, next, warehouse) end end end # need to account for 2-width boxes when pushing vertically # @<--pushing down moves down all RHS of stack # [][][][] # [][][] @ # [][] []<--make sure RHS moves too # [] # @<- pushing up pushes all def get_pushable_boxes2(box, side, move, warehouse, pushable_boxes) do next = move.(box) case abs(elem(next, 1) - elem(box, 1)) do # horizontal 0 -> case Map.fetch(warehouse, next) do :error -> pushable_boxes {:ok, :wall} -> :wall {:ok, {:box, next_side}} -> get_pushable_boxes2(next, next_side, move, warehouse, [ {next, next_side} | pushable_boxes ]) end # vertical 1 -> {x, y} = box {other_box, other_side} = case side do :left -> {{x + 1, y}, :right} :right -> {{x - 1, y}, :left} end other_next = move.(other_box) pushable_boxes = [{other_box, other_side} | pushable_boxes] side |> case do :left -> {{Map.fetch(warehouse, next), Map.fetch(warehouse, other_next)}, next, other_next} :right -> {{Map.fetch(warehouse, other_next), Map.fetch(warehouse, next)}, other_next, next} end |> case do {{:error, :error}, _, _} -> pushable_boxes {{{:ok, :wall}, _}, _, _} -> :wall {{_, {:ok, :wall}}, _, _} -> :wall {{{:ok, {:box, side}}, :error}, left, _right} -> get_pushable_boxes2(left, side, move, warehouse, [{left, side} | pushable_boxes]) {{:error, {:ok, {:box, side}}}, _left, right} -> get_pushable_boxes2(right, side, move, warehouse, [{right, side} | pushable_boxes]) {{{:ok, {:box, :left}}, {:ok, {:box, :right}}}, left, _} -> # if left-right, just one box, only need to check one side # [] # [] get_pushable_boxes2(left, :left, move, warehouse, [{left, :left} | pushable_boxes]) {{{:ok, {:box, :right}}, {:ok, {:box, :left}}}, left, right} -> # if right-left, two boxes, so need to check both... # [][] # [] left = get_pushable_boxes2(left, :right, move, warehouse, [{left, :right}]) right = get_pushable_boxes2(right, :left, move, warehouse, [{right, :left}]) if left == :wall or right == :wall do :wall else pushable_boxes ++ left ++ right end end end end def input do with [input_filename] <- System.argv(), {:ok, input} <- File.read(input_filename) do [raw_map, raw_moves] = String.split(input, "\n\n") {_, _y, robot, map} = for < "\n">>, reduce: {0, 0, nil, %{}} do {x, y, robot, map} -> case char do "." -> {x + 1, y, robot, map} "#" -> {x + 1, y, robot, Map.put(map, {x, y}, :wall)} "O" -> {x + 1, y, robot, Map.put(map, {x, y}, :box)} "@" -> {x + 1, y, {x, y}, map} "\n" -> {0, y + 1, robot, map} end end moves = for <> do case char do "^" -> fn {x, y} -> {x, y - 1} end "v" -> fn {x, y} -> {x, y + 1} end "<" -> fn {x, y} -> {x - 1, y} end ">" -> fn {x, y} -> {x + 1, y} end end end {map, robot, moves} else _ -> :error end end ####################### # HERE BE BOILERPLATE # ####################### def run do case input() do :error -> print_usage() input -> run_parts_with_timer(input) end end defp run_parts_with_timer(input) do run_with_timer(1, fn -> part1(input) end) run_with_timer(2, fn -> part2(input) end) end defp run_with_timer(part, fun) do {time, result} = :timer.tc(fun) IO.puts("Part #{part} (completed in #{format_time(time)}):\n") IO.puts("#{inspect(result)}\n") end defp format_time(μsec) when μsec < 1_000, do: "#{μsec}μs" defp format_time(μsec) when μsec < 1_000_000, do: "#{μsec / 1000}ms" defp format_time(μsec), do: "#{μsec / 1_000_000}s" defp print_usage do IO.puts("Usage: elixir day15.exs input_filename") end end Day15.run()