Camille

u/Camille@lemmy.ml
2 posts · 91 comments

Recent posts

Recent comments

Ouais mais c'est pas dans les intérêts du capitalisme. C'est important dans notre système d'entretenir la fatigue et les problèmes mentaux des travailleurs pour obtenir leur soumission et leur vague consentement à la torture quotidienne qui leur est infligée. Donc fondamentalement, peu importe si c'est un bon objectif ou non, le capitalisme cherchera toujours à appauvrir nos conditions de vie et de travail

on Becky Chambers · c/solarpunk · 7 pts · 128d

I have read the first book of the Wayfarer series and I am at the 1/4 of the second book. I don't have questions but I do love her writings. The story is interesting, the writing is clearly and it feels good and relaxing to read her stories in our mad and depressing world. All I'd want to say is "thank you so much"

I've seen so many IT projects, associations, communities of minorities using discord in lieu of wikis, forums, documentation, etc. I personally know a marginalized community with a dire need of visibility, information and stuff that has everything locked behind their discord server and everything is about to be lost. It's both extremely sad and a huge "told you so years ago"

on Oh yes · c/onehundredninetysix · 19 pts · 264d

Just finished season one. It's so precious, so important for many people. I had many good cries. 10/10 will rewatch

Back when I was a student, we had to implement the following projects to validate and I think they are a really nice set of training projects for C (memory management, handling your own types, algorithms and off-by-one traps):

  • a basic implementation of diff
  • a fully functional hashmap (subproject of diff actually)

I personally wrote the followings too:

  • a program to compress/decompress archives using only the LZ77 algorithm
  • math expression evaluator (i.e. a basic interpreter)
  • a basic garbage collector (mark&sweep, mark&copy, whatever grinds your gears)

If you are already a competent developer in other languages, these should be reasonable projects. The difficulty lies in doing them in C. For the hashmap, the evaluator and the archiver, you have to correctly manage your data and memory. this makes them excellent projects for learning what makes C... C. For the garbage collector... well... you are the memory manager. Basic GC algorithms are easy to understand but what you want to learn here is the actual management of the memory. See it as a more advanced project if you want to really grok dirty C.

Most importantly: have fun :)

on *Permanently Deleted* · c/videos · 3 pts · 280d

Don't let big pharma fool you, it's not the alcool chemically killing the bacteria, it's clearly the wind. Look how they get wiped by the sheer force of power nature /joke

Go

Well... I was about to dive into bin packing and stuff. I started by pruning the obvious candidates (those which can fit all the shapes one next to the other, without more computation) so save CPU time for the real stuff. I ran my code on the real input just to see and... what to do mean there are no candidate left? I write the number of obvious boys into the website's input box, just to check and... Ah. I understand why people on reddit said they felt dirty x)

Anyway the code:

::: spoiler day12.go

package main

import (
	"aoc/utils"
	"fmt"
	"slices"
	"strconv"
	"strings"
)

type shape [3][3]bool

func parseShape(input chan string) shape {
	// remove the header
	_ = <-input

	sh := shape{}
	idx := 0
	for line := range input {
		if line == "" {
			break
		}

		row := [3]bool{}
		for idx, c := range []rune(line) {
			if c == '#' {
				row[idx] = true
			}
		}

		sh[idx] = row
		idx++
	}

	return sh
}

func (sh shape) usedArea() int {
	sum := 0
	for _, row := range sh {
		for _, cell := range row {
			if cell {
				sum++
			}
		}
	}
	return sum
}

type regionConstraints struct {
	width, height int
	shapes        []int
}

func parseRegionConstraint(line string) (rc regionConstraints) {
	parts := strings.Split(line, ":")
	dims := strings.Split(parts[0], "x")
	rc.width, _ = strconv.Atoi(dims[0])
	rc.height, _ = strconv.Atoi(dims[1])

	shapes := strings.Fields(parts[1])
	rc.shapes = make([]int, len(shapes))
	for idx, shape := range shapes {
		rc.shapes[idx], _ = strconv.Atoi(shape)
	}
	return rc
}

type problem struct {
	shapes      []shape
	constraints []regionConstraints
}

func newProblem(input chan string) problem {
	shapes := make([]shape, 6)
	for idx := range 6 {
		shapes[idx] = parseShape(input)
	}

	regionConstraints := []regionConstraints{}
	for line := range input {
		rc := parseRegionConstraint(line)
		regionConstraints = append(regionConstraints, rc)
	}

	return problem{shapes, regionConstraints}
}

