CSCI 233 Python Exercise 10

In this lab, you will be further developing the “Life” simulation that we have been working on in class, including adding a graphical user interface for it.

Some general principles

The guidelines here for how you should structure your program may seem a bit odd—or more complicated that necessary. In particular, it sometimes seems like there are two objects for everything, one for the internals and one for the interface. While this makes for a little more work initially, experience has proven that it is wise to try to isolate the main part of a program from the details of the library (or modules) used to construct an interface for it. We will go so far as to devdelop the GUI as a separate Python module.

You can copy the current version of the program from

/cslab/class/csci233/in-class/03-24/life.py

What we have worked on so far is the internals of the simulation, which consists of the simulation itself, its grid, and the cells in the grid. We have represented this in terms of two main classes, the Grid for representing the simulation and its grid together, and the Cell for the cells within the grid. We also have an alternative class for cells, the BoundaryCell, which provides the same methods as Cell but gives different behavior.

Initial changes

There are two additions/changes that we need to make to the Grid class before we start working on the GUI.

First, we want to keep track of the time in the simulation. Add an instance variable to the Grid class to keep track of how many generations (steps) the simulation has been run, along with an accessor method to get that value.

Next, we need uncouple the Grid.display() method from printing. We can do that by making a GridPrinter class that has the following methods:

    def start(self):
        "Do whatever is needed to start displaying a grid."
    def startRow(self, r):
        "Do whatever is needed to start display row number r."
    def showElement(self, r, c, value):
        "Show value for element at row r, column c.’’
    def endRow(self, r):
        "Do whatever is needed to end display of row number r."
    def end(self):
        "Do whatever is needed to complete display of the grid."

Add this class to life.py, with the methods filled in appropriately, and modify the method Grid.display so that you pass it an instance of GridPrinter as a parameter.

You should now be able to test the program from the Python prompt with a sequence something like:

    p = GridPrinter()
    g = Grid(...) # fill in the parameters
    g.display(p)
    g.toggle(...)
    g.step()
    g.display(p)

If you want to play around with this a little bit, you could make another class with the same methods as GridPrinter, but have it do things like label the rows and columns, and indent what it prints. You should then be able display a grid using an instance of your new class or an instance of the original GridPrinter.

What we have done here is to abstract a pattern of interaction into a class’s interface. This frees us to provide more than one kind of class the implements that interface, so that we can plug in different behaviors.

Adding a GUI interface

We will build our GUI interface in a new file, lifegui.py, which will take the place of the calls we’ve been typing at the Python prompt. So create that new file, fill in its heading, and start it out by importing from Tkinter (for the widgets) and life (for our simulation).

Recall from last week that we structure GUI programs as an “application” object that corresponds to the root window. You might want to copy some of the common code from the template in last week’s lab. We’ll be filling in the application class with two frames. The first will hold our controls, while the second shows the grid. The application will also create and use an instance of Grid that holds the simulation’s state.

In the controls frame, there should be a button for causing the simulation to advance one step, and there should be a widget that displays the number of steps that have been simulated. Build that frame, leaving the frame for the grid display empty. Provide a method for the “step” button; note that it needs to both run the simulation for a step, update the displayed time, and redisplay the grid. For right now, you can create a GridPrinter and use it to print out the new grid on each step.

(To make testing more interesting, you might want to have toggle a few cells in your grid after you initialize it.)

The next step will be to fill in the display frame with an array of buttons that correspond to the cells. I suggest that you do this with two classes, one for the displayed grid, and one for a displayed cell. The displayed grid should provide the methods you defined for GridPrinter, but with the changes somehow showing up in the GUI instead of printing. That way you can call pass your display grid object to the Grid.display() method in place of the GridPrinter.

Tip: The end method on your displayed grid can call the update method on the root window to cause everything to show up on the screen.

To make this work, you’ll need to create Button objects that know where they are in the grid. We do this by extending the Button class, which looks like this:

    class GridButton(Button):
        def __init__(self, other arguments, row, column):
            ... some initialization ...
            Button.__init__(self, parent, ... other args ...)
            self.row = row
            self.column  = column

An instance of GridButton is said to inherit from Button, its parent class. This means that is has all of the methods of its parent. We can add our own methods and instance variables to it.

Work first just to get your grid working as a display. Then add a method for the command parameter that will toggle the appropriate cell of the underlying simulation grid.

Turning it in

There is nothing to turn in today. An assigment to complete this program will come out shortly.