Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

Advent of Code 2023 retrospective

Last month was spent once again helping elves figure out this Christmas thing. I’m of course talking about Advent of Code, developers’ puzzle advent calendar by the wonderful Eric Wastl. As I’ve done the past few years, I solved the puzzles with Python using Jupyter Notebook and wrote explanations of my solutions with Python education sparkled in.

This blog post contains spoilers for some of the puzzles so if you don’t want those, head out to /blog and find another blog post to read.

By the 25th day, I reached 36 stars. I did not even take a look at the puzzles for days 20-25 though as I was spending those days preparing for and celebrating Christmas with the family. I might revisit them though on a better time.

This post is a collection of thoughts that crossed my mind during these 25 days.

Philosophical ponderings: what makes a number?

The first day kicked off the month with quite a puzzle. I can’t remember when early day puzzles have generated as much discussion as this one - especially the kind of more philosophical discussion. In the puzzle’s first part, we were tasked with finding the first and last digit in each line. For example, a line ghd4kjsdfksd78 would have resulted in the first one being 4 and last 8.

In the second part, there was a twist (as usual). This time,

It looks like some of the digits are actually spelled out with letters: one, two, three, four, five, six, seven, eight, and nine also count as valid "digits".

This means something like onegjdf7sdjs12 would result in 1 (from the one) and 2 (from 2).

What was not mentioned though was that these numeric strings could over lap: onegjdf7sdjs1eightwo would result in 1 (from one) and 2 (from two).

I argued, to very little success in convincing others, that there are three different ways to interpret this:

  1. Start from beginning until you find the first number (in this case, one). Then start from the end to find the last (two). Combine these two.
  2. Start from beginning, convert written numbers to digits and then take the first and last. In my example, this would have created a string of 1gjdf7sdjs18wo and resulted in 1 and 8.
  3. Start from the end, convert written numbers to digits and then take the first and last. In my example, this would have created a string of 1gjdf7sdjs1eigh2 and resulted in 1 and 2.

From a pure perspective of puzzle solving, I can see this being argued but I do disagree that it would have been a straight-forward case.

If we look at a simpler example: eightwo. How many numbers are in that string? The case 1 argues there are two: 8 and 2. Case 2 argues there’s only 8 and case 3 argues there’s only 2.

Given the quoted instruction, I’m arguing that the only cases could be 8wo or eigh2. Even with elves, I’d argue that there’s no way to reach a string eightwo by spelling out digits with letters.

This doesn’t take anything away from the puzzle but in general, I feel like this was a bit sloppy, especially if this was (as I believe it to be) intentional. I believe a good puzzle doesn’t have any tricks outside its description. A lot of “traditional” puzzles were full of these gotchas that required outside information and I never liked those. For me, a good puzzle is one where the difficulty comes from inside a well-defined puzzle.

I like to parse input into well-defined data structures

Many programming puzzles outside Advent of Code are created in a way where near-optimal performance is a minimum requirement. I like Advent of Code because it’s not a focus. Sometimes, the second part of the puzzle requires a bit more performant solution than straight-forward solution but often you can safely solve them with a piece of code that you would write in your normal day-to-day programming.

In day 2 for example, I created a data structure as follows:

from collections import namedtuple

Game = namedtuple('Game', ['id', 'rounds'])
Round = namedtuple('Round', ['blue', 'red', 'green']) 

and then parsed the inputs into these Games and Rounds.

I did spend a bit of extra time in the parsing into data structures phase but it made especially the second part of the puzzle rather simplistic to solve and even more importantly, easy to read:

def calculate_power(game):
    min_green = max(rnd.green for rnd in game.rounds)
    min_blue = max(rnd.blue for rnd in game.rounds)
    min_red = max(rnd.red for rnd in game.rounds)
    return min_green * min_blue * min_red

In puzzles, these are less needed as the code is run once, the scope of the code is smaller and the time investment in the beginning can be easily argued is too much. But in real life it’s very valuable as that investment happens usually once (with small iterative improvements) and it gets used and referenced in the code base over and over again.

re.finditer finds values and their locations

So many good things in early days. In day 3, after using other methods and then discussing it with friends, I discovered that Python’s regular expression library has method finditer that would have been really handy for this.

