Lab 05

The Game of Life
Due by 6pm on Tuesday, March 10

The purpose of this lab is to:

Before you begin, please create a folder called lab05 inside your cs150 folder. This is where you should put all files made for this lab.

Part 1 - Printing yourself

Describe the Problem:
Write a program printMe.py that prints its code to the terminal.

Understand the Problem:
Python programs live in text files, which are easy for Python programs to read. For this problem you need to write a program in file printMe.py that opens file printMe.py and prints it.

Design an Algorithm:
This algorithm couldn't be easier:

Implement a Design:
Unlike previous assignments in which data was entered by the user or hard-coded into the program, here your data will come from a file. As such, you'll need a few tools for handling files.

Reading from a File

To work with an external file, you'll use the open function:

    <variable> = open(<filename>, <mode>)
This function opens the file with the given name and loads it into the specified variable. The <mode> can be either "r" or "w", depending on whether you intend to read from the file or write to the file (you can only do one type of operation at a time). In this case we're just reading from the file, so you'll want something like this:
    inputFile = open("test.txt","r")
    
You can now use functions associated with a file object, including:
  • <file>.read() Returns the rest of the file as a single string.
  • <file>.readline() Returns the next line of the file as a string.
  • <file>.readlines() Returns a list of all remaining lines.
Note that the <file> keeps track of what you've read so far. So if you call the read() function twice, the first call will return the first line of the file while the second call will return the second. If you want to start at the beginning again, <file>.seek(0) resets the implicit cursor to the beginning of the file.

Keep in mind that the lines returned by all these functions include the newline character at the end of each line. So if you call the print function on one of these lines, you'll print two newlines (creating a blank line). If you want to avoid this, you can either tell print not to add a newline at the end (end='') or you can just work with all but the last character in the line (myLine[:-1]).

You can also use a for loop to iterate through all the remaining lines in the file. For example,

 	
 	for line in inputFile :
 	   print(line, end="")


will print the entire file without double-spacing the lines.



Test the Program:
There isn't a lot of testing to do here; it either prints the file that contains its code or it doesn't.

Maintain:
There isn't much maintenance to do, either. Just be sure you understand how this works; you'll use something very similar in the next problem.

Handin
Be sure to hand in what you have finished so far.



Part 2 - The Game of Life

The Game of Life, created by mathematician John Conway, is a cellular automaton. It has a set of rules that are used to generate patterns that evolve over time. Despite the name, the Game of Life is not a game; it is really a simulation. In some ways, it can be thought of as an extremely simplified biological simulator which produces unexpectedly complex behavior.

