CS303E Project 2

Instructor: Dr. Bill Young
Due Date: Monday, November 11, 2024 at 11:59pm

Copyright © William D. Young. All rights reserved.

Game of Life

The Game of Life was devised by the British mathematician John Horton Conway in 1970. It is a zero-player game, meaning that its evolution is determined by its initial state, requiring no further input. One interacts with the Game of Life by creating an initial configuration and observing how it evolves.

The rules are quite simple, but it's actually surprisingly sophisticated. The game is known to be Turing complete. This means that any computation that can be performed could be performed by setting up an appropriate instance of the game. The most powerful computer on the planet could be simulated in the Game of Life, though it would be incredibly complicated to do so.

The universe of the Game of Life is an infinite, two-dimensional grid of cells, each of which is in one of two possible states, live or dead. Every cell interacts with its eight neighbors, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:

  1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
  2. Any live cell with two or three live neighbours lives on to the next generation.
  3. Any live cell with more than three live neighbours dies, as if by overpopulation.
  4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
The pictures below illustrate this (for the middle cell in each grid).

The initial pattern constitutes the seed of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed, live or dead; births and deaths occur simultaneously. The rules continue to be applied repeatedly to create further generations.

The Assignment

Your task is to implement a slightly simplified version of the Game of Life. The only real simplification is that the grid will be of a fixed finite size rather than infinite. Of course, it's impossible to implement an infinite grid, so we'll use a square grid (2D list) of size x size, with size provided as a parameter to your class constructor. I use various values in the sample output below just to save space in this document.

You must implement your grid as an instance of a class. Then you will write a function that updates the grid for one step according the Game of Life rules and another function that updates the grid n successive steps.

Defining the Grid Class: Each cell of the grid will contain either DEAD or LIVE. Your Grid class will have at least the methods shown in the template below. Implement your grid as a 2D list of strings where DEAD is the string "." and LIVE is the string "X". That way when you print the grid you can easily see where the cells are and see patterns of live cells. In my __str__ function, I added an extra space between cells in each row so that the grid appears more square when printed. See the sample output below.

One of the trickiest things about playing Game of Life on a finite grid is dealing with cells on the border. E.g., how many live neighbors does cell (0, 0) have? It's possible to do that by programming in a bunch of special cases. But, as with many problems in programming, there's a much easier and clever way to do it. Cells outside the grid border should be considered DEAD, but if you write your code carelessly it will crash as you try to access cells that aren't defined. My suggestion is to ask first if either or both of a cell's coordinates would make it fall outside the grid; if so, call it DEAD. Otherwise, ask if grid[row][col] contains LIVE. That way, when counting the live neighbors of cell grid[0][0] you can safely ask about the "cell" at grid[-1][-1], for example, without crashing your program. Think about this when you program the class method isCellLive.

A template for the Grid class is below. For each method, replace pass with your code.

LIVE  = 'X'
DEAD  = '.'

class Grid:
    """This defines the playing surface for Conway's Game of Life."""
    
    def __init__(self, size):
        """Create the grid as a 2D list of dimensions size x size. Initially,
        each cell of the grid contains DEAD."""
        pass
        
    def __str__(self):
        """Return a string to display when printing the grid.
        I put in an extra space between columns so that it would
        appear more square.  Don't forget to include a newline
        after each row."""
        pass

    def getGridSize(self):
        """Return the size of the grid."""
        pass
    
    def getCell(self, row, col):
        """Fetch the contents of the cell at coordinates (row, col).
        Assume that these are legal coordinates."""
        pass

    def setCell(self, row, col, value):
        """Set the contents of the cell at coordinates (row, col) to
        value (presumably this should be either DEAD or LIVE. 
        Assume that the coordinates are legal."""
        pass

    def isCellLive(self, row, col):
        """A cell is live if it's within the grid and contains LIVE.
        Otherwise, it's considered DEAD. Rather than returning a
        boolean, this returns 0 or 1 so we can also use this in counting
        live cells.  Note that in a Boolean context, 1 and 0 work just
        as well as True and False."""
        pass

    def countLiveNeighbors( self, row, col ):
        """Count the live neighbors of the cell with coordinates (row, col).
        To do this we just need to ask of each for the 8 possible neighbors
        whether it's alive and count those that are. Return the count."""
        pass
Other Functions: In addition to the class described above, you must define several other functions that makes use of the class. These are the functions that update the grid according to the rules of the game, along with a function that makes it more convenient to set up the initial seed. These functions should be defined outside of the Grid class, but in the same file. Feel free to define auxiliary functions if you like, but you must have at least the following functions:
def makeCellsLive( grid, cellsList ):
    """Given a list of coordinate pairs (x, y), make each of these
    LIVE in the grid."""
    pass

def stepGameOfLife( grid ):
    """This implements one step in the Game of Life.  Given a grid, update
    it following the rules of the games. I.e., for each cell in the grid, count
    its live neighbors and decide whether the cell is LIVE or DEAD in
    the next generation.  Then update the grid to reflect the changes."""
    pass

def playGameOfLife( grid, n ):
    """Given an initial grid, take n steps in the Game of Life."""
    pass
Notice that in stepGameOfLife you need to define another 2D array to hold the results temporarily. You can't just modify your input grid in place as you go. Otherwise, by the time you get to row k you'll have already updated row k-1 which will change the results. Instead, create an empty 2D list of the same size as your grid, put the results there, and at the end copy them back into your grid.

The coordinates of a cell are two integers row and col, where 0 ≤ row < size and 0 ≤ col < size. In function makeCellsLive the second argument is a list of coordinate pairs. Each element of this list is of the form (x, y) and you can access the components of a pair p by p[0] and p[1]. This is all explained in the Tuples material from Week 11.

Sample Output:

Below is some sample output for this program. Note that this is run in the Python interactive loop. You can run yours from your IDE, but the TAs should be able to import your module and run your functions in an interactive loop.

The pattern shown is a "glider." In four steps the pattern repeats but has moved down and to the right.

>>> from Project2 import *
>>> grid = Grid( 10 )
>>> print(grid)
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 

>>> GLIDER = [ (0, 1), (1, 2), (2, 0), (2, 1), (2, 2) ]
>>> makeCellsLive( grid, GLIDER )
>>> print(grid)
. X . . . . . . . . 
. . X . . . . . . . 
X X X . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 

>>> stepGameOfLife( grid )
>>> print(grid)
. . . . . . . . . . 
X . X . . . . . . . 
. X X . . . . . . . 
. X . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 

>>> stepGameOfLife( grid )
>>> stepGameOfLife( grid )
>>> stepGameOfLife( grid )
>>> print(grid)               # glider again, but moved
. . . . . . . . . . 
. . X . . . . . . . 
. . . X . . . . . . 
. X X X . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 

>>> playGameOfLife( grid, 4 )
>>> print(grid)
. . . . . . . . . . 
. . . . . . . . . . 
. . . X . . . . . . 
. . . . X . . . . . 
. . X X X . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 
. . . . . . . . . . 

>>> 

Another interesting pattern is the "pentadecathlon." It goes through 14 different patterns before it repeats. Note: it's necessary to build it away from the borders because some of the intermediate patterns are larger than the initial pattern.

> python
Python 3.8.10 (default, Jul 29 2024, 17:02:10) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from Project2 import *
>>> grid = Grid( 20 )
>>> print(grid)
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 

>>> PENTADECATHLON = [ (5, 5), (5, 6), (4, 7), (6, 7), (5, 8), (5, 9), (5, 10), (5, 11), (4, 12), (6, 12), (5, 13), (5, 14) ]
>>> makeCellsLive( grid, PENTADECATHLON )
>>> print(grid)
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . X . . . . X . . . . . . . 
. . . . . X X . X X X X . X X . . . . . 
. . . . . . . X . . . . X . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 

>>> stepGameOfLife( grid )
>>> print( grid )
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . X X X X X X X X . . . . . . 
. . . . . . X . X X X X . X . . . . . . 
. . . . . . X X X X X X X X . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 

>>> stepGameOfLife( grid )
>>> print( grid )
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . X X X X X X . . . . . . . 
. . . . . . X . . . . . . X . . . . . . 
. . . . . X . . . . . . . . X . . . . . 
. . . . . . X . . . . . . X . . . . . . 
. . . . . . . X X X X X X . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 

>>> playGameOfLife( grid, 13 )
>>> print( grid )             # back where we started
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . X . . . . X . . . . . . . 
. . . . . X X . X X X X . X X . . . . . 
. . . . . . . X . . . . X . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 
. . . . . . . . . . . . . . . . . . . . 

>>> 
You should try some of your own patterns. If you Google "Conway's Game of Life" you can find some interesting ones. Some repeat (after a certain period), some move across the grid, some disappear, some grow, etc. The TAs should be able to import your module and run their your own patterns to see that your functions work correctly. Here are some patterns you can explore:
BLINKER = [ (3, 3), (4, 3), (5, 3) ]
BLOCK3x3 = [(3, 3), (3, 4), (3, 5), (4, 3), (4, 4), (4, 5), (5, 3), (5, 4), (5, 5)]
BLOCK2x4 = [(3, 3), (3, 4), (3, 5), (3, 6), (4, 3), (4, 4), (4, 5), (4, 6)]
CROSS = [(7, 3), (7, 4), (7, 5), (7, 6), (7, 7), (7, 8),
         (8, 3), (8, 4), (8, 5), (8, 6), (8, 7), (8, 8),
         (5, 5), (5, 6), (6, 5), (6, 6), (9, 5), (9, 6), (10, 5), (10, 6)]

Turning in the Assignment:

The program should be in a file named Project2.py. Submit the file via Canvas before the deadline shown at the top of this page. Submit it to the assignment project2 under the assignments sections by uploading your Python file. Make sure that you following good coding style and use comments.

Your file must compile and run before submission. It must also contain a header with the following format:

# File: Project2.py
# Student: 
# UT EID:
# Course Name: CS303E
# 
# Date: 
# Description of Program: 

Programming Tips:

Program incrementally: Writing any complex program should be done in pieces. It's crazy to do the entire thing and then try to debug it. Yet that's what I frequently see students do. Instead, write the simplest version first and test it before you move on. For example, write the init, str, and setCell methods of the Grid class and get them working before moving on. Then write the isCellLive and countLiveNeighbors methods and test them thoroughly. Finally, move on to the functions outside the class. The extra time will more than pay off in greatly reduced frustration!