Build Conway's Game of Life With Python

Build Conway's Game of Life With Python

by Leodanis Pozo Ramos Nov 22, 2023 intermediate gamedev projects python

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Create Conway's Game of Life With Python

Wouldn’t it be cool to build a Python game that only requires initial user input and then seems to take on a mind of its own, creating mesmerizing patterns along the way? You can do exactly that with Conway’s Game of Life, which is about the evolution of cells in a life grid.

Implementing the Game of Life algorithm is a good exercise with many interesting challenges that you’ll have to figure out. Specifically, you’ll need to build the life grid and find a way to apply the game’s rules to all the cells on the grid so that they evolve through several generations.

In this tutorial, you’ll:

  • Implement Conway’s Game of Life algorithm with Python
  • Build a curses view to display the Game of Life grid
  • Create an argparse command-line interface for the game
  • Set up the game app for installation and execution

To get the most out of this tutorial, you should know the basics of writing object-oriented code in Python, creating command-line interface (CLI) apps with argparse, and setting up a Python project.

You can download the complete source code and other resources for this project by clicking the link below:

Demo: Conway’s Game of Life With Python

In this tutorial, you’ll implement Conway’s Game of Life for your command line using Python. Once you’ve run all the steps to build the game, then you’ll end up with a fully working command-line app.

The demo shows how the app works and walks you through the evolution of multiple life patterns or seeds, which define the game’s initial state and work as starting points for evolving the cells in the life grid:

Throughout this tutorial, you’ll run through several challenges related to the game’s algorithm and also to writing and setting up a command-line application in Python. At the end, you’ll have a Game of Life app that will work like the demo above.

Project Overview

The Game of Life by the British mathematician John Horton Conway isn’t a game in the traditional sense. In technical terms, it’s a cellular automaton, but you can think of Game of Life as a simulation whose evolution depends on its initial state and doesn’t require further input from any players.

The game’s board is an infinite, two-dimensional grid of cells. Each cell can be in one of two possible states:

  1. Alive
  2. Dead

Each cell evolves to the next generation depending on the state of itself and its neighbor cells. Here’s a summary of the evolution rules:

  1. Alive cells die if they have fewer than two (underpopulation) or more than three living neighbors (overpopulation).
  2. Alive cells stay alive if they have two or three living neighbors.
  3. Dead cells with exactly three living neighbors become alive (reproduction).

The game’s initial state is the seed, or initial life pattern. In this implementation, the life pattern will be a set of alive cells. The first generation results from applying the above rules to every cell in the seed. The second generation results from applying the rules to the first generation, and so on. So, each generation is a pure function of the preceding one.

The challenge in this project is to program the evolution algorithm in Python and then provide a command-line interface (CLI) to run the game with different life patterns.

Prerequisites

The project that you’ll build in this tutorial will require familiarity with general Python programming and especially with object-oriented programming. So, you should have basic knowledge of the following topics:

However, if you don’t have all this knowledge yet, then that’s okay! You might learn more by going ahead and giving the project a shot. You can always stop and review the resources linked here if you get stuck.

With this short overview of your Game of Life project and its prerequisites, you’re ready to start Pythoning. Have fun while coding!

Step 1: Set Up the Game of Life Project

Every time you start a new Python project, you should take some time to think about how you’ll organize the project itself. You need to create your project layout, which is the directory structure of your project.

For a Python project that implements Conway’s Game of Life, you may end up with many different layouts. So, it’s best to think of what you want to or need to do first. Here’s a summary:

  • Implement the Game of Life algorithm, including the life grid and the seeds or patterns
  • Provide a way to visualize the life grid and its evolution
  • Allow the user to set a pattern and run the game a given number of generations

Following these ideas, you’ll create the following directory structure for your Game of Life project:

rplife/
│
├── rplife/
│   ├── __init__.py
│   ├── __main__.py
│   ├── cli.py
│   ├── grid.py
│   ├── patterns.py
│   ├── patterns.toml
│   └── views.py
│
├── README.md
└── pyproject.toml

In this tutorial, you’ll name the project rplife, which is a combination of Real Python (rp) and life. The README.md file will contain the project’s description and the instructions for installing and running the application.

The pyproject.toml file is a TOML file that specifies the project’s build system and many other configurations. In modern Python, this file replaces the setup.py script that you may have used before. So, you’ll use pyproject.toml instead of setup.py in this project.

Inside the rplife/ directory, you have the following files:

  • __init__.py enables rplife/ as a Python package.
  • __main__.py works as an entry-point script for the game.
  • cli.py contains the command-line interface for the game.
  • grid.py provides the life grid implementation.
  • patterns.py and patterns.toml handle the game’s patterns.
  • views.py implements a way to display the life grid and its evolution.

Now go ahead and create all these files as empty files. You can do this from your favorite code editor or IDE. Once you finish creating the project’s layout, then you can start implementing the Game of Life’s rules in Python.

The entire project code for this Game of Life project is available on GitHub. To download the project’s skeleton first step, click the following link and navigate to the source_code_step_1/ folder:

Step 2: Code the Game of Life’s Grid

As you already learned, the main component of the Game of Life is an infinite, two-dimensional grid of cells. This component might seem hard to implement because it’s infinite. So, you need a way to abstract out this requirement. To do this, you’ll focus on the alive cells rather than on all the cells in the grid.

