day 17


*slams fists on table* more cellular automata but in 3D (...and in 4D)!! i'm really glad i didn't have much to do today so i could just take a chill day and work on this problem

a lot of people did this one with just coordinates but uh.. i wanted to try to do it like last time for the hell of it. also because i didn't expect part 2 to add another dimension.

the input is one plane (2D), but we need to nest this into another list for the space (3D):

# input
with open('17.txt', 'r') as file:
    input = file.read()
# turn the input into a list, one element is one row of cubes
input_list = list(input.split('\n'))
# turn each row into a list, so cube_start has a list of lists
# (cube_start is a plane)
cube_start = [list(row) for row in input_list]
# one more dimension to get the space
cubes = [cube_start]

as with day 11, we've been given some states: # for an active cube, and . for an inactive cube. i introduced two more again: I for currently inactive but will become active, and A for currently active but will become inactive. 

the overall space is arbitrarily large, but we can make things easier by looking at the rules given:

  • if a cell is active, and the number of active cells around it is NOT 2 or 3, it becomes inactive
  • if a cell is inactive, and it is surrounded by exactly 3 cells, it becomes active

the next state of a cube is determined only by the number of active cells surrounding it, and as such we're fine so long as we start off each step (a boot cycle, within the context of the problem) with a space where every outermost cube is inactive. i wrote a function that expands the space we're looking at (cubes) in every direction, by adding a cube to the end of each row (covering left and right), a row to the end of each plane (covering top and bottom), and a plane to each end of the space (covering front and back):

def add_environment(cubes):
    Z = len(cubes) # number of planes in a space cubes
    Y = len(cubes[0]) # number of rows in a plane cubes[i]
    X = len(cubes[0][0]) # number of cubes in a row cubes[i][j]
    # a cube is cubes[i][j][k]
    # surround each row
    for z in range(0,Z):
        for y in range(0,Y):
            cubes[z][y] = ['.'] + cubes[z][y] + ['.']
    X += 2 # added two more cubes to each row
    # surround each plane
    for z in range(0,Z):
        cubes[z] = [['.']*X] + cubes[z] + [['.']*X]
    Y += 2 # added two more rows to each plane
    cubes = [[['.']*X for y in range(0,Y)]] + cubes + [[['.']*X for y in range(0,Y)]]
    Z += 2
    return cubes

here, the expression ['.']*3 would give the list ['.', '.', '.']. to go up another level, i had to use list comprehension, since [['.']]*2 would give two pointers to the same list ['.'], whereas [['.'] for i in range(0,2)] has two distinct elements.

i modified my code from day 11 to get the states of adjacent cubes (this will probably display poorly bc itch.io will make it really narrow):

def get_adj(x,y,z):
    offsets = [-1,0,1]
    adj_states = []
    Z = len(cubes)
    Y = len(cubes[0])
    X = len(cubes[0][0])
    for i in offsets:
        for j in offsets:
            for k in offsets:
                if x+i in range(0,X) and y+j in range(0,Y) and z+k in range(0,Z) and (not (i,j,k) == (0,0,0)):
                    adj_states.append(cubes[z+k][y+j][x+i])
    return adj_states

and i wrote another function to change the cube at position (x,y,z) - given by cubes[z][y][x] - based on its state and the number of active cubes surrounding it:

def alter_cube(x,y,z):
    cube = cubes[z][y][x]
    adj_states = get_adj(x,y,z)
    if cube in ['#', 'A']: # active
        # if number of active neighbours is NOT exactly 2 or 3
        if not len([state for state in adj_states if state in ['#', 'A']]) in [2,3]:
            # cube is flagged to become inactive
            cubes[z][y][x] = 'A'
    elif cube in ['.', 'I']: # inactive
        # if exactly 3 neighbours are active
        if len([state for state in adj_states if state in ['#', 'A']]) == 3:
            # cube is flagged to become active
            cubes[z][y][x] = 'I'

we now set up the actual calculation, setting step (the number of boot cycles run) to 0, and counting the number of active cubes with some more list comprehension:

step = 0
active_cubes = len([cube for row in cube_start for cube in row if cube == '#'])
print('started with ' + str(active_cubes) + ' active cubes.')

this time we have 6 boot cycles (instead of just repeating it until we hit a stable state), so we have a while loop that continues until step = 6. within this, we add the environment (surrounding the current state with inactive cubes), get the dimensions of the part of the space that we're looking at, flag each cube to be in/active according to the rules, change the state of every flagged cube, and then print out the number of active cubes and increment the step.

while step < 6:
    # start boot cycle
    # get environment and new dimensions
    cubes = add_environment(cubes)
    Z = len(cubes)  # number of planes in a space cubes
    Y = len(cubes[0])  # number of rows in a plane cubes[i]
    X = len(cubes[0][0])  # number of cubes in a row cubes[i][j]
    # a cube is cubes[i][j][k]
    for z in range(0, Z):
        for y in range(0, Y):
            for x in range(0, X):
                alter_cube(x, y, z)
    # after this ends, we need to change I to # and A to .
    for z in range(0, Z):
        for y in range(0, Y):
            for x in range(0, X):
                if cubes[z][y][x] == 'I':
                    cubes[z][y][x] = '#'
                elif cubes[z][y][x] == 'A':
                    cubes[z][y][x] = '.'
    step += 1
    active_cubes = len([cube for plane in cubes for row in plane for cube in row if cube == '#'])
    print('done ' + str(step) + ' boot cycle(s) - we have ' + str(active_cubes) + ' active cubes')