func (pb *problem) pruneRegionsTooSmall() {
	toPrune := []int{}
	for idx, rc := range pb.constraints {
		availableArea := rc.height * rc.width
		neededArea := 0
		for shapeId, count := range rc.shapes {
			neededArea += pb.shapes[shapeId].usedArea() * count
			if neededArea > availableArea {
				toPrune = append(toPrune, idx)
				break
			}
		}
	}

	slices.Reverse(toPrune)
	for _, idx := range toPrune {
		pb.constraints = slices.Delete(pb.constraints, idx, idx+1)
	}
}

func (pb *problem) pruneObviousCandidates() int {
	toPrune := []int{}

	for idx, rc := range pb.constraints {
		maxShapePlacements := (rc.width / 3) * (rc.height / 3)
		shapeCount := 0
		for _, count := range rc.shapes {
			shapeCount += count
		}
		if maxShapePlacements >= shapeCount {
			toPrune = append(toPrune, idx)
		}
	}

	slices.Reverse(toPrune)
	for _, idx := range toPrune {
		pb.constraints = slices.Delete(pb.constraints, idx, idx+1)
	}

	return len(toPrune)
}

func stepOne(input chan string) (int, error) {
	pb := newProblem(input)
	pb.pruneRegionsTooSmall()
	obviousCandidates := pb.pruneObviousCandidates()

	fmt.Println(pb)
	fmt.Println(obviousCandidates)
	return 0, nil
}

func stepTwo(input chan string) (int, error) {
	return 0, nil
}

func main() {
	input, err := utils.DownloadTodaysInputFile()
	if err != nil {
		_ = fmt.Errorf("error fetching the input: %v", err)
	}

	utils.RunStep(utils.ONE, input, stepOne)
	utils.RunStep(utils.TWO, input, stepTwo)
}

:::

Go

Yeeeaaay graphs! This one has been a pleasure to program and here comes my solution, with pruning of irrelevant branches (cursed nodes) and memoïzation for better performance.

::: spoiler day11.go

package main

import (
	"aoc/utils"
	"fmt"
	"slices"
	"strings"
)

type graph map[string][]string

func graphFromInput(input chan string) graph {
	g := graph{}
	for line := range input {
		firstSplit := strings.Split(line, ":")
		currentNode := firstSplit[0]
		followers := strings.Fields(firstSplit[1])
		g[currentNode] = followers
	}
	return g
}

var memo = make(map[string]map[string]int)

func (g *graph) dfsUntil(currentNode, targetNode string, cursedNodes map[string]any) int {
	subMemo, ok := memo[currentNode]
	if !ok {
		memo[currentNode] = make(map[string]int)
	} else {
		sum, ok := subMemo[targetNode]
		if ok {
			return sum
		}
	}

	followers, _ := (*g)[currentNode]
	sum := 0
	for _, follower := range followers {
		if follower == targetNode {
			memo[currentNode][targetNode] = 1
			return 1
		} else if _, ok := cursedNodes[follower]; ok {
			continue
		}
		sum += g.dfsUntil(follower, targetNode, cursedNodes)
	}

	memo[currentNode][targetNode] = sum
	return sum
}

func stepOne(input chan string) (int, error) {
	g := graphFromInput(input)
	return g.dfsUntil("you", "out", make(map[string]any)), nil
}

func (g *graph) dfsFromSvrToOut(
	currentNode string, hasDac, hasFft bool, cursedNodes map[string]any) int {

	if currentNode == "dac" && hasFft {
		return g.dfsUntil("dac", "out", cursedNodes)
	}

	hasDac = hasDac || currentNode == "dac"
	hasFft = hasFft || currentNode == "fft"

	followers := (*g)[currentNode]
	sum := 0
	for _, follower := range followers {
		switch follower {
		case "out":
			if hasDac && hasFft {
				return 1
			} else {
				return 0
			}
		default:
			sum += g.dfsFromSvrToOut(follower, hasDac, hasFft, cursedNodes)
		}
	}
	return sum
}

