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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
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:
- Alive
- 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:
- Alive cells die if they have fewer than two (underpopulation) or more than three living neighbors (overpopulation).
- Alive cells stay alive if they have two or three living neighbors.
- 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:
- Working with conditional statements
- Writing
for
loops and comprehensions - Working with Python strings, tuples, lists, and sets
- Creating regular classes and data classes
- Building command-line interfaces with
argparse
- Working with TOML files in Python
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.
Note: Adding a README
file to your projects is a best practice in programming, especially if you’re planning to release the project as an open-source solution. To learn more about writing good README
files, check out How to write a great README for your GitHub project.
You can also read Document Your Python Code and Projects With ChatGPT if you want to take advantage of a large language model to assist you in the process of documenting your code.
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
enablesrplife/
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
andpatterns.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.
Note: When you start a new Python project, it’s a best practice to create a dedicated Python virtual environment for the project. You can do this by running the appropriate command for your current operating system:
With these two commands, you create and activate a Python virtual environment called venv
in your working directory.
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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
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:
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:
- Evolving the grid to the next generation
- 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:
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:
- Alive cells die if they have fewer than two (underpopulation) or more than three living neighbors (overpopulation).
- Alive cells stay alive if they have two or three living neighbors.
- 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:
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:
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 thecollections
module to build the counter with theint
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
:
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:
>>> 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:
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:
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:
>>> 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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
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:
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:
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:
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 try
… except
block:
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
:
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:
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:
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:
>>> 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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
To kick things off, you’ll start by defining a class called CursesView
:
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 ofPattern
.gen
is the number of generations that you want the game to evolve through. It defaults to10
generations.frame_rate
represents the frames per second, which is an indicator of the time between displaying one generation and the next. It defaults to7
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:
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:
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 thetime
module. You’ll use this function to control the frames per second of your view. - Line 4 imports the
LifeGrid
class from thegrid
module in yourrplife
package. - Line 8 defines
._draw()
, which takes a screen orcurses
window object as an argument. This object is automatically passed in when you callcurses.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 use0
as an argument, which means that the cursor will be invisible. - Lines 13 to 18 define a
try
…except
block that raises aValueError
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:
>>> 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 ofCursesView
.-g
,--gen
will take the number of generations, with a default of10
.-f
,--fps
will take the frames per second, with a default of7
.
To download the code for this step, click the following link and look into the source_code_step_5/
folder:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
To start writing this CLI, you’ll add the following code to cli.py
:
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:
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:
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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
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:
__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 thesys.stderr
file object where you’ll be writing any error that occurs during the app’s execution. - Line 3 imports the
patterns
andviews
modules fromrplife
. You’ll use them to define the life pattern and view to use. - Line 4 imports
get_command_line_args()
from thecli
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 inargs
. - Line 8 uses the
args.view
command-line argument to access the desired view on theviews
module. To access the view, you use the built-ingetattr()
function. This way, you’re ensuring that your code is scalable, allowing new views without having to modifymain()
. Note that because the call togetattr()
returns a class, you’ve used a capital letter inView
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:
__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 try
… except
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:
__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:
$ 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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
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.
Note: To learn more about pyproject.toml
files and their format and options, check out the Everyday Project Packaging With pyproject.toml
video course. You can also listen to Episode 136: Package Python Code With pyproject.toml
& Listing Files With pathlib
of the Real Python Podcast.
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:
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:
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:
$ 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:
Get Your Code: Click here to download the source code to build Conway’s Game of Life with Python.
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