diff --git a/elixir/livebook/2024/day15.livemd b/elixir/livebook/2024/day15.livemd new file mode 100644 index 0000000..20cadc4 --- /dev/null +++ b/elixir/livebook/2024/day15.livemd @@ -0,0 +1,392 @@ +# AOC 2024 Day 15 + +## Section + +```elixir +defmodule Grid2D do + defstruct width: nil, height: nil + + @directions %{ + straight: [ + {1, 0}, + {-1, 0}, + {0, 1}, + {0, -1} + ], + diagonal: [ + {1, 1}, + {1, -1}, + {-1, 1}, + {-1, -1} + ] + } + + def new(width, height) do + %Grid2D{width: width, height: height} + end + + @doc """ + ## Examples + iex> Grid2D.index_to_coord(Grid2D.new(5,5), 0) + {0,0} + + iex> Grid2D.index_to_coord(Grid2D.new(5,5), 5) + {0,1} + + iex> Grid2D.index_to_coord(Grid2D.new(5,5), 6) + {1,1} + + iex> Grid2D.index_to_coord(Grid2D.new(5,5), 3) + {3,0} + + iex> Grid2D.index_to_coord(Grid2D.new(5,5), 24) + {4, 4} + """ + def index_to_coord(%Grid2D{width: w}, index) do + {rem(index, w), floor(index / w)} + end + + @doc """ + ## Examples + iex> Grid2D.coord_to_index( Grid2D.new(5,5), {0,0}) + 0 + + iex> Grid2D.coord_to_index( Grid2D.new(5,5), {4,4}) + 24 + + iex> Grid2D.coord_to_index( Grid2D.new(5,5), {2,2}) + 12 + """ + def coord_to_index(%Grid2D{width: w}, {x, y}) do + y * w + x + end + + @doc """ + + ## Examples + iex> Grid2D.neighbors({0, 0}, Grid2D.new(5,5)) |> MapSet.new() + MapSet.new([{0, 1}, {1, 1}, {1,0}]) + + iex> Grid2D.neighbors({2, 2}, Grid2D.new(5,5)) |> MapSet.new() + [{1, 1}, {1, 2}, {1, 3}, {2, 1}, {2, 3}, {3, 1}, {3, 2}, {3, 3}] |> MapSet.new() + + iex> Grid2D.neighbors({4, 4}, Grid2D.new(5,5)) |> MapSet.new + [{3, 3}, {3, 4}, {4, 3}] |> MapSet.new + """ + def neighbors(p, g, allowed_directions \\ :all) do + directions(allowed_directions) + |> Stream.map(&add(p, &1)) + |> Stream.filter(&is_in_grid?(&1, g)) + end + + @doc """ + Distance from two points in a grid + + iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {0,1}, :radial) + 1 + + iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {1,1}, :radial) + 1 + + iex> Grid2D.distance(Grid2D.new(5,5), {0,0}, {1,2}, :radial) + 2 + """ + def distance(grid, p1, p2, type) + def distance(_, {x, y}, {a, b}, :radial), do: max(abs(x - a), abs(y - b)) + + @doc """ + Add two points together pairwise + + iex> Grid2D.add({0,0}, {0,1}) + {0,1} + + iex> Grid2D.add({2,3}, {3,2}) + {5,5} + """ + def add({x, y}, {j, k}), do: {x + j, y + k} + + @doc """ + Test if a point is in a grid + + iex> Grid2D.is_in_grid?({1, 2}, %Grid2D{width: 3, height: 4}) + true + + iex> Grid2D.is_in_grid?({2, 2}, %Grid2D{width: 2, height: 4}) + false + """ + def is_in_grid?({x, y}, %Grid2D{width: w, height: h}), do: x < w and y < h and x >= 0 and y >= 0 + + @spec directions(allowed :: :all | :straight | :diagonal) :: any() + def directions(allowed \\ :all) + def directions(:all), do: directions(:straight) ++ directions(:diagonal) + def directions(:straight), do: @directions |> Map.get(:straight) + def directions(:diagonal), do: @directions |> Map.get(:diagonal) + + def mul({x, y}, n), do: {n*x, n*y} +end + +``` + +```elixir +input = """ +""" +``` + +```elixir +[map, instructions] = input |> String.trim() |> String.split("\n\n") + +map = map + |> String.trim() + |> String.split("\n", trim: true) + |> Stream.with_index() + |> Stream.flat_map(fn {line, y_idx} -> + line + |> String.graphemes() + |> Stream.with_index() + |> Enum.map(fn {c, x_idx} -> {{x_idx, y_idx}, c} end) + end) +|> Enum.into(%{}) + +instructions = instructions |> String.trim() |> String.split("", trim: true) |> Stream.reject(&(&1 == "\n")) |> Enum.map(&( + case &1 do + "<" -> {-1, 0} + ">" -> { 1, 0} + "^" -> { 0, -1} + "v" -> { 0, 1} + end +)) + +``` + +```elixir +w = map |> Map.keys() |> Enum.max_by(&elem(&1, 0)) |> elem(0) +h = map |> Map.keys() |> Enum.max_by(&elem(&1, 1)) |> elem(1) +start = map |> Enum.find(fn {_, t} -> t == "@" end) |> elem(0) +grid = Grid2D.new(w, h) +``` + +```elixir +defmodule WarehouseWoes do + def move(map, grid, direction) do + [{coord, _}] = map |> Enum.filter(&(elem(&1, 1) == "@")) + move(map, grid, coord, direction) + end + + def move(map, _, _, []), do: map + def move(map, grid, coord, [dir | rest]) do + if can_move?(map, grid, coord, dir) do + next = coord |> Grid2D.add(dir) + tile = Map.get(map, next) + + (if tile == "O" do + move(map, grid, next, [ dir ]) + else + map + end) + |> Map.put(next, Map.get(map, coord)) + |> Map.put(coord, ".") + |> move(grid, rest) + else + move(map, grid, coord, rest) + end + end + + def can_move?(map, grid, coord, direction) do + next = coord |> Grid2D.add(direction) + in_grid? = next |> Grid2D.is_in_grid?(grid) + tile = Map.get(map, next) + in_grid? and tile != "#" and (tile != "O" or can_move?(map, grid, next, direction)) + end + + def print(map, grid) do + for y <- 0..(grid.height) do + for x <- 0..(grid.width) do + IO.write(Map.get(map, {x, y})) + if x == grid.width do + IO.write("\n") + end + end + end + :ok + end +end +``` + +```elixir +defmodule WarehouseWoes2 do + def parse(input) do + [map, instructions] = input |> String.trim() |> String.split("\n\n") + + map = map + |> String.trim() + |> String.split("\n", trim: true) + |> Stream.with_index() + |> Stream.flat_map(fn {line, y_idx} -> + line + |> String.graphemes() + |> Stream.with_index() + |> Enum.map(fn {c, x_idx} -> {{x_idx, y_idx}, c} end) + end) + |> Enum.reduce(%{}, fn {{x, y}, tile}, acc -> + case tile do + "O" -> acc |> Map.put({2*x, y}, "[") |> Map.put({2*x + 1, y}, "]") + "." -> acc |> Map.put({2*x, y}, ".") |> Map.put({2*x + 1, y}, ".") + "#" -> acc |> Map.put({2*x, y}, "#") |> Map.put({2*x + 1, y}, "#") + "@" -> acc |> Map.put({2*x, y}, "@") |> Map.put({2*x + 1, y}, ".") + end + end) + + instructions = instructions |> String.trim() |> String.split("", trim: true) |> Stream.reject(&(&1 == "\n")) |> Enum.map(&( + case &1 do + "<" -> {-1, 0} + ">" -> { 1, 0} + "^" -> { 0, -1} + "v" -> { 0, 1} + end + )) + {map, instructions} + end + + def solve(input) do + {map, instructions} = parse(input) + w = map |> Map.keys() |> Enum.max_by(&elem(&1, 0)) |> elem(0) + h = map |> Map.keys() |> Enum.max_by(&elem(&1, 1)) |> elem(1) + grid = Grid2D.new(w, h) + move(map, grid, instructions) + |> Stream.filter(&(elem(&1, 1) == "[")) + |> Stream.map(&(elem(&1,0))) + |> Enum.reduce(0, fn {x, y},acc -> acc + 100*y + x end) + end + + def move(map, grid, direction) do + [{coord, _}] = map |> Enum.filter(&(elem(&1, 1) == "@")) + move(map, grid, coord, direction) + end + + @doc " + Recursively moves a coordinate on the map. + " + def move(map, _, _, []), do: map + # Only used when the turtle is pushing up on a block + # We gather all the coordinates for blocks being pushed in a direction, + # then try to move them up as a unit. + # The whole unit moves up if: + # - The space above all the blocks is empty (".") + # - The space above all the blocks is either empty or other blocks and those blocks can be move up. + # If the space above the blocks contains any walls ("#") or the blocks above them cannot be moved, + # the blocks will not move. + # + def move(map, grid, coords, [{0, n} = dir]) when is_list(coords) and (n == 1 or n == -1) do + nexts = coords |> Enum.flat_map(fn i -> + next = Grid2D.add(i, dir) + tile = Map.get(map, next) + case tile do + "[" -> [next, Grid2D.add(next, {1, 0})] + "]" -> [Grid2D.add(next, {-1, 0}), next] + _ -> [next] + end + end) + + cond do + Enum.any?(nexts, &(Map.get(map, &1) == "#")) -> map + Enum.all?(nexts, &(Map.get(map, &1) == ".")) -> + coords + |> Enum.reduce(map, fn coord, acc -> + acc + |> Map.put(coord |> Grid2D.add(dir), Map.get(map, coord)) + |> Map.put(coord, ".") + end) + true -> + moved = move(map, grid, nexts |> Enum.filter(fn c -> + tile = Map.get(map, c) + tile == "[" or tile == "]" + end), [dir]) + if moved != map do + move(moved, grid, coords, [dir]) + else + map + end + end + end + + # This method is only ever actually called when `coord` represents the coord of robot (") + def move(map, grid, coord, [{0, n} = dir | rest]) when n == 1 or n == -1 do + next = Grid2D.add(coord, dir) + next_tile = Map.get(map, next) + map = case next_tile do + "." -> map + |> Map.put(next, Map.get(map, coord)) + |> Map.put(coord, ".") + "#" -> map + "]" -> + next = [ Grid2D.add(next, {-1, 0}), next] + moved = move(map, grid, next, [dir]) + if moved != map do + move(moved, grid, coord, [dir]) + else + moved + end + "[" -> + next = [next, Grid2D.add(next, {1, 0})] + moved = move(map, grid, next, [dir]) + if moved != map do + move(moved, grid, coord, [dir]) + else + moved + end + end + map |> move(grid, rest) + end + + def move(map, grid, coord, [dir | rest]) do + next = Grid2D.add(coord, dir) + next_tile = Map.get(map, next) + map = case next_tile do + "." -> map + |> Map.put(next, Map.get(map, coord)) + |> Map.put(coord, ".") + "#" -> map + _ -> + moved = map |> move(grid, next, [dir]) + if moved == map do + moved + else + move(moved, grid, coord, [dir]) + end + end + map |> move(grid, rest) + end + + @doc " + Prints the ASCII character for an instruction. + " + def instruction(i) do + case i do + {-1, 0} -> "<" + {1, 0} -> ">" + {0, -1} -> "^" + {0, 1} -> "v" + end + end + + @doc " + Returns the current map as a string. + " + def print(map, grid) do + for y <- 0..(grid.height) do + for x <- 0..(grid.width) do + (Map.get(map, {x, y}) || ".") <> + (if x == grid.width do + "\n" + else + "" + end) + end + end |> Enum.join("") + end +end +``` + +```elixir +WarehouseWoes2.solve(input) +```