Advent of Code 2021, Day 4
Following on from my previous unpost, I took a bit of a break and did day 4 today. Solution on my git repo as always.
The premise is cute. There’s a giant squid attached to the submarine! What could it want? Why, to play bingo, of course!
This makes perfect sense. Giant squid (Architeuthis dux) are well known for their love of games of chance, and their deep sea bingo halls are legendary.
So, the puzzle sets us the challenge of cheating at bingo, so that we can beat the giant squid at its game of choice. This seems unethical at best - the giant squid is our guest, and I feel we should allow it at least a fair chance.
There are a few different ways of approaching this. It can be hard to know which to pick, because one of the quirks of Advent of Code is that you can’t see part 2 of the puzzle until you beat part 1. You might go with one solution because it’s easier for part 1, only to find that a different one could have been extended to part 2 much more easily.
In many ways, this is very much like real software development.
There are two methods that present themselves here:
- go through the sequence of numbers step by step, playing off each board in turn until one wins, effectively simulating a real game of bingo
- run each board in sequence until it wins and record how long it takes
Guessing that part 2 will involve some sort of statistics on the different boards, I decides to go with method 2 and set up code to collect when each board in turn will win, then take the one that wins soonest. It’s less efficient for part 1 but may make things easier on me later.
This method is also easier to write tests for. Thanks to the worked example on the site, I know exactly when board 3 in the test data will win and what its “score” will be when it does, so I can run board 3 in isolation and check its results.
But I’m getting ahead of myself. I first need to actually load the data. For this, I decide to use a Java feature called Streams, which lets Java pretend to be a functional language (it’s very not) and can result in very compact code.
That makes loading the sequence of numbers the data starts with a one-line affair:
Arrays.stream(rawInput.get(0).split(",")).map(Integer::valueOf).collect(Collectors.toList());
There are a few things going on here. First, Arrays.stream
. That turns an array into a stream so you can do the chained function calls that the pattern calls for. To get the array to pass to it, I use rawInput.get(0).split(",")
, which takes the first line of the list I’ve loaded my input file into and splits it into an array of strings across every comma.
The second half of the line is the actual stream processing. First, map(Integer::valueOf)
is functionally equivalent to calling Integer.valueOf(...)
on every element of the stream, then spitting out a new stream of changed values. It’s just like the map operator in a real functional language!
Then, to close it out, collect(Collectors.toList())
wraps the stream up in a neat little List object for me to do stuff with back in procedural Java land. Nice.
I use a similar (but slightly different) line to read off the numbers for each row of the bingo boards that follow, splitting them up by groups of five since I know from the spec that that’s how many rows there are per board.
If I was writing this as a proper, production-ready piece of code, I’d check that assumption, watching for the double line break that separates boards in the input data instead of just blindly taking groups of five lines. Buuuut I can’t be bothered today.
With the draw sequence and boards loaded, it was then time to implement actually playing each board. I decided to make this a method on the BingoBoard object itself, leaning into the object-oriented way of doing things. The contract of the method is pretty simple: given a list of drawn numbers, it’ll play the board, then spit out how many draws it takes for the board to win.
In the end, I didn’t do anything special for this. I just took the naive, brute-force approach: for each draw, check whether all the numbers of each row and column have been drawn, and decide we’ve won if one has.
Look, it’s Monday afternoon and I’ve had a long year, what do you want from me?
From there compute the board’s “score” (a mostly arbitrary number consisting of the sum of all the boards numbers that haven’t been drawn) and return both numbers for the main function to do its thing with.
And what’s its thing? Collect all the results together, and pick the one with the minimum draws-to-win. All that work, and we throw most of it away. Multiply the last number drawn by the score to get my answer, 74320, job done.
I don’t really like cheating the squid, but there’s the answer it wants. Now, what will part 2 bring?
Oh. Part 2 has us let the squid win. I feel better about this now. It’s also really easy to do - same as part 1, but take the board with the maximum finish time rather than the minimum, simple. 17884.
I was right about this method being easier to extend, but I’m almost disappointed that’s all it was after imagining doing some sort of statistics on the whole board set.