/**
* Solver for Day 8 of Advent of Code 2025
*
* The puzzle asks us to repeatedly connect the pairs of junction boxes that are
* closest to each other (based on Euclidean distance) and then examine the
* resulting electrical circuits. After connecting the 1000 closest pairs, we
* must multiply together the sizes of the three largest circuits.
*/
type Point3D = { x: number; y: number; z: number }
type JunctionInfo = {
index: number
point: Point3D
circuitId: number
closestIndex: number | null
closestDistance: number
}
type DistancePair = { i: number; j: number; distance: number }
type CircuitSummary = { circuitId: number; members: number[] }
class CircuitManager {
private parent: number[]
private size: number[]
private circuits: Map<number, Set<number>>
private circuitCount: number
constructor(
private readonly count: number,
private readonly junctions: Map<number, JunctionInfo>,
) {
this.parent = Array.from({ length: count }, (_, index) => index)
this.size = Array.from({ length: count }, () => 1)
this.circuits = new Map()
this.circuitCount = count
for (let index = 0; index < count; index++) {
this.circuits.set(index, new Set([index]))
}
}
private find(node: number): number {
if (this.parent[node] !== node) {
this.parent[node] = this.find(this.parent[node])
}
return this.parent[node]
}
union(a: number, b: number): { merged: boolean; root: number } {
let rootA = this.find(a)
let rootB = this.find(b)
if (rootA === rootB) {
return { merged: false, root: rootA }
}
if (this.size[rootA] < this.size[rootB]) {
;[rootA, rootB] = [rootB, rootA]
}
this.parent[rootB] = rootA
this.size[rootA] += this.size[rootB]
const rootACircuit = this.circuits.get(rootA)
const rootBCircuit = this.circuits.get(rootB)
if (!rootACircuit || !rootBCircuit) {
throw new Error('Circuit data missing during union operation')
}
for (const junctionIndex of rootBCircuit) {
rootACircuit.add(junctionIndex)
const junctionInfo = this.junctions.get(junctionIndex)
if (junctionInfo) {
junctionInfo.circuitId = rootA
}
}
this.circuits.delete(rootB)
this.circuitCount -= 1
return { merged: true, root: rootA }
}
getCircuitSizes(): number[] {
return Array.from(this.circuits.values(), (members) => members.size)
}
getCircuitSummaries(): CircuitSummary[] {
return Array.from(this.circuits.entries()).map(([circuitId, members]) => ({
circuitId,
members: [...members].sort((a, b) => a - b),
}))
}
getCircuitCount(): number {
return this.circuitCount
}
}
const DEFAULT_CONNECTIONS = 1000
function getConnectionsTarget(): number {
const override = process.env.AOC_DAY08_CONNECTIONS
if (!override) return DEFAULT_CONNECTIONS
const parsed = Number(override)
if (!Number.isFinite(parsed)) {
return DEFAULT_CONNECTIONS
}
return Math.max(0, Math.floor(parsed))
}
function parsePoints(input: string): Point3D[] {
return input
.trim()
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line, idx) => {
const [x, y, z] = line.split(',').map((value) => Number(value.trim()))
if ([x, y, z].some((coord) => Number.isNaN(coord))) {
throw new Error(`Invalid coordinate on line ${idx + 1}: "${line}"`)
}
return { x, y, z }
})
}
function distance(a: Point3D, b: Point3D): number {
return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z)
}
function buildJunctionMap(points: Point3D[]): Map<number, JunctionInfo> {
const junctions = new Map<number, JunctionInfo>()
points.forEach((point, index) => {
junctions.set(index, {
index,
point,
circuitId: index,
closestIndex: null,
closestDistance: Number.POSITIVE_INFINITY,
})
})
return junctions
}
function buildDistancePairs(
points: Point3D[],
junctions: Map<number, JunctionInfo>,
): DistancePair[] {
const pairs: DistancePair[] = []
for (let i = 0; i < points.length; i++) {
const pointA = points[i]
for (let j = i + 1; j < points.length; j++) {
const pointB = points[j]
const dist = distance(pointA, pointB)
pairs.push({ i, j, distance: dist })
const junctionA = junctions.get(i)
const junctionB = junctions.get(j)
if (junctionA && dist < junctionA.closestDistance) {
junctionA.closestDistance = dist
junctionA.closestIndex = j
}
if (junctionB && dist < junctionB.closestDistance) {
junctionB.closestDistance = dist
junctionB.closestIndex = i
}
}
}
return pairs
}
function multiplyTopThree(sizes: number[]): number {
if (sizes.length < 3) {
throw new Error(
'Need at least three circuits to compute the requested product',
)
}
const [first, second, third] = sizes
.slice()
.sort((a, b) => b - a)
.slice(0, 3)
return first * second * third
}
function asRecord<T>(
entries: Array<[number, T]>,
): Record<number, T> | Record<string, T> {
return Object.fromEntries(entries)
}
export function solve(input: string): Promise<string | number | object> {
const points = parsePoints(input)
if (points.length < 2) {
return Promise.resolve({
part1: 0,
part2: {
circuits: {},
junctions: {},
},
})
}
const junctions = buildJunctionMap(points)
const pairs = buildDistancePairs(points, junctions).sort(
(a, b) => a.distance - b.distance,
)
const circuitManager = new CircuitManager(points.length, junctions)
const connectionsTarget = getConnectionsTarget()
let part1: number | null = null
let finalConnectionProduct: number | null = null
let finalConnectionPair: DistancePair | null = null
for (let idx = 0; idx < pairs.length; idx++) {
const { i, j } = pairs[idx]
const { merged } = circuitManager.union(i, j)
const processedConnections = idx + 1
if (processedConnections === connectionsTarget && part1 === null) {
part1 = multiplyTopThree(circuitManager.getCircuitSizes())
}
if (
merged &&
finalConnectionProduct === null &&
circuitManager.getCircuitCount() === 1
) {
finalConnectionProduct = points[i].x * points[j].x
finalConnectionPair = pairs[idx]
}
if (part1 !== null && finalConnectionProduct !== null) {
break
}
}
if (part1 === null) {
part1 = multiplyTopThree(circuitManager.getCircuitSizes())
}
if (finalConnectionProduct === null || finalConnectionPair === null) {
throw new Error('Failed to connect all junction boxes into one circuit')
}
const circuitSummaries = circuitManager
.getCircuitSummaries()
.sort((a, b) => b.members.length - a.members.length)
const circuitsRecord = asRecord(
circuitSummaries.map(({ circuitId, members }) => [
circuitId,
{ size: members.length, members },
]),
)
const junctionsRecord = asRecord(
Array.from(junctions.entries()).map(([index, info]) => [
index,
{
circuitId: info.circuitId,
closestIndex: info.closestIndex,
closestDistance:
Number.isFinite(info.closestDistance) &&
info.closestDistance !== Number.POSITIVE_INFINITY
? info.closestDistance
: null,
},
]),
)
return Promise.resolve({
part1,
part2: {
product: finalConnectionProduct,
finalConnection: {
indices: [finalConnectionPair.i, finalConnectionPair.j],
points: [
points[finalConnectionPair.i],
points[finalConnectionPair.j],
],
distance: finalConnectionPair.distance,
},
totalCircuits: circuitSummaries.length,
circuits: circuitsRecord,
junctions: junctionsRecord,
},
})
}
Create a file at src/solvers/day08.ts with the following structure:
export async function solve(input: string): Promise<string | number | object> {
// Your solution here
// The input parameter contains the puzzle input as a string
// Example:
const lines = input.trim().split('\n');
// Process and return your answer
return 'Your answer here';
}Then, import it in src/solvers/index.ts and add it to the solvers object:
import * as day08 from './day08'
export const solvers = {
// ... existing solvers
'08': day08,
}The solver function will receive the puzzle input as a string and should return the solution (string, number, or object).