Welcome to Conway's beautifully simple and marvelously complex Game of Life!
As we said in the introduction to this project, the rules are simple:
-
Our world is a flat torus, discretised in quadrilaterals
-
Each square is a cell which can be either live or dead. A configuration of live and dead cells is called a generation.
-
Going from one generation to the next, cells die, survive or are (re)born based on the state of their 8 neighbours:
-
Any live cell with fewer than two live neighbours dies, as if by underpopulation
-
Any live cell with two or three live neighbours lives on to the next generation
-
Any live cell with more than three live neighbours dies, as if by overpopulation
-
Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction
-
And that's it!
The project contains 3 scripts and a Jupyter notebook, the latter is just a scrapbook to try out ideas.
This file contains the basic logic of the game.
It contains the simple cell class Cell
which contains the basic attribute Cell.alive
which is a boolean value representing whether the cell is alive (True
) or dead (False
).
The main class is Lattice
which represents the world.
A configuration can be set by the Lattice.set_states()
method which takes a list of integer coordinates [...,(i,j),...]
and sets the corresponding cells to (a)live.
The Lattice
class has also a class method Lattice.plot()
with which the current configuration can be displayed.
The most important method of this class is the update method Lattice.update()
which passes to the next generation according to the classical rules, which we recalled in the beginning.
This file contains the visual logic, the GUI built with Python's tkinter
module.
It creates a window displaying the grid which runs the simulation.
Each square in the grid represents a cell and their color represents their current status: white for dead and black for (a)live.
One can click on the grid to activate cells.
Once an initial configuration has been chosen, the Play button starts the simmulation.
The grid is updated according to Lattice.update()
.
The Pause button allows to pause the simmulation. One can then (de)activate cells and continue the simulation form here by clicking once again the Play button.
Finally, the Reset button resets the simmulation.
This is the main file. Running
python main.py
starts the game!
The Lattice.update()
method:
def update(self):
live_or_die = np.zeros_like(self.grid)
for dir in [-1,1]:
for ax in [0,1]:
# direct nbrs: bottom, right, up, left
live_or_die += self.get_states(np.roll(self.grid,dir,axis=ax)).astype(int)
for ddir in [-1,1]:
# diagonal nbrs: bottom right, upper right, bottom left, upper left
live_or_die += self.get_states(np.roll(np.roll(self.grid,dir,axis=1),ddir,axis=0)).astype(int)
'''
EXPLICIT VERSION: [...]
'''
# update according to Conway's Game of Life
# conditions for live cells
underpopulation = np.where( (live_or_die < 2) & (self.get_states() == True) ) # dies
overpopulation = np.where( (live_or_die > 3) & (self.get_states() == True) ) # dies
stable = np.where( ( (live_or_die == 2) | (live_or_die == 3) ) & (self.get_states() == True) ) # lives
# conditions for dead cells
reborn = np.where((live_or_die == 3) & (self.get_states() == False)) # lives
# update
for cell in self.grid[*underpopulation]:
cell.set(False)
for cell in self.grid[*overpopulation]:
cell.set(False)
for cell in self.grid[*stable]:
cell.set(True)
for cell in self.grid[*reborn]:
cell.set(True)
The GUI.next_gen()
method:
def next_gen(self):
self.lattice.update()
live = self.lattice.get_alive()
dead = self.lattice.get_dead()
# update colors
for (a,b) in live:
rectangle_id = self.grid[(b,a)] # access via (col,row)
self.canvas.itemconfig(rectangle_id, fill='black')
for (a,b) in dead:
rectangle_id = self.grid[(b,a)] # access via (col,row)
self.canvas.itemconfig(rectangle_id, fill='white')
# display count of gens
self.gens += 1
self.gen_label.config(text=f"generation: {self.gens}")
if self.loop:
self.canvas.after(200, self.next_gen)
else: # if paused (self.loop = False), update live cells
self.live_cells = self.get_live_from_lattice()
Here are some nice examples from various starting configurations!
The Toad
The Glider
Heavy Weight Space Ship (HWSS)
And that's it! Happy Hunting!