Lobby Layout
2021-01-06
Original Prompt Part 1
The only real curveball today is the hex grid. Though we know it to be the best shape, a lot of folks (myself included) don’t have a lot of experience working with them programmatically. If today’s puzzle used a standard Cartesian grid, it would be a walk in the park. Luckily, hexes aren’t too different.
Like I mentioned, this was my first time using hexes in a program. We can define the points however we want as long as we’re consistent about it. I turned to an expert on the subject, Amit Patel, and his blog, Red Blob Games. It’s a great resource for algorithms in videogames (and a great place to learn, even if you’re not writing a game). He’s written the veritable bible on hex grids. The whole thing is great, but this is the pertinent bit. He describes a system where each point is expressed as (x, y, z)
and for any point, x + y + z == 0
. He’s included a helpful diagram:
You can click through to his site, as the diagram itself is interactive. It’s a great way to visualize the grid we’re about to write.
If our start point is (0, 0, 0)
, we can assign offsets for each of the 6 directions pictured above (and used in our propt). Immediately to the left (W
) of the start is (-1, 1, 0)
, NW
is (0, 1, -1)
, and so forth. Whenever we parse that string out of input, we want to get offset in that direction:
offsets = { "e": (1, -1, 0), "w": (-1, 1, 0), "se": (0, -1, 1), "nw": (0, 1, -1), "ne": (1, 0, -1), "sw": (-1, 0, 1),}
Grokking that concept is honestly the lion’s share of the work in part 1; the rest of the code is straightforward.
First, We read the next instruction based on its width:
from typing import Set, Tuple
black_tiles: Set[Tuple[int, int, int]] = set()
for line in self.input: tile = (0, 0, 0) i = 0 while i < len(line): if line[i] in ["w", "e"]: width = 1 else: width = 2
Then we use zip
to combine our current tile
with the offset, which can be written very concisely:
... tile = tuple(map(sum, zip(tile, offsets[line[i : i + width]])))
# or offset = offsets[line[i : i + width]] tile = tuple(a + b for a, b in zip(tile, offset))
i += width
Once the while
loop is done, we check if that tile is in black_tiles
. If it is, we remove it; otherwise add it. Our final answer is the number of black tiles:
for line in self.input: ... if tile in black_tiles: black_tiles.remove(tile) else: black_tiles.add(tile)
return len(black_tiles)
Part 2
Hopefully this sounds familiar- it’s another take on Conway’s Game of Life. So similar, in fact, that we can take the same approach as we did in day 17. On each loop, we’ll build a Counter
with each neighbor of each black tile:
from collections import Counter
Point = Tuple[int, int, int]
def calculate_offset(tile: Point, offset: Point) -> Point: return tuple(map(sum, zip(tile, offset)))
def neighbors(tile: Point) -> List[Point]: return [calculate_offset(tile, o) for o in offsets.values()]
# part 1 code, builds the first set of `black_tiles`...
for _ in range(100): c = Counter() for t in black_tiles: c.update(neighbors(t))
Now we start a new set
and walk through the counter to apply the rules from the prompt:
for _ in range(100): ...
tomorrows_tiles = set() for tile, count in c.items(): if tile in black_tiles and (count == 0 or count > 2): # isn't stored as black continue
if tile not in black_tiles and count == 2: tomorrows_tiles.add(tile) continue
if tile in black_tiles: # the rest of the black tiles stay black tomorrows_tiles.add(tile)
black_tiles = tomorrows_tiles
Once we’ve done that loop, len(black_tiles)
again has our answer.