Lab 3 in Object Oriented Programming

Background: The Game of Life

In this lab, we will devote ourselves to the Game of Life, a very simple model of a population of organisms that can live, die and reproduce in a two-dimensional world. Despite its simplicity, simulations of this population show very complex behavior, and the model has therefore become very well known. Web Search provides an impressive number of hits and leads to places that catalog interesting situations, analyze the model in various respects, and provide programs in various programming languages ​​and environments to perform simulations. It is not very difficult to find Java applications that work similarly to the program that you will develop in this lab. Nevertheless, it is probably not easy to download a program and adapt it to our requirements. Start from scratch and follow the instructions and the advices that we give here.

This lab instruction is quite long, but the amount of code to be written is not larger than in the second lab. We hope that you will still have time to read the introduction and make the suggested preparations.

A screenshot on my Mac of the program is shown on the picture below.

On other platforms (Linux, Windows) the decorations on the top will look a bit different, tailored to the respective platform "look and feel". In this lab we will implement the program on Linux.

The main part of the window is occupied by a two-dimensional world, which consists of a grid of cells (in this case 50x50 cells). A cell can be living (black) or dead (white). In one step, the entire population is moving to a new generation. The rules for this are as follows. For each cell (both living and dead), we count the number of live neighbors that it has (the eight surrounding cells; cells along the edges have only five neighbors and the four corner cells have only three neighbors). A cell that is alive in generation n remains alive in generation n+1 if it has two or three live neighbors. A cell that is dead in generation n comes alive in generation n+1 if it has three living neighbors. In all other cases, the cell becomes dead in generation n+1.

The interface above contains, apart from the world, three buttons. It should not be hard to guess that the Step button makes a step, i.e. computes and displays the next generation, while the Run button sets in motion the development of one generation after another, something which can be stopped with the Stop button. What is not visible on the image on the left is that by clicking the mouse on a cell you can change it from living to dead or vice versa, and that by clicking on one of the small circles on the top left you can terminate the program.

Graphical User Interfaces

Our program is a simple example of a program with a graphical user interface - with buttons and other visual components that offer the user different ways to interact with the program by using the mouse and a keyboard (in this case the keyboard has no role to play). Practically all modern applications that have a human user interface are of this type and an important goal in the development of Java has been to provide many powerful graphical widgets. At the same time this means that there are lots of different types of components, lots of abilities to configure these and a lot of details when deploying these components in a window. Initially we want to avoid going deeper into these details.

Fortunately, programs with graphical interface are designed in a way that separates the graphics and the visual components from the parts of the programs that describe the underlying data and its manipulation. In this program you will have therefore given all the parts that have to do with the graphics and will instead program the underlying model.

Preparation

Download lab3.zip to your directory for the course and unpack the file with unzip: you will receive a subdirectory lab3, containing a program consisting of four files:

You should now do the following:

  1. Go to directory lab3 and compile all Java files.
  2. Start the program:
    > java Main 0.3
    

    the command line argument is intended to indicate the probability that a single cell in the initial generation is alive, but LifeModel does not care about this, it just creates a checkerboard-like grid by returning true for the cell at position (i, j) if and only if (i+j) % 2 == 0

    Now try to interact with the program by pressing the buttons or clicking with the mouse in the grid and find that the program does not seem to react at all.   

  3. What we have seen so far is that a program with graphical interface will not automatically respond when the user clicks on the different components. To get any reaction, there must be listeners registered in the components. Open classes LifeWorld.java and LifeView.java and you will see that there are some lines commented out: a row in LifeWorld and three lines in LifeView. It is exactly these lines that add the listeners to the components. Change to commented out these lines (i.e. remove // in each row) and recompile the classes. Anyone who wants to understand better how the listeners work can read here, but it is also possible to move on without reading this now.   
  4. When you run the application now, it will react to the the mouse clicks, but only with the prints that are defined in LifeModel. By clicking with the mouse in the grid you can see which methods in the model class are called.   
  5. Resize the window and see that the buttons vary in size in a rather strange way, while the image of the world is of fixed size regardless of the window size. We can not understand the details here, but must content ourselves with the realization that it is not so easy for components to adapt when the user changes the window size in an unplanned manner. You can solve this problem in a better way at the expense of making the graphical classes are more complex.   

Event-driven programs.

So far, our picture of a program has been that it mainly consists of a main routine that describes what we want the program to do. The routine is typically comprised of the successive statements to be executed - perhaps a loop that repeats a task several times. When the lines are executed the program is finished. In addition, we have learned to split the work in a program by writing functions and subroutines that can be called for subtasks.

The program in this lab is completely different. Please look at the main routine in Main.java. It first creates (with new) three items:    

After that it interprets the command-line argument and tells the model object to create a random start population with the probability given on the command line.

Finally, the four operations on the window:    

Then main finishes, but the program does not end. On the contrary, it is only now that we as users can start interacting with the program by pressing buttons or by clicking on the world.

the task of main in this program is only to create and configure a few items, which are kept alive and interact with the user long after the main is completed.

It will take time to fully understand how this kind of program works. For this lab, it is important to understand that the class you intend to implement, LifeModel is a class from which one (and only one) object is created. It interacts with the other objects in the program.    

Your programming task

You will improve LifeModel so that the program gets the desired behavior as was described initially.

An object of this class has knowledge of a population in the Game of Life, i.e. it can answer questions about population and it can also change the population when someone from the outside world calls its methods. Between method calls the object is just passively waiting for the next method call. One should make it so that the object provides a number of services, which other objects can request them to get done.

One can imagine a program that creates several objects of this class for different populations and then let them develop separately, but in our case the program creates only a single population. The class does not need to be modified to be used in a program that creates multiple instances.

The class contains, besides the constructor seven methods:

int getWidth()
int getHeight()
int getGen()
boolean getCell(int i, int j)
void setCell (int i, int j, boolean b)
void randomPopulation(double fill)
void newGeneration()

The first three methods are already implemented in the class you have received. It remains for you to define the other four methods. We suggest that you follow the steps below.

The first four methods provide information on the current population:    

Step 1.

Define the method:

boolean getCell(int i, int j)

by performing the following substeps.

      
  1. add another instance variable:
    private boolean[][] world;
    

    to class LifeModel. This line will be added at line three, right after the previous three instance variables. Note that these variables are declared inside the class but outside all methods. This is characteristic of the instance variables; all methods in the class can access these variables and it is the methods' common responsibility to update and inform about the state.

  2. All instance variables must be given appropriate initial values in the class constructor. The constructor looks like a method but has the same name as the class itself and also it does not have result type.
    public LifeModel(int w, int h) {
       width = w;
       height = h;
    }
    

    For the object model of this class to exist, it must be created somewhere else in the program (in this case it is created in the main routine):

    LifeModel model = new LifeModel(50, 50);
    

    This example creates a world with 50 rows and 50 columns. Other choices are possible, but the important thing is that the creator of the object determines the size of the world by choosing the desired parameters. Once the the class constructor is executed, the object is created with its own instances of the instance variables. In the constructor, the parameter values are stored in the instance variables width and height. The parameters are only accessible while the constructor is executed. If we need them in the future, we need to store the information in instance variables.

    You must now give the world the correct starting state by creating an array of the right size. In the previous task you declared the variable world, but as we know, an array must be also created (with new). Add a line in the constructor to do this. You do not need to fill the created array with values. In a boolean array the elements are automatically initialized to false, which corresponds to a completely dead world. Modifying now:

  3. the method getCell, so that it returns the value in the right place in the matrix world.
  4. to check what you have done so far, add another couple of lines in the constructor that places alive cells in some places, for example:
    world[2][2] = true;
    world[4][4] = true;
    

    Now you recompile and run the program. You should now see a world that is completely empty except for the two organisms you placed in (2,2) and (4,4).

    Even if you do not understand how the rest of the program works, you should understand that it is your class that is responsible for the world condition. If you decided that the living organisms are in these two cells, then that is what the other classes will see when they draw the world.

Step 2.

First remove the last additions made to the constructor in the previous step.

The remaining three methods change the population. A hint for this is that their result type is void. They do not provide information, but you call them to cause an effect.

First we look at the method:

void setCell(int i, int j, boolean b)

By calling this method, the outside world can set the value in the cell (i, j) to a value b which of course can be true or false. The method is easy to implement (one line). After you do this, compile and run the program. You can now click the mouse in the world to change a cell from living to dead or the opposite.

One can proceed without understanding how the rest of the program does this, but the curious can look in mouseClicked in LifeWorld. The method is called (by Java) when the user clicks the mouse. What is happening is that you take the coordinates of the cell that is clicked and you ask the model for the value of this cell (call b = model.getCell(i, j)). After that you set the value of the same cell to the opposite value by calling model.setCell with the opposite value of the last parameter (i.e. !b "not b").

Step 3.

The next method

void randomPopulation(double fill)

creates a new random population, where the probability of a living organism is fill for each cell. Implement this method. You need a loop to go through all the elements in the world and to put them to true or false by using random numbers. Compile the program and run it with different command line arguments. 0 will give a completely white world 1 all black and 0.5 a half-full world that looks a little different every time.

Step 4.

Remains the last method:

void newGeneration()

It replaces the current generation with the next one under the rules of Game of Life. We suggest you to do this exercise in three sub-steps:

  1. Look at your implementation of getCell. How does it behave if it is called with parameters (i, j) that correspond to a cell outside the world, i.e. if i or j is negative or greater than 49 (for the case with 50 rows and columns)? It only returns the value from the array world in position (i, j). This means that your program will crash if a call has incorrect parameters. Improve your implementation so that it controls the parameter values ​​and returns false without looking at world if (i, j) is outside the world.
  2. Define an auxiliary method:
    int nrOfNeighbours(int i, int j)
    
    that calculates the number of living neighbors of cell (i, j). This must be made with a small loop that scans all the neighboring cells and increases a counter if the neighbor contains a living cell. If you now use getCell to determine if the neighboring cell is living, then you do not have to treat the edges and corners specifically. You can think that all cells have eight neighbors. (If you are instead looking directly into world then you risk an exception if (i, j) is on the edge).

    If you want you can make this method private to emphasize that it is an auxiliary method, which is only used by other class methods (and can not be called from outside).

  3. Define method newGeneration. It is important to realize that you can not update the world's cells directly in a nested for-loop because this would mean that when updating the cell (i, j) to generation n+1, one can no longer figure out the number of living neighbors in generation n for the surrounding cells. Instead, you must declare a new boolean array newWorld, fill it with the new generation, and last of all make sure that the instance variable world points to the new generation.    

    For testing, we recommend you that you run the program with command line argument 0, so you get a completely empty world, that you then fill in with a small population with the mouse and then checks that the population develop correctly. Simple patterns with known development can be found in Wikipedia's article Game of Life. A program with almost exactly the same behavior is here.

If you have come so far, then you have a full program. We urge you to look a little more at the other classes and try to understand the role that your class have in the full program.