Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

Tips for Advent of Code

Second week of Advent of Code is in the books! If you wanna check my solutions, they can be found at hamatti/adventofcode-2021 with commentary and documentation.

As I've been solving this year's Advent of Code puzzles, discussing them with others, reading other people's solutions and helping with a lot of juniors with debugging and solving issues, I've noticed a few patterns that I think can make your puzzle solving easier. Some of these tips are great for other programming too outside puzzle solving.

Start with the example input

One thing that makes Advent of Code a premium puzzle challenge platform is the quality of specs and example inputs and outputs (not to mention the captivating lore!). For simpler puzzles, they provide a single input-output combo that you can test towards but for more complex ones (like Day 12, 2021) they provide multiple sets of examples and for some (like Day 6, 2021) they provide output examples for different steps of iteration.

My recommendation is to always build your solution with this input. The real puzzle input is usually so large that it's near impossible to manually verify steps but with these contrived examples that's definitely doable.

This is one piece of advice that I've been bit lazy with this year to be honest. After writing this blog post, I refactored my utility function to have support for loading example data (named day_[nro]_example.txt compared to day_[nro].txt for real input).

Utility functions to read inputs

This one is something I do very diligently to make puzzle solving less worrysome but that I don't often see others do that often.

Each day, you're required to read in an input from a file and then perform some computation on the data. This means that 25 times during December, you're essentially doing the same thing over and over again.

I like to hide that into a separate, reusable part of my codebase each year. Most of the days, the input is one-input-per-line:

15
14
16
71
304