To represent the initial set of alive cells, you’ll use a class called Pattern, which represents the game’s seed. Then, you’ll code a LifeGrid class that will take the pattern and evolve it to the next generation of alive cells by applying the game’s rules. This class will also provide a string representation of the grid so that you can display it on your screen.

To download the code for this step, click the following link and look inside the source_code_step_2/ folder:

Sketch the Pattern and LifeGrid Classes

To start implementing the game’s algorithm, fire up your code editor and go to patterns.py. Once there, create the Pattern class using the @dataclass decorator from the dataclasses module:

Python patterns.py
from dataclasses import dataclass

@dataclass
class Pattern:
    name: str
    alive_cells: set[tuple[int, int]]

At this point, Pattern only needs to hold the pattern’s name and the living cells. The .alive_cells attribute is a set of two-value tuples. Each tuple represents the coordinate of an alive cell in the life grid. Using a set to hold the alive cells allows you to use set operations to determine the cells that will be alive in the next generation.

Now, you need the LifeGrid class, which will take care of two specific tasks:

  1. Evolving the grid to the next generation
  2. Providing a string representation of the grid

So, your class will have the following attributes and methods. Remember that this class will live in the grid.py module:

Python grid.py
class LifeGrid:
    def __init__(self, pattern):
        self.pattern = pattern

    def evolve(self):
        pass

    def as_string(self, bbox):
        pass

    def __str__(self):
        pass

Here, you’ve used pass to set up the skeleton of LifeGrid. The class initializer takes a pattern as an argument. This argument will be a Pattern instance. Then, you have .evolve(), which will check the currently alive cells and their neighbors to determine the next generation of alive cells.

Finally, in .as_string(), you’ll provide a way to represent the grid as a string that you can display in your terminal window. Note that this method takes an argument that provides a bounding box for the life grid. This box will define which part of the grid you display in your terminal window.

Evolve the Life Grid to the Next Generation

Now it’s time to write the .evolve() method, which must determine the cells that will pass to the next generation as living cells. So, it has to check the currently alive cells and their neighbors to determine the number of alive neighbors and decide which cell stays alive, which dies, and which comes alive. Recall the rules of how cells evolve:

  1. Alive cells die if they have fewer than two (underpopulation) or more than three living neighbors (overpopulation).
  2. Alive cells stay alive if they have two or three living neighbors.
  3. Dead cells with exactly three living neighbors become alive (reproduction).

Wait, you only have the coordinates of living cells. How can you check the neighbors of a given living cell? Consider the following diagram, which represents a small portion of the grid:

Neighbor of a Cells

Now, say that you’re checking the neighbors of the cell at (1, 1), which are all the green cells. How can you determine their coordinates in the grid? For example, to compute the coordinate of the first row of cells, you can do something like this:

  • For (0, 0), add (-1, -1) to (1, 1) value by value.
  • For (0, 1), add (-1, 0) to (1, 1) value by value.
  • For (0, 2), add (-1, 1) to (1, 1) value by value.

From these examples, you can conclude that the tuples (-1, -1), (-1, 0), (-1, 1) represent the difference between the target cell and its neighbors. In other words, they’re deltas that you can add to the target cell’s coordinates to grab its neighbors. You can extend this thinking to the rest of the neighbors and find the appropriate delta tuples.

With these ideas in mind, you’re ready to implement the .evolve() method:

Python grid.py
 1import collections
 2
 3class LifeGrid:
 4    # ...
 5    def evolve(self):
 6        neighbors = (
 7            (-1, -1),  # Above left
 8            (-1, 0),  # Above
 9            (-1, 1),  # Above right
10            (0, -1),  # Left
11            (0, 1),  # Right
12            (1, -1),  # Below left
13            (1, 0),  # Below
14            (1, 1),  # Below right
15        )
16        num_neighbors = collections.defaultdict(int)
17        for row, col in self.pattern.alive_cells:
18            for drow, dcol in neighbors:
19                num_neighbors[(row + drow, col + dcol)] += 1
20
21        stay_alive = {
22            cell for cell, num in num_neighbors.items() if num in {2, 3}
23        } & self.pattern.alive_cells
24        come_alive = {
25            cell for cell, num in num_neighbors.items() if num == 3
26        } - self.pattern.alive_cells
27
28        self.pattern.alive_cells = stay_alive | come_alive

Here’s a breakdown of what this code does line by line:

  • Lines 6 to 15 define the delta coordinates for the neighbors of the target cell.
  • Line 16 creates a dictionary for counting the number of living neighbors. In this line, you use the defaultdict class from the collections module to build the counter with the int class as its default factory.
  • Line 17 runs a loop over the currently alive cells, which are stored in the .pattern object. This loop allows you to check the neighbors of each living cell so that you can determine the next generation of living cells.
  • Line 18 starts a loop over the neighbor deltas. This inner loop counts how many cells the current cell neighbors. This count allows you to know the number of living neighbors for both living and dead cells.
  • Lines 21 to 23 build a set containing the cells that will stay alive. To do this, you first create a set of neighbors that have two or three alive neighbors themselves. Then, you find the cells that are common to both this set and .alive_cells.
  • Lines 24 to 26 create a set with the cells that will come alive. In this case, you create a set of neighbors that have exactly three living neighbors. Then, you determine the cells that come alive by removing cells that are already in .alive_cells.
  • Line 28 updates .alive_cells with the set that results as the union of the cells that stay alive and those that come alive.

