9
votes
Day 16: Packet Decoder
Today's problem description: https://adventofcode.com/2021/day/16
Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python
with any of the "short names" listed in this page of supported languages):
<details>
<summary>Part 1</summary>
```python
Your code here.
```
</details>
This kind of thing is exactly up my alley, and I really enjoyed working on this puzzle.
Unfortunately, this sort of bit-level work isn't really a strong suit of Ruby, so I ended up "cheating" by just converting the input to a string of
'0'
and'1'
characters in memory, which was a little easier to work with.There's a lot of extra code and helper functions this time, so I'll try to thread some extra comments in to explain things as I go.
Parsing
I messed around a little bit with bitwise operators, but quickly decided that it'd be simpler to just convert the whole input to a string of
'1'
s and'0'
s and work with that. I couldn't just usehex.to_i(16).to_s(2)
, because I'd lose the leading zeros that are actually important, so I use the string interpolation/formatter to make sure that the string is 0-padded to the right multiple of 4.Once we have the bit-string in memory, it's not hard to write the helpers for packet version and type, as well as the logic to read groups of bits for the variable-length literal values.
Note that
parse_literal_groups
returns the parsed value, and also the number of bits used. This is important later when we need to know where a packet ends.This gives us enough to parse a complete "type 4"/literal packet:
Here we take an offset/index into the bit-string and use the helpers to parse out the fields of the packet, keeping track of how many bits we've used so that we can return the full size of the packet along with a hash object representing its properties.
Next up are "operator" packets, which are a little more complex since we need to account for nested packets (and two different ways to encode the number/size of those packets).
Note that now we can actually define a general top-level
parse_packet
function that can be used for any kind of packet.Once we have everything we need to parse our bit-string into a tree of packet objects, we can actually solve the puzzle.
Part 1 Ruby
Part 2 Ruby
Now that everyone who solves this has a functioning AST parser and evaluator, there's a chance that some future puzzle will involve extending this format to something Turing-complete, which would allow for some pretty cool puzzles a-la Intcode from 2019. Here's hoping :p
After being Big Mad about yesterday's problem I really enjoyed this one. It was much more satisfying, for me at least. It definitely took a few tries to understand part of the problem description though
Day 16 Part A - Python
I built a recursive read packet function that basically returns anything it doesn't use. Just sliced everything up with a bunch of string manipulation and walked my way through the nested packets
Day 16 Part B - Python
Part A left me in a really good position, I just updated my return functions to pass the values between the levels, gathered them for anything with sub packets and added the operation to the end so that it could work with any gathered subpackets. It worked great
Tips
It took me several reads to figure out what the hell the problem was asking and how to divide up sub packets. I particularly got stuck on the "38006F45291200" example. The first 11 bits are a fully formed packet and the next 16 are what's left over. And you can tell the difference between them because the first one ends
For the most part you can't tell how long a packet is until you're done reading it. You need a way to keep track of what is and isn't being used so you don't lose what you're not looking at at the moment
Make sure not to lose track of your leading zeros when you convert from hex to binary. It will definitely mess up your results, if you do lose them you should be able to add them back in
For me, recursion handled the bulk of traveling between different packet levels.
For part B, each packet will only ever pass one value up
I think this code will get expanded upon in later challenges, but I'm not sure
Question
I'm still on part 1 and having a bit of trouble totally understanding the premise.
For reading literal values, is the idea that you don't know how long the value is, so you have to try multiple options? i.e., try to read it as though each of the 4 chunks is 2 bits long (checking for 1, 1, 1, and 0 leading chars), then try to read it as though each of the 4 chunks is 3 bits, then each of the chunks as 4 bits, etc, until the pattern of leading digits 1, 1,1, 0 is met?
Yeah, the problem description definitely could have used a few more editorial passes. For the literal values you look at 5-bit chunks. The leading bit determines if you keep reading or it this will be the last one. The other 4 bits are data.
Edit: You keep going until you hit a 0 leading bit
Oooh it's groups of 4 (5) bits, not 4 groups. Gotcha--that's much more straightforward. So glad I asked rather than waste time trying to implement what I thought it was. Thanks!
Edit: oof I really just needed to read more carefully. It's pretty clear my original interpretation was wrong given even the example didn't have 4 groups of bits.
Elixir
Fun! This problem is something that Elixir (or really Erlang, which Elixir is built on top of/interops with) has batteries-included support for. It has a bitstring type and a whole special subset of pattern matching syntax that makes for fast, concise, declarative code for parsing bitstrings. This strong support exists because Erlang was purpose-built for low latency communication gateways and distributed systems.
Runtime: My solution runs in about 260 - 270μs for each part.
Both parts
Day 16 (Rust)
What a mess. But it seems like this is what you would do to parse a real-world file-format such as .PNG, so I'm kind of happy with it.
Data structures
Parsing...
Edit: I refactored this after the fact; I think it's reasonably clean now.
Uses the
hex
crate for parsing the hex string into a Vec<u8> and thennom
s it with a bit-level parser. This is probably fast, but it (a) took a while to write and (b) its verbose. I really hope this can be re-used tomorrow...Helper functions
Solving
I love parsing and language evaluation stuff.
Something really wrong with me today though. Had to put it down, step away and return later. Ranked 10103/10995. I'm still upset and embarrassed.
No Comment
Rust
Many thanks to PapaNachos, without whom I'd still be trying to parse the literals.
Edit: Cleaned up more and added tests.
Rust