🍺 Beer Advent of Code 💻

What's better than code? Beer!
What's better than beer? Beer and code!
How to improve on even that? Getting both for free!

About

This year (2021) I've gotten a wonderful gift from my wonderful friends, a Beer Advent. And since I also enjoy me some code, I figured I would combine it with the advent of code. So every day this december I'll drink a beer, solve an advent of code puzzle, and write about both!

Day 1

It's a late wednesday evening on 2021-12-01, and I'm heading north with the Nørd Bliss, how appropriate. Taking a sip I enjoy the cold IPA with hints of citrus and an overall refreshing taste. Meanwhile I notice I'm being transported to the chaotic world of the Christmas Elves 🧝, where every day there's a new problem to fixchallenge to overcome. Today I find myself in a submarine, searching for the sleigh keys that have been accidentally dropped in the ocean.

The first thing I need to do is find out how quickly the depth increases by reading the sonar scan measurements and figuring out how many times the depth increases compared to the previous measure. I'm given a list of measurements for which I quickly open up dev-tools to place in an array measures = document.querySelector('pre').textContent.split('\n'). Now I just need to filter for those whose previous measure was lower measures.filter((e,i) => +e > +measures[i-1]).length which for me is 1446, earning me one gold star⭐!

However there was too much noise in the data and I need a more robust way of figuring out how often the depth increases. So what I have to do instead is consider sums of a three-measurement sliding window, counting the number of times the sums of the sliding window increases. I've still got the measures so what I'll do is map it to the sliding window sum slidingMeasures = measures.map((e,i) => +e + +measures[i+1] + +measures[i+2]) and with this new list perform the same trick as before slidingMeasures.filter((e,i) => +e > +slidingMeasures[i-1]).length giving me 1486 and my second star⭐!

The beer is finished the puzzle too, good night for now and see you on day 2.

Day 2

The second of december and a beer that beckons to be remembered with their creepy face, it's Creepy kid. Even if I won't remember the face I'll definitely remember the taste, which is NOT to mine. To me it tastes waaaay too much like coffee, of which I'm already not particularly a fan. But I'll soldier on and make it soldier, hopefully the Christmas Elves can distract my mind enough to make my tastebuds blind to stuff.

Considering I apparently have to figure out how to pilot a submarine it may just do. But first I have to find out where the currently programmed route is taking us. The route are instructions telling the submarine to go forward/up/down x units. Of course I start with placing all instructions in an array again ins = document.querySelector('pre').textContent.split('\n'). And then just create h = d = 0 to track our horizontal position and our depth, loop over the ins and update the position.

ins.forEach(e => {
 let [dir,x] = e.split` `
 if (dir=='forward') h += +x
 if (dir=='up') d += -x
 if (dir=='down') d += +x
})

Giving a horizontal position of 1980 and a depth of 866, making the solution 1714680 and earning another gold star⭐!

But oh no! my interpretation of the instructions was wrong, the up/down affect our aim. So let's reset our positions h=d=a=0 and re-write the loop to factor in the aim.

ins.forEach(e => {
 let [dir,x] = e.split` `
 if (dir == 'forward') {
  h += +x
  d += a * x
 }
 if (dir == 'up') a += -x
 if (dir == 'down') a += +x
})

Keeping our horizontal position on 1980 but changing our depth to 991459 and giving me the second gold star of the day⭐!

I still need to stomach a few swigs of luke-warm Creepy kid, let us hope tomorrow's beer doesn't taste like shit.

Day 3

It's Friday so you know I gotta get down while the beat gose on. Right after opening this fruited gose a pleasant aroma fills my nostrils making me think I just took a pear pie out of the oven. Taking a sip the sweetness gives way for a more sour sensation but nonetheless very tasty leaving a spicy trace of cinnamon.

While enjoying the refreshment I'm brought back to virtual reality by an odd creaking noise coming from the submarine. Obviously I request a diagnostic report to check the power consumption. However I need to extract the gamma and epsilon rate (constructed from respectively the most and least common bits in corresponding positions) from the report and multiply them to find the power consumption. Of course I boot up dev-tools on the report and collect the diagnostics ns = document.querySelector('pre').textContent.split('\n'). I'll also convert every element into an array ns = ns.map(e => [...e]) to easily sum each position individually ns = ns.reduce((a,b) => a.map((e,i) => +e + +b[i])). (Oh and of course I remembered to ns.pop() the empty element before wasting half an hour trying to figure out why the abovementioned code didn't work.) Now I've got that simply divide by the amount of diagnostics (n) and round it gamma = ns.map(e => Math.round(e/n)).join(''). Now to get the epsilon simply invert the gamma epsilon = gamma.replace(/./g, e => +e?0:1) and finally (convert them to numbers and) multiply them ('0b'+gamma)*('0b'+epsilon). Which results to 841526 and another gold star⭐!

Now I know our power consumption is a whopping 841526 (kWh? hamsters in a wheel? Rudolph noses in front of solar panels? who knows) I should verify our life support rating which can be determined by multiplying the oxygen generator rating by the CO2 scrubber rating. Considering the lovely fruited gose gone and went already it's a good thing I don't need to factor in any H2O. Extracting the O2/CO2 ratings is a bit more involved, but basically start with the diagnostics, loop over each bit positions and only keep the numbers that match the most/least common bit. Sadly enough it needs to match the most/least common bit of the remaining numbers, so I can't easily solve it with gamma or epsilon. Instead I'll loop (by doing a reduce) over the bits determining most/least common and filtering every pass.

O2 = [...ns[0]].reduce((acc,_,i) => {
  let b = acc.map(e => +e[i]).reduce((a,b) => a+b)
  b = Math.round(b/acc.length)
  return acc.filter(e=> e[i]==b)
}, ns)[0]

For the CO2 aside from negating the filter I also need to check if there's still any left (since the last number's bit will be most common).

CO2 = [...ns[0]].reduce((acc,_,i) => {
  let b = acc.map(e => +e[i]).reduce((a,b) => a+b)
  b = Math.round(b/acc.length)
  let rem = acc.filter(e => e[i]!=b)
  return rem[0] ? rem : acc
}, ns)[0]

Now finally multiplying them ('0b'+O2)*('0b'+CO2) gives me 4790390 and the sixth star⭐!

With my mind eased and my mouth pleased there's only my sleep left to be seized.

Day 4

