Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 18

day18.livemd

Day 18

Setup

https://adventofcode.com/2022/day/18

defmodule Load do
  def input do
    File.read!(Path.join(Path.absname(__DIR__), "input/18.txt"))
  end
end
defmodule Cube do
  defstruct x: 0,
            y: 0,
            z: 0,
            x_fwd: :exposed,
            x_rev: :exposed,
            y_fwd: :exposed,
            y_rev: :exposed,
            z_fwd: :exposed,
            z_rev: :exposed

  def new({x, y, z}) do
    %Cube{x: x, y: y, z: z}
  end

  def coords(cube) do
    {cube.x, cube.y, cube.z}
  end

  # The six faces of the cube
  def neighbor_coords(cube) do
    x = cube.x
    y = cube.y
    z = cube.z

    %{
      :x_fwd => {x + 1, y, z},
      :x_rev => {x - 1, y, z},
      :y_fwd => {x, y + 1, z},
      :y_rev => {x, y - 1, z},
      :z_fwd => {x, y, z + 1},
      :z_rev => {x, y, z - 1}
    }
  end

  def opposite_direction(direction) do
    case direction do
      :x_fwd -> :x_rev
      :x_rev -> :x_fwd
      :y_fwd -> :y_rev
      :y_rev -> :y_fwd
      :z_fwd -> :z_rev
      :z_rev -> :z_fwd
    end
  end

  def cover_direction(cube, direction) do
    Map.put(cube, direction, :covered)
  end

  def exposed_sides(cube) do
    [
      cube.x_fwd,
      cube.x_rev,
      cube.y_fwd,
      cube.y_rev,
      cube.z_fwd,
      cube.z_rev
    ]
    |> Enum.filter(fn side -> side == :exposed end)
    |> Enum.count()
  end
end

Cube.new({2, 2, 2})
# |> Cube.neighbor_coords()
|> Cube.exposed_sides()
defmodule Parse do
  def input(input_str) do
    input_str
    |> String.split("\n", trim: true)
    |> Enum.map(fn line ->
      line
      |> String.split(",", trim: true)
      |> Enum.map(fn str -> elem(Integer.parse(str), 0) end)
      |> List.to_tuple()
    end)
    |> Enum.map(&Cube.new(&1))
    |> MapSet.new()
  end
end

test_input =
  """
  2,2,2
  1,2,2
  3,2,2
  2,1,2
  2,3,2
  2,2,1
  2,2,3
  2,2,4
  2,2,6
  1,2,5
  3,2,5
  2,1,5
  2,3,5
  """
  |> Parse.input()
real_input =
  Load.input()
  |> Parse.input()

Part 1

defmodule Part1 do
  def solve(cubes) do
    cubes
    |> Enum.reduce(%{}, fn cube, board ->
      # board is map of x,y,z coords to cubes

      neighbor_coords = Cube.neighbor_coords(cube)

      {updated_board, placed_cube} =
        neighbor_coords
        |> Enum.reduce({board, cube}, fn {direction, coord}, {acc_board, acc_cube} ->
          opposite_direction = Cube.opposite_direction(direction)

          case acc_board[coord] do
            nil ->
              {acc_board, acc_cube}

            neighbor_cube ->
              # IO.puts("covering " <> inspect(opposite_direction) <> " of " <> inspect(neighbor_cube))
              # IO.puts("covering " <> inspect(direction) <> " of " <> inspect(acc_cube))
              {
                acc_board
                |> Map.put(coord, Cube.cover_direction(neighbor_cube, opposite_direction)),
                Cube.cover_direction(acc_cube, direction)
              }
          end
        end)

      updated_board
      |> Map.put(Cube.coords(placed_cube), placed_cube)
    end)
    |> Map.values()
    |> Enum.map(fn cube -> Cube.exposed_sides(cube) end)
    |> Enum.sum()
  end
end

Part1.solve(test_input)
Part1.solve(real_input)

Part 2

defmodule Part2 do
  def adjacent_coords({x, y, z}) do
    [
      {x + 1, y, z},
      {x - 1, y, z},
      {x, y + 1, z},
      {x, y - 1, z},
      {x, y, z + 1},
      {x, y, z - 1}
    ]
  end

  def edge_search(_, edge_count, _, _, _, _, []) do
    edge_count
  end

  def edge_search(filled_coords, edge_count, visited, x_range, y_range, z_range, [curr | to_visit]) do
    updated_visited = MapSet.put(visited, curr)

    in_bounds_neighbors =
      adjacent_coords(curr)
      |> Enum.filter(fn {x, y, z} -> x in x_range and y in y_range and z in z_range end)

    num_cube_neighbors =
      in_bounds_neighbors
      |> Enum.filter(fn coord -> MapSet.member?(filled_coords, coord) end)
      |> Enum.count()

    visitable_neighbors =
      in_bounds_neighbors
      |> Enum.reject(fn coord -> MapSet.member?(updated_visited, coord) end)
      |> Enum.reject(fn coord -> MapSet.member?(filled_coords, coord) end)
      |> Enum.reject(fn coord -> Enum.member?(to_visit, coord) end)

    updated_to_visit = to_visit ++ visitable_neighbors

    edge_search(
      filled_coords,
      edge_count + num_cube_neighbors,
      updated_visited,
      x_range,
      y_range,
      z_range,
      updated_to_visit
    )
  end

  def solve(cubes) do
    filled_coords =
      cubes
      |> Enum.map(fn cube -> {cube.x, cube.y, cube.z} end)
      |> MapSet.new()

    max_x = filled_coords |> Enum.map(&amp;elem(&amp;1, 0)) |> Enum.max()
    max_y = filled_coords |> Enum.map(&amp;elem(&amp;1, 1)) |> Enum.max()
    max_z = filled_coords |> Enum.map(&amp;elem(&amp;1, 2)) |> Enum.max()
    min_x = filled_coords |> Enum.map(&amp;elem(&amp;1, 0)) |> Enum.min()
    min_y = filled_coords |> Enum.map(&amp;elem(&amp;1, 1)) |> Enum.min()
    min_z = filled_coords |> Enum.map(&amp;elem(&amp;1, 2)) |> Enum.min()

    edge_search(
      filled_coords,
      0,
      MapSet.new(),
      (min_x - 1)..(max_x + 1),
      (min_y - 1)..(max_y + 1),
      (min_z - 1)..(max_z + 1),
      [{min_x - 1, min_y - 1, min_z - 1}]
    )
  end
end

Part2.solve(test_input)
Part2.solve(real_input)