for which I have a read_input function in my utils.py that accepts a transformer function that is run on each line (for example in the above, I'd run int to turn everything into numbers before it arrives to my puzzle solver).

Some days, you'll see a multisection input (sections separated by empty line):

KHSNHFKVVSVPSCVHBHNP

FV -> H
SB -> P
NV -> S
BS -> K
KB -> V
HB -> H
NB -> N
VB -> P

for which I have read_multisection_input function in utils.py that accepts a list of transformers to transform each section to a format I want it to be.

And some days, the input is just too custom for a generic solution so I write a custom input reader.

Doing it like this achieves two things:

  1. I don't have to think about parsing each day
  2. I get the data in the right format so I can have higher confidence level that my parsing is not the cause of bugs

I also like that it hides implementation details from the puzzle solving to a utility library.

Assertions to protect from later mistakes

If you're doing proper automated tests for your puzzles, then this can part can be skipped. But for most, unless doing tests is what you're focusing on as a technique, it's quite likely people are not doing full on TDD on these puzzles.

Most scripts print out the final result into terminal and then copy-paste from there to the website.

But then you go towards the second part or you do some refactoring to clean up your code and change things in your functions. Suddenly, it might be that the result is no longer correct.

After I get a correct result, I like to add an assert like this:

result = solve(input)
assert result == 102
print(f'Solution: {result}')

next to my print call. This way, if I change something that causes incorrect behaviour, my code will throw an uncaught error and break.

Just the other night I was helping a friend debug their part 2 solution for a puzzle and it turned out they had refactored something after part 1, didn't test it with part 1 again and assumed everything inside that helper function was correct because they had originally gotten the right result.

An assertion there would have saved them from a lot of trouble.

Assumptions are always the root of evil but we make them even when we try not to so let's not trust ourselves and let's make the computer keep us in check.

Debug like a champion

I've written a longer piece on debugging earlier so I won't go through everything here but I do want to share some here because most of you won't read the linked guide anyway.

  1. First rule of debugging is that you're never good enough not to debug.
  2. Second rule of debugging is that you need to leave any and all assumptions at the door.

Often, when people get a wrong result, I see the same behaviour (regardless of their seniority level): people keep looking at the end result and make somewhat random changes (based on educated guesses on where the problem might be at).

Looking at your code is pretty much the last thing you should be doing when you end up in debugging mode. Adding debugging statements or print calls is the way to go: your priority number 1 is to find out where things start going wrong.

If you have split up things into functions, verify (not just by reading the code but by printing out values (or by writing tests)) that each of those function correctly. Until you've done that, you don't know they do.

Then, instead of running the required 20, 40 or 100 iterations, only run one and see if you're getting a verifiably correct result. Then run the next iteration only and see if it still holds.

Create a smallest possible example you can to test out a single part of your code. Run only that and not the entire thing. We don't care about the result of the puzzle solver at this point. We just care that individual lines of code and individual functions work correctly.

Share inputs with friends: it's sometimes helpful to ask a friend to run their correct solution with your input to double check that the input is good; or to run your friend's input with your code to see where the solution differs.

Printer helpers for grids

Some of these puzzles deal with grid systems and coordinates. Inside the code, these are often computed with (x, y) points and/or lists of lists (or numpy arrays in Python).

While it's great system for a computer, it's hard to see if

[[True, False, True, False, False], [False, False, True, False, True], [True, False, False, True, True], [True, True, True, True, False], [True, False, True, False, True]]

is what we want our grid system to be.

Building a helper function just for printing out the grid for debugging purposes is a great approach:

def print_grid(grid):
  for row in grid:
    for col in row:
      if col:
        print('#', end='')
      else:
        print('.', end='')
    print()

print_grid([[True, False, True, False, False], [False, False, True, False, True], [True, False, False, True, True], [True, True, True, True, False], [True, False, True, False, True]])

## Prints:
#  #.#..
#  ..#.#
#  #..##
#  ####.
#  #.#.#

Split your code into smaller functions

Often Advent of Code puzzles are such that you can write a single script with no extra functions to run the calculations and get a result.

And that's perfectly fine and good, until your code doesn't work. Because debugging a long code that does multiple things is very difficult.

Improves debuggability

A good example of this is Day 13, 2021 in which we had to fold transparent paper multiple times. It's possible to run these folds without separating them into a function but testing that out becomes challenging.

Creating a fold function that takes an input and produces an output gives you a superpower of being able to run a single fold, irregardless of the rest of the puzzle. So you can handcraft an input, run it through fold, print the output and compare it with your notes.

Pure functions (meaning functions that don't have side effects and always create the same output with the same input) are really nice for that very reason.

Helps with part 2

Over the years I've participated in Advent of Code, I've kind of learned to guess what kind of changes happen in part 2 and that influences what kind of code I write in part 1.

A good example of this is Day 9, 2021 where we first find low points and then count areas of subareas. In part 1, the output is just a number of these low points but I like write functions like find_low_points in Day 9 that return a list of those points rather than just a number of them.

So when part 2 rolls in and we need a list from part 1, I can just call that function and continue without having to rewrite the loops and operations from part 1 like I would have if I just ran flat scripts without dividing to sub functions.

You can test them

If you're running automated tests, it's nice to test not only the end result but also the individual helper functions.

This year there has been multiple puzzles where we've ended up creating a 2D coordinate system and then moving values around or doing calculations on cells and it can get complex quite fast – and it's hard to find the issues in complex code.

Talk & share, ask for help

My favorite part of Advent of Code is the social community aspect. Pretty much every developer community I'm active in is buzzing with discussions and people sharing their solutions and us debugging problems together.

Twitter is also another place where I've found a lot of good discussions, different solutions and especially in 2020 when I was learning Rust, I got so much help from the Rust community in Twitter when sharing my solutions and blog posts and asking for help.

When everyone is working on same problems at the same time, there's immense power in reading through other's solutions even if they are in different programming language than what you use. Since you don't have to understand a new domain or context (like you'd have to in real-life problems and solutions), it's easier to focus on the puzzle solving part.

Bonus Tip: Write a blog

It's not a secret that I'm an ambassador for developers writing blogs. There's so much that you can learn from writing for others.

Last year I wrote three weekly blog posts about my learning journey and struggles of doing Advent of Code with Rust and this year I've been writing a few blog posts but also experimenting with writing about stuff in Jupyter Notebooks with my solutions as mini blogs. Each day, I not only talk about how I approached the problem but often share tidbits of Python knowledge about different standard library functions that are handy for Python developers.

Split between solving the puzzles and writing the other stuff around it, more than half of my time every day goes to writing and research (including testing out how things work). Each day I learn a bit more and I strengthen my basic skills with the language and programming because I need to explain them to others.

I also like how it improves my code. Many of my best refactorings happen when I write and explain what my code does and then realize that there's a better way to express that same thing in code. When I write code, I'm in a functional mode trying to make things work but when I write for people, I'm in a communication mode trying to make things understandable and that really helps improve my code.