Saturdays are for the boys, and you know it involves beer. Today we've got the Halcyon Days Porter on the menu. I must admit I'm not a beer connoisseur (and you might remember I'm not a fan of coffee) so when I read "packed with chocolate, coffee and dried fruit" I immediately got Creepy Kid flashbacks. However I'm pleasantly surprised, it's still not my choice of beer but it doesn't feel like a chore to drink. Perhaps I'm already refining my taste or maybe things just taste different 1.5km below sea-level. Or alternatively I'm just going bonkers, which would explain why I'm about to play bingo with a Giant Squid attached to the outside of Christmas Elves' submarine.

As any respectable submarine, the one I'm on currently has a bingo subsystem which generates a random order of numbers to draw and a random set of boards. Because I don't like to lose against a Giant Squid I'm going to be sneaky and determine which board will win in advance. I'll also need that winning board's score (which is the sum of unmarked numbers times the last called number.) This time I have to change my beloved numbers parsing a bit [ns,...boards] = document.querySelector('pre').innerText.replace(/\n$/,'').split(`\n\n`) remembering to remove the last LF. And also change the boards to an easier structure boards = boards.map(e => e.split`\n`.map(e => e.trim().split(/\s+/))). To find the board that wins I'll first create a function that checks if a board has won, assuming an X is used to mark a number hasWon = b => b.some(row => row.every(n => n[0]=='X'))|| b[0].some((_,i) => b.every(r => r[i][0]=='X')). Next I'll loop over the numbers (and convert them to a list first ns = ns.split`,`), marking the number on each board and check if any has won, if so, stopping the loop and calculating the score.

let winner
for(n of ns) {
  boards= boards.map(b => b.map(row => row.map(e => e == n ? 'X'+e : e)))
  winner= boards.find(hasWon)
  if (winner) break
}
score = n * winner.flat().filter(e => e[0] != 'X').reduce((a,b) => +a + +b)

Which for me (after some trial and error, remembering to reset n and boards between tries) gives me 14093 and a gold star⭐!

But on second thought it might be better to let the Giant Squid win, if it gets angry the whole submarine could be at it's mercy! So to be safe I'll pick the last winning board and just hope the Giant Squid doesn't get bored.

for(n of ns) {
  boards= boards.map(b => b.map(row => row.map(e => e == n ? 'X'+e : e)))
  boardsLeft= boards.filter( =>!hasWon(e))
  if (!boardsLeft[0]) break
  else boards = boardsLeft
}
score = n * boards[0].flat().filter(e => e[0] != 'X').reduce((a,b) => +a + +b)

Luckily I didn't have to change too much code, but I'm afraid of a bored Giant Squid. Finding the losing board took find loser: 2026.039794921875 ms which in this day and age is an eternity. But then again the Giant Squid has probably been living an eternity already so what's another eternity. All that matters now is that I've obtained the next gold star⭐!

Now with a game won and lost by having numbers crossed I'll see you all after morrow's dusk.

Day 5

Sunday the fifth and what a fun day it is, for not yet Santa Clause but Sinterklaas is bringing gifts. The present Sint has gotten for me is fermented not rotten you see. No facade straight from god, beyond holy water the blonde Halo lager. But saying it's The Mass for me might be akin to blasphemy. The lager is honestly not that great, just a normal beer to satiate. Sate the thirst but not the mental one, because first a puzzle must be done.

We're on the ocean floor with hydrothermal vents so hold unto your hats this'll be a rocky ride my gents. The vents are typically in a line, the task to find dangerous spots is mine. At how many spots do vents intersect? I'll start with lines = document.body.innerText. lines = lines.replace(/\n$/,'').split('\n') to have an array we can use again. lines = lines.map(line => line.split(' -> ').map(pos => pos.split(','))) still has diagonals in it. So to have diagonals be gone, we'll lines = lines.filter(([a, b]) => a[0]==b[0]||a[1]==b[1]). Rhyming in code isn't really my strength, but to solve it we'll need:

field = {}
lines.forEach(([from, to])=>{
  ;[x,X] = [from[0],to[0]].map(e=>+e).sort((a,b)=>a-b)
  ;[y,Y] = [from[1],to[1]].map(e=>+e).sort((a,b)=>a-b)
  incX = x != X
  for(; x<=X&&y<=Y; incX?x++:y++) {
    c = x+':'+y
    field[c] = 1+field[c]|0
  }
})
Object.values(field).filter(e=>e).length

Giving 5167 danger spots to avoid and earning another golden star from the void⭐!

Yet something's still amiss, there are more spots where the vents will hiss. In diagonals across the field so at those spots we'll also need to yield. Luckily the diagonals are at 45 degrees so fixing the code should be a breeze. Let's do the same as before except we'll filter no more. And add a branch in the loop to add diagonals in our group.

field = {}
lines.forEach(([from, to])=>{
 if(from[0]==to[0]||from[1]==to[1]) {  
  ;[x,X] = [from[0],to[0]].map(e=>+e).sort((a,b)=>a-b)
  ;[y,Y] = [from[1],to[1]].map(e=>+e).sort((a,b)=>a-b)
  incX = x != X
  for(; x<=X&&y<=Y; incX?x++:y++) {
   c = x+':'+y
   field[c] = 1+field[c]|0
  }
 } else {
  ;[x,y] = from.map(e=>+e)
  ;[X,Y] = to.map(e=>+e)
  xD = x<X?1:-1
  yD = y<Y?1:-1
  for(; xD>0?x<=X:X<=x; x+=xD,y+=yD) {
   c = x+':'+y
   field[c] = 1+field[c]|0
  }
 }
})
Object.values(field).filter(e=>e).length

A lot of dangerous spots now: 17604, but at least I'll have an increased golden star score⭐!

Although I'm secretly writing this a day late, I'll say we'll await the sixth day's fate.

Day 6

The first monday of the month as I was kindly reminded by the public warning sirens. And today we're seduced by the mermaid Dangerousse. Right off the bat it's the best looking beer so far... what can I say, I'm a simple man. However I'm not too sure what to think of the taste, it's a bit sweet but not a fruity sweet I was initially expecting. All in all it's okay but not great. While I'm being lured into the depths by the mermaid (and well, the general trajectory of the submarine) I see a massive school of glowing lanternfish swimming past.

Since I haven't got a lot to do I start wondering about how fast these lanternfish procreate to reach such large numbers. It seems every lanternfish creates a new lanternfish once every 7 days with new lanternfish taking 2 more days for their first cycle. The submarine can read my mind and automatically produces a list of the ages of several hundred nearby lanternfish for me. I wonder, given the current population how many lanternfish would there be in 80 days? Instead of trying to keep track of every individual fish I'll group them by how many days until they procreate.

population = document.body.innerText.split`,`.map(e=>+e).reduce((acc,e)=>{
  acc[e] = 1 + (acc[e]|0)
  return acc
}, {})

With that data I only have to loop 80 times updating the days until procreation and adding the new born lanternfish.

for(n=80;n--;) {
  newP = Object.entries(population).reduce((acc, e) => {
    acc[e[0]-1] = e[1]
    return acc
  }, {})
  if(born = newP[-1]) {
    delete newP[-1]
    newP[8] = born
    newP[6] = (newP[6]|0) + born
  }
  population = newP
}
Object.values(population).reduce((a,b)=>a+b)

Giving me 354564 lanternfish so quite some glowing effect, almost as much as my new earned golden star⭐!

So what happens if the lanternfish live forever and have unlimited food and space? How many lanternfish would there be after 256 days? Luckily this isn't too hard, I just need to switch from normal numbers to BigInt

population = document.body.innerText.split`,`.map(e=>+e).reduce((acc,e)=>{
  acc[e] = 1n + BigInt(acc[e]||0n)
  return acc
}, {})

for(n=256;n--;) {
  newP = Object.entries(population).reduce((acc, e) => {
    acc[e[0]-1] = e[1]
    return acc
  }, {})
  if(born = newP[-1]) {
    delete newP[-1]
    newP[8] = born
    newP[6] = (newP[6]||0n) + born
  }
  population = newP
}
Object.values(population).reduce((a,b)=>a+b)

Giving me 1609058859115n lanternfish, definitely glowing way more now than my 12 golden stars combined⭐!

Even though the mermaid tried to seduce, instead I calculated out how fast lanternfish reproduce. Let's sleep and find out what tommorrow will introduce.

Day 7

It's the end of the first week, let's have a round of applause for ourselves (both the writer and the reader) for making it so far👏👏! As a treat the box has provided the Hazy Pale Ale whose name (and packaging colour) allude to the hop's cousin of the Cannabaceae family. But officially it's the appearance that's hazy, I can't check it for you as I drink straight from the container... sue me. It's very fruity and to me also quite herby, which makes it really soft and easily drinkable. In fact it's already almost finished, which is a good thing because I can't permit too many distrections since a giant whale decided the submarine would be its next meal!

Unfortunately the whale is much faster than our submarine but fortunately a swarm of submarined crabs are willing to help by blasting a hole in the ocean floor to escape in the underground cave system. However all the crabs their horizontal position must be aligned to succesfully blast a large enough hole. I must help them figure out what the least amount of fuel is they need to all align horizontally. As always I start with collecting the current positions ns = document.body.innerText.split`,`.map(e=>+e). From these I'll want to find the median so I sort and pick the middle mid = ns.sort((a,b)=>a-b)[ns.length/2]. All that's left to do now is calculate how much each crab needs to travel and accumulate that ns.reduce((acc,e)=>acc+Math.abs(mid-e),0). Which is 345197 earning me another gold star⭐!

But the crabs don't seem interested in the solution. It turns out, contrary to popular belief and logic, crab submarines burn fuel at a linear rate, not a constant one. The first step costs 1, the second 2 etc. Of course you can imagine this complicates the problem somewhat. Luckily I've got a magic submarine to do our calculations so I don't have to be efficient and can just bruteforce my way out the whale's mouth. I'll check for each position how much the total fuel expenditure would be if the crabs move there. First I'll get the min = ns[0] and the max = ns[ns.length-1] and create all positions ps = [...Array(max-min)].map((_,i)=>i+min). And simply get the fuel expenditure per target position fs = ps.map(target => ns.map(e => Math.abs(target-e)).reduce((acc,n) => acc+(n*(n+1))/2, 0)). Getting the Math.min(...fs) is 96361606, I sure do hope crabs don't pay the same gas prices as us. Anyway we've succesfully blasted a hole and I've gained another gold star⭐!

Which concludes our first week with more adventure than I would normally seek. Less than three more weeks to go, I'll see you at day doubled-O.

Day 8

The start of the second week and definitely not ready to Call it a Day, Christmas depends on it! However IRL it's certainly Feierabend, so I've got my Feierabend-Bier in my hand and my warm slippers on my feet. The beer is decent, quite sweet and a bit bitter with an almost licorish aftertaste. While relaxing with the beer I notice the seven-segment number display in the submarine is malfunctioning, it must have been damaged during the collapse of the cave entrance while escaping the whale.

The seven-segment display uses (as it's name suggests) seven segments to display numbers. Each segment is labeld a-g but it seems the labels have gotten jumbled. I'm given segment labels and need to figure out the numbers that correspond to them. As an easy start I can begin with 1,4,7,8 since they all have a unique amount of segments that need to be turned on. For this part I'm only interested in the output values (the ones after |). So I collect those into an array of arrays (element per output digit) ns = document.body.innerText.trim().split`\n`.map(e=>e.replace(/.*\| /,'').split` `). And then simply flatten it and filter out all that can't be a 1,4,7 or 8 ns.flat().filter(e => [2,4,3,7].includes(e.length)).length giving me 344 and the gold star⭐!

However, the real challenge is to figure out which signals are which numbers. Which honestly I'm not even too sure of how to do, I think Constraint Propagation might be a good theory but I'm not the acadamic type so I've got no idea what it entails and how to apply it. What I'll do instead is try not to figure out which label is for which segment, but what number needs what segments on, which for the uniques is easy to do. The other numbers with non-unique length are with a length of 5: 2,3,5 (which I'll call "fivers") and with a length of 6: 0,6,9 (which I'll call "sixers"). Now I need to find if I can use earlier knowledge of the uniques to figure out what the other numbers are. For 3 it's easy, it's the only fiver with all segments of 1. Splitting between 2 and 5 can be done by using 4 since 2 matches 2 segments, but 5 matches 3 segments. Again I'll use the 1, now for finding the 6 because it's the only sixer that doesn't contain both segments. The 0 vs 9 I'll find by using the 3 since 0 contains only 4 segments of it, while 9 contains all 5. For convenience I'll reverse the map and now all that's left is looping over the output digits to find their number and putting it all together.

displayOut = line => {
  let [samples, output] = line.split(' | ').map(e => e.split(' ').map(e => [...e].sort()))

  const uniqs = {2: 1, 3: 7, 4: 4, 7: 8}
  const numToSegs = {}
  const fivers = []
  const sixers = []

  samples.forEach(segs => {
    let l = segs.length
    if(uniqs[l]) numToSegs[uniqs[l]] = segs
    else (l==5?fivers:sixers).push(segs)
  })

  fivers.forEach(segs => {
    if (numToSegs[1].every(e=>segs.includes(e))) numToSegs[3] = segs
    else if (numToSegs[4].filter(e=>segs.includes(e)).length == 2) numToSegs[2] = segs
    else numToSegs[5] = segs
  })

  sixers.forEach(segs => {
    if (!numToSegs[1].every(e=>segs.includes(e))) numToSegs[6] = segs
    else if(numToSegs[3].every(e=>segs.includes(e))) numToSegs[9] = segs
    else numToSegs[0] = segs
  })

  const segsToNum = Object.entries(numToSegs).reduce((acc,e) => ({...acc, [e[1].join``]: e[0]}), {})

  return +output.map(segs => segsToNum[segs.join``]).join``
}
document.body.innerText.trim().split`\n`.map(displayOut).reduce((a,b)=>a+b)

Giving me 1048410 and the second gold star of the day⭐!

That's the sum of all the numbers of the display, for now I'll say goodnight and call it a day.

Day 9

It's the Nine-Nine! Or well just the ninth, but you bet I'm going to be a computer detective with my Smoking Gun. And sssssmoking it is indeed, but if I'm honest it tastes a bit like an old tobacco smoke, not too great. After a few sips I'm more used to it and it's easier on the palate, perhaps when pairing it with some smoked meats it's better. However maybe it isn't the beer that I'm tasting but the hydrothermal vents releasing smoke into the caves. In fact, it's so much smoke that I need to stay clear of it to be safer.

Since smoke flows to the lowest points, I should find what the lowest points in the cave-system are. Luckily the submarine has generated a height map for me with each number being the height of that cavern. The smoke only flows through direct neighbours so if a cavern has the lowest height of all neighbours then it's a low point and dangerous. Every low point also has a risk level, which is 1 plus its height, I need to find the total risk level (the sum of all low points' risk). Since every low point need to be the lowest I can simply loop through all points and filter those that are the lowest of all neighbours.

ns = document.body.innerText.trim().split('\n')
nghbs = (x,y) => [[-1,0],[0,-1],[1,0],[0,1]].map(([xD,yD]) => ns[y+yD]?.[x+xD]).filter(e=>e!=null)

ns.flatMap((row,y) => [...row].filter((e,x) => nghbs(x,y).every(n=>+e<+n))).map(e=>+e+1).reduce((a,b)=>a+b)

Resulting in a total risk level of 475.

However I don't only need to find the low points, I need to find the entire basins. That is, all locations that eventually flow down to the same single low point are part of the same basin. Since 9s aren't part of a basin I'm pretty sure every basin is surrounded by Nine-Nines! Or at a minimum two nines and 2 cave edges. This means I'm probably able to perform a "floodfill" to get the basins. I'm not too sure how one usually does a floodfill for an entire field, but I reckon I'll just floodfill every position and when one is already filled it'll pretty much be a no-op. Although I only need the three largest basins (their size, and multiply them) I'll just filter for the sizes after getting all basins.

ns = document.body.innerText.trim().split('\n').map(e=>[...e])
neighbours = [[-1,0],[0,-1],[1,0],[0,1]]

floodFill = (x,y) => {
 if((ns[y]?.[x]??9) == 9) return 0
 ns[y][x]=9
 return 1+neighbours.map(xy=> floodFill(x+xy[0],y+xy[1])).reduce((a,b)=>a+b)
}

ns.flatMap((row,y) => row.map((,x)=>floodFill(x,y)))
.sort((a,b)=>a-b).slice(-3)
.reduce((a,b)=>a*b)

The multiplied size of the three biggest is 1092012 for me, earning my next golden star⭐!

For the dangerous smoke I won't be a target, see you tomorrow since I won't stop now I started.

Day 10

And we're already on day two! Oh wait oops, wrong numeral system, I meant ten of course. Maybe I'm already delirious from the white mamba's citrusy venom. It's real refreshing, sweet and somehow flowery. It might be the venom kicking in but after every sip it feels like an Elf from the Droomvlucht detonates a flower grenade in my mouth. Or perhaps there's a feud between them and the Christmas Elves and I'm caught up in between. Maybe they've also sabotaged the navigation subsystem since it's now giving syntax errors.

The navigation subsystem syntax is made up of lines containing paired brackets [](){}<> called chunks. Some lines are corrupted which means the opening and closing brackets aren't the same. Other lines can be incomplete which means it's just missing closing brackets at the end. For now I need to find the corrupted lines, and calculate the total syntax error score. (I should stop at the first wrong closing bracket on a line and each wrong closing bracket has a certain score. There's something funky going on with how those brackets are scored, it's times 19,21,21 but I'll ignore that.) How I'll want to solve this is to just read every line from left to right, put every opening bracket on a stack and when encountering a closing bracket pop it off and make sure it matches. First I'll define brackets to refer to, then collect all lines. Next I transform the lines to the (first) offending character (or none), and lastly get their score and sum it.

brackets='()[]{}<>'
ls = document.body.innerText.trim().split('\n')
ls.map(l => {
  stack = []
  for(e of l) {
    i = brackets.indexOf(e)
    if (i%2) {
      if (stack.pop() != brackets[i-1]) return e
    } else stack.push(e)
  }
  return '';
})
.map(e => ({'':0,')':3,']':57,'}':1197,'>':25137})[e])
.reduce((a,b)=>a+b)

Which is 366027 and gives me a gold star⭐!

Now that I've found the syntax error score I need to divert my attention to the incomplete lines and find the autocorrect score. I'll need to finish the incomplete lines and then keep a total score per line which gets multiplied with 5 for each character and added with 1,2,3,4 for ),},},> respectively. The final score is the middle score. Luckily it's not that different from what has to happen for finding the corrupted line. I can just use the remaining stack.

