Advent of Code: Day 18

Source

Part 1: Simulate Conway's Game of Life. Count how many lights are on after 100 iterations.

class Grid(object):
    def __init__(self, str):
        self.data = [
            [(char == '#') for char in line]
            for line in str.split()
        ]
        self.width = len(self.data)
        self.height = len(self.data[0])

    def __getitem__(self, pt):
        row, col = pt
        if 0 <= row < self.height and 0 <= col < self.width:
            return self.data[row][col]
        else:
            return False

    def neighbors(self, row, col):
        return sum(
            (1 if self[row + row_delta, col + col_delta] else 0)
            for row_delta in range(-1, 2)
            for col_delta in range(-1, 2)
        ) - (1 if self[row, col] else 0)

    def step(self):
        new_data = copy.deepcopy(self.data)

        for row in range(self.height):
            for col in range(self.width):
                if self[row, col]:
                    new_data[row][col] = (2 <= self.neighbors(row, col) <= 3)
                else:
                    new_data[row][col] = (self.neighbors(row, col) == 3)

        self.data = new_data

    def __repr__(self):
        return '\n'.join(
            ''.join(
                '#' if self[row, col] else '.'
                for col in range(self.width)
            )
            for row in range(self.height)
        )

    def __len__(self):
        return sum(
            1 if self[row, col] else 0
            for row in range(self.height)
            for col in range(self.width)
        )

if __name__ == '__main__':
    grid = Grid(sys.stdin.read())

    for i in range(int(sys.argv[1])):
        grid.step()

    print(grid)
    print(len(grid))

I don’t use actually use object oriented programming that much, but this problem just called for it. It let me abstract away the counting of accessing of elements while dealing with edge cases. Also, it will be even more helpful in part 21.

The three interesting bits are the __getitem__, neighbors, and step functions. The first is interesting, since it actually takes a tuple as as argument, but looks like it takes two parameters: grid[row, col] is actually equivalent to grid[(row, col)], since it’s actually the , that makes a tuple, not the parenthesis.

Next, neighbors will count up the positive neighboring elements using __getitem__. We subtract the one at the end if the node is enabled to account for the fact that we counted the node as its own neighbor.

With all of that, step becomes much easier. We just have to loop and using the neighbors function. Shiny.

Part 2: Repeat, but this time the four lights in the corners will be stuck on.

This is another reason why I wanted to write this in an object oriented style (in highsight):

part1 = imp.load_source('part1', 'part-1.py')

class FixedCornerGrid(part1.Grid):
    def __getitem__(self, pt):
        row, col = pt
        if (row in (0, self.width - 1) and col in (0, self.height - 1)):
            return True
        else:
            return part1.Grid.__getitem__(self, pt)

if __name__ == '__main__':
    grid = FixedCornerGrid(sys.stdin.read())

    for i in range(int(sys.argv[1])):
        grid.step()

    print(grid)
    print(len(grid))

The weirdness comes from the fact that I for some reason decided to use dashes in my filenames. Otherwise, I could just have done import part1. But other than that, we only have to slightly tweak the __getitem__ behavior for the four corners. Other than that, we just directly reuse all of the code from the original Grid class.

I actually really like how this turned out. It brings a slightly new twist to a problem I’ve solved a half dozen or more times before.


  1. Spoilers ↩︎