import re

line = '...345....44..'
numbers = re.finditer(r'(\d+)', line)
for number in numbers:
  print(f'''
    Number: {number} starts 
    at index {number.start()} 
    and has {len(number.groups(0)[0])} digits.
  ''')

I should use it more!

Python defaulting to iterator-based functions was a great move

Python’s move from major version 2 to 3 was not the smoothest operation and caused quite a bit of hassle. Looking back now, 15 years later, I’m happy we made it.

One change that was made was to move from range/xrange style double functions to dropping the extra functions and making the base functions (like range and zip) return iterators instead of lists.

Personally, I was way more junior 15 years ago than I am now and it didn’t feel like this was big back then. This December I realized how great this was. Since moving to 3.0, I haven’t had to think about these at all.

Last month I was reading through some PHP solutions where the code read exactly the same on the surface as my Python solutions but they ran into memory issues due to the arrays/lists being looped over grew too big. With Python 3.10, I never had to think about it once because so much of standard library returns a generator and “just works”.

Making connections between available tools and abstract problem settings

On the seventh day, I solved the Camel Cards game puzzle by relying on muscle memory. I’ve written similar code for poker hands so many times during my studies and teaching. I used classes with inheritance and wrote individual functions for each different hand.

When I saw Toni’s solution that used Counter.most_common and pattern matching, I was at awe:

def hand_score(hand):
    cards, _ = hand
    scores = [score(c) for c in cards]
    cnt = Counter(cards)
    match cnt.most_common():
        case [(_, 5)]: return (Hand.FIVE_OF_A_KIND, scores)
        case [(_, 4), _]: return (Hand.FOUR_OF_A_KIND, scores)
        case [(_, 3), (_, 2)]: return (Hand.FULL_HOUSE, scores)
        case [(_, 3), _, _]: return (Hand.THREE_OF_A_KIND, scores)
        case [(_, 2), (_, 2), _]: return (Hand.TWO_PAIR, scores)
        case [[_, 2], _, _, _]: return (Hand.ONE_PAIR, scores)
        case [_, _, _, _, _]: return (Hand.HIGH_CARD, scores)

I think this function is so beautiful. Each different case is solved with just one pattern case.

I used Counter myself too and I knew most_common() exists and how it works but it did not even pop to my mind that I could use it to solve this particular problem.

To become better at these is mostly a question of experience with different kinds of problems and different kinds of tools.

The negative side of Advent of Code

Advent of Code is not all just roses and fun times. Challenging puzzles and thriving community can lead to us putting too much pressure on ourselves and getting lost in the journey. Two years ago, Advent of Code was a way for me to completely drown myself into something when I was on a burnout sick leave and didn’t want to worry about real life.

Zoe Aubert’s blog post Advent of Code is not healthy for me had a lot of thoughts that resonated very much with me. I sometimes struggle with coming to terms with the fact that I can’t solve all the puzzles. Which is weird because I know that I’m not a great developer and especially not great at puzzles so the lower expectations don’t match my emotional response to it.

My impostor syndrome also kicks in often with Advent of Code. I write open my puzzle solutions and sprinkle in educational Python and puzzle solving content so when I fail to even solve a puzzle, I feel like such a fraud: “Who am I to teach anything when I can’t even solve these all myself”. I know it’s not like that and explaining the things I know and can solve is a net positive even if I can’t reach 100% of the stars.

And while in life I’ve gotten much better at not trying to complete everything I start, with Advent of Code, due to its social and community aspect, the threshold do make that decision is higher than it should be. This year, it hasn’t (yet) been an issue but in the past few years, I think I’ve pushed a bit too much at the expense of my mental health.

Other people’s solutions I enjoyed

During the December, after finishing my own solutions, I read solutions and explanations of bunch of other developers who wrote them down and published. Here are some and the languages they used to solve puzzles.

I had a great time this year

All in all, 2023 was a great year with Advent of Code for me. The puzzles provided a nice challenge and a brewing ground for discussions without being too much. I usually took a few hours in the morning to go through them, writing code, then writing explanations and doing a few rounds of refactoring.

Comments

Comment by replying to this post in Mastodon.

Loading comments...

Continue discussion in Mastodon »