brackets='()[]{}<>'
ls = document.body.innerText.trim().split('\n')
acScores = ls.map(l => {
  stack = []
  for(e of l) {
    i = brackets.indexOf(e)
    if (i%2) {
      if (stack.pop() != brackets[i-1]) return ''
    } else stack.push(e)
  }
  return stack.reverse().map(e => (brackets.indexOf(e)/2|0)+1).reduce((a,b)=>a*5+b,0);
}).filter(e=>e)
.sort((a,b)=>a-b)
acScores[acScores.length/2|0]

The autocorrect score is way higher with 1118645287 earning another gold star⭐!

With no more navigation subsystem syntax errors I can sleep tight without any night terrors.

Day 11

Day eleven and today I've got the Bleu Métal Spirit. It doesn't have a particularly strong smell or flavour. It's very thirst quenching although I'm not sure if that's because it's a cold refreshing drink or because I've had some salty snacks earlier. I mainly taste a sweet and sour citrusy flavor with some bitter at the end. Normally I try to tie in the AoC day a bit better, but let me just segue right into bioluminescent dumbo octopuses.

I'm in a room full of 'm neatly arranged in a 10x10 grid and they flash brightly every once in a while when their energy level is maxed. They also didn't seem to like the Christmas lights on my submarine so I'll try to navigate by their flashes instead. The submarine magically prints out their current energy levels and I need to figure out when the flashes will occur. When an octopus gets an energy level greater than 9 it flashes and gives an energy level to all adjacent (including diagonal) octopus. It also gets one energy level every step and if it flashed it's energy is reset to 0. I'll of course start with extracting the grid from the submarine's magical print out. With the grid extracted I loop over all cells increasing their energy, then loop over all that should flash and increasing neighbours until there are no more left to flash.

grid = document.body.innerText.trim().split('\n').map(e=>[...e].map(e=>+e))

eachCell = block => grid.forEach((row,y) => row.forEach((c,x) => block(y,x,c)))
neighbours = [...Array(3)].flatMap((_,y) => [...Array(3)].map((_,x) => [y-1,x-1]).filter(([y,x]) => y|x))
neighboursOf = (y,x) => neighbours.map(([yD,xD]) => [y+yD,x+xD]).filter(([y,x]) => y in grid && x in grid[y])

flashes = 0
for(steps=100;steps--;) {
  eachCell((y,x,c) => grid[y][x] = (c|0)+1)

  do {
    finished = true
    eachCell((y,x,c) => {
      if (c > 9) {
        flashes++
        neighboursOf(y,x).forEach(([y,x]) => grid[y][x]++)
        grid[y][x] = finished = NaN
      }
    })
  } while(!finished)
}
flashes

Which, after by some off-by-one errors and the likes, gives me 1697 and a gold star⭐!

I'm still not able to navigate though, the individual flashes just aren't bright enough. According to my data the octopuses will flash simultaneously after a while though, I just need to find out at which step it starts. Luckily I can easily tweak my simulation to count how many flashed in a step and then just check if it were all of them.