To check whether your code works as expected, you need a way to know the living cells in each generation. Go ahead and add the following method to LifeGrid:

Python grid.py
import collections

class LifeGrid:
    # ...
    def evolve(self):
        # ...

    def __str__(self):
        return (
            f"{self.pattern.name}:\n"
            f"Alive cells -> {sorted(self.pattern.alive_cells)}"
        )

The .__str__() special method provides a way to represent the containing object in a user-friendly manner. With this method in place, when you use the built-in print() function to print an instance of LifeGrid, you get the name of the current pattern and the set of alive cells in the next line. This information gives you an idea of the current state of the life grid.

Now you’re ready to try out your code. Go ahead and start a new terminal window on the project’s root directory. Then start a Python REPL session and run the following code:

Python
>>> from rplife import grid, patterns

>>> blinker = patterns.Pattern("Blinker", {(2, 1), (2, 2), (2, 3)})
>>> grid = grid.LifeGrid(blinker)
>>> print(grid)
Blinker:
Alive cells -> [(2, 1), (2, 2), (2, 3)]

>>> grid.evolve()
>>> print(grid)
Blinker:
Alive cells -> [(1, 2), (2, 2), (3, 2)]

>>> grid.evolve()
>>> print(grid)
Blinker:
Alive cells -> [(2, 1), (2, 2), (2, 3)]

In this code snippet, you first import the grid and patterns modules from the rplife package. Then, you create a Pattern instance. You’ll get to explore the variety of patterns in a moment. But for now, you use the Blinker pattern as a sample.

Next up, you create a LifeGrid object. Note that when you print this object, you get the pattern’s name and live cells. You have a working grid with a proper seed. Now, you can evolve the grid by calling .evolve(). This time, you get a different set of living cells.

If you evolve the grid again, then you get the same set of live cells that you use as the initial seed for the game. This is because the Blinker pattern is an oscillator pattern that evolves like this:

Blinker Pattern
Image source

The Blinker pattern displays three horizontal alive cells in one generation and three vertical alive cells in the next generation. Your code does the same, so it works as expected.

Represent the Life Grid as a String

Now that you’ve implemented .evolve() to move the game to the next generation, you need to implement .as_string(). As you already learned, this method will build a representation of the life grid as a string so that you can display it on your screen.

Below is the code snippet where you define the method:

Python grid.py
import collections

ALIVE = "♥"
DEAD = "‧"

class LifeGrid:
    # ...
    def as_string(self, bbox):
        start_col, start_row, end_col, end_row = bbox
        display = [self.pattern.name.center(2 * (end_col - start_col))]
        for row in range(start_row, end_row):
            display_row = [
                ALIVE if (row, col) in self.pattern.alive_cells else DEAD
                for col in range(start_col, end_col)
            ]
            display.append(" ".join(display_row))
        return "\n ".join(display)

In this code, you first define two constants, ALIVE and DEAD. These constants hold the characters that you’ll use to represent the alive and dead cells on the grid.

Inside .as_strings(), you unpack the bounding box coordinates into four variables. These variables define which part of the infinite grid your program will display on the screen. Then, you create the display variable as a list containing the pattern’s name. Note that you use .center() to center the name over the grid’s width.

The for loop iterates over the range of rows inside the view. In the loop, you create a new list containing the alive and dead cells in the current row. To figure out if a given cell is alive, you check if its coordinates are in the set of alive cells.

Then, you append the row as a string to the display list. At the end of the loop, you join together every string using a newline character (\n) to create the life grid as a string.

To give your updates a try, go ahead and run the following code in your interactive session:

Python
>>> from rplife import grid, patterns

>>> blinker = patterns.Pattern("Blinker", {(2, 1), (2, 2), (2, 3)})
>>> grid = grid.LifeGrid(blinker)

>>> print(grid.as_string((0, 0, 5, 5)))
 Blinker
 ‧ ‧ ‧ ‧ ‧
 ‧ ‧ ‧ ‧ ‧
 ‧ ♥ ♥ ♥ ‧
 ‧ ‧ ‧ ‧ ‧
 ‧ ‧ ‧ ‧ ‧

>>> grid.evolve()
>>> print(grid.as_string((0, 0, 5, 5)))
 Blinker
 ‧ ‧ ‧ ‧ ‧
 ‧ ‧ ♥ ‧ ‧
 ‧ ‧ ♥ ‧ ‧
 ‧ ‧ ♥ ‧ ‧
 ‧ ‧ ‧ ‧ ‧

When you print the life grid, you get a rectangular area containing dots and hearts. If you call .evolve() and print the grid again, then you get a representation of the next generation. That’s cool, isn’t it?

Step 3: Define and Load the Life Patterns

Up to this point, you’ve coded the LifeGrid class and the first half of the Pattern data class. Your code works. However, providing the seed manually seems like too much work. It’d be nice to have some predefined patterns and load them as part of the game execution.

In the following sections, you’ll create a few sample patterns in a TOML file and write the required code to load the patterns into Pattern instances.

Click the link below to download the code for this step so that you can follow along with the project. You’ll find what you need in the source_code_step_3/ folder:

Define Life Patterns in a TOML File

To build a pattern for your Game of Life, you need the pattern’s name and a set of coordinates for the living cells. For example, using the TOML file format, you can do something like the following to represent the Blinker pattern:

TOML patterns.toml
["Blinker"]
alive_cells = [[2, 1], [2, 2], [2, 3]]

In this TOML file, you have a table named after the target pattern. Then, you have a key-value pair containing an array of arrays. The inner arrays represent the coordinates of the living cells in the Blinker pattern. Note that the TOML format doesn’t support sets or tuples, so you use arrays instead.

Following this same structure, you can define as many patterns as you want. Click the following collapsible section to get the patterns that you’ll use in this tutorial:

TOML patterns.toml
["Blinker"]
alive_cells = [[2, 1], [2, 2], [2, 3]]

["Toad"]
alive_cells = [[2, 2], [2, 3], [2, 4], [3, 1], [3, 2], [3, 3]]

["Beacon"]
alive_cells = [[1, 1], [1, 2], [2, 1], [4, 3], [4, 4], [3, 4]]

["Pulsar"]
alive_cells = [
    [2, 4],
    [2, 5],
    [2, 6],
    [2, 10],
    [2, 11],
    [2, 12],
    [4, 2],
    [5, 2],
    [6, 2],
    [4, 7],
    [5, 7],
    [6, 7],
    [4, 9],
    [5, 9],
    [6, 9],
    [4, 14],
    [5, 14],
    [6, 14],
    [7, 4],
    [7, 5],
    [7, 6],
    [7, 10],
    [7, 11],
    [7, 12],
    [9, 4],
    [9, 5],
    [9, 6],
    [9, 10],
    [9, 11],
    [9, 12],
    [10, 2],
    [11, 2],
    [12, 2],
    [10, 7],
    [11, 7],
    [12, 7],
    [10, 9],
    [11, 9],
    [12, 9],
    [10, 14],
    [11, 14],
    [12, 14],
    [14, 4],
    [14, 5],
    [14, 6],
    [14, 10],
    [14, 11],
    [14, 12]
]

["Penta Decathlon"]
alive_cells = [
    [5, 4],
    [6, 4],
    [7, 4],
    [8, 4],
    [9, 4],
    [10, 4],
    [11, 4],
    [12, 4],
    [5, 5],
    [7, 5],
    [8, 5],
    [9, 5],
    [10, 5],
    [12, 5],
    [5, 6],
    [6, 6],
    [7, 6],
    [8, 6],
    [9, 6],
    [10, 6],
    [11, 6],
    [12, 6]
]

["Glider"]
alive_cells = [[0, 2], [1, 0], [1, 2], [2, 1], [2, 2]]

["Glider Gun"]
alive_cells = [
    [0, 24],
    [1, 22],
    [1, 24],
    [2, 12],
    [2, 13],
    [2, 20],
    [2, 21],
    [2, 34],
    [2, 35],
    [3, 11],
    [3, 15],
    [3, 20],
    [3, 21],
    [3, 34],
    [3, 35],
    [4, 0],
    [4, 1],
    [4, 10],
    [4, 16],
    [4, 20],
    [4, 21],
    [5, 0],
    [5, 1],
    [5, 10],
    [5, 14],
    [5, 16],
    [5, 17],
    [5, 22],
    [5, 24],
    [6, 10],
    [6, 16],
    [6, 24],
    [7, 11],
    [7, 15],
    [8, 12],
    [8, 13]
]

["Bunnies"]
alive_cells = [
    [10, 10],
    [10, 16],
    [11, 12],
    [11, 16],
    [12, 12],
    [12, 15],
    [12, 17],
    [13, 11],
    [13, 13]
]

The patterns.toml file above defines eight different patterns. You can define a few more if you want, but these are enough for the purpose of this tutorial.

Load the Life Patterns From TOML

You have a TOML file with a bunch of patterns for your Game of Life. Now, you need a way to load these patterns into your Python code. First, you’ll add an alternative constructor to your Pattern class, which will allow you to create instances from TOML data:

Python patterns.py
from dataclasses import dataclass

@dataclass
class Pattern:
    name: str
    alive_cells: set[tuple[int, int]]

    @classmethod
    def from_toml(cls, name, toml_data):
        return cls(
            name,
            alive_cells={tuple(cell) for cell in toml_data["alive_cells"]},
        )

The .from_toml() method is a class method because you’re using the @classmethod decorator. Class methods are great when you need to provide an alternative constructor in a class. These types of methods receive the current class as their first argument, cls.

Then, you take the pattern’s name and the TOML data as arguments. Inside the method, you create and return an instance of the class using the cls argument. To provide the .alive_cells argument, you use a set comprehension.

In the comprehension, you create a set of tuples from the list of lists that you get from the TOML file. Each tuple will contain the coordinates of a living cell on the life grid. Note that to access the alive cells in the TOML data, you can use the dictionary lookup notation with the name of the target key in square brackets.

Next up, you need to create two functions. The first function will allow you to load all the patterns from the TOML file. The second function will load a single pattern at a time.

To parse a TOML file and read its content into Python objects, you can use the standard-library module tomllib if you’re using Python 3.11 or later. Otherwise, you should use the third-party library tomli, which is compatible with tomllib.

For your code to work with either tool, you can wrap the import statements for the TOML libraries in a tryexcept block:

Python patterns.py
from dataclasses import dataclass

try:
    import tomllib
except ImportError:
    import tomli as tomllib