func (g *graph) getCursedNodes() map[string]any {
	cursedNodes := make(map[string]any)

	for node := range *g {
		if node == "dac" || node == "fft" || node == "svr" {
			continue
		}

		fftToNode := g.dfsUntil("fft", node, cursedNodes)
		dacToNode := g.dfsUntil("dac", node, cursedNodes)
		nodeToFft := g.dfsUntil(node, "fft", cursedNodes)
		nodeToDac := g.dfsUntil(node, "dac", cursedNodes)

		if dacToNode > 0 {
			continue
		}

		if fftToNode > 0 && nodeToDac > 0 {
			continue
		}

		if nodeToFft == 0 {
			cursedNodes[node] = nil
		}
	}

	return cursedNodes
}

func stepTwo(input chan string) (int, error) {
	g := graphFromInput(input)
	cursedNodes := g.getCursedNodes()
	for cursedKey := range cursedNodes {
		delete(g, cursedKey)
		for gkey := range g {
			g[gkey] = slices.DeleteFunc(g[gkey], func(n string) bool {
				return n == cursedKey
			})
		}
	}
	memo = make(map[string]map[string]int)
	sum := g.dfsUntil("svr", "out", nil)
	return sum, nil
}

func main() {
	input, err := utils.DownloadTodaysInputFile()
	if err != nil {
		_ = fmt.Errorf("%v\n", err)
	}

	utils.RunStep(utils.ONE, input, stepOne)
	utils.RunStep(utils.TWO, input, stepTwo)
}

:::

I have also finally written a function to automatically download the input file (which will prove useful for... ehm... tomorrow):

::: spoiler download today's input file

func DownloadTodaysInputFile() (FilePath, error) {
	today := time.Now().Day()
	url := fmt.Sprintf("https://adventofcode.com/2025/day/%d/input", today)

	client := &http.Client{}
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return FilePath(""), err
	}

	// const sessionValue = 'hehehehehehe'
	req.Header.Set("Cookie", fmt.Sprintf("session=%s", sessionValue))
	resp, err := client.Do(req)
	if err != nil {
		return FilePath(""), err
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return FilePath(""), err
	}

	path := fmt.Sprintf("day%d.txt", today)
	f, err := os.Create(path)
	if err != nil {
		return FilePath(""), err
	}
	defer f.Close()

	_, err = f.Write(data)
	if err != nil {
		return FilePath(""), err
	}

	return FilePath(path), nil
}

:::

Go

Linear programming all over again! I solved part 1 with a BFS like many. But for part 2 a proper solver was needed. I'm terrible at LP so I tried to use a simplex implementation, but it was not enough to find a solution, Z3 was needed but I couldn't bother to install anything so... screw it.

Here is my proposal part 2 doesn't work but the modelisation is interesting enough that it is worth posting:

::: spoiler day10.go

package main

import (
	"aoc/utils"
	"fmt"
	"math"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"sync"

	"gonum.org/v1/gonum/mat"
	"gonum.org/v1/gonum/optimize/convex/lp"
)

type button []int8

type problem struct {
	want                int
	buttons             []button
	joltageRequirements []int8
}

func parseLights(lights string) (want int) {
	want = 0
	for idx, ch := range lights {
		if ch == '#' {
			want += (1 << idx)
		}
	}
	return want
}

func parseProblem(line string) problem {
	reg := regexp.MustCompile(`\[([.#]+)\] ((?:(?:\([^)]+\))\s*)+) {([^}]+)}`)
	matches := reg.FindStringSubmatch(line)

	lights := matches[1]
	buttonString := matches[2]
	joltage := matches[3]

	buttonString = string(slices.DeleteFunc(
		[]rune(buttonString),
		func(ch rune) bool {
			return ch == '(' || ch == ')'
		}))

	buttons := []button{}
	for switchString := range strings.SplitSeq(buttonString, " ") {
		switches := []int8{}
		for sw := range strings.SplitSeq(switchString, ",") {
			val, _ := strconv.Atoi(sw)
			switches = append(switches, int8(val))
		}
		buttons = append(buttons, switches)
	}

	joltageRequirements := []int8{}
	for req := range strings.SplitSeq(joltage, ",") {
		val, _ := strconv.Atoi(req)
		joltageRequirements = append(joltageRequirements, int8(val))
	}

	return problem{
		want:                parseLights(lights),
		buttons:             buttons,
		joltageRequirements: joltageRequirements,
	}
}

func getProblemChannel(input chan string) chan problem {
	ch := make(chan problem, cap(input))
	go func() {
		for line := range input {
			ch <- parseProblem(line)
		}
		close(ch)
	}()
	return ch
}

