Game of Life is a simple two dimensional iterative array devised by John Conway more than 50 years ago. I used it as an experiment to apply NumPy and Pygame's surfarray to quickly process data on screen.

The full Python code, less than 300 lines, can be found in GitHub.

### Life is an Algorithm

The simplest rule of life is: you are either alive or dead. The game area (e.g. 1280 x 720 = 921,600 cells or pixels is easily calculated 60 frames per second on my basic PC) is setup in a NumPy array with one extra row and column on both sides to allow a "continuous" area. Setting life probability allows control over the original seed for the game; however, values in a reasonable range (like 0.15 to 0.5) seem to quickly converge to similar games.

def setup_life_array(self): # setup the initial array of zeroes (dead) and ones (alive) at the shape and size of screen + extra row & column at both ends (w, h) = (self.width, self.height) self.life_array = (self.rng.random((w + 2, h + 2)) + self.life_probability).astype(np.uint8) # copy edge data from actual area other side self.life_array[0:1, :] = self.life_array[w:w + 1, :] self.life_array[w + 1:w + 2, :] = self.life_array[1:2, :] self.life_array[:, 0:1] = self.life_array[:, h:h + 1] self.life_array[:, h + 1:h + 2] = self.life_array[:, 1:2]

Each new generation is simple to calculate by counting the number of live cells in each (overlapping) 3x3 grid of cells. Applying the rules in an efficient way the number of live cells in each grid (one grid for each cell / pixel) is calculated in one sum, and the resulting new generation based on it in two statements applying three conditions. Then, for a continuous game area, the edges are copied to the extra rows and columns.

def new_generation(self): # calculate the next generation. self.generation += 1 (w, h) = (self.width, self.height) # calculate the number of neighbours + cell itself so that the result is always between 0 and 9. # life_array has one extra row and column on both sides of screen area for "continuing" on the opposite side. nb_array = self.life_array[1:w + 1, 1:h + 1] \ + self.life_array[1:w + 1, 0:h] \ + self.life_array[1:w + 1, 2:h + 2] \ + self.life_array[0:w, 1:h + 1] \ + self.life_array[0:w, 0:h] \ + self.life_array[0:w, 2:h + 2] \ + self.life_array[2:w + 2, 1:h + 1] \ + self.life_array[2:w + 2, 0:h] \ + self.life_array[2:w + 2, 2:h + 2] # apply the rules: # 1. if cell is alive and has 2 or 3 live neighbours, it stays alive. # 2. if a dead cell has exactly 3 live neighours, it becomes live # 3. otherwise, cell is/becomes dead. # translated to: # A. if cell + neighbours count = 3 --> it is alive (either live + 2 neighbours (1) or dead + three neighbours (2)) # B. if cell + neighbours count = 4 --> it stays as it is (either live + 3 neighbours (1) or dead + four neighbours (3)) # C. otherwise it is dead. self.life_array[1:w + 1, 1:h + 1][nb_array[:, :] == 3] = 1 # applying (A) - (B) needs no action self.life_array[1:w + 1, 1:h + 1][(nb_array[:, :] < 3) | (nb_array[:, :] > 4)] = 0 # applying (C) # copy edge data from actual area other side self.life_array[0:1, :] = self.life_array[w:w + 1, :] self.life_array[w + 1:w + 2, :] = self.life_array[1:2, :] self.life_array[:, 0:1] = self.life_array[:, h:h + 1] self.life_array[:, h + 1:h + 2] = self.life_array[:, 1:2]

Updating the screen using surfarray is then extremely simple and easy:

(w, h) = (self.width, self.height) rgb_array = pygame.surfarray.pixels2d(self.screen) rgb_array[:, :] = self.life_array[1:w + 1, 1:h + 1] * self.use_color

The game will update the number of generations and the amount of life (defined as changed cells between checks) and show these and some other info on screen. Screen resolution (and hence game resolution as well) can be selected freely.