The import in the try clause targets the standard-library module tomllib. If this import raises an exception because you’re using a Python version lower than 3.11, then the except clause imports the third-party library tomli, which you need to install as an external dependency of your project.

With the TOML library in place, it’s time to write the required functions. Go ahead and add get_pattern() to patterns.py:

Python patterns.py
# ...

def get_pattern(name, filename=PATTERNS_FILE):
    data = tomllib.loads(filename.read_text(encoding="utf-8"))
    return Pattern.from_toml(name, toml_data=data[name])

This function takes the name of a target pattern and the name of the TOML file as arguments and returns a Pattern instance representing the pattern whose name matches the name argument.

In the first line of get_pattern(), you load the content of patterns.toml using the TOML library of choice. The .loads() method returns a dictionary. Then, you create the instance of Pattern using the .from_toml() constructor and return the result.

Note that the filename argument has a default value that you provide using a constant. Here’s how you can define this constant after your imports:

Python patterns.py
from dataclasses import dataclass
from pathlib import Path
# ...

PATTERNS_FILE = Path(__file__).parent / "patterns.toml"

# ...

This constant holds a pathlib.Path instance that points to the patterns.toml file. Remember that this file lives in your rplife/ directory. To get the path to this directory, you use the __file__ attribute, which holds the path to the file from which the module was loaded, patterns.py. Then, you use the .parent attribute of Path to get the desired directory.

The get_pattern() function retrieves a single pattern from the TOML file using the pattern’s name. This function will be useful when you want to run your Game of Life using a single pattern. What if you want to run multiple patterns in a row? In that case, you’ll need a function that gets all the patterns from the TOML file.

Here’s the implementation of that function:

Python patterns.py
# ...

def get_all_patterns(filename=PATTERNS_FILE):
    data = tomllib.loads(filename.read_text(encoding="utf-8"))
    return [
        Pattern.from_toml(name, toml_data) for name, toml_data in data.items()
    ]

This function takes the path to the TOML file as an argument. The first line is the same as in get_pattern(). Then, you create a list of instances of Pattern using a comprehension. To do this, you use the .items() method on the dictionary that .loads() returns.

Once you have these two functions in place, you can give them a try:

Python
>>> from rplife import patterns

>>> patterns.get_pattern("Blinker")
Pattern(name='Blinker', alive_cells={(2, 3), (2, 1), (2, 2)})

>>> patterns.get_all_patterns()
[
    Pattern(name='Blinker', alive_cells={(2, 3), (2, 1), (2, 2)}),
    ...
]

Great! Both functions work as expected. In this example, you first get the Blinker pattern using the get_pattern() function. Then, you get the complete list of available patterns using get_all_patterns().

Step 4: Write the Game’s View

You’ve implemented most of the back-end code for your Game of Life project. Now, you need a way to display the game’s evolution on your screen. In this tutorial, you’ll use the curses package from the standard library to display the evolution of the game. This package provides an interface to the curses library, enabling you to build a text-based user interface (TUI) with portable advanced terminal handling.

To download the code for this step, click the link below, then check out the source_code_step_4/ folder:

To kick things off, you’ll start by defining a class called CursesView:

Python views.py
class CursesView:
    def __init__(self, pattern, gen=10, frame_rate=7, bbox=(0, 0, 20, 20)):
        self.pattern = pattern
        self.gen = gen
        self.frame_rate = frame_rate
        self.bbox = bbox

The class initializer takes several arguments. Here’s a breakdown of them and their meanings:

  • pattern represents the life pattern that you want to display on your screen. It should be an instance of Pattern.
  • gen is the number of generations that you want the game to evolve through. It defaults to 10 generations.
  • frame_rate represents the frames per second, which is an indicator of the time between displaying one generation and the next. It defaults to 7 frames per second.
  • bbox is the bounding box for the life grid. This is a tuple that represents which part of the life grid will be displayed. It should be a tuple of the form (start_col, start_row, end_col, end_row).

This class will have only one method as part of its public interface. The .show() method will have the responsibility of displaying the life grid on the screen:

Python views.py
import curses

class CursesView:
    # ...
    def show(self):
        curses.wrapper(self._draw)

The .show() method is quite short. It only includes a call to the wrapper() function from curses. This function initializes curses and calls another callable object. In this case, the callable object is the non-public ._draw() method, which has the responsibility of displaying consecutive generations of cells.

Here’s a possible implementation of the ._draw() method:

Python views.py
 1import curses
 2from time import sleep
 3
 4from rplife.grid import LifeGrid
 5
 6class CursesView:
 7    # ...
 8    def _draw(self, screen):
 9        current_grid = LifeGrid(self.pattern)
10        curses.curs_set(0)
11        screen.clear()
12
13        try:
14            screen.addstr(0, 0, current_grid.as_string(self.bbox))
15        except curses.error:
16            raise ValueError(
17                f"Error: terminal too small for pattern '{self.pattern.name}'"
18            )
19
20        for _ in range(self.gen):
21            current_grid.evolve()
22            screen.addstr(0, 0, current_grid.as_string(self.bbox))
23            screen.refresh()
24            sleep(1 / self.frame_rate)

