Conway’s Game of Life, a captivating cellular automaton devised by the mathematician John H. Conway, provides a mesmerizing exploration into the emergent behavior of simple rules applied to a grid of cells. In this blog post, we’ll delve into the intricacies of the game, its rules, and how to implement it using Python.
Understanding Conway’s Game of Life
The Game of Life unfolds on an infinite two-dimensional grid, where each cell can either be alive or dead. The game evolves through generations, following simple rules:
- Survival: Any live cell with two or three live neighbors survives to the next generation.
- Death: Any live cell with fewer than two live neighbors dies due to underpopulation, while a live cell with more than three live neighbors dies due to overpopulation.
- Birth: Any dead cell with exactly three live neighbors becomes a live cell through reproduction.
These rules encapsulate the dynamic interplay between neighboring cells, leading to an ever-changing pattern of life and death.
Developing a Game of Life Program
To implement Conway’s Game of Life, we need to break down the problem into manageable components and address each one.
World and Cells
We represent the world as a set of live cells, where each cell is a tuple (x, y)
representing its coordinates on the grid.
from typing import Set, Tuple
Cell = Tuple[int, int]
World = Set[Cell]
In the code snippet above, we define the Cell
and World
types using Python’s type hinting. This helps us understand the structure of the data we’re working with.
Generating Next Generation
The next_generation
function computes the next generation of live cells based on the rules of the game. It iterates through each cell in the current generation, calculates the number of live neighbors for each cell, and applies the rules to determine the next state.
def next_generation(world: World) -> World:
"""Compute the next generation of live cells."""
return {cell for cell, count in neighbor_counts(world).items()
if count == 3 or (count == 2 and cell in world)}
Here, we utilize a set comprehension to efficiently generate the next generation based on the counts of live neighbors.
Counting Neighbors
The neighbor_counts
function counts the number of live neighbors for each cell in the world. It uses Python’s Counter
class from the collections
module to efficiently tally the counts.
from collections import Counter
def neighbor_counts(world: World) -> Counter[Cell]:
"""Count the number of live neighbors for each cell."""
return Counter(xy for cell in world
for xy in neighbors(cell))
This function leverages a generator expression to iterate through each cell in the world and generate the coordinates of its neighbors.
Determining Neighbors
The neighbors
function determines all eight adjacent neighbors of a given cell. It employs list comprehensions to generate the coordinates of neighboring cells based on the given cell’s position.
def neighbors(cell: Cell) -> List[Cell]:
"""Return all eight adjacent neighbors of a cell."""
(x, y) = cell
return [(x + dx, y + dy)
for dx in (-1, 0, 1)
for dy in (-1, 0, 1)
if not (dx == 0 == dy)]
This function demonstrates the elegance of list comprehensions in Python for concise and readable code.
Displaying the World
The picture
function returns a graphical representation of the world as a grid of characters, with live cells represented by ‘@’ and empty cells by ‘.’.
LIVE = '@'
EMPTY = '.'
PAD = ' '
def picture(world: World, Xs: range, Ys: range) -> str:
"""Return a picture of the world as a grid of characters."""
def row(y): return PAD.join(LIVE if (x, y) in world else EMPTY for x in Xs)
return '\n'.join(row(y) for y in Ys)
This function showcases how we can use nested functions to build complex behavior from simpler components.
Animating the Evolution
The animate_life
function animates the evolution of the game over multiple generations, displaying each generation one by one. It utilizes Python’s clear_output
function from the IPython.display
module to clear the output and update it with the latest generation.
def animate_life(world: World, n: int = 10, Xs=range(10), Ys=range(10), pause=1/5):
"""Animate the evolution of the game for `n` generations."""
for g, world in enumerate(life(world, n)):
clear_output(wait=True)
display_html(pre(f'Generation: {g:2}, Population: {len(world):2}\n' +
picture(world, Xs, Ys)), raw=True)
sleep(pause)
This function demonstrates the power of Python in creating dynamic and interactive visualizations.
Learn How To Build AI Projects
Learn How To Build AI Projects
Now, if you are interested in upskilling in 2024 with AI development, check out this 6 AI advanced projects with Golang where you will learn about building with AI and getting the best knowledge there is currently. Here’s the link.
Conclusion
Conway’s Game of Life offers a captivating glimpse into the emergent behavior of complex systems governed by simple rules. With Python, we can easily explore and visualize the evolution of life patterns on a two-dimensional grid. By implementing the game, we gain insights into cellular automata and the principles underlying their behavior.