grid = document.body.innerText.trim().split('\n').map(e=>[...e].map(e=>+e))

eachCell = block => grid.forEach((row,y) => row.forEach((c,x) => block(y,x,c)))
neighbours = [...Array(3)].flatMap((_,y) => [...Array(3)].map((_,x) => [y-1,x-1]).filter(([y,x]) => y|x))
neighboursOf = (y,x) => neighbours.map(([yD,xD]) => [y+yD,x+xD]).filter(([y,x]) => y in grid && x in grid[y])

for(steps=0;steps++<1e3;) {
  eachCell((y,x,c) => grid[y][x] = (c|0)+1)
  
  flashes = 0
  do {
    finished = true
    eachCell((y,x,c) => {
      if (c > 9) {
        flashes++
        neighboursOf(y,x).forEach(([y,x]) => grid[y][x]++)
        grid[y][x] = finished = NaN
      }
    })
  } while(!finished)
  if (flashes == 100) break
}
steps

And they're synchronized after 344 steps, so I can just sit back and enjoy my beer and new golden star⭐!

After all the octopi synchronized I'll sleep and see what tomorrow will provide.

Day 12

Woah, we're halfway there, at least regarding the beer affair. The Elves probably need me christmas day itself too, I guess I'll find out if I can deal with their magical fantasyland sober. But those are matters for another day, today I've got the Viaemilia. Very refreshing smelling flowery and herby without it tasting sweet. From Italy I need to find my way back to the submarine. Finally arriving back in the submarine I'm yet again tasked with finding a path out of the cave system.

For some reason I don't just need to find a or the best path, but all paths! So sadly I can't just open my graph-theory book and point to A* but I'll actually have to do some thinking. (Though I guess if I actually had a graph-theory book I could probably point to something else useful...) As a start I don't necessarily have to know the paths, just how many there are, which might be able to be calculated. But I also don't have a graph-theory-formulas book and I'm more comfortable mindlessly writing suboptimal bruteforcers than do any actual thinking. As a start I'll get the submarine's representation of the cave system. It's a bunch of lines where each line represents two caves connected. Caves with names in upper-case can be visited multiple times while ones in lower-case can be visited only once. I'll convert the input into a map of cave-name to connections. The searching for paths will honestly be kind of similar to the floodfill, just keep searching all neighbours but keep track of (small) caves we've visited before.

graph = document.body.innerText.trim().split('\n').map(e => e.split('-')).reduce((acc,[l,r])=> ({
  ...acc,
  [l]: [...acc[l]||[], r],
  [r]: [...acc[r]||[], l]
}), {})

paths = (cur, visited = []) => {
  neighbours = graph[cur].filter(e => !visited.includes(e))
  possiblePaths = 0
  for (e of neighbours) {
    if (e == 'end') possiblePaths++
    else possiblePaths += paths(e, [...visited, ...(/\p{Lu}/u.test(cur)?[]:[cur])])
  }
  return possiblePaths
}
paths('start')

There's 5457 possible paths from start to end, I really hope I don't have to try 'm all out because I'll probably miss Christmas. I do get to try on my new golden star though⭐!

Considering finding all paths went so fast I might have time to visit a single small cave (apart fom start and end) twice. And since I want to make an informed decision I'll alter the earlier path finding to find out how many paths are possible if one small cave is visited twice. For some reason I thought the solution was easy (just treat a small cave as a big one once, and flip a switch after) but I couldn't get that solution to work. After a good night sleep I rewrote the recursive loop to make a bit more sense, and after failing to generalize the small/big cave I just branched for them and got it to work.

graph = document.body.innerText.trim().split('\n').map(e => e.split('-')).reduce((acc,[l,r])=> ({
  ...acc,
  [l]: [...acc[l]||[], r],
  [r]: [...acc[r]||[], l]
}), {})

paths = (cur, visited = [], usedSecondVisit = false) => {
  visited = [...visited, cur]

  let possiblePaths = 0
  for (next of graph[cur]) {
    if (next == 'start') continue
    else if (next == 'end') possiblePaths++
    else {
      let small = /[a-z]+/.test(next)
      if (small) {
        let visitedBefore = visited.includes(next)
        let canVisit = !visitedBefore || !usedSecondVisit
        if (canVisit) possiblePaths += paths(next, visited, visitedBefore || usedSecondVisit)
      } else {
        possiblePaths += paths(next, visited, usedSecondVisit)
      }
    }          
  }
  return possiblePaths
}
paths('start')

Which gave me 128506 and finally the second gold star⭐!

There's no point for me to lie because you've already seen, I'm going straight through to day thirteen.

Day 13

Straight through to day thirteen it is, no need to be awakened by Raptu Rouse. If I hadn't read the all-caps "RED IPA" directly beneath it's name I would've thought I had a Kriek in front of me after opening the can and smelling a sweet cherryish aroma. After tasting it however it became clear as day this isn't a fruit beer. Although still somewhat sweet the bitter and malty taste take over completely. Just like how I've practically taken over the whole submarine, by now I more about it than the Evles.

Speaking of which, I need to use it's thermal camera but upon trying to use it I'm asked to input the activation code found on page 1 of the manual. After opening the manual I'm presented with a sheet of transparant paper with dots on it and instructions on how to fold the paper. I need to find out how many dots remain after the folding and for now I can just do the first fold only. As always I start with converting the input to something more workable, which'll be a grid and I'll need the width and height to set it up. Once I've got the grid set up I start filling in the dots. From the text/example it isn't clear to me if a fold is always exactly in the middle but I'll start with assuming it is because it makes things a bit easier. And also the fold lines in the given instructions seem to hint to the folds always being in the middle. With the fold line always in the middle I can quite trivially half the grid in the correct dimension and loop over the old one combining the two sides. For the first part I'll just foldX once since that's my first instruction.

[points, instructions] = document.body.innerText.trim().split('\n\n').map(e => e.split('\n'))

width = 1+Math.max(...points.map(e => +e.split(',')[0]))
height = 1+Math.max(...points.map(e => +e.split(',')[1]))

grid = [...Array(height)].map(_ => Array(width).fill(''))
points.forEach(e => {
  [x,y] = e.split(',')
  grid[y][x] = 'O'
})

foldX = () => {
  let foldLine = grid[0].length/2|0

  grid.forEach(row => {
    let right = row.splice(foldLine).reverse()
    row.forEach((e,x) => row[x] = e||right[x])
  })
}

foldY = () => {
  let foldLine = grid.length/2|0

  let down = grid.splice(foldLine).reverse()
  grid.forEach((row, y) => {
    row.forEach((e,x) => row[x] = e||down[y][x])
  })
}

dots = () => grid.flat().filter(e => e).length

foldX()
dots()

Which gives me 837 and the first gold star today (or well, of the day)⭐!

Now I'm supposed to keep folding the paper which in the end should visually look like eight capital letters. Unfortunately though, using above code that doesn't look like anything. Funnily enough I earlier had an assertion if given x/y folds were in the center but took it out because I figured they always were. Turns out though I figured wrong so now I could either a) fix the current approach to folding or b) take a whole different approach to folding. Any observant reader will by now have picked up on the fact that I would rather mindlessly bruteforce than think, so it won't come as a surprise I went with option c) use an ugly hack.

[points, instructions] = document.body.innerText.trim().split('\n\n').map(e => e.split('\n'))

width = 1+Math.max(...points.map(e => +e.split(',')[0]))
height = 4+Math.max(...points.map(e => +e.split(',')[1]))

grid = [...Array(height)].map(_ => Array(width).fill(''))
points.forEach(e => {
  [x,y] = e.split(',')
  grid[y][x] = 'O'
})

foldX = () => {
  let foldLine = grid[0].length/2|0

  grid.forEach(row => {
    let right = row.splice(foldLine).reverse()
    row.forEach((e,x) => row[x] = e||right[x])
  })
}

foldY = () => {
  let foldLine = grid.length/2|0

  let down = grid.splice(foldLine).reverse()
  grid.forEach((row, y) => {
    row.forEach((e,x) => row[x] = e||down[y][x])
  })
}

instructions.forEach(ins => ins.includes('x') ? foldX() : foldY())
console.log(grid.map(row => row.map(e => e||' ').join('')).join('\n'))

Just add 3 to the height (my height was 892 while fold instruction was 447) and instead of something unreadable, the beautiful output becomes:

OOOO OOO  OOOO  OO  O  O  OO  O  O O  O 
O    O  O    O O  O O O  O  O O  O O  O 
OOO  O  O   O  O    OO   O    OOOO O  O 
O    OOO   O   O OO O O  O    O  O O  O 
O    O    O    O  O O O  O  O O  O O  O 
OOOO O    OOOO  OOO O  O  OO  O  O  OO  

Giving me the second star⭐!

I may not have solved it in the best manner, but at least now I've got a working thermal camera scanner!

Day 14