There’s a lot happening in this code snippet. Here’s a line-by-line breakdown:

  • Line 2 imports the sleep() function from the time module. You’ll use this function to control the frames per second of your view.
  • Line 4 imports the LifeGrid class from the grid module in your rplife package.
  • Line 8 defines ._draw(), which takes a screen or curses window object as an argument. This object is automatically passed in when you call curses.wrapper() with ._draw() as an argument.
  • Line 9 defines the life grid by instantiating the LifeGrid class with the current pattern as an argument.
  • Line 10 calls .curs_set() to set the cursor’s visibility. In this case, you use 0 as an argument, which means that the cursor will be invisible.
  • Lines 13 to 18 define a tryexcept block that raises a ValueError exception when the current terminal window doesn’t have enough space to display the life grid. Note that you need to run this check only once, so you don’t have to include the check in the loop on line 20.
  • Line 20 starts a loop that will run as many times as the number of generations.
  • Line 21 calls .evolve() on the grid to evolve the game to the next generation.
  • Line 22 calls .addstr() on the current screen object. The first two arguments define the row and column where you want to start drawing the life grid. In this tutorial, you’ll start the drawing at (0, 0), which is the upper left corner of the terminal window.
  • Line 23 refreshes the screen by calling .refresh(). This call updates the screen immediately to reflect the changes from the previous call to .addstr().
  • Line 24 calls sleep() to set the frame rate that you’ll use to display consecutive generations in the grid.

With your view in place, you can give your Game of Life another try by running the following code:

Python
>>> from rplife.views import CursesView
>>> from rplife.patterns import get_pattern

>>> CursesView(get_pattern("Glider Gun"), gen=100).show()

In this snippet, you import the CursesView class from views and the get_pattern() function from patterns. Next, you create a new instance of CursesView using the Glider Gun pattern and one hundred generations. Finally, you call the .show() method on the view instance. This code will run the game and display its evolution through 100 life generations.

The output will look something like this:

Wow! That looks great! Your Game of Life project is shaping up. In the following two steps, you’ll define the command-line interface (CLI) for your users to interact with the game, and finally, you’ll put everything together in the game’s entry-point script, __main__.py.

Step 5: Implement the Game’s CLI

In this section, you’ll create the command-line interface (CLI) for your Game of Life project. This interface will allow your users to interact with the game and run it with different life patterns. You’ll use the argparse module from the standard library to build the CLI. It’ll provide the following command-line options:

  • --version will show the program’s version number and exit.
  • -p, --pattern will take a pattern for the Game of Life, with a default of Blinker.
  • -a, --all will show all available patterns in a sequence.
  • -v, --view will display the life grid in a specific view, with a default of CursesView.
  • -g, --gen will take the number of generations, with a default of 10.
  • -f, --fps will take the frames per second, with a default of 7.

To download the code for this step, click the following link and look into the source_code_step_5/ folder:

To start writing this CLI, you’ll add the following code to cli.py:

Python cli.py
import argparse

from rplife import __version__, patterns, views

def get_command_line_args():
    parser = argparse.ArgumentParser(
        prog="rplife",
        description="Conway's Game of Life in your terminal",
    )

In this code snippet, you first import the argparse module. Then, you import some required objects from the rplife package.

Up to this point, you haven’t defined the __version__, so go ahead and open the __init__.py file. Then, write __version__ = "1.0.0" at the beginning of the file. This attribute will power the --version command-line option, which is commonplace in CLI apps and allows you to display the app’s current version.

Next, you define the get_command_line_args() function to wrap up the CLI definition. Inside get_command_line_args(), you create an argument parser by instantiating ArgumentParser. In this example, you only provide the program’s name and description as arguments in the class instantiation.

With this code in place, you can start adding command-line options. Here’s the required code to implement the planned options:

Python cli.py
# ...

def get_command_line_args():
    # ...
    parser.add_argument(
        "--version", action="version", version=f"%(prog)s v{__version__}"
    )
    parser.add_argument(
        "-p",
        "--pattern",
        choices=[pat.name for pat in patterns.get_all_patterns()],
        default="Blinker",
        help="take a pattern for the Game of Life (default: %(default)s)",
    )
    parser.add_argument(
        "-a",
        "--all",
        action="store_true",
        help="show all available patterns in a sequence",
    )
    parser.add_argument(
        "-v",
        "--view",
        choices=views.__all__,
        default="CursesView",
        help="display the life grid in a specific view (default: %(default)s)",
    )
    parser.add_argument(
        "-g",
        "--gen",
        metavar="NUM_GENERATIONS",
        type=int,
        default=10,
        help="number of generations (default: %(default)s)",
    )
    parser.add_argument(
        "-f",
        "--fps",
        metavar="FRAMES_PER_SECOND",
        type=int,
        default=7,
        help="frames per second (default: %(default)s)",
    )
    return parser.parse_args()

In this piece of code, you add all the planned options to your game’s CLI by calling .add_argument() on the parser object. Each option has its own arguments depending on the desired functionality.

It’s important to note that the -p, --pattern option is a choice option, which means that the input value must match the exact name of an available pattern. To get the name of all the available patterns, you use a list comprehension and the get_all_patterns() function.

The get_command_line_args() function returns a namespace object containing the parsed command-line arguments and their corresponding values. You can access the arguments and their values using dot notation on the namespace. For example, if you want to access the --view argument’s value, then you can do something like get_command_line_args().view.

