Python Arena
Project · After module 19: More lists

The Game of Life

The Game of Life was invented by the mathematician John Horton Conway in 1970. Nobody plays it: you set up a pattern of cells and watch what happens to it. The board is a grid of cells, and each cell is either alive or dead. Every cell has eight neighbors, the cells that touch it: above, below, left, right and the four diagonals. From one generation to the next, every cell follows the same rules, all at the same time:

  • A live cell with 2 or 3 live neighbors stays alive.
  • A live cell with fewer than 2 live neighbors dies, as if of loneliness, and one with more than 3 dies of overcrowding.
  • A dead cell with exactly 3 live neighbors comes alive.
  • Any other dead cell stays dead.

That's all. Yet some patterns blink forever, some freeze, some die out, and some even crawl across the board.

In this project the grid is a list of lists, as in module 19: one list per row, with 1 for a live cell and 0 for a dead one. On the screen, a live cell is # and a dead one is ..

When it's finished, the program asks for a starting pattern, typed one row at a time and ended with an empty line, and for the number of generations to show. With this input:

......
.###..
.#....
......
......

9

it prints:

Type the grid, one row per line, and an empty line at the end:
......
.###..
.#....
......
......

Generations: 9
Generation 0: 4 alive
......
.###..
.#....
......
......

Generation 1: 4 alive
..#...
.##...
.#....
......
......

Generation 2: 6 alive
.##...
.##...
.##...
......
......

Generation 3: 6 alive
.##...
#..#..
.##...
......
......

Nothing changes any more.

Nine generations were asked for, but after the third one the pattern has settled into a shape that never changes, so the program stops there.

What you need

Everything up to module 19: functions that return values, with the program's own code under if __name__ == "__main__": (modules 13 and 14), lists and for loops with range (modules 15 and 16), f-strings (module 17), and above all lists of lists, read with two indexes such as grid[row][column] (module 19).

How you build it

Each milestone starts from your own code of the milestone before, and adds a function or two:

  1. The grid: print it, and count its live cells.
  2. Neighbors: count the live cells around a cell, and decide whether it lives on.
  3. The next generation: a whole new grid, worked out from the old one.
  4. A start from input: read the starting pattern that the user types.
  5. Several generations: run as many generations as the user asks for.
  6. Stopping early: stop when everything is dead or nothing changes any more.

The checks of the first milestones call your functions directly, as in the lessons. From milestone 4 on, others type into the whole program and compare everything it prints.

Milestones

  1. The grid
  2. Neighbors
  3. The next generation
  4. A start from input
  5. Several generations
  6. Stopping early

Milestone 1: The grid

The starting code has a grid called glider: a list of 5 rows, each a list of 5 cells, where 1 is a live cell and 0 a dead one. Its main block uses two functions that aren't written yet. Write them:

  1. print_grid(grid) prints the grid, one row per line: # for a live cell and . for a dead one, with nothing between the cells.
  2. count_alive(grid) returns how many cells of the grid are alive.

Both work for a grid of any size: any number of rows, each with the same number of cells. The check runs:

grid = [[0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 1]]
print_grid(grid)
print(count_alive(grid))

and expects:

.#..
.##.
...#
4

Milestone 2: Neighbors

Whether a cell lives on depends on its eight neighbors. Add two functions:

  1. count_neighbors(grid, row, column) returns how many of the eight cells around the cell grid[row][column] are alive. The cell itself doesn't count. A cell on the edge of the grid has fewer neighbors: a place outside the grid counts as a dead cell.
  2. will_live(grid, row, column) returns True when the cell is alive in the next generation, and False when it isn't, by the rules of the game:
    • a live cell stays alive with 2 or 3 live neighbors, and dies otherwise;
    • a dead cell comes alive with exactly 3 live neighbors, and stays dead otherwise.

Watch out at the edges. Rows and columns are counted from 0, so the cell above row 0 would be in row -1, and Python reads grid[-1] without any error: it is the last row. The edges would then wrap around to the other side of the grid without you noticing. Check that a row and a column are inside the grid before you read the cell.

The check runs:

grid = [
    [0, 1, 1, 0, 0],
    [1, 1, 0, 0, 1],
    [0, 0, 0, 1, 1],
    [0, 0, 0, 0, 1],
]
print(count_neighbors(grid, 1, 1), count_neighbors(grid, 1, 2), count_neighbors(grid, 0, 0), count_neighbors(grid, 0, 4), count_neighbors(grid, 3, 0))
print(will_live(grid, 1, 1), will_live(grid, 1, 2), will_live(grid, 0, 0), will_live(grid, 0, 4), will_live(grid, 3, 4))

and expects:

3 4 3 1 0
True False True False True

Milestone 3: The next generation

Now the whole grid takes a step. Every cell changes at the same time: the next generation is worked out from the current grid alone. So you can't change the cells of grid one by one, because a cell you have already changed would give its neighbors a wrong count. Build the next generation as a new grid instead.

Write next_generation(grid), which returns a new grid of the same size, where each cell is 1 when will_live says it lives and 0 otherwise. The grid it was given stays as it was.

To watch it in your own program, add print() and print_grid(next_generation(glider)) to the end of the main block. The check runs:

grid = [
    [0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0],
]
print_grid(next_generation(grid))
print()
print_grid(grid)

and expects the next generation, then the grid it was given, unchanged:

.....
.....
.###.
.....
.....

.....
..#..
..#..
..#..
.....

This pattern is called a blinker: it turns from standing to lying and back again, forever.

Milestone 4: A start from input

Typing a pattern into the code as a list of lists is slow. From now on, the user types it, one row per line, as it looks on the screen: # for a live cell and . for a dead one. An empty line ends the grid.

  1. Write read_grid(), which reads the rows with input() (with no prompt) until it reads an empty line, and returns the grid as a list of lists of 1s and 0s. Every character other than # is a dead cell.
  2. Write print_generation(grid, number), which prints a line such as Generation 0: 5 alive (the number it's given, and how many cells are alive), then the grid, and then an empty line.
  3. Write a function main(), and replace everything in the main block with a call to it. main prints Type the grid, one row per line, and an empty line at the end:, reads the grid, and prints it as generation 0 and its next generation as generation 1.

When the user types a glider and an empty line:

.....
..#..
...#.
.###.
.....

the program prints (the typed rows show up in the output, as on a terminal):

Type the grid, one row per line, and an empty line at the end:
.....
..#..
...#.
.###.
.....

Generation 0: 5 alive
.....
..#..
...#.
.###.
.....

Generation 1: 5 alive
.....
.....
.#.#.
..##.
..#..

Milestone 5: Several generations

Two generations show only the beginning of the story. After reading the grid, the program now asks how many generations to show, with the prompt Generations: . Then it prints generation 0, the grid as it was typed, and after it each new generation up to the number asked for, each worked out from the one before.

With the input

.....
.....
.###.
.....
.....

2

the program prints:

Type the grid, one row per line, and an empty line at the end:
.....
.....
.###.
.....
.....

Generations: 2
Generation 0: 3 alive
.....
.....
.###.
.....
.....

Generation 1: 3 alive
.....
..#..
..#..
..#..
.....

Generation 2: 3 alive
.....
.....
.###.
.....
.....

With 0 generations, the program prints only generation 0.

Milestone 6: Stopping early

Many patterns die out or freeze after a while, and printing the same empty grid fifty times helps nobody. Before it works out each new generation, the program now looks at the generation it printed last:

  • When it has no live cells, the program prints Everything is dead. and stops.
  • When the next generation would be exactly the same as it, the program prints Nothing changes any more. and stops.

Otherwise it goes on as before, up to the number of generations asked for. Comparing two grids takes no loops: == compares two lists item by item, and the lists inside them as well, so new_grid == grid is True when every cell is the same.

With the input

#...
.#..
....

5

the two lonely cells die at once, and the program prints:

Type the grid, one row per line, and an empty line at the end:
#...
.#..
....

Generations: 5
Generation 0: 2 alive
#...
.#..
....

Generation 1: 0 alive
....
....
....

Everything is dead.