func (b button) value() (val int) {
	val = 0
	for _, sw := range b {
		val += 1 << sw
	}
	return val
}

func bfs(goal int, buttons []button) int {
	type bfsState struct {
		epoch int8
		value int
	}

	visited := []int{0}
	queue := []bfsState{}

	for _, bt := range buttons {
		value := bt.value()
		queue = append(queue, bfsState{epoch: 1, value: value})
		visited = append(visited, value)
	}

	for len(queue) > 0 {
		state := queue[0]
		queue = queue[1:]

		if state.value == goal {
			return int(state.epoch)
		}

		for _, bt := range buttons {
			nextValue := state.value ^ bt.value()
			if !slices.Contains(visited, nextValue) {
				visited = append(visited, nextValue)
				queue = append(queue, bfsState{
					epoch: state.epoch + 1,
					value: nextValue,
				})
			}
		}
	}

	return math.MaxInt
}

func (p problem) solve() int {
	depth := bfs(p.want, p.buttons)
	return int(depth)
}

func parallel(threadCount int, f func(thrId int)) {
	var wg sync.WaitGroup
	for id := range threadCount {
		wg.Go(func() {
			f(id)
		})
	}
	wg.Wait()
}

func stepOne(input chan string) (int, error) {
	sum := 0
	pbChan := getProblemChannel(input)
	for pb := range pbChan {
		depth := pb.solve()
		fmt.Println(depth)
		sum += depth
	}
	return sum, nil
}

type linearEquation struct {
	solution int
	coeffs   []int
}

func (p problem) toLinearEquations() linearProblem {
	equations := make([]linearEquation, len(p.joltageRequirements))
	varCount := len(p.buttons)

	for equIdx, solution := range p.joltageRequirements {
		equ := linearEquation{
			solution: int(solution),
			coeffs:   make([]int, varCount),
		}

		for btIdx, bt := range p.buttons {
			if slices.Contains(bt, int8(equIdx)) {
				equ.coeffs[btIdx] = 1
			}
		}
		equations[equIdx] = equ
	}
	return equations
}

type linearProblem []linearEquation

func (lp linearProblem) A() mat.Matrix {
	rows := len(lp)
	cols := len(lp[0].coeffs)
	data := make([]float64, rows*cols)

	for r := range rows {
		for c := range cols {
			data[r*cols+c] = float64(lp[r].coeffs[c])
		}
		fmt.Println(data[r*cols : (r+1)*cols])
	}

	return mat.NewDense(rows, cols, data)
}

func (lp linearProblem) c() []float64 {
	count := len(lp[0].coeffs)
	consts := make([]float64, count)
	for idx := range count {
		consts[idx] = 1.0
	}
	return consts
}

func (lp linearProblem) b() []float64 {
	bees := make([]float64, len(lp))
	for idx := range bees {
		bees[idx] = float64(lp[idx].solution)
	}
	return bees
}

func stepTwo(input chan string) (int, error) {
	sum := 0
	pbChan := getProblemChannel(input)
	for pb := range pbChan {
		lpb := pb.toLinearEquations()
		c := lpb.c()
		A := lpb.A()
		b := lpb.b()

		optF, _, err := lp.Simplex(c, A, b, 1.0, nil)
		if err != nil {
			fmt.Errorf("simplex error: %v\n", err)
		}
		fmt.Println(optF)
		sum += int(optF)
	}

	return sum, nil
}

func main() {
	input := utils.FilePath("day10.txt")
	utils.RunStep(utils.ONE, input, stepOne)
	utils.RunStep(utils.TWO, input, stepTwo)
}

:::

Go

Oh my god. I'm calling: tomorrow will probably be the day I fail lmao. It has been so hard to get a correct result. Then I spent a solid half hour to optimize the solution so that :

  • it terminates
  • it doesn't need 600GB of RAM

It seems that using a int8 to encode color instead of the default int64 was the right idea lmao. I'm also happy to have understood how to multithread a Go application and I'm totally bought by the simplicity. This language really helped me by abstracting everything in goroutines and call it a day.

I have also been able to create small goroutines which job was to generate values one by one and abstract the real control-flow in order not to allocate a whole big array at once, reducing further my memory footprint.

This is the second time in my life I want to say I loved using Go. Last time was when I waited to pretty print json files, my colleague told me how to write a wannabe-oneliner in Go to do it myself. Never looked back.

::: spoiler day09.go

package main

import (
	"aoc/utils"
	"math"
	"slices"
	"strconv"
	"strings"
	"sync"
)

type point struct {
	x, y int
}

func (p point) areaUntil(other point) int {
	dx := other.x - p.x
	if dx < 0 {
		dx = -dx
	}
	dy := other.y - p.y
	if dy < 0 {
		dy = -dy
	}

	return (dx + 1) * (dy + 1)
}

func parsePoint(line string) point {
	parts := strings.Split(line, ",")
	x, _ := strconv.Atoi(parts[0])
	y, _ := strconv.Atoi(parts[1])
	return point{x, y}
}

func getPointChannel(input chan string) chan point {
	ch := make(chan point, cap(input))

	go func() {
		for line := range input {
			ch <- parsePoint(line)
		}
		close(ch)
	}()

	return ch
}

func stepOne(input chan string) (int, error) {
	points := []point{}
	maxArea := math.MinInt

	for p := range getPointChannel(input) {
		points = append(points, p)

		if len(points) == 1 {
			continue
		}

		areas := make([]int, len(points))
		for idx, p2 := range points {
			areas[idx] = p.areaUntil(p2)
		}

		max := slices.Max(areas)
		if max > maxArea {
			maxArea = max
		}
	}

	return maxArea, nil
}

type color int8

const (
	white color = iota
	green
	red
)

type matrix struct {
	tiles  [][]color
	points []point
}

func min(x, y int) int {
	if x < y {
		return x
	} else {
		return y
	}
}

func max(x, y int) int {
	if x > y {
		return x
	} else {
		return y
	}
}

func (m *matrix) squareContainsWhiteTiles(sq segment) bool {
	xMin := min(sq[0].x, sq[1].x)
	yMin := min(sq[0].y, sq[1].y)
	xMax := max(sq[0].x, sq[1].x)
	yMax := max(sq[0].y, sq[1].y)

	for y := yMin; y < yMax; y++ {
		if m.tiles[y][xMin] == white || m.tiles[y][xMax] == white {
			return true
		}
	}

	for x := xMin; x < xMax; x++ {
		if m.tiles[yMin][x] == white || m.tiles[yMax][x] == white {
			return true
		}
	}

	return false
}

func (m *matrix) addReds() {
	for _, p := range m.points {
		m.addRed(p)
	}
}

func newMatrix(points []point) (m matrix) {
	coords := getMatrixSize(points)
	rows := coords[1] + 1
	cols := coords[0] + 1
	array := make([][]color, rows)
	for idx := range array {
		array[idx] = make([]color, cols)
	}
	m = matrix{
		tiles:  array,
		points: points,
	}

	return m
}

func (m *matrix) addRed(p point) {
	m.tiles[p.y][p.x] = red
}

func (m *matrix) makeBorders() {
	for i := 0; i < len(m.points)-1; i++ {
		m.makeBorder(m.points[i], m.points[i+1])
	}
	m.makeBorder(m.points[len(m.points)-1], m.points[0])
}

func (m *matrix) makeBorder(p1 point, p2 point) {
	if p1.x == p2.x {
		m.makeVerticalBorder(p1.x, p1.y, p2.y)
	} else {
		m.makeHorizontalBorder(p1.x, p2.x, p1.y)
	}
}

func (m *matrix) makeHorizontalBorder(x1, x2, y int) {
	if x1 > x2 {
		tmp := x1
		x1 = x2
		x2 = tmp
	}

	for i := x1; i <= x2; i++ {
		if m.tiles[y][i] == white {
			m.tiles[y][i] = green
		}
	}
}

func (m *matrix) makeVerticalBorder(x, y1, y2 int) {
	if y1 > y2 {
		tmp := y1
		y1 = y2
		y2 = tmp
	}

	for i := y1; i <= y2; i++ {
		if m.tiles[i][x] == white {
			m.tiles[i][x] = green
		}
	}
}

func (m *matrix) makeGreens() {
	m.makeBorders()

	iterCount := len(m.tiles) / MagicThreadCount

	parallel(func(thrId int) {
		for i := range iterCount {
			row := m.tiles[iterCount*thrId+i]
			inside := false
			lastWasWhite := false

			for idx, cell := range row {
				switch cell {
				case white:
					if inside {
						row[idx] = green
					}
				default:
					if lastWasWhite {
						inside = !inside
					}
				}
				lastWasWhite = cell == white
			}
		}
	})
}

func getMatrixSize(points []point) [2]int {
	var x, y int
	for _, p := range points {
		if p.x > x {
			x = p.x
		}
		if p.y > y {
			y = p.y
		}
	}
	return [2]int{x, y}
}

func (m *matrix) String() string {
	iterCount := len(m.points) / MagicThreadCount
	lines := make([]string, len(m.tiles))

	parallel(func(thrId int) {
		for i := range iterCount {
			row := m.tiles[iterCount*thrId+i]
			runes := make([]rune, len(row))
			for idx, col := range row {
				switch col {
				case white:
					runes[idx] = '.'
				case green:
					runes[idx] = 'X'
				case red:
					runes[idx] = '#'
				}
			}
			lines[iterCount*thrId+i] = string(runes)
		}
	})

	return strings.Join(lines, "\n")
}

type segment [2]point

func newSegment(p1, p2 point) segment {
	seg := [2]point{p1, p2}
	return seg
}

func (m *matrix) allValidSquaresWith(p1 point) chan segment {
	ch := make(chan segment)

	go func() {
		for _, p2 := range m.points {
			seg := newSegment(p1, p2)
			if p2 != p1 {
				ch <- seg
			}
		}
		close(ch)
	}()

	return ch
}

// 495 == 55 * 9
// 495 == 99 * 5
// 495 == 165 * 3
var MagicThreadCount = 9

func parallel(f func(int)) {
	var wg sync.WaitGroup
	for thrId := range MagicThreadCount {
		wg.Go(func() {
			f(thrId)
		})
	}

	wg.Wait()
}

func stepTwo(input chan string) (int, error) {
	points := []point{}
	for p := range getPointChannel(input) {
		points = append(points, p)
	}
	m := newMatrix(points)
	m.addReds()
	m.makeGreens()

	iterCount := len(m.points) / MagicThreadCount
	boys := make([]int, MagicThreadCount)

	parallel(func(thrId int) {
		largestBoy := math.MinInt
		for i := range iterCount {
			p := m.points[iterCount*thrId+i]

			squareCh := m.allValidSquaresWith(p)
			for sq := range squareCh {
				area := sq[0].areaUntil(sq[1])
				if area > largestBoy && !m.squareContainsWhiteTiles(sq) {
					largestBoy = area
				}
			}
		}
		boys[thrId] = largestBoy
	})

	return slices.Max(boys), nil
}

func main() {
	input := utils.FilePath("day09.txt")
	utils.RunStep(utils.ONE, input, stepOne)
	utils.RunStep(utils.TWO, input, stepTwo)
}

:::

Go

God damn it, I thought I would never see the end of part 1. I had a hard time finding a good representation and then I failed at not eliminating valid distances etc. The code is quite messy, but I did it!!!

::: spoiler day08.go

package main

import (
	"aoc/utils"
	"cmp"
	"math"
	"slices"
	"strconv"
	"strings"
)

type pos3D struct {
	x, y, z int
}

func (p pos3D) Compare(other pos3D) int {
	dx := cmp.Compare(p.x, other.x)
	if dx != 0 {
		return dx
	}

	dy := cmp.Compare(p.y, other.y)
	if dy != 0 {
		return dy
	}

	return cmp.Compare(p.z, other.z)
}

func (p pos3D) distance(other pos3D) float64 {
	dx := float64(other.x - p.x)
	dy := float64(other.y - p.y)
	dz := float64(other.z - p.z)

	d2 := math.Pow(dx, 2.0) + math.Pow(dy, 2.0) + math.Pow(dz, 2.0)
	return math.Sqrt(d2)
}

func getPosChannel(input chan string) chan pos3D {
	ch := make(chan pos3D, cap(input))

	go func() {
		for line := range input {
			parts := strings.Split(line, ",")
			x, _ := strconv.Atoi(parts[0])
			y, _ := strconv.Atoi(parts[1])
			z, _ := strconv.Atoi(parts[2])
			ch <- pos3D{x, y, z}
		}
		close(ch)
	}()

	return ch
}

type circuits struct {
	circuits map[pos3D]int
	nextID   int
}

func (cc *circuits) newCircuit() (id int) {
	id = cc.nextID
	cc.nextID++
	return id
}

func (cc *circuits) mergeCircuits(id1, id2 int) {
	for p, id := range cc.circuits {
		if id == id2 {
			cc.circuits[p] = id1
		}
	}
}

func createSingletonCircuits(points []pos3D) (cc circuits) {
	cc.circuits = make(map[pos3D]int)
	for _, p := range points {
		id := cc.newCircuit()
		cc.circuits[p] = id
	}
	return cc
}

func (cc circuits) reverseMap() (m map[int][]pos3D) {
	m = make(map[int][]pos3D)

	for p, id := range cc.circuits {
		if _, ok := m[id]; !ok {
			m[id] = []pos3D{}
		}

		m[id] = append(m[id], p)
	}

	return m
}

func (cc circuits) sizeMap() map[int]int {
	circuitSizeMap := make(map[int]int)
	for _, id := range cc.circuits {
		circuitSizeMap[id]++
	}
	return circuitSizeMap
}

type targetedDistance struct {
	distance float64
	p1, p2   pos3D
}

func (p pos3D) distanceWithAll(
	points []pos3D,
	alreadyVisited *map[pos3D]any,
) (tds []targetedDistance) {
	tds = []targetedDistance{}
	for _, op := range points {
		if _, ok := (*alreadyVisited)[op]; ok {
			continue
		}

		var td targetedDistance
		td.distance = math.MaxFloat64
		td.p1 = p
		d := p.distance(op)
		if d < td.distance {
			td.distance = d
			td.p2 = op
		}
		tds = append(tds, td)
	}
	return tds
}

type pointPair [2]pos3D

func (pp pointPair) equals(other pointPair) bool {
	pp1 := pp[0]
	pp2 := pp[1]
	o1 := other[0]
	o2 := other[1]
	return (pp1 == o1 && pp2 == o2) || (pp1 == o2 && pp2 == o1)
}

var IterationCount = 1000

func stepOne(input chan string) (int, error) {
	ch := getPosChannel(input)

	points := []pos3D{}
	for p := range ch {
		points = append(points, p)
	}
	slices.SortFunc(points, pos3D.Compare)
	cc := createSingletonCircuits(points)

	alreadyVisited := make(map[pos3D]any)
	tds := []targetedDistance{}
	for _, p := range points {
		alreadyVisited[p] = nil
		dsts := p.distanceWithAll(points, &alreadyVisited)
		tds = append(tds, dsts...)
	}

	slices.SortFunc(tds, func(a, b targetedDistance) int {
		return cmp.Compare(a.distance, b.distance)
	})

	for idx := range IterationCount {
		td := tds[idx]
		cc.mergeCircuits(cc.circuits[td.p1], cc.circuits[td.p2])
	}

	circuitSizeMap := cc.sizeMap()

	circuitSizes := []int{}
	for _, v := range circuitSizeMap {
		circuitSizes = append(circuitSizes, v)
	}
	slices.Sort(circuitSizes)
	largestThree := circuitSizes[len(circuitSizes)-3:]

	product := 1
	for _, v := range largestThree {
		product *= v
	}

	return product, nil
}

func stepTwo(input chan string) (int, error) {
	ch := getPosChannel(input)

	points := []pos3D{}
	for p := range ch {
		points = append(points, p)
	}
	slices.SortFunc(points, pos3D.Compare)
	cc := createSingletonCircuits(points)

	alreadyVisited := make(map[pos3D]any)
	tds := []targetedDistance{}
	for _, p := range points {
		alreadyVisited[p] = nil
		dsts := p.distanceWithAll(points, &alreadyVisited)
		tds = append(tds, dsts...)
	}

	slices.SortFunc(tds, func(a, b targetedDistance) int {
		return cmp.Compare(a.distance, b.distance)
	})

	idx := 0
	var lastConnection pointPair

	for {
		td := tds[idx]
		idx++
		cc.mergeCircuits(cc.circuits[td.p1], cc.circuits[td.p2])

		circuitSizeMap := cc.sizeMap()
		if len(circuitSizeMap) == 1 {
			lastConnection = pointPair{td.p1, td.p2}
			break
		}
	}

	return lastConnection[0].x * lastConnection[1].x, nil
}

func main() {
	inputFile := utils.FilePath("day08.txt")
	utils.RunStep(utils.ONE, inputFile, stepOne)
	utils.RunStep(utils.TWO, inputFile, stepTwo)
}

:::