The Game of Life is played on an infinite board made up of square cells. It takes way too long to draw an infinite board, so we'll make do with a small finite piece. Each cell can be either live or dead. We'll indicate a live cell as a red square, and a dead cell as a black one. The board begins in some initial configuration, which just mean a setting of each cell to be either live or dead (generally we'll start mostly dead cells, and only a few live cells).

A configuration of a 10-by-10 portion of the board with 9 live cells.

The board is repeatedly updated according to a set of rules, thereby generating a new configuration based on the previous configuration. Some dead cells will become live, some live cells will die, and some cells will be unchanged. This configuration is then updated according to those same rules, producing yet another configuration. This continues in a series of rounds indefinitely (or until you get bored of running your simulation).

Rules of Life

The rules are pretty simple: to figure out whether a cell (x,y) will be live or dead in the following round, you just look at the 8 neighbors of that cell (those that share a corner or an edge, so N, S, W, E, NW, NE, SW and SE). What happens to (x,y) next round depends on the number of its neighbors who are live and whether it is currently live or not. In particular:

For example, consider the following 3 initial configurations, and the two configurations that follow each.

Three initial configurations and two subsequent iterations.

In the first example, both live cells have only one live neighbor, so they both die. Any dead cell has at most two live neighbors, so no new live cells spawn. Thus in one step, there are no live cells. Clearly, at this point, the configuration is stable.

In the second example, two live cells have only one neighbor, so both die. But the third cell lives, and the cell to its immediate left has exactly 3 live neighbors, so it spawns. On the next iteration, we find ourselves in a case similar to the previous example, and all cells die.

Note that we can't set a cell to be live or dead the instant we determine its status for the subsequent round; we will likely need to know whether it is alive or dead on this round to determine the future status of other nearby cells. To see this, consider the second example. We can immediately tell that the top-most live cell will die. But had we set it to dead immediately, then when we got to the second live cell, it would have only had 1 live neighbor and we would have (erroneously) determined that it too must die. Thus it is critical that we first determine for every cell whether or not it will be live, and only after doing so update the status of each.

In the last example, all currently living cells die; the middle cell has too many neighbors, and the other have too few. However, four dead cells have exactly 3 live neighbors, and so those cells spawn. In the following round, there are neither cells that die nor cells that spawn, so we have a stable configuration that will remain the same generation after generation, like some small towns.

While all of these patterns stabilized quickly, some patterns take a long time to stabilize, and some never do. Of those that never stabilize, some at least have a regularity to them; they eventually eventually repeat states. Others never repeat the same state again, and produce an infinite number of configurations.

Describe the Problem:
Write a program life.py that simulates the game of life for a given number of iterations, displaying the state of the board graphically at each step. Your program should take its initial configuration from a text file, each line of which is a row,column pair that indicates one live cell.

Understand the Problem:
Make sure your answers from your prelab were correct. If not, go back and figure out what went wrong.

Design an Algorithm:
Let's think about how we might go about setting up our Life simulator. Since this program is relatively complex, we won't try to get everything working at once. For example, we won't do anything graphically until the logic of the simulation is working. We'll also give more details here than we usually do for lab problems.

So what components do we need to run our simulation? First, we'll want to keep track of our current board: which cells are alive, and which aren't. We'll encode these with a 2-dimensional table of booleans, called board, using True to represent live cells and False to represent dead cells.

Now let's think about how to perform a single update to board. At a high level, we simply want to update every cell in board. But remember, we'll run into problems if we update one cell and then update the cell next to it, since each update needs to be done as if none of the others have been done. To solve this, we'll do our updates on a new board, cleverly called newBoard, and then swap board and newBoard.:

Here is pseudocode for the entire program:

 

Dealing with Edges (Donuts!)

The edges of the board tend to be problematic. For example, if you are considering cell (0, 0), which is the upper left-hand corner of the board, and you attempt to check the liveness of its neighbors as you would for most cells, you'll end up attempting to access positions whose indices are negative. We can't have that.

One natural fix is to simply create a board that is 2 units taller and 2 units wider than you actually want, and only do updates on non-border cells. This approach would work but is a bit ugly. On this lab you're going to do something a bit different; we're going to use a torus rather than a plane for our game board. A torus is just the technical name for the shape of a donut.

This means that if you were to walk off the right side of the board, you'd appear on the left side at the corresponding position, and vice-versa. Likewise if you walked off the top of the board, you'd appear on the bottom. Every cell new has 8 neighbors. The reason this is so handy is that if you use the mod operator appropriately, you don't need any special cases to handle edges or corners. If we are currently at row i, column j, the row above is (i-1)%h (where h is the number of rows in the grid) and the row below is (i+1)%h. Similary, the column to the left is (j-1)%w (where w is the number of columns in the grid) and that to the right is (j+1)%w.

What did this have to do with donuts? Well, since the top of the board is now effectively connected to the bottom of the board, you could imagine the board as a flexible square of rubber, and this connection could be represented by gluing the top edge to the bottom edge. Now we've created a rubber tube. But the left and right edges are also connected. If we bend our tube and glue these edges together, we've created a torus, which is the mathematical name for a donut.


Implement a Design Let's think about the pieces of our algorithm.

We want the board to be a 2-dimensional array. Array of what? We could use strings "alive" and "dead" to represent those kinds of cells. We could use 1's to represent live cells and 0's to represent dead ones. It is up to you, but I suggest using Boolean's: True for live cells and False for dead ones. There is some economy to this: Booleans take up less space than other data types; I think there is also a certain elegance to saying
     if board[i][j]:
rather than
     if board[i][j] == 1:
when we test for whether a cell is live. Here is a way to make an array of Booleans with h rows and w columns:

board = []
for i in range(w):
    board.append([False]*h)

Make a method newBoard( w, h) that does this and returns the board it creates.

We we also need a way to initialize the board. Make a method inititalizeBoard(board) that sets specific squares of the board to True. I suggest using three squares in a row, such as the sqares at (3, 4), (3, 5) and (3, 6). Once the program is running correcly replace this with a method that asks the user for the name of an initialization file. . Each line of this file should have format
        row, col
as in
        3, 4

If you read the file with a loop
        for line in F:

then you can say

       for line in F:
               i, j = eval(line)
               board[i][j] = True

Just a few lines of code here give us a flexible way to specify starting configurations. This shows the power of Python.


   pic = picture.Picture((w,h))	    # create a w-by-h pixel picture object
   pic.setFillColor((r,g,b))	    # set shape fill color to (r,g,b)
   pic.setOutlineColor((r,g,b))	    # set shape border color to (r,g,b)
   pic.getWidth()                   # return the picture width in pixels
   pic.getHeight()                  # return the picture height in pixels
   pic.drawRectFill(x,y,w,h)        # draw a w-by-h rectangle with upper-left corner at (x,y)
   pic.display()                    # display any updates in the picture to the screen

If you keep a square canvas and a board that has the same number of rows and columns, then the size of each square is the height of the canvas divided by the number of rows. The starting position of the square in row i and column j is x = j*size, y = i*size. This means we could use a loop that runs through the rows and columns of the board. If the row i, column j entry is live, set the fill color to (0, 0, 0) (i.e, black). If it is not live set the fill color to (255, 255, 255) (i.e, white). Then call canvas.drawRectFill(j*size, i*size, size, size). Don't forget to call canvas.display() at the end to make the grid appear.

After the initialization, my main( ) method calls the nextGen( ) function in a loop. I use a for-loop for 100 generations, but if you prefer you could use a while-loop that runs forever. To control the speed of the animation make the last line of this loop

canvas.delay(200)

where the numerical argument is the number of milliseconds to wait before going back to the top of the loop. 200 works well for this on my laptop; you'll have to play around a bit to see what works on your computer or the lab machines.

Data

Like many graphical programs, this one has a lot of data that needs to be accessed in a lot of places. Classes give a good way to organize such programs, but we haven't discussed them in enough detail yet. One way to handle this problem is to use a few global variables. Variables that are assigned values outside of any function are accessible at any point in the program below where their assignments. I suggest that you put at the top of the program statements creating some of the variables you will use all over:

ROWS = 10 # number of rows on the board
COLS = 10  # number of columns on the board
SIZE = 60    # size of one square on the board

By putting these at the top they will be visible throughout the program and will be easy to find if you want to change them.

Getting Started

Here are my global variables and my main( ) function. You don't need to use this, but it is at least one way to break the problem into reasonable-sized, coherent chunks.

import picture

ROWS=20
COLS = 20
SIZE = 30
NUM_GENERATIONS = 50

def main():
       canvas = picture.Picture(COLS*SIZE, ROWS*SIZE)
       board = newBoard()
       initializeBoard( board )
       for g in range(NUM_GENERATIONS):
                board = nextGeneration(board)
                displayBoard( canvas, board )
                canvas.delay(100)

In summary, you should build a program life.py that animates the Game of Life. The version of this you should hand in should be set to 20 rows and 20 columns and should prompt the user for the name of an initialization file.

Test the Program:
Try your program on a few of the configurations you've solved manually. Make sure the program works properly not just on the first iteration, but on subsequent iterations as well. Does the simulation behave correctly on the edges of the board? If something isn't working, what functions might you add to try to pinpoint the problem?

Here are configuration files for some standard Game of Life figures:


Maintain:
Make sure your code is "readable": use short but meaningful variable names, use constants where appropriate, use functions where you can, and comment any code that does anything substantial (for example, it would be a good idea to put a comment before your for loops explaining the purpose of the for loops, before each function explaining the function's parameters and purpose, etc.) It is not necessary to comment assignment statements and simple things like that.


Handin
Be sure to hand in what you have finished so far.


Improvements

Better Animation: If you run your program long enough you will find that your animation slows down over time. This is because you are continually drawing new rectangles on the canvas and the TkInter system keeps track of all of these rectangles, even if they aren't visible. If you have time and are interested in graphics, here is a way to improve the program. Rather than calling drawRectFill( ) every time you want to update the screen, make a new 2D array called tiles where each tile is one of the rectangles. The drawRectFill( ) method of the picture class returns a "shape" object whose color can later be changed. A loop such as

tiles = newBoard()
canvas.setFillColor(255, 255, 255)
for i in range(ROWS):
      for j in range(COLS):
            tiles[i][j] = canvas.drawRectFill(j*size, i*size, size, size)

creates the tiles array. Then, instead of redrawing rectangle (i, j) in color (r, g,, b) you can call

tiles[i][j].changeFillColor( (r, g, b) )

Notice the extra parentheses around (r, g, b) changeFillColor takes one argument, which is a tuple with the fill color.

Aging Cells: You might keep track of how many rounds each live cell has been alive, and color the cell based on that age. For example, you might have newborn cells begin red but slowly turn blue as long as they stay alive.

Handin

If you followed the Honor Code in this assignment, make a README file that says

I affirm that I have adhered to the Honor Code in this assignment.

You now just need to electronically handin all your files. As a reminder

 
     % cd             # changes to your home directory
     % cd cs150       # goes to your cs150 folder
     % handin         # starts the handin program
                      # class is 150
                      # assignment is 5
                      # file/directory is lab05
     % lshand         # should show that you've handed in something

You can also specify the options to handin from the command line

 
     % cd ~/cs150     # goes to your cs150 folder
     % handin -c 150 -a 5 lab05

File Checklist


You should have submitted the following files:
   printMe.py
   life.py
   README.txt

T. Wexler, A. Sharp, C. Taylor, R. Geitz