Advent of Code CLI Client and Library
Maximizing the fun of Advent of Code by removing the tedious parts.
Mariah Carey has awakened from her yearly slumber. Shops are frantically tearing down pumpkins and putting up hollies and reindeer. It is time. Brace yourselves, Christmas is coming.
Advent of Code is a fantastic community event happening each December (starting in 2015). Each day until the 25th a new puzzle unlocks - they get harder as the month goes. They always have two parts, you can access the second part after solving the first. It's language agnostic, hell, sometimes it's quicker to solve it on paper by hand.
There are, however, some bits that are not so enjoyable for me. I started with a simple setup to not write logging every day. Then I figured out there are a couple of libs I'd like to write so I don't have to spend time in off-by-one error hell. And many many more.
So, I wrote a tool to help me out. Since to solve the Advent of Code you need to code, it won't be a classic packaged CLI tool, but rather a repository to be cloned and used. It will only interest programmers anyway. Of course, it started from my cookiecutter template.
Advent of Code From CLI
The first part to tackle is a click-based CLI tool that wraps up some functions and interactions. Let's get into it.
Login To Advent of Code
The first thing you need to do is to obtain a token so you are able to communicate with Advent of Code services. Now, I thought about grabbing it from the browser storage, but that's more work (also, chrome encrypts them now). So I stuck with the simple method - send the user to the browser using the standard webbrowser
module, then instruct them to pry the session
cookie from the browser and paste it into the tool.
Due to "historical reasons" I expect this token either in an .env
file or an env variable and I've decided to use the former in this command.
Preparing Files For The Day From Jinja Templates
Since every task follows the same format, I've decided to include a preparation step. Right now I have two Jinja templates - one for the daily solution file and one for the daily test file.
I know that we'll be reading a string with newlines as our only input parameter, so I decided to include a simple parse_data
function to save those 5s of coding it every time:
def parse_data(in_data):
data = list()
for line in in_data.splitlines():
data.append(line)
return data
The test template is slightly more involved - it's a simple format I got quite used to over time. It separates the testing logic from the testing data, gaining 0.3s when writing tests:
# test {{ data.slug }}
import pytest
from {{ data.module }} import part1, part2
def test_part1():
cases = [
{
"label": "small",
"input": {
"in_data": """Lines
"""
},
"output": "part1 output {{ data.slug }}",
"ex": None,
},
]
for case in cases:
try:
output = part1(**case["input"])
assert str(output) == str(case["output"]), "case '{}', output: exp {}, got {}".format(
case["label"], case["output"], output
)
except Exception as e:
assert type(e) == case["ex"], "case '{}', ex: exp {}, got {}".format(case["label"], case["ex"], type(e))
# ... part2 is the same
These are then rendered as Jinja templates into the folder structure (code here).
Dynamically Import the Solution
Once you've written your solution, you need to use it and submit the solution to Advent of Code. That means loading the respective module, grabbing the input, and sending the output.
At least in the usual cases - there are some custom cases that involve drawing pixels or reading letters from ASCII art. I've decided to ignore those.
This was quite new to me as I delved into the nitty gritty of Python imports and functional thinking. In the end, I decided to return a solution function like so:
def get_solution_f(year, day, part):
try:
solve_day = __import__(
get_module_str(year, day),
globals(),
locals(),
[f"part{part}"],
0,
)
return vars(solve_day)[f"part{part}"]
except ModuleNotFoundError as e:
if f"No module named '{get_module_str(year, day)}'" in str(e):
raise ModuleNotFoundError(
f"Solution for {year}, day {day} part {part} not implemented!"
)
else:
raise e
This function should return a function that should solve the part in question. Then there's some quality-of-life logic around it, like solving only the unsolved part, auto-submit options, prompts, some pretty colors, etc. Some of the logic is based on the special day 25 part 2. This part is a simple star check - if you have 49, you can pass, otherwise you can't. All you need to do is to submit a zero.
Date Validation
As for CLI, you need to provide a day and a year so that the tools know how to process it. By default, it checks today's date and it has some more checks to ensure that the date makes sense within the context of Advent of Code. Code here.
These are then used as validator callback functions for the click options.
Advent of Code Web Client
Since the tool needs to communicate with the Advent of Code server, I decided to put the logic into a dedicated class. It's classic web stuff, requests, requests cache, and BeautifulSoup for html parsing. After some back and forth with the website, it was working. The site is very simple and pleasant to automate. If you want to do it yourself, just remember: be nice, polite, and slow.
These are the functions:
get the inputs for the solutions,
submit solutions and get information about the success of answers.
check the current user,
get the leaderboards (global and private),
I've separated the HTML parsing from request logic, so it's slightly easier to read and modify.
Leaderboard Drawing
Speaking of leaderboards, I've decided to include a command for them as well. I am happy with the result, it's a small thing. Probably the most notable is the usage of clicks pager support to display a scrollable leaderboard and not to spam the terminal. Also, colors! Colored stars and highlighting of the current user.
Library for Advent of Code Problems
The other motivation was to have the standard bits prepared. I am fully aware that most of this code is not optimal and there are likely better tools for the job. But the point of the Advent of Code is to code (at least for me) so that's what I am going to do.
Graphs
Graphs have nodes connected with edges. It's one of the most investigated areas in all computer science and for good reason - the applications are plentiful and we need a performant set of algorithms and their implementations. You'll be deciding on connectivity, shortest paths, longest paths, full paths, subgraphs, partitions, etc.
I hate them. I hate implementing them, I don't have good intuition for them, so I'd rather spend my time elsewhere. Even if I muscle through such a problem, I always feel bad, because I know it's not the proper way of solving the problem. I can practically hear my tutors: Oh, your solution is nice and you came with it yourself, but it's O(n^4) so it'll never finish. You should have realized you are dealing with a Dingledong graph so then you can utilize Flumperworths transformation to deploy the optimized Bingabong algorithm that runs in O(n^2*log(n)) and finishes under 20ms.
So, I decided to roll with the NetworkX library and I have my eyes on the Graph tool as well.
2D Vectors, 3D Vectors
A simple set of operations to avoid writing the most common operations for tuples. In the Vector2d module, I am using tuples and in the Vector3d module, I've tried dataclasses. I think I'll rewrite it to tuples in my next Vector3d encounter - you live and you learn.
Pixels and Images
If I had a cent for each time I got a set of coordinates with color to draw an image from, I still wouldn't be able to buy any ice cream, but it'd be damn close.
So this small pixel module does just that. I haven't figured out the testing process for this yet, I'll leave it for future me to address.
Combinatorics
In the combinatorics module, I've got some selected algorithms with more or less naive implementations. The highlight is the Buckles algorithm which enables you to iterate over k-combinations of n elements.
Intervals
In the year 2023, there were a couple of interval-based tasks so this set of methods just appeared. We can check for containment, overlaps, partitions, and the like.
Incode2019
Ah, the Intcode2019 computer from the year 2019. It was so much fun extending this class with new and new functionality. It's an interpreter for the weird integer-assembly language that was present in the inputs of many puzzles in 2019.
Over time it got multiple instructions, threading, ASCII translations, and even a "NIC". I've talked about it some more in the year 2019 recap.
2D Maps
Finally, the one I am most proud of (and recently refactored as well). It's a tool to process maps such as this one:
#######...#####
#.....#...#...#
#.....#...#...#
......#...#...#
......#...###.#
......#.....#.#
^########...#.#
......#.#...#.#
......#########
........#...#..
....#########..
....#...#......
....#...#......
....#...#......
....#####......
Usually, the .
is the empty tile, the #
is an obstacle and there are a couple of other symbols involved (usually a special symbol for the starting position.
The core of the map is the obstacle string and the index-point duality. In some cases, it's easier to walk through the map in line, but sometimes we also need to treat it as 2d coordinates to get nearby points.
Usually, you want to get from point A to B on the map in the shortest amount of steps, to do that we can use the "Flood" algorithm (BFS) - and related methods.
It also contains some helper functions to parse the map, invert the points, draw it for debugging purposes, etc. The more wacky features include raytracing, portals, and finding reflection axes. While I've tried to support diagonal movement, I have yet to use it anywhere in Advent of Code.
A fun extension of this idea is the PipeMap
, where symbols are expanded into 3x3 grids. I needed it for a puzzle in 2023, it might be useful later still.
Conclusion
This project will keep evolving. I am already hyped up for the 2024 Advent of Code, I'll be sure to publish my solutions as I write them (more or less). I don't expect to expand the CLI tool that much, I expect to expand the puzzle library slightly more. I'd recommend everyone to try the Advent of Code. If you are using Python, take a look into my codebase - maybe it'd make your experience more pleasant.
Good luck to y'all in 2024!