PreLab 05

The Game of Life
Due in class on Wednesday, March 4.

In this prelab you will formulate some of the ideas necessary to complete Lab 05. Please write legibly or type your solutions, and hand in a paper copy at the beginning of lecture on Wednesday. Remember, no late prelabs allowed!

Part 1 - List Practice

1. Write a chunk of code that starts with the list [1, 1] and creates a list containing the first 30 fibonacci numbers (1, 1, 2, 3, 5, 8, and so on). Don't use any variables other than the list itself, and an index variable (i.e. don't duplicate the code we developed earlier to compute the fibonacci numbers -- use the list!).

2. Write a chunk of code that prints the second largest integer in a list A of integers. You may assume A has already been declared and given values, and that all values are distinct. Don't use any Python list methods (like sort), and don't change the list. You should be able to find the second largest integer in a single pass through the list.

3. Write pseudocode for an algorithm that counts the number of occurrances of each letter in a string s. For example, if s is the string "Oberlin College" the output of this algorithm might be

b: 1
c: 1
e: 3
i: 1
l: 3
n: 1
o: 2

You probably want to make a list of 26 counters and count a's at index 0, b's at index 1 and so forth. To find the right index for letter x, use alphabet.index(x), where alphabet is the string "abcdefghijklmnopqrstuvwxyz".

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:
The problem you will solve on your lab is as follows.
input: an initial configuration file, whose name is supplied by the user
goal: simulate the game of life starting with this initial configuration.

Understand the problem:
Consider the following four intial configurations.

Initial configurations A, B, C and D.

For each of these, you should specify the next four generations of each. You can either draw pictures, or type solutions. If you do the latter, I suggest you use a fixed width-font (like courier) and use a period (.) for a dead cell, and an X for a live cell. For example, the initial configurations would look like:

.....   .....   .....   .X...
.....   .XXX.   .XX..   ..X..
.XXX.   XXX..   .XX..   XXX..
.....   .....   .....   .....
.....   .....   .....   .....

4. Specify the next 4 generations of configuration A.

5. Specify the next 4 generations of configuration B.

6. Specify the next 4 generations of configuration C.

7. Specify the next 4 generations of configuration D.

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.

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 integers (a list of lists), called board, using 1 to represent live cells and 0 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, based on -- but without changing -- the original board. Now we can view our overall strategy as follows:

This list immediately suggests a few functions that will be useful to create: a function to print the board in a manner as suggested on the prelab so we can see if things are working; a function that creates a board of zeroes of a given width and height; a function that counts the number of live cells neighboring a particular cell in an array; and finally, a function that uses the previous function to update a board.

As we saw in class, we can make a two dimensional array -- a list of lists -- with all values initialized to 0 as follows:

   a = []
   for i in range(w) :

We will do something similar, only we'll make our board be an array of Boolean values and initialze all of the entries to False.

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.

Honor Code

If you followed the Honor Code in this assignment, write the following sentence attesting to the fact at the top of your homework.

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