The last day of the second week and I'm ending it with an ice cold (I may have put it in the freezer) Malheur 6°. A fresh Belgian blonde beer although not entirely to my taste. I'm not entirely sure why but somehow it's a bit too "beery" for me. Maybe too sour and bitter for my tastes. Or perhaps it's contaminated with nano-plastics and other polymers (although is there any foodstuff that isn't these days?). Thank god for polymers though because I need to re-inforce the submarine to withstand the incredible pressures and I've got all the necessary elements in the volcanically-active caves.

The submarine provides me with a polymer template and a list of pair insertion rules. I just need to figure out what polymer results after repeating the pair insertion process a few times. According to the submarine, if I start with a polymer (template), every processing step causes for each pair to get a new element inserted and thus creating a new polymer. The pairs overlap and are considered simultaneously. Now I'm no (bio-)chemist but I don't think this is exactly accurate, however I am a software developer and the given instructions are quite precise. So I can just do what the instructions tell me. I start with parsing the input (into a map of pairs) and to perform a step of pair insertions I'll use a regex (TIL you can use capture groups in a look-around). At the end I need to find what the quantity of the most commen element subtracted by the quantity of the least commen element is.

[template, pairInsertions] = document.body.innerText.trim().split('\n\n')

pairs = pairInsertions.split('\n')
  .map(e => e.split(' -> '))
  .reduce((acc, e) => ({...acc,
    [e[0]]: e[1]
  }), {})

for(n = 10; n--;)
  template = template.replace(/(?=(..))./g, (e,pair) => e + (pairs[pair] || ''))

quantities = Object.values(
  [...template].reduce((acc, e) => ({...acc, [e]: (acc[e]||0)+1}), {})
).sort((a,b)=>b-a)

quantities[0]-quantities[quantities.length-1]

The result is 2408 giving me yet another gold star⭐!

Sadly enough the resulting polymer of 10 iterations isn't nearly strong enough to reinforce the submarine. Instead I need to do 40 iterations! And for those not versed in Big-O (and tbh myself) let's just say that trying to simulate the whole thing will be intractable (as in ~22TB of polymer string intractable). So I somehow need to find something clever where I don't need to build the actual whole polymer. My first instinct is to do the 40 steps for the first pair, delete and count the elements and then do the next pair. However I'm not yet entirely sure how since only the initial pairs need 40 processing steps... the second generation only 39, third 38 and so on. Maybe something recursive can help me out, but I'm afraid I need to sleep a night on it.

I've slept on it, but before I went to actually sleep I may or may not have visited reddit on my phone and more specifically the day 14 solutions thread. I abandoned the recursive train of thought although I still think that would be feasible (maybe with some memoization), but it isn't the best/easiest solution. On the reddit was a hint (well, solution really) on how one should keep track of pairs and every x amount of [ab] pair produces x new letters [c] and thus x new [ac] and [cb] pairs. It should be possible to only keep track of the pairs and count the elements that way but de-duplicate elements you've counted twice (because they exist in the left and right pair). But I can't figure out how to achieve that, so I rather keep track of the quantities of elements individually too.

let [template, pairInsertions] = document.body.innerText.trim().split('\n\n')

let quants = [...template].reduce((acc, e) => ({...acc, 
  [e]: (acc[e]|0)+1
}), {})

let pairs = [...template]
  .map((e, i, arr) => arr[i+1] && e + arr[i+1])
  .filter(e => e).reduce((acc, e) => ({...acc,
    [e]: (acc[e]||0)+1
  }), {})

let rules = pairInsertions.split('\n')
  .map(e => e.split(' -> '))
  .reduce((acc, [pair, insertion]) => ({...acc,
    [pair]: insertion
  }), {})

for (let steps = 40; steps--;) {
  pairs = Object.entries(pairs).reduce((acc, [[l,r], v]) => {
    let m = rules[l+r]
    quants[m] = (quants[m]||0)+v

    return {...acc,
      [l+m]: (acc[l+m]||0)+v,
      [m+r]: (acc[m+r]||0)+v
    }
  }, {})
}

quants = Object.values(quants)
Math.max(...quants) - Math.min(...quants)

Which is 2651311098752 earning me the gold star⭐!

With day 14 done I'll go to the fridge, get day 15's beer and be back in a smidge.

Day 15

The start of the third (and final full) week! Against all odds still drinking, puzzling and writing, I guess the drinking part increased the odds considerably. Today I've got the aptly named Defiance Pale Ale. A self proclaimed "deliciously rebellious fruit salad of a beer" and though a bit of an embellishment I've got to admit it's tasty and fruity. To be honest it kind of reminds me of hard seltzers but with a fuller and more bitter taste. I don't mind it, but you may decide for yourself if that's a good or a bad thing. What's unequivocally a bad thing though, is needlessly hurting chitons.

And yet as I'm almost getting out of the cave with the submarine it's becoming harder and harder to dodge the chitons since the walls are getting closer together. Luckily the submarine can do a scan of chiton density to produce a risk level map. I start at the top left and need to find a path with the lowest total risk level to the bottom right. Now this sounds like something A* or Dijkstra's is made for, or more probably even other algorithms. If only I knew them, the differences and how to implement them. So obviously I'm off to a google rabbit-hole... After umpteen open half/partial/non-read browser tabs with wiki pages, research papers and github gists and more time passed than I would like to admit. I finally feel confident enough to disclose to you that I honestly don't understand more than half I read and haven't really learned a thing. So instead I yoinked Dijkstra's algorithm pseudo-code from wikipedia, converted it to javascript not even bothering to make it neat. Ran it on the sample which worked like a charm and feeling confident it would work on my actual input I got the answer 437. ...which as you might guess wasn't correct, however it did tell me the answer was too high. Looking at the distances map created I did immediately see some peculiarities around the direct neighbours of the starting node. It looked as if it also contained the starting node's value. Of course I tried to subtract it now entering 430, but alas the answer was too low! So obviously the answer was 435... because I swapped two variables and somehow it didn't go completely but just enough wrong to drive me completely insane. After spending an eternity on it and another eternity cleaning the code a tid-bit I ended up with the following.

let grid = document.body.innerText.trim().split('\n').map(e=>[...e].map(e=>+e))

let toPoint = xy => xy?.split(',').map(e=>+e)

let points = grid.flatMap((row,y) => row.map((_,x) => [x,y]))
let dists = points.reduce((acc, e) => ({...acc, [e]: Infinity}), {})
let unvisited = points.map(e=>''+e) // So we can simply use includes

dists[[0,0]] = 0
let cur
while (cur = toPoint(unvisited.sort((a,b) => dist[a]-dist[b]).shift())) {
  [[1,0],[0,1],[-1,0],[0,-1]]
    .map(([xD,yD]) => [cur[0]+xD,cur[1]+yD])
    .filter(e => unvisited.includes(''+e))
    .forEach(n => dists[n] = Math.min(dists[n], dists[cur] + grid[n[1]][n[0]]))
}

dists[[grid[0].length-1,grid.length-1]]

Now this is an abhorantly inefficient piece of code, and since it's a planar graph there should be a linear time solution out there. But hey, it found the path in under a minute (find path: 32572.64208984375 ms) and more importantly earned me a golden star&x#2b50;!

Sadly though that inefficiency is going to come bite me in the arse, because the map is actually 5 times larger than my submarine scanned. The map repeats 5 times to the right and down, increasing the risk level by 1 each time (wrapping back to 1 if it was 9). Since I'm going to sleep anyway, I might as well let it carry on and see how far it got in the morning. So I've altered it a bit (so it won't block the thread) and I'll start it and see if it got anywhere tomorrow (hopefully the laptop doesn't auto-sleep or w/e).

console.time('find lowest risk')
let grid = document.body.innerText.trim().split('\n').map(e=>[...e].map(e=>+e))
grid = [...Array(5)].flatMap(
  (_,i) => grid.map(
    row => [...Array(5)].flatMap((_,j) => row.map(e=> e+i+j).map(e=> e<10?e:e-9))
  )
)
console.log('built grid')

let toPoint = xy => xy?.split(',').map(e=>+e)

let points = grid.flatMap((row,y) => row.map((_,x) => [x,y]))
let dists = points.reduce((acc, e) => (acc[e] = Infinity, acc), {})
let unvisited = points.map(e=>''+e) // So we can simply use includes

dists[[0,0]] = 0

console.log('set up dists/unvisited start searching')
async function getNext() {
  await new Promise(resolve => setTimeout(resolve))
  return toPoint(unvisited.sort((a,b) => dists[a]-dists[b]).shift())
}

let cur
while (cur = await getNext()) {
  [[1,0],[0,1],[-1,0],[0,-1]]
    .map(([xD,yD]) => [cur[0]+xD,cur[1]+yD])
    .filter(e => unvisited.includes(''+e))
    .forEach(n => dists[n] = Math.min(dists[n], dists[cur] + grid[n[1]][n[0]]))
}

console.timeEnd('find lowest risk')
dists[[grid[0].length-1,grid.length-1]]

And it spit out the answer 2842 in find lowest risk: 4365288.771972656 ms. Which is only about 70minutes, not that bad at all. I guess that Dijkstra fella was a pretty smart cookie.

Now I'm going to the fridge again, for another beer, this time not a can.

Day 16

Only 10 days to go, the count-down has begun. Luckily it's not a count-up in the mother-tongue of this lovely Blanche de Cambrai. A soothing sweet wheat beer with a clean taste. And as I leave the cave the water gets just as clean as the beer's taste. No more pesky chitons to dodge. As soon as I reach the open waters I receive a transmission from the Elves back on the ship.

The transmission is sent in a binary protocol akin to TLV. The specification is a bit wordy so to summarize: The transmission is one packet which may contain multiple packets, a packet can be a literal or an operator (meaning it is composed of other packets). The first 3 bits of a packet are its version, the second 3 bits its type (a 4 means literal, anything else an operator). For a literal the last bits are groups of 5 bits with the first bit telling if it's the last in the series and the other 4 bits being part of the number. For an operator there's two modes which the bit after the type indicates, 0) total length in bits denoted in 15 bits and 1) number of sub-packets denoted in 11 bits. The first objective is to parse all the packets (and the hierarchy) and sum all te version numbers. Kind of straight-forward I'll create a small object to keep track of parsing and use a recursive function to parse a packet.

let trans = (_ => {
  let bits = document.body.innerText.trim()
    .replace(/./g, 
      e => (+('0x'+e)).toString(2).padStart(4, 0)
    )
  let idx = 0
  return {
    num(l = 1) { return +('0b'+this.str(l)) },
    str(l = 1) { return bits.slice(idx, idx+=l) },
    get length() { return bits.length - idx },
    get idx() { return idx },
  }
})()

let parsePacket = () => {
  let version = trans.num(3)
  let type = trans.num(3)

  let specifics
  if (type == 4) {
    let allBits = ''
    let cont
    do {
      cont = trans.num()
      allBits += trans.str(4)
    } while(cont)

    specifics = { value: +('0b'+allBits) }
  } else {
    let mode = trans.num()
    let packets = []
    if (mode) {
      let amtPackets = trans.num(11)
      while(amtPackets--) {
        packets.push(parsePacket())
      }
    } else {
      let length = trans.num(15)
      let end = trans.idx + length
      while (trans.idx < end) packets.push(parsePacket()) 
    }

    specifics = {packets}
  }
  return {
    version, type, ...specifics,
    get allPackets() { 
      return [this, ...this.packets?.flatMap(e => e.allPackets) ?? []] 
    }
  }
}

parsePacket().allPackets
  .map(e => e.version).reduce((a,b) => a + b)

The total sum of versions give me 989, giving me a gold star⭐!

It isn't as easy as just summing the versions though. The real exercise is evaluating the whole expression. Every operator packet perform an operation on their sub-packets, the full list can be read here (or in the code below). Having the base parsing that creates the hierarchy it's just a matter of implementing the operations.

let trans = (_ => {
  let bits = document.body.innerText.trim()
    .replace(/./g, 
      e => (+('0x'+e)).toString(2).padStart(4, 0)
    )
  let idx = 0
  return {
    num(l = 1) { return +('0b'+this.str(l)) },
    str(l = 1) { return bits.slice(idx, idx+=l) },
    get length() { return bits.length - idx },
    get idx() { return idx },
  }
})()

let parsePacket = () => {
  let version = trans.num(3)
  let type = trans.num(3)

  let specifics
  if (type == 4) {
    let allBits = ''
    let cont
    do {
      cont = trans.num()
      allBits += trans.str(4)
    } while(cont)

    specifics = { value: +('0b'+allBits) }
  } else {
    let mode = trans.num()
    let packets = []
    if (mode) {
      let amtPackets = trans.num(11)
      while(amtPackets--) {
        packets.push(parsePacket())
      }
    } else {
      let length = trans.num(15)
      let end = trans.idx + length
      while (trans.idx < end) packets.push(parsePacket()) 
    }

    let ops = {
      0: (a,b) => a + b,
      1: (a,b) => a * b,
      2: (a,b) => a < b ? a : b,
      3: (a,b) => a > b ? a : b,
      5: (a,b) => a > b ? 1 : 0,
      6: (a,b) => a < b ? 1 : 0,
      7: (a,b) => a == b ? 1 : 0,
    }
    specifics = { packets,
		value: packets.map(e=>e.value).reduce(ops[type]) 
	}
  }
  return {
    version, type, ...specifics
  }
}

parsePacket().value

I've just calculated the value directly when the packet was built instead of using a getter or function. Anyway it gives me 7936430475134 and yet another gold star⭐!

Now to the fridge for the third day in a row, I guess with this advent I'm a bit too slow.

Day 17

It's the seventh prime day of the Advent and to close this prime week I'm having the Chabron. A smoky but also vanilla sweet stout which I must say is very well balanced. It's not too smokey and the sweetness is subtle. While I'm daydreaming about the stout Dochter van de Korenaar I get the decoded Elves' message which just says "HI"... very helpful. In front of me is a large ocean trench and I'll need to send in a probe to investigate.

I can shoot the probe with an initial x,y velocity. The x velocity will decrease (towards 0) by 1 every step due to drag. The y velocity will decrease by 1 every step due to gravity. I need to launch the probe into a target area, conveniently calculated by the submarine. However I'll need to calculate with which x,y velocity I need to launch the probe. Of course I want to shoot the probe with style, so I want to reach the maximum height before it reaching it's target. I'm pretty sure I won't need to take the x velocity in account to calculate this, that's something for the next part I'm guessing. I'm also pretty sure this can be solved with math, but I have no idea how so obviously I'm going to try to brute-force my way to the solution. I'll start with simulating the y part of the launch and telling if it would travel through the target area or not. When I've got that simulation it's just a matter of brute-forcing velocities through it. The problem however is knowing when to stop trying to go higher. My initial guess would be "whenever the y velocity is larger than the y area because otherwise it overshoots". However I don't think that's entirely accurate since even if a single step velocity is larger than the area, it could still land in it... so just set some arbitrary bounds?

let [,x,X,y,Y] = [...document.body.innerText
	.match(/.*x=(\d+)\.\.(\d+).*y=(-?\d+)\.\.(-?\d+).*/)
].map(e => +e)

async function shoot(vY) {
  let [cY,maxY] = [0,0]

  while (cY > y) {
    maxY = Math.max(maxY, cY)
    cY += vY--    
    if (y <= cY && cY <= Y) return maxY
  }
}

for(max=v=0; v<10000; v++) max = (await shoot(v)) || max
max

Giving me 4851 (although I still can't tell you why it can't go higher) and the first golden star⭐!

However it's probably wise to find all possible velocities that land so I can pick the best one. So instead of finding the highest I'll want to find how many different velocities can reach the target. For this I'll slightly alter the above code to also include x axis and count how many will hit.

let [,x,X,y,Y] = [...document.body.innerText
  .match(/.*x=(\d+)\.\.(\d+).*y=(-?\d+)\.\.(-?\d+).*/)
].map(e => +e)

function shoot(vX,vY) {
  let [cX,cY] = [0,0]

  while (cY > y) {
    cY += vY--
    cX += vX--
    if (vX < 0) vX = 0

    if (x <= cX && cX <= X && y <= cY && cY <= Y) return 1
  }
}

let count = 0
for(vX=0;vX<1000;vX++)for(vY=y;vY<1000;vY++)if(shoot(vX,vY))count++
count

Which gives me 1739 possible velocities and 1 gold star⭐!

Though it might've been a bit of a hack, at least I'm finally back on track.

Day 18

Even though I was on track not too long ago, I'm currently writing this on the 19th oh no! I'll blame the untimeliness on The Witcher, let's hope two puzzles in one day doesn't get too cumbersome. What won't (ever) be too cumbersome is drinking two beers on one day and I'm starting with the Palim Palim Pale Ale. It doesn't really taste like french fries in a bottle but I'm pretty sure that's a good thing. Instead it's fresh and fruity and pretty sweet. But perhaps ideal to pair it with some french fries. For me there's no french fries or food at all unfortunately as I need to help a snailfish in the ocean trench with their math homework.

For some reason snailfish numbers are a pair where each element of a pair can either be a regular number or another pair. The numbers must also always be reduced by exploding or splitting. The given representation is the same as a JSON array, so one might be tempted to just use arrays to do all the calculations. However an easier representation might be a (binary) tree structure especially for the exploding since we can more easily find the left and right leaves. Still, even as a binary tree structure it's still involved and easy to let mistakes slip in. I had to debug for way too long before finding mistakes such as not halving the value when splitting.

function parse(e, parent) {
  if (typeof(e) == 'string')
    return parse(JSON.parse(e))
  if (e?.constructor?.name == 'Object') {
    e.parent = parent
    return e
  }

  let n = {
    parent, 
    get lvl() { return 1 + (this.parent?.lvl ?? 0) }, 
    get leaves() { return ('v' in this) ? this : [this.l.leaves, this.r.leaves].flat()},
    toString() { return this.v ?? `[${this.l},${this.r}]` }
  }
  if (typeof(e) == 'number')
    n.v = e
  else {
    n.l = parse(e[0], n)
    n.r = parse(e[1], n)
  }
  return n
}

let ns = document.body.innerText.trim().split('\n').map(e => parse(e))
let cur = ns.shift()
while (ns.length) {
  cur = parse([cur, ns.shift()])
  while(reduce());
  
  function reduce() {
    return explode() || split()
  }

  function explode() {
    let explodee = findToExplode(cur)
    if (!explodee) return false

    let leaves = cur.leaves
    let l = leaves[leaves.indexOf(explodee.l)-1]
    let r = leaves[leaves.indexOf(explodee.r)+1]
    if (l) l.v += explodee.l.v
    if (r) r.v += explodee.r.v
    explodee.v = 0
    delete explodee.l
    delete explodee.r
    return true

    function findToExplode(node) {
      if (!node || ('v' in node)) return
      if (node.lvl > 4) return node
      return findToExplode(node.l) || findToExplode(node.r)
    }
  }

  function split() {
    let splittee = findToSplit(cur)
    if (!splittee) return false

    let half = splittee.v/2
    splittee.l = parse(Math.floor(half), splittee)
    splittee.r = parse(Math.ceil(half), splittee)
    delete splittee.v
    return true
    
    function findToSplit(node) {
      if (!node) return
      if (node?.v > 9) return node
      return findToSplit(node.l) || findToSplit(node.r)
    }   
  }
}

function magnitude(node) {
  return node?.v ?? (3*magnitude(node.l) + 2*magnitude(node.r))
}
magnitude(cur)

Luckily I've got it working in the end though, giving me a result of 4202 and a golden star⭐!

Turning the page of the homework assignment uncovers an additional question: What is the largest magnitude possible from adding only two of the total numbers. Luckily this should be easy to solve with the trusty brute-forcer. In fact it should've been solved within a few minutes... As long as there weren't any bugs in the previous code such as not adding a parent if we're parsing a string. So of course it took way longer than necessary again, but hey it works now.

function parse(e, parent) {
  if (typeof(e) == 'string')
    return parse(JSON.parse(e), parent)
  if (e?.constructor?.name == 'Object') {
    e.parent = parent
    return e
  }

  let n = {
    parent, 
    get lvl() { return 1 + (this.parent?.lvl ?? 0) }, 
    get leaves() { return ('v' in this) ? this : [this.l.leaves, this.r.leaves].flat()},
    toString() { return this.v ?? `[${this.l},${this.r}]` }
  }
  if (typeof(e) == 'number')
    n.v = e
  else {
    n.l = parse(e[0], n)
    n.r = parse(e[1], n)
  }
  return n
}
  
let ns = document.body.innerText.trim().split('\n')
let largestMagnitude = 0
for (let a of ns) for (let b of ns) {
  let cur = parse(a)
  cur = parse([cur, b])
  while(reduce());
  largestMagnitude = Math.max(largestMagnitude, magnitude(cur))
    
  function reduce() {
    return explode(cur) || split(cur)
  }
}

function explode(root) {
  let explodee = findToExplode(root)
  if (!explodee) return false
  
  let leaves = root.leaves
  let l = leaves[leaves.indexOf(explodee.l)-1]
  let r = leaves[leaves.indexOf(explodee.r)+1]
  if (l) l.v += explodee.l.v
  if (r) r.v += explodee.r.v
  explodee.v = 0
  delete explodee.l
  delete explodee.r
  return true

  function findToExplode(node) {
    if (!node) return
    if (!('v' in node) && node.lvl > 4) return node
    return findToExplode(node.l) || findToExplode(node.r)
  }
}

function split(root) {
  let splittee = findToSplit(root)
  if (!splittee) return false
 
  let half = splittee.v/2
  splittee.l = parse(Math.floor(half), splittee)
  splittee.r = parse(Math.ceil(half), splittee)
  delete splittee.v
  return true
    
  function findToSplit(node) {
    if (!node) return
    if (node?.v > 9) return node
    return findToSplit(node.l) || findToSplit(node.r)
  }   
}

function magnitude(node) {
  return node?.v ?? (3*magnitude(node.l) + 2*magnitude(node.r))
}
largestMagnitude

Giving me 4779 as the answer and the gold star⭐!

That was day 18th but I still need to catch up, I'm not sure if I'll finish day 19 on the 19th, just a heads-up.

Day 19

To be honest I'm still drained from day 18, but luckily I've got a Passion Fruit to lift my spirits. And it's going to need to do some heavy lifting because I already peeked at day 19, and boy I'm not happy. Although the beer is light and fruity it does a decent lifting job. I certainly prefer these over stouts. I am however not too happy about the probe scouts. It released beacons and scanners, but I need to do a lot of calculations to have those be useful.

The scanners can can detect all beacons at most 1000 units in each (x,y,z) axis away. However I don't know the position nor orientation of each scanner and they can all be different. So I somehow need to find which scanned beacons overlap for each scanner (12 overlaps seem to be the magic number) and map out the 3d plane that way. (Or for starters I just need to know how many beacons in total there are, but aside from that already being plenty difficult IMO, surely it'll become more complicated for part 2.) I'm not even really sure how to detect overlapping beacons... I guess a brute-forcy approach would be to check a "fixed" scanner (assume the first scanner as fixed) against every other scanner. For the check loop over the fixed's points (as an anchor) then loop all other's points as reference and loop over all orientations creating list of all beacons in fixed's perspective. Then if at least 12 overlap I should have found a match. If I found that match I can "fix" the current scanner (adjust its beacons positions to the previous fixed scanner perspective) and repeat the process until all scanners are "fixed". Now I must admit that I looked at a few solutions and even with cheating like that it took me way too long to solve it.

const rots = [
  ([x,y,z])=>[x,y,z], ([x,y,z])=>[-y,x,z],
  ([x,y,z])=>[-x,-y,z], ([x,y,z])=>[y,-x,z]
]
const faces = [
  ([x,y,z])=>[x,y,z], ([x,y,z])=>[-z,y,x], ([x,y,z])=>[-x,y,-z],
   ([x,y,z])=>[z,y,-x], ([x,y,z])=>[x,-z,y], ([x,y,z])=>[x,z,-y]
]
const dirs = faces.flatMap(faceFn => 
  rots.map(rotFn => xyz => rotFn(faceFn(xyz)))
)

class Scanner {
  constructor(block) {
    [this.name, ...this.beacons] = block.split('\n').map(e => 
      e.match(/scanner \d+/)?.[0] ?? e.split(',').map(e => +e)
    )
  }

  async fixIfMatches(otherScanner) {
    console.log(`Matching ${this.name} against ${otherScanner.name}...`)
    await new Promise(resolve => setTimeout(resolve))

    for (let anchor of this.beacons) {
      for (let ref of otherScanner.beacons) {
        let refDirs = dirs.map(e => e(ref))
        for (let dir = 0; dir < refDirs.length; dir++) {
          let offset = anchor.map((e,axis) => e - refDirs[dir][axis])
          let fixedBeacons = otherScanner.beacons
            .map(e => dirs[dir](e))
            .map(beacon => beacon.map((e,i) => e + offset[i]))
          let matchedBeacons = fixedBeacons.filter(beacon => 
            this.beacons.find(e => beacon[0]==e[0]&&beacon[1]==e[1]&&beacon[2]==e[2])
          )
          if (matchedBeacons.length >= 12) {
            console.log(`Matched ${this.name} against ${otherScanner.name} with dir ${dir}!`)
            otherScanner.beacons = fixedBeacons
            otherScanner.position = offset
            return otherScanner.fixed = dir+1
          }
        }
      }
    }
    return false
  }
}

let unknownScanners = document.body.innerText.trim()
    .split('\n\n').map(e => new Scanner(e))

let firstScanner = unknownScanners.shift()
await firstScanner.fixIfMatches(firstScanner)
let fixedScanners = [firstScanner]

let notPairs = {}
while (unknownScanners.length) {
  const amtFixed = fixedScanners.length
  outer:
  for (fixed of fixedScanners) {
    for (unknown of unknownScanners) {
      if(notPairs[fixed.name]?.includes(unknown.name)) continue

      if (await fixed.fixIfMatches(unknown)) {
        fixedScanners.push(unknownScanners.splice(unknownScanners.indexOf(unknown), 1)[0])
        break outer;
      } else {
          notPairs[fixed.name] = [...notPairs[fixed.name] ?? [], unknown.name]
      }
    }
  }
  if (amtFixed == fixedScanners.length) throw new Error('No scanners fixed')
}

Having ran that it's trivial to get the total amount of beacons.

new Set(fixedScanners.flatMap(e => e.beacons).map(e => e.join`,`)).size

Which for me results in 440 and finally gives me the first 19th day star⭐!

But of course I'm not finished with day 19 yet! For some inexplicable reason I want to appreciate just how big the ocean is. Or at least, the tiny part my probe has scanned. So I need to figure out how far apart the scanners get using the manhatten distance. Luckily with all the scanners positions it's now trivial to find that.

let dist = (a,b) => a.map((e,i) => Math.abs(e-b[i])).reduce((a,b)=>a+b)
Math.max(...fixedScanners.flatMap(a => fixedScanners.map(b => dist(a.position,b.position))))

Resulting in 13382 (nautical miles?), but more importantly finishing this day for good with a golden star⭐!

Even though I'm now a full day behind I think it's time to stop this days' grind

Day 20

Day 20... although it's the 21st for me let's hope the puzzles get easier now... At least I've got a cold Delicious IPA in front of me. Although I can't completely agree with its name. It's a bit too bitter for me, in a metalic kind of way if that makes any sense. Perhaps that's my own fault for drinking it out of the can though... or maybe it's because the heavy metals in the ocean floor are somehow entering the submarine and my beer. Whatever it may be it might be a good idea to map out floor of the ocean trench though.

The scanners have generated an image but it seems to be random noise. I've got access to an image enhancement algorithm though perhaps I can apply it to the image to clean it up. The image and its enhancement algorithm is some NCIS technology though. It appears it can be repeatedly enhanced, using the initial enhancement as input for a next enhancement. The enhancement works by taking a 3x3 square of pixels with the center being the enhanced image's pixel. Then converting that 3x3 square of pixels to a binary number (0 for . or dark 1 for # or light), that binary number is an index into the enhancement algorithm which tells us if the target pixel is dark or light. The trick here is to have a possible infinite image, all pixels around our initial image are dark but by repeatedly enhancing we can enhance infinitely big. The first goal is to enhance our given image twice and see how many pixels are light. Of course we start with transforming our input to useful structures. Since the image is infinitely big it's probably best to not try to store all of it. My idea is to only store the light pixels. A thing that throws me off though is the fact that in my input the first indexed pixel in the algorithm is a light pixel. That means that a dark 3x3 block gets a light pixel in the middle, and since the image is infinitely big pretty much the whole infinite image should turn light after the first enhancement? However, the last indexed pixel is a dark pixel... So I guess the algorithm is just a bit flashy which shouldn't be too hard to account for. This makes only storing the light pixels hard though because it gets difficult to differentiate between an actual dark pixel and a flashed pixel. So perhaps just store the whole image sample and make it grow each enhancement. To be honest this puzzle took me way too long because honestly it isn't that complicated. I messed up a lot by accidently using global vars and whatnot.

let [alg, img] = document.body.innerText.trim().split('\n\n')
img = img.split('\n')

let i = 0
function enhance() {
  let inFlash = ++i%2==0
  
  let newImg = []
  for(let y = -1; y < img.length + 1; y++) {
    let row = ''
    for(let x = -1; x < img[0].length + 1; x++) {
      row += enhancePix(x,y)
    }
    newImg.push(row)
  }

  img = newImg

  function enhancePix(X,Y) {
    let bin = '0b'
    for(let y = -1; y <= 1; y++) {
      for(let x = -1; x <= 1; x++) {
        bin += (getPix(x+X,y+Y) == '#') ? 1 : 0
      }
    }
    return alg[+bin]
  }

  function getPix(x,y) {
    return img[y]?.[x] ?? (inFlash ? '#' : '.')
  }
}

enhance()
enhance()
img.flatMap(e=>[...e]).filter(e=>e=='#').length

Enhancing twice and counting the lights give me 5619 and a gold star⭐!

Just two enhancement isn't enough though, I better enhance it 50 times. Or 48 more for(let n=48;n--;)enhance() and then counting the lights gives me 20122 and the last gold star of the day⭐!

I'm still a full day behind but I guess the catching up is something for another time!

Day 21

Oh man these last days I'm falling more and more behind, it's already technically the 23rd. I've got some serious drinking to do the coming days. But I'll start with the Stria. Taste wise it's nothing very special but I certainly don't mind it. It doesn't have a lot of flavour but I personally take a watery beer over a coffee beer any day of the week. Since I'm just enjoying the beer while descending to the bottom of the ocean the submarine challenges me to a game of Dirac Dice.

Since the whole game instructions can be read on the site I won't repeat them here. I'll immediately start with my solution to finding the losing player's score and die rolls in the most straightforward and inefficient way.

let [p1,p2] = document.body.innerText.trim().split('\n')
  .map(e => +e.match(/^.*(\d+)$/)[1])
  .map(pos => ({pos, score: 0}))

let dieRoll = 0
let turn = p1
while (p1.score < 1000 && p2.score < 1000) {
  turn.pos += ++dieRoll + ++dieRoll + ++dieRoll
  turn.pos = (turn.pos-1)%10 + 1
  turn.score += turn.pos
  
  turn = turn == p1 ? p2 : p1
}
turn.score * dieRoll

Giving me 995904 and the first gold star&@x2b50;!

This was way too easy though so I'm a little bit afraid to click to continue to part 2... And my fear wasn't unfounded since I now have to use a three sided "Dirac dice". Which is a special quantum die that splits the universe into three copies for each possible outcome. The score to win is now only 21 but I need to find in how many universes the player winning in the most wins. Now the most straightforward and inefficient way for me would be a recursive call but I highly doubt that's going to be possible without some cleverness. I guess the cleverness is not making a universe per roll but only for each combination of rolls outcome. But even with that cleverness I still seem to be exceeding stack size or maybe I'm doing something wrong... And after a lot of debugging and some cheating looking for hints I finally got it working.

let rollUnivs = [...Array(3)].flatMap((_,i) => 
                [...Array(3)].flatMap((_,j) => 
                [...Array(3)].map((_,k) => 3+i+j+k)))
  .reduce((acc, e) => ({...acc,
    [e]: (acc[e]|0)+1
}), {})

let [p1p,p2p] = document.body.innerText.trim().split('\n')
  .map(e => +e.match(/^.*(\d+)$/)[1])
let cache = {}
function play(p1p,p1s,p2p,p2s,turn) {
  if (cache[[p1p,p1s,p2p,p2s,turn]]) return cache[[p1p,p1s,p2p,p2s,turn]]

  if (p1s >= 21) return cache[[p1p,p1s,p2p,p2s,turn]] = [1,0]
  if (p2s >= 21) return cache[[p1p,p1s,p2p,p2s,turn]] = [0,1]

  let wins = [0,0]
  for (let [roll, univs] of Object.entries(rollUnivs).map(e => e.map(e=>+e))) {
    let win
    if (turn) {
      let nextPos = (p1p + roll - 1)%10 + 1
      win = play(nextPos, p1s + nextPos, p2p, p2s, !turn)
    } else {
      let nextPos = (p2p + roll - 1)%10 + 1
      win = play(p1p, p1s, nextPos, p2s + nextPos, !turn)
    }
    wins[0] += univs * win[0]
    wins[1] += univs * win[1]
  }
  return cache[[p1p,p1s,p2p,p2s,turn]] = wins
}
Math.max(...play(p1p,0,p2p,0,1))

Giving me 193753136998081 and my answer to life, the universe and everything's golden star⭐!

I'm spent for now but I'll try to catch up the next days somehow.

Day 22

Only 3 more beers and 4 more puzzles to go. Today's beer is the Kazbek Kodiak with a very sweet floral aroma. Drinking it gives hints of green tea, almost passable as an iced tea. A very nice refreshing beer to get some fresh insights into rebooting the submarine's reactor.

The reactor consists of cubes with their own x,y,z coordinates and I need to reboot it by following instructions to turn certain cubes on/off. I'm given a "on" or "off" and a range of x,y,z coordinates and need to turn all the cubes within that region on/off. As a starter I only need to consider the ranges -50 to 50 so I'll start with a 3d array for all cubes since it's small enough.

let reactor = [...Array(101)].map(_ => [...Array(101)].map(_ => Array(101).fill(false)))

document.body.innerText.trim().split('\n')
  .map(e => e.match(/(on|off) x=(-?\d+)..(-?\d+),y=(-?\d+)..(-?\d+),z=(-?\d+)..(-?\d+)$/))
  .forEach(([_,onOff, x1, x2, y1, y2, z1, z2]) => {
    [x1,x2,y1,y2,z1,z2]=[x1,x2,y1,y2,z1,z2].map(e=>+e)

    for (let x = Math.max(-50, Math.min(x1, x2)); x <= Math.min(50, Math.max(x1, x2)); x++)
    for (let y = Math.max(-50, Math.min(y1, y2)); y <= Math.min(50, Math.max(y1, y2)); y++)
    for (let z = Math.max(-50, Math.min(z1, z2)); z <= Math.min(50, Math.max(z1, z2)); z++)
      reactor[x+50][y+50][z+50] = onOff == 'on'
  })

reactor.flat(3).filter(e=>e).length

Easy enough, giving me 647062 and a gold star⭐!

However now I need to consider every cube in the reactor, not the 100 side subsection. So obviously above solution isn't going to cut it. My go-to would be to create an object which only stores the "on" cells. And although I'll try it out, I'm relatively sure even that is not going to be feasible with the amount of cells. ...So yea my efforts were fruitless. Instead I did some more thorough cheating research and found out that a/the way to solve the problem is to: Process the instructions from the end and turn on (if needed) all new "volumes". Where a "new volume" is the volume of the cuboid minus the intersections with all already processed cuboids. (The intersections need to recurse because otherwise you'll count the same intersections multiple times.) So the plagiarism I ended up with does just that.

let ins = document.body.innerText.trim().split('\n')
  .map(e => [e[1] == 'n', e.match(/-?\d+/g).map(e=>+e)])

let intersect = (a,b) => [
  Math.max(a[0], b[0]), Math.min(a[1], b[1]),
  Math.max(a[2], b[2]), Math.min(a[3], b[3]),
  Math.max(a[4], b[4]), Math.min(a[5], b[5]),
]

let vol = e => 
  Math.max(0, e[1] - e[0] + 1) *
  Math.max(0, e[3] - e[2] + 1) *
  Math.max(0, e[5] - e[4] + 1)

let intersectionVol = (c, all) => all
  .map(e => intersect(c, e))
  .map((e, i) => vol(e) ? vol(e) - intersectionVol(e, all.slice(i+1)) : 0)
  .reduce((a,b) => a+b, 0)

ins.reduceRight(([v, all], [on, c]) => [
  v + (on && vol(c) - intersectionVol(c, all)),
  [c, ...all]
], [0,[]])

Giving me 1319618626668022 and a gold star⭐!

Only 3 more puzzles, 2 more beer and I better finish them before the end of the year.

Day 23

I'm a few days late, possibly because I had to climb the whole mont blanc to get to the Sylvanus Blonde. A nice, quite sweet beer with some hints of vanilla. Very easy to drink even though it's not my favourite. Not sure if it's worth the climb and delay, but none the matter it's day 23 for all intensive purposes. And pfew, intensive these advent of codes are getting... but luckily the internet exists to aid me. Today I'm greeted by a group of amphipods to help them solve their own Tower of Hanoi problem.