for the second part, it's the same rules but with another dimension added. there isn't too much to comment on here

# input
with open('17.txt', 'r') as file:
    input = file.read()
input_list = list(input.split('\n'))
# plane (2D)
hypercube_start = [list(row) for row in input_list]
# one more dimension to get the space (3D)
space = [hypercube_start]
# another dimension to get the hyperspace (4D)
hypercubes = [space]

adding the surrounding inactive hypercubes (featuring some chaotic list comprehension for adding two more spaces to the hyperspace -  i imagined ):

def add_environment(hypercubes):
    W = len(hypercubes) # number of spaces in a hyperspace
    Z = len(hypercubes[0]) # number of planes in a space
    Y = len(hypercubes[0][0]) # number of rows in a plane
    X = len(hypercubes[0][0][0]) # number of hypercubes in a row
    # a hypercube is hypercubes[][][][]
    # surround each row: one . either side (L and R)
    for w in range(0,W):
        for z in range(0,Z):
            for y in range(0,Y):
                # hypercubes[w][z][y] is a row
                hypercubes[w][z][y] = ['.'] + hypercubes[w][z][y] + ['.']
    X += 2 # added two more hypercubes to each row
    for w in range(0,W):
        for z in range(0,Z):
            # hypercubes[w][z] is a plane
            hypercubes[w][z] = [['.']*X] + hypercubes[w][z] + [['.']*X]
    Y += 2 # added two more rows to each plane
    for w in range(0,W):
        # hypercubes[w] is a space
        hypercubes[w] = [[['.']*X for y in range(0,Y)]] + hypercubes[w] + [[['.']*X for y in range(0,Y)]]
    Z += 2 # added two more planes to each space
    hypercubes = [[[['.']*X for y in range(0,Y)] for z in range(0,Z)]] + hypercubes + [[[['.']*X for y in range(0,Y)] for z in range(0,Z)]]
    W += 2 # added two more spaces to the hyperspace
    return hypercubes

getting the states of adjacent hypercubes was an easy change, just another for loop and two condition changes:

def get_adj(x,y,z,w):
    offsets = [-1,0,1]
    adj_states = []
    W = len(hypercubes) # number of spaces in a hyperspace
    Z = len(hypercubes[0]) # number of planes in a space
    Y = len(hypercubes[0][0]) # number of rows in a plane
    X = len(hypercubes[0][0][0]) # number of hypercubes in a row
    for i in offsets:
        for j in offsets:
            for k in offsets:
                for l in offsets:
                    if x+i in range(0,X) and y+j in range(0,Y) and z+k in range(0,Z) and w+l in range(0,W) and (not (i,j,k,l) == (0,0,0,0)):
                        adj_states.append(hypercubes[w+l][z+k][y+j][x+i])
    return adj_states

for changing a particular hypercube, it was just a matter of switching coordinates from (x,y,z) to (x,y,z,w) and [z][y][x] to [w][z][y][x]:

def alter_hypercube(x,y,z,w):
    hypercube = hypercubes[w][z][y][x]
    adj_states = get_adj(x,y,z,w)
    if hypercube in ['#', 'A']: # active
        # if number of active neighbours is NOT exactly 2 or 3
        if not len([state for state in adj_states if state in ['#', 'A']]) in [2,3]:
            # hypercube is flagged to become inactive
            hypercubes[w][z][y][x] = 'A'
    elif hypercube in ['.', 'I']: # inactive
        # if exactly 3 neighbours are active
        if len([state for state in adj_states if state in ['#', 'A']]) == 3:
            # hypercube is flagged to become active
            hypercubes[w][z][y][x] = 'I'

and the main loop was also just a matter of switching coordinates and adding an extra layer to the list comprehension used to count individual hypercubes (since now a hypercube is in a row in a plane in a space in a hyperspace)

step = 0
active_hypercubes = len([hypercube for space in hypercubes for plane in space for row in plane for hypercube in row if hypercube == '#'])
print('started with ' + str(active_hypercubes) + ' active hypercubes.')
while step < 6:
    # start boot cycle
    # get environment and new dimensions
    hypercubes = add_environment(hypercubes)
    W = len(hypercubes) # number of spaces in a hyperspace
    Z = len(hypercubes[0]) # number of planes in a space
    Y = len(hypercubes[0][0]) # number of rows in a plane
    X = len(hypercubes[0][0][0]) # number of hypercubes in a row
    for w in range(0,W):
        for z in range(0,Z):
            for y in range(0,Y):
                for x in range(0,X):
                    alter_hypercube(x,y,z,w)
    # after this ends, we need to change I to # and A to .
    for w in range(0,W):
        for z in range(0,Z):
            for y in range(0,Y):
                for x in range(0,X):
                    if hypercubes[w][z][y][x] == 'I':
                        hypercubes[w][z][y][x] = '#'
                    elif hypercubes[w][z][y][x] == 'A':
                        hypercubes[w][z][y][x] = '.'
    step += 1
    active_hypercubes = len([hypercube for space in hypercubes for plane in space for row in plane for hypercube in row if hypercube == '#'])
    print('done ' + str(step) + ' boot cycle(s) - we have ' + str(active_hypercubes) + ' active hypercubes')

this solution isn't the most flexible for adding dimensions, so i'd probably go with just storing the coordinates of active (hyper)cubes if it were generalised to n dimensions

Files

17a.py 5.1 kB
Dec 17, 2020
17b.py 5.8 kB
Dec 17, 2020

Get aoc 2020

Leave a comment

Log in with itch.io to leave a comment.