

uhh, how do these wheels turn exactly?


uhh, how do these wheels turn exactly?


Very messy bruteforce.
I’ve had some problems with parsing in part 2 - I didn’t account for double digit numbers before dna sequences and that caused my code to work on example, but silently fail only on the real input. I’ve figured it out after ~30 minutes with some external help.
Part 3 runs in 700ms - not great, but not too bad either.
proc similarity(a, b: string): int =
for i, c in a:
if c == b[i]: inc result
proc solve_part1*(input: string): Solution =
var sim: seq[int]
var dnaList: seq[string]
for line in input.splitLines():
dnaList.add line[2..^1]
for i in 0 .. dnaList.high:
for j in i+1 .. dnaList.high:
let s = similarity(dnaList[i], dnaList[j])
sim.add s
sim.sort()
result := sim[^2] * sim[^1]
proc parentTest(ch, p1, p2: string): bool =
for i, c in ch:
if (c != p1[i]) and (c != p2[i]): return false
true
proc simTable(dnaList: seq[string]): seq[seq[int]] =
result = newSeqWith(dnaList.len, newseq[int](dnaList.len))
for i in 0 .. dnaList.high:
for j in i+1 .. dnaList.high:
let s = similarity(dnaList[i], dnaList[j])
result[i][j] = s
result[j][i] = s
proc solve_part2*(input: string): Solution =
var dnaList: seq[string]
for line in input.splitLines():
dnaList.add line.split(':')[1]
let sim = simTable(dnaList)
var indices = toseq(0..dnaList.high)
for i, childDna in dnaList:
var indices = indices
indices.del i
block doTest:
for k in 0 .. indices.high:
for j in k+1 .. indices.high:
let p1 = indices[k]
let p2 = indices[j]
if parentTest(childDna, dnaList[p1], dnaList[p2]):
result.intVal += sim[i][p1] * sim[i][p2]
break doTest
proc solve_part3*(input: string): Solution =
var dnaList: seq[string]
for line in input.splitLines():
dnaList.add line.split(':')[1]
var families: seq[set[int16]]
var indices = toseq(0..dnaList.high)
for ch, childDna in dnaList:
var indices = indices
indices.del ch
block doTest:
for k in 0 .. indices.high:
for j in k+1 .. indices.high:
let p1 = indices[k]
let p2 = indices[j]
if parentTest(childDna, dnaList[p1], dnaList[p2]):
families.add {ch.int16, p1.int16, p2.int16}
break doTest
var combined: seq[set[int16]]
while families.len > 0:
combined.add families.pop()
var i = 0
while i <= families.high:
if (combined[^1] * families[i]).len > 0:
combined[^1] = combined[^1] + families[i]
families.del i
i = 0
else: inc i
let maxInd = combined.mapIt(it.len).maxIndex
result := combined[maxInd].toseq.mapIt(it.int+1).sum()
Full solution at Codeberg: solution.nim


Part 2 - I just really didn’t want to think that day. So when puzzle asked me to check if lines intersect - I wrote the intersection checking solution with 2D points.
Part 3 is geometry + bruteforce.
proc solve_part1*(input: string): Solution =
let pins = input.split(',').mapIt(parseInt(it))
for i in 0 ..< pins.high:
let d = abs(pins[i] - pins[i+1])
if d == 16: inc result.intVal
proc ccw(A,B,C: Vec2): bool = (C.y-A.y) * (B.x-A.x) > (B.y-A.y) * (C.x-A.x)
proc isIntersection(A,B,C,D: Vec2): bool = ccw(A,C,D) != ccw(B,C,D) and ccw(A,B,C) != ccw(A,B,D)
proc solve_part2*(input: string): Solution =
const two_pi = PI * 2
const pin_count = 256
var pins: array[pin_count, Vec2]
for i in 0 ..< pin_count:
let angle = two_pi * (i / pin_count)
let point: Vec2 = (cos(angle), sin(angle))
pins[i] = point
let inst = input.split(',').mapIt(parseInt(it))
var lines: seq[(Vec2, Vec2)]
for i in 0 ..< inst.high:
let A = pins[inst[i]-1]
let B = pins[inst[i+1]-1]
for (C, D) in lines:
if isIntersection(A,B,C,D):
inc result.intVal
lines.add shortenSegment(A, B, 0.0001)
proc solve_part3*(input: string): Solution =
const two_pi = PI * 2
const pin_count = 256
var pins: array[pin_count, Vec2]
for i in 0 ..< pin_count:
let angle = two_pi * (i / pin_count)
let point: Vec2 = (cos(angle), sin(angle))
pins[i] = point
let inst = input.split(',').mapIt(parseInt(it))
var lines: seq[(Vec2, Vec2)]
for i in 0 ..< inst.high:
let A = pins[inst[i]-1]
let B = pins[inst[i+1]-1]
lines.add shortenSegment(A, B, 0.0001)
var bestSum = 0
for i in 0 ..< pin_count:
for j in i+1 ..< pin_count:
let A = pins[i]
let B = pins[j]
var sum = 0
for (C, D) in lines:
if isIntersection(A,B,C,D): inc sum
if sum > bestSum: bestSum = sum
result := bestSum
Full solution at Codeberg: solution.nim


Part 3 is a recursive solution with caching (memoization).
proc isValid(name: string, rules: Table[char, set[char]]): bool =
for i in 0 ..< name.high:
if name[i+1] notin rules[name[i]]: return false
true
proc allNames(prefix: string, rules: Table[char, set[char]], range: Slice[int]): int =
var memo {.global.}: Table[(int, char), int]
if prefix.len >= range.b: return
if (prefix.len, prefix[^1]) in memo: return memo[(prefix.len, prefix[^1])]
for ch in rules.getOrDefault(prefix[^1]):
if prefix.len + 1 >= range.a:
inc result
result += allNames(prefix & ch, rules, range)
memo[(prefix.len, prefix[^1])] = result
proc solve_part1*(input: string): Solution =
let (names, rules) = parseInput(input)
for name in names:
if name.isValid(rules):
return Solution(kind: skString, strVal: name)
proc solve_part2*(input: string): Solution =
let (names, rules) = parseInput(input)
for ni, name in names:
if name.isValid(rules):
result.intVal += ni + 1
proc solve_part3*(input: string): Solution =
let (names, rules) = parseInput(input)
var seen: seq[string]
for name in names:
if not name.isValid(rules): continue
if seen.anyIt(name.startsWith it): continue
result.intVal += allNames(name, rules, 7..11)
seen.add name
Full solution at Codeberg: solution.nim


parts 1 and 2 - easy
For part 3 - When I first looked at the example input - it seemed a bit daunting to solve. But then I had a hunch and decided to check the real input and turns out - I was right! The real input is easier to solve because it’s longer than 1000 chars.
This means that there is only 3 possible configurations we care about in repeated input: leftmost section, rightmost section and 998 identical sections in the middle. We solve each individually and sum them.
Another trick I used is looking up mentors with modulo to avoid copying the input.
proc solve_part1*(input: string): Solution =
var mentors: CountTable[char]
for c in input:
if c notin {'a','A'}: continue
if c.isUpperAscii: mentors.inc c
else:
result.intVal += mentors.getOrDefault(c.toUpperAscii)
proc solve_part2*(input: string): Solution =
var mentors: CountTable[char]
for c in input:
if c.isUpperAscii: mentors.inc c
else:
result.intVal += mentors.getOrDefault(c.toUpperAscii)
proc solve_part3*(input: string): Solution =
var mentors: Table[char, seq[int]]
for index in -1000 ..< input.len + 1000:
let mi = index.euclMod input.len
if input[mi].isLowerAscii: continue
let lower = input[mi].toLowerAscii
if mentors.hasKeyOrPut(lower, @[index]):
mentors[lower].add index
var most, first, last = 0
for ci, ch in input:
if ch.isUpperAscii: continue
for mi in mentors[ch]:
let dist = abs(mi - ci)
if dist <= 1000:
inc most
if mi >= 0: inc first
if mi <= input.high: inc last
result := first + (most * 998) + last
Full solution at Codeberg: solution.nim


Nothing fancy. Simple iterative tree construction and sort, using the std/algorithm and a custom < operator on types.
type
LeafNode = ref object
value: int
Node = ref object
value: int
left, right: LeafNode
center: Node
Sword = object
id, quality: int
levels: seq[int]
fishbone: Node
proc add(node: var Node, val: int) =
var curNode = node
while not curNode.isNil:
if val < curNode.value and curNode.left.isNil:
curNode.left = LeafNode(value: val)
return
elif val > curNode.value and curNode.right.isNil:
curNode.right = LeafNode(value: val)
return
elif curNode.center.isNil:
curNode.center = Node(value: val)
return
else: curNode = curNode.center
node = Node(value: val)
proc calcQuality(sword: Sword): int =
var res = ""
var curNode = sword.fishbone
while not curNode.isNil:
res &= $curNode.value
curNode = curNode.center
return parseInt(res)
proc getLevels(s: Sword): seq[int] =
var curNode = s.fishbone
while not curNode.isNil:
var strVal = ""
strVal &= (if curNode.left.isNil: "" else: $curNode.left.value)
strVal &= $curNode.value
strVal &= (if curNode.right.isNil: "" else: $curNode.right.value)
result.add parseInt(strVal)
curNode = curNode.center
proc `<`(s1, s2: seq[int]): bool =
for i in 0..min(s1.high, s2.high):
if s1[i] != s2[i]: return s1[i] < s2[i]
s1.len < s2.len
proc `<`(s1, s2: Sword): bool =
if s1.quality != s2.quality: s1.quality < s2.quality
elif s1.levels != s2.levels: s1.levels < s2.levels
else: s1.id < s2.id
proc parseSwords(input: string): seq[Sword] =
for line in input.splitLines:
let numbers = line.split({':',','}).mapIt(parseInt(it))
var node= Node(value: numbers[1])
for num in numbers.toOpenArray(2, numbers.high):
node.add num
result &= Sword(id: numbers[0], fishbone: node)
proc solve_part1*(input: string): Solution =
let swords = parseSwords(input)
result := swords[0].calcQuality()
proc solve_part2*(input: string): Solution =
let qualities = parseSwords(input).mapIt(it.calcQuality())
result := qualities.max - qualities.min
proc solve_part3*(input: string): Solution =
var swords = parseSwords(input)
for i in 0..swords.high:
swords[i].levels = swords[i].getLevels()
swords[i].quality = swords[i].calcQuality()
swords.sort(Descending)
for pos, id in swords.mapit(it.id):
result.intVal += (pos+1) * id
Full solution at Codeberg: solution.nim


For part 3 I parse gears as tuples, with regular gears having same value on both ends e.g.
3|5 -> (3, 5)
3 -> (3, 3)
proc parseGears(input: string): seq[int] =
for line in input.splitLines():
result.add parseInt(line)
proc parseNestedGears(input: string): seq[(int, int)] =
for line in input.splitLines():
let nested = line.split('|').mapIt(it.parseInt)
result.add:
if nested.len == 1: (nested[0], nested[0])
else: (nested[0], nested[1])
proc solve_part1*(input: string): Solution =
let gears = parseGears(input)
result := 2025 * gears[0] div gears[^1]
proc solve_part2*(input: string): Solution =
let gears = parseGears(input)
result := ceil(10000000000000'f64 / (gears[0] / gears[^1])).int
proc solve_part3*(input: string): Solution =
let gears = parseNestedGears(input)
let ratios = (0..gears.high-1).mapIt(gears[it][1] / gears[it+1][0])
result := int(100 * ratios.prod)
Full solution at Codeberg: solution.nim


Reading this day’s quest I was at first a bit confused what it asks me to calculate. Took me about a minute to realize that most of descriptions are not important and actual calculations for each part are very simple:
part 1 wants sum of each unique crate size
part 2 is same but only 20 smallest
part 3 is max count, because you can always put most crates in one large set and you only need 1 extra set per duplicate crate
proc solve_part1*(input: string): Solution =
var seen: set[int16]
for num in input.split(','):
let num = parseInt(num).int16
if num in seen: continue
else:
seen.incl num
result.intVal += num
proc solve_part2*(input: string): Solution =
var set = input.split(',').mapIt(parseInt(it).uint16)
set.sort()
result := set.deduplicate(isSorted=true)[0..<20].sum()
proc solve_part3*(input: string): Solution =
var cnt = input.split(',').toCountTable()
result := cnt.largest.val
Full solution at Codeberg: solution.nim


For quest 2, I decided to implement my own Complex type and operators, because I expected parts 2 and 3 to have something unconventional, but alas it’s just a regular Mandelbrot fractal.
Nim again, Nim forever:
type Complex = tuple[x,y: int]
proc `$`(c: Complex): string = &"[{c.x},{c.y}]"
proc `+`(a,b: Complex): Complex = (a.x+b.x, a.y+b.y)
proc `-`(a,b: Complex): Complex = (a.x-b.x, a.y-b.y)
proc `*`(a,b: Complex): Complex = (a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x)
proc `/`(a,b: Complex): Complex = (a.x div b.x, a.y div b.y)
proc `/`(a: Complex, b: int): Complex = (a.x div b, a.y div b)
proc parseInput(input: string): Complex =
let parts = input.split({'=','[',',',']'})
(parseInt(parts[2]), parseInt(parts[3]))
proc isStable(point: Complex, iter: int): bool =
var num: Complex
for _ in 1..iter:
num = (num * num) / 100_000 + point
if num.x notin -1_000_000 .. 1_000_000 or
num.y notin -1_000_000 .. 1_000_000:
return false
true
proc solve_part1*(input: string): Solution =
let start = parseInput(input)
var point: Complex
for _ in 1..3:
point = (point * point) / 10 + start
result := $point
proc solve_part2*(input: string): Solution =
let start = parseInput(input)
for y in 0..100:
for x in 0..100:
let point: Complex = (start.x + 10 * x, start.y + 10 * y)
if point.isStable(iter=100): inc result.intVal
proc solve_part3*(input: string): Solution =
let start = parseInput(input)
for y in 0..1000:
for x in 0..1000:
let point: Complex = (start.x + x, start.y + y)
if point.isStable(iter=100): inc result.intVal
Full solution at Codeberg: solution.nim


So far I really like the difficulty level of EC, compared to AOC.
Quest 1 is pretty straightforward:
0..limit e.g. min(limit, max(0, index))index mod names.len until done =)My solution in Nim:
proc parseInput(input: string): tuple[names: seq[string], values: seq[int]] =
let lines = input.splitLines()
result.names = lines[0].split(',')
let values = lines[2].multiReplace({"R":"","L":"-"})
for num in values.split(','):
result.values.add parseInt(num)
proc solve_part1*(input: string): Solution =
let (names, values) = parseInput(input)
var pos = 0
for value in values:
pos = min(names.high, max(0, pos + value))
result := names[pos]
proc solve_part2*(input: string): Solution =
let (names, values) = parseInput(input)
let pos = values.sum()
result := names[pos.euclMod(names.len)]
proc solve_part3*(input: string): Solution =
var (names, values) = parseInput(input)
for value in values:
swap(names[0], names[euclMod(value, names.len)])
result := names[0]
Full solution at Codeberg: solution.nim
If something is a propaganda it does not automatically mean it’s false.
propaganda
n 1: information that is spread for the purpose of promoting some cause
If you are me, there is no brain space for remembering new commands. I can already barely hold on to few dozens that I use often. And occasionally when I need “that one that does that niche thing… how was it?” program - I just sit there sifting through logs for couple minutes.
Today it was od (tbh it’s od almost half the time; not really the best name to memorize (I really need to make a note or something, so I stop forgetting it, lol))
Also, for this reason I went to great lengths to keep my ~/.zsh_history protected from being randomly deleted/overwritten by mistake, as it happened a couple of times. Currently it’s sitting at around 30_000 lines, oldest command is 2 years old.
I think it’s ok to add this in a personal .zshrc, not on a distro level:
If it breaks something - I’d probably know why and can easily fix it by removing alias/calling cat directly.
Also, scripts almost always use bash or sh in shebang, not zsh. So it only triggers if I type cat in terminal.
history | fzfalias cat="bat --plain --theme=gruvbox-dark"Same thing. Coding agents just automate the third step and waste even more money and energy.
Vibecoding:


One of coolest Nim projects. I’ve had a lot of fun just messing around in it.
I’ve looked it up and anyone calling this “the most complex …” clearly have never played 80s board games e.g. The Campaign for North Africa (takes more than 1000 hours to finish)