Powered by AppSignal & Oban Pro

--- Day 8: Playground ---

2025/day_8.livemd

— Day 8: Playground —

Mix.install([{:kino_aoc, "~> 0.1"}])

Setup

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2025", "8", System.fetch_env!("LB_AOC_SESSION_COOKIE"))
test_input = Kino.Input.textarea("test_input")
test_input = Kino.Input.read(test_input)
defmodule Playground do
  def parse(input) do
    input
    |> String.split()
    |> Enum.map(fn coords ->
      coords |> String.split(",") |> Enum.map(&String.to_integer/1) |> List.to_tuple()
    end)
  end

  def sort_junction_boxes(coordinates) do
    coordinates
    |> Enum.flat_map(fn node_a ->
      for node_b <- coordinates, node_b != node_a do
        [euclidian_dist(node_a, node_b), node_a, node_b]
        |> Enum.sort()
        |> List.to_tuple()
      end
    end)
    |> MapSet.new()
    |> Enum.sort_by(&amp;elem(&amp;1, 0))
  end

  def euclidian_dist({p1, p2, p3}, {q1, q2, q3}) do
    # don't need to take square root since we're only comparing to other euclidian distances
    (p1 - q1) ** 2 + (p2 - q2) ** 2 + (p3 - q3) ** 2
  end

  @spec connects_to_circuit([MapSet.t()], Tuple.t(), Tuple.t()) ::
          integer() | {integer(), integer()} | nil
  def connects_to_circuit(circuits, node_a, node_b) do
    a_member_index = Enum.find_index(circuits, &amp;MapSet.member?(&amp;1, node_a))
    b_member_index = Enum.find_index(circuits, &amp;MapSet.member?(&amp;1, node_b))

    case {a_member_index, b_member_index} do
      {nil, nil} -> nil
      {index, nil} -> index
      {nil, index} -> index
      {index_a, index_b} when index_a == index_b -> index_a
      # two different indexes means two existing circuits need to be merged
      {index_a, index_b} -> {index_a, index_b}
    end
  end

  @type junction_box :: {integer(), integer(), integer()}

  @spec update_circuits([MapSet.t()], junction_box(), junction_box()) :: [MapSet.t()]
  def update_circuits(circuits, node_a, node_b) do
    new_circuit = MapSet.new([node_a, node_b])

    case connects_to_circuit(circuits, node_a, node_b) do
      # new circuit
      nil ->
        [new_circuit | circuits]

      # nodes common to 2 circuits, so we have to merge them together
      {index_a, index_b} ->
        circuit_b = Enum.at(circuits, index_b)

        circuits
        |> List.update_at(index_a, fn circuit_a ->
          circuit_a |> MapSet.union(circuit_b) |> MapSet.union(new_circuit)
        end)
        |> List.delete_at(index_b)

      # add nodes to existing circuit
      index ->
        List.update_at(circuits, index, &amp;MapSet.union(&amp;1, new_circuit))
    end
  end
end

Part 1

import Playground

coordinates = parse(puzzle_input)

coordinates
|> sort_junction_boxes()
|> Enum.take(1000)
|> Enum.reduce([], fn {_, node_a, node_b}, acc ->
  update_circuits(acc, node_a, node_b)
end)
|> Enum.sort_by(&amp;MapSet.size/1, :desc)
|> Enum.take(3)
|> Enum.product_by(&amp;MapSet.size/1)

Part 2

import Playground

coordinates = parse(puzzle_input)
input_size = length(coordinates)

coordinates
|> sort_junction_boxes()
|> Enum.reduce_while([], fn {_, node_a, node_b}, acc ->
  connecting_nodes = [node_a, node_b]

  [circuit | _] = acc = update_circuits(acc, node_a, node_b)

  if MapSet.size(circuit) == input_size do
    {:halt, connecting_nodes}
  else
    {:cont, acc}
  end
end)
|> Enum.product_by(&amp;elem(&amp;1, 0))