The -v, --view option is also a choice option. In this case, you get the available views from the __all__ attribute defined in the views module. Of course, you haven’t defined __all__ yet, so you need to do that now. Open views.py and add the following assignment statement right after your imports:

Python views.py
import curses
from time import sleep

from rplife.grid import LifeGrid

__all__ = ["CursesView"]

# ...

The special __all__ attribute allows you to define the list of names that the underlying module will export as part of its public interface. In this example, __all__ contains only one view object because that’s what you have so far. You can implement your own views as part of your practice and add them to this list so that the user can use them when running the game.

Great! Your game now has a user-friendly command-line interface. However, there’s no way to try out this interface. You need to write an entry-point script first. That’s what you’ll do in the following section.

Step 6: Write the Game’s Entry-Point Script

In Python, executable programs have an entry-point script or file. As this name suggests, an entry-point script is a script that contains the code that starts the program’s execution. In this script, you typically put the program’s main() function.

Again, you can download the code for this step by clicking the link below and looking into the source_code_step_6/ folder:

In modern Python, you’ll typically find that the __main__.py file is the right place for this entry-point code. So, go ahead and open __main__.py in your code editor. Then add the following code to it:

Python __main__.py
 1import sys
 2
 3from rplife import patterns, views
 4from rplife.cli import get_command_line_args
 5
 6def main():
 7    args = get_command_line_args()
 8    View = getattr(views, args.view)
 9    if args.all:
10        for pattern in patterns.get_all_patterns():
11            _show_pattern(View, pattern, args)
12    else:
13        _show_pattern(
14            View,
15            patterns.get_pattern(name=args.pattern),
16            args
17        )

Here’s a line-by-line explanation of the above code:

  • Line 1 imports the sys module from the standard library. You’ll use this module to access the sys.stderr file object where you’ll be writing any error that occurs during the app’s execution.
  • Line 3 imports the patterns and views modules from rplife. You’ll use them to define the life pattern and view to use.
  • Line 4 imports get_command_line_args() from the cli module. You’ll use it to parse the command-line arguments and options.
  • Line 6 defines the main() function.
  • Line 7 calls get_command_line_args() and stores the resulting namespace object in args.
  • Line 8 uses the args.view command-line argument to access the desired view on the views module. To access the view, you use the built-in getattr() function. This way, you’re ensuring that your code is scalable, allowing new views without having to modify main(). Note that because the call to getattr() returns a class, you’ve used a capital letter in View to denote this fact.
  • Line 9 defines a conditional statement to check if the user has chosen to run all the available patterns in a row. If that’s the case, then lines 10 and 11 run a loop over all the patterns and display them on the screen using the _show_pattern() helper function. You’ll define this function in a moment.
  • Lines 13 to 17 run whenever the user selects a specific life pattern to run the game.

The _show_pattern() helper function is an important part of main(). Here’s its definition:

Python __main__.py
# ...

def _show_pattern(View, pattern, args):
    try:
        View(pattern=pattern, gen=args.gen, frame_rate=args.fps).show()
    except Exception as error:
        print(error, file=sys.stderr)

In this function, you take the current view, pattern, and command-line arguments as input. Then, you use a tryexcept block to create the view object and run its .show() method. This block will catch and handle any exception that may occur during the game’s evolution and print an error message to the standard error stream. This way, you show a user-friendly error message instead of a complicated exception traceback.

Great! The entry-point script is almost ready. You just need to add a tiny detail. You need to call main() so that the program’s execution can start:

Python __main__.py
# ...

if __name__ == "__main__":
    main()

The Pythonic way to run main() in an executable script is to use the name-main idiom as you did in the above code snippet. This idiom ensures that the main() function runs only when you run the file as an executable program.

With the entry-point script in place, you can give your Game of Life a try. Go ahead and execute the following command to run the game with all the available life patterns:

Shell
$ python -m rplife -a

This command will run your Game of Life with all the currently available life patterns. You’ll see something like this in your terminal window:

That looks really amazing, doesn’t it? You can explore how the rest of the command-line options work. Go ahead and give it a try! The --help option will let you know how to use the app’s CLI.

Step 7: Set Up the Game for Installation and Execution

Up to this point, you’ve run all the required steps to have a fully functional implementation of Conway’s Game of Life. The game now has a user-friendly command-line interface that allows you to run it with different options. You can run the game with a single pattern, with all the available patterns, and more.

Even though your Game of Life works nicely, you still need to use the python command to run the game. This is a bit annoying and can make you feel like the game isn’t a real CLI app.

In the following sections, you’ll learn how to set up your Game of Life project for installation using a pyproject.toml file. You’ll also learn how to install the game in a Python virtual environment so that you can run it as a stand-alone CLI application.

To download the code for this final step, click the following link and look into the source_code_step_7/ folder:

Write a pyproject.toml File

In recent years, the Python community has been moving to the adoption of pyproject.toml files as the central configuration file for packaging and distributing Python projects. In this section, you’ll learn how to write a minimal pyproject.toml file for your Game of Life project.

As its extension indicates, pyproject.toml uses the TOML format. You can find the complete specification for writing pyproject.toml files in PEP 621. Following this specification, here’s a pyproject.toml file for your Game of Life project:

TOML pyproject.toml
[build-system]
requires = ["setuptools>=64.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "rplife"
dynamic = ["version"]
description = "Conway's Game of Life in your terminal"
readme = "README.md"
authors = [{ name = "Real Python", email = "info@realpython.com" }]
dependencies = [
    'tomli; python_version < "3.11"',
]

[project.scripts]
rplife = "rplife.__main__:main"

[tool.setuptools.dynamic]
version = {attr = "rplife.__version__"}

The first table in this file defines the build system that you want to use when building the project. In this tutorial, you’ll use the setuptools package for building purposes.

In the project table, you define the project’s name as rplife. Then, you declare the project’s version as a dynamic field, which you’ll load later on in the tool.setuptools.dynamic table at the end of the file.

Next, you provide a project description, README file, and authors. The dependencies key holds a list of external dependencies for this project. In this case, you need to install the tomli library to process the TOML file if the Python version is less than 3.11.

In the project.scripts table, you define the app’s entry point, which is the main() function from __main__.py in the rplife package.

Finally, you load the application’s version number from the __version__ dunder constant that you defined in the __init__.py file of your rplife package.

That’s it! You have a minimal viable pyproject.toml file for your Game of Life project. Now, you and your users can install the application and use it as a regular command-line app.

Install and Run Your Game of Life

Once you’ve created a suitable pyproject.toml file for your Game of Life project, then you can proceed to install the project in a dedicated Python virtual environment. Go ahead and run the following commands on your terminal if you didn’t do so in the first step of this tutorial:

Windows Command Prompt
PS> python -m venv venv
PS> venv\Scripts\activate
(venv) PS> python -m pip install -e .
Shell
$ python -m venv venv
$ source venv/bin/activate
(venv) $ python -m pip install -e .

With these commands, you create and activate a new virtual environment. Once the environment is active, you install the project in editable mode with the -e option of pip install.

The editable mode is quite handy when you’re working on a Python project. It allows you to install the project as a stand-alone app and try it out as you’d do in production. This mode allows you to continue adding features and modifying the code while you test it in real time.

Now, you can run your Game of Life as a regular command-line app. Go ahead and run the following command:

Shell
$ rplife -p "Glider Gun" -g 100

Here, you use the rplife command directly to run the game with the Glider Gun pattern for one hundred generations. Note that you don’t have to use the python command any longer. Your project now works as a regular command-line application. Isn’t that cool?

Conclusion

You’ve implemented Conway’s Game of Life using Python and object-oriented programming. To make the game usable, you’ve built a user-friendly command-line interface using argparse. In the process, you’ve learned how to structure and organize a CLI app and set up the application for distribution and installation. That’s a great set of skills for you as a Python developer.

In this tutorial, you’ve learned how to:

  • Implement Conway’s Game of Life algorithm using OOP
  • Write a curses view to display the Game of Life grid
  • Provide the game with an argparse command-line interface
  • Set up the game for installation and execution

With all this knowledge and skill, you’re ready to start digging into more complex projects and challenges.

Again, you can download the complete source code and other resources for this project by clicking the link below:

Next Steps

Now that you’ve finished building your Game of Life project, you can go a step further by implementing a few additional features. Adding new features by yourself will help you learn about exciting new topics.

Here are some ideas for new features:

  • Implement other views: Having other views apart from the one based on curses would be a great addition to your project. For example, you could write a Tkinter view where you display the life grid in a GUI window.
  • Add exciting new life patterns: Adding new life patterns to patterns.toml will allow you to explore other behaviors of the game.
  • Change the rules: So far, you’ve been working with the traditional rules, where dead cells with three living neighbors are born, and living cells with two or three living neighbors survive. The shorthand for this is B3/S23, but there are several variations that use different rules to evolve to a new generation. Changing the rules allows you to experience other life-like universes.

Once you implement these new features, then you can change gears and jump into other cool projects. If you’d like to create more traditional games, then check out some of the following tutorials:

  • Build a Dice-Rolling Application With Python: In this step-by-step project, you’ll build a dice-rolling simulator app with a minimal text-based user interface using Python. The app will simulate the rolling of up to six dice. Each individual die will have six sides.
  • Build a Hangman Game for the Command Line in Python: In this step-by-step project, you’ll learn how to write the game of Hangman in Python for the command line. You’ll learn how to structure the game as a text-based interface (TUI) application.
  • Build a Tic-Tac-Toe Game Engine With an AI Player in Python: In this step-by-step tutorial, you’ll build a universal game engine in Python with tic-tac-toe rules and two computer players, including an unbeatable AI player using the minimax algorithm. You’ll also create a text-based graphical front end for your library and explore two alternative front ends.
  • Build a Maze Solver in Python Using Graphs: In this step-by-step project, you’ll build a maze solver in Python using graph algorithms from the NetworkX library. Along the way, you’ll design a binary file format for the maze, represent it in an object-oriented way, and visualize the solution using scalable vector graphics (SVG).
  • Build a Wordle Clone With Python and Rich: In this step-by-step project, you’ll build your own Wordle clone with Python. Your game will run in the terminal, and you’ll use Rich to ensure your word-guessing app looks good. Learn how to build a command-line application from scratch and then challenge your friends to a wordly competition!

What will you do next? Share your ideas in the comments!

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Create Conway's Game of Life With Python

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Leodanis Pozo Ramos

Leodanis is an industrial engineer who loves Python and software development. He's a self-taught Python developer with 6+ years of experience. He's an avid technical writer with a growing number of articles published on Real Python and other sites.

» More about Leodanis

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning