I feel like

`if (x != self.N and y != self.N):`

s += self.old_grid[x][y]

# The remaining branches handle the case where the neighbour is off the end of the grid.

# In this case, we loop back round such that the grid becomes a "toroidal array".

elif (x == self.N and y != self.N):

s += self.old_grid[0][y]

elif (x != self.N and y == self.N):

s += self.old_grid[x][0]

else:

s += self.old_grid[0][0]

could've been

`int newX = x % self.N`

int newY = y % self.N

# We need to make sure that the neighbor is still on the grid, so we make it wrap around using modulo.

s += self.old_grid[newX][newY]

saves a bunch of lines.

it uses the modulo (%) operator, which in x % y results in x wrapping around within range y, exactly what you want.

`if (self.old_grid[i][j] == 1 and live < 2):`

self.new_grid[i][j] = 0 # Dead from starvation.

elif (self.old_grid[i][j] == 1 and (live == 2 or live == 3)):

self.new_grid[i][j] = 1 # Continue living.

elif (self.old_grid[i][j] == 1 and live > 3):

self.new_grid[i][j] = 0 # Dead from overcrowding.

elif (self.old_grid[i][j] == 0 and live == 3):

self.new_grid[i][j] = 1 # Alive from reproduction.

"continue living" seems like a useless line, doesn't change anything considering you added the "starvation or overcrowding only" part.

also have you considered a list of boolean values to hold the rule? so you could get all the outer totalistic rules.

you could have like a 2*9 array, containing birth rules to survival, so you can explore highlife and life without death and seeds and more!

I'm not really a Python programmer, more of a C++ coder though.