Breeding and Dying

After implementing a few fish variations that the marine biologists asked for, I talked to them about other modifications they wanted in the simulation. They decided that the next modification would be to create a dynamic, or changing, population, with new fish being born and other fish dying as the program ran. This would allow them to study the interactions of populations of different sizes.


Problem Specification

The marine biologists wanted the simulation to model more complex fish behavior. They asked me to modify the simulation so that, in any given timestep, a fish might breed, move, or die. We talked about keeping track of a fish’s age and having the likelihood of breeding or dying depend on that, but the biologists decided that they didn’t need that level of sophistication right away. Instead they decided to build the model around some probabilities, giving fish a certain chance of breeding and a certain chance of dying in any given timestep. After some analysis of experimental data they had gathered from a sample real-world environment, the marine biologists specified that in general a fish should:

As we talked about it a little more, it became clear that the probabilities listed here represent averages for the various types of fish they were studying. We decided that our general Fish class would incorporate these probabilities, but it would be useful if various subspecies (represented by subclasses) could specify different probabilities.


Design and Implementation

My first step was to write pseudo-code for the act method that described what I thought it should do. Rather than putting the code for breeding and dying in the act method, I decided to break out these activities into separate breed and die methods. On Jamie's advice, I also decided to deal with the “error condition” (or at least unexpected condition) of a fish that is not in the environment grid at the very beginning of the method. Addressing error conditions at the beginning of a method is a common practice.

  Pseudo-code for the act method:
        If the fish is no longer in the environment,
            Do nothing (return immediately).

        If this is the 1 in 7 chance of breeding,
            Call the breed method.
        Else
            Call the move method.

        If this is the 1 in 5 chance of dying,
            Call the die method.

I decided to store the probability of breeding and the probability of dying in instance variables of the Fish class rather than as hard-coded constants, both to give them meaningful names and because this would make it easier if the biologists chose to model different species of fish with different probabilities of breeding and dying. I made the instance variables protected so that subclasses could easily initialize them to different values.

    protected double probOfBreeding;  // defines likelihood in each timestep
    protected double probOfDying;     // defines likelihood in each timestep

I then initialized the new instance variables in the four-parameter Fish constructor and tested the program to make sure that my changes so far had not (yet) changed its behavior.

    public Fish(Grid env, Location loc, Direction dir, Color col)
    {
        ⋮
        this.probOfBreeding = 1.0 / 7.0;  // 1 in 7 chance of breeding
        this.probOfDying = 1.0 / 5.0;     // 1 in 7 chance of dying
    }

Dying

I thought that dying would be easier to implement than breeding, so I decided to start with the die method. When a fish is constructed, it has to initialize its instance variables and add itself to the grid, but when it dies, all it has to do is remove itself from the grid.

Garbage collection: The Java garbage collector "uninitializes" the instance variables and reclaims the memory used by fish that are no longer referenced within the program.

Before I started to write the code, I needed to make a couple of decisions: Should the method be public, private, or protected? Did it need any parameters? Should it return anything to the act method, or should it be a void method? I decided to make it a protected void method with no parameters. The code itself was just a single line to remove the fish from the grid.

Once I had the die method written, I needed to call it from the act method. After the call to move, I inserted code similar to what I had already written in SlowFish, but instead of checking to see if the fish should move, now it was deciding whether it should call the die method.

Exercise and Analysis Questions: Dying Fish

  1. Modify your Fish class to add instance variables representing the probabilities of breeding and dying, and add code to the four-parameter constructor to initialize those variables. Test your program to make sure that its behavior is so far unchanged.
  2. Analysis: Pat chose to represent the probabilities as double values. Why? If you wanted to represent the probability of breeding using integers, instead, how would you do that? Would it require additional instance variables? (A Markdown template for writing up answers to this Analysis Question and the next one is here.)
  3. Analysis: Why did Pat decide to make the die method protected, and not public or private?
  4. Review the set of methods available to a BoundedGrid in either the Grid Package class documentation or this quick reference.

    Add a protected void die method with no parameters to the Fish class, and implement it to remove the fish from the grid environment.

  5. Modify the act method so that it returns immediately if the fish is not in the grid. Otherwise, it moves, as in the code below.

        if ( ! isInAGrid() )   // or if ( isInAGrid() == false )
        {
            return;
        }
    
        move();
    

    Then add code after the call to move that checks whether the fish should die, and calls the die method if it should. You may use your code in the SlowFish class as an example.

  6. Run your program enough timesteps to be confident that fish are dying with the correct probability. Since your fish aren't breeding yet, all of the fish should eventually die without being replaced, leaving the grid empty.

Breeding

Next I went on to implement the breed method. According to the problem specification, a fish should breed into all of its empty neighboring locations. This means that the first step is the same as in the nextLocation method — getting a list of the empty neighboring locations — although, in this case, there’s no reason to remove the location behind the fish from the list. As in nextLocation, if there are no empty neighboring locations then we’re done with the method. Once we have the list of empty neighbors, we want to add a new fish to all of them rather than move to just one of them. From that, I developed the following pseudo-code:

  Pseudo-code for the breed method (first version):
        Get list of neighboring empty locations.  (call emptyNeighbors)
        If there are no empty neighboring locations,
            Return

        For each location in the list of empty neighboring locations,
            Construct a new fish to put in that location.

Looking at this pseudo-code I realized that there are two different reasons a fish might not breed. One reason is that it only has a 1 in 7 chance of breeding in any given timestep, but even then, if there are no empty neighboring locations, then it stil can’t breed This means that the fish’s chance of breeding is actually less than 1 in 7. I talked to the marine biologists about this, and they decided that a 1 in 7 chance of attempting to breed was fine. I then decided that, rather than have some of the logic about whether a fish breeds or not in the act method and some in the breed method, I would put both tests in breed and have it return a boolean value indicating whether the fish successfully bred or not.

This led to the following revised pseudo-code for the breed method (which also resulted in small changes to my act method.

  Revised pseudo-code for breed method:
        If this is not the 1 in 7 chance of breeding,
            Return false.

        Get list of neighboring empty locations.  (call emptyNeighbors)
        If there are no empty neighboring locations,
            Return false.

        For each location in the list of empty neighboring locations,
            Construct a new fish to put in that location.
        Return true.

The instruction to "Construct a new fish" led to an interesting question: what kind of fish? If I were to hard-code the construction using the new Fish(...) instruction, then either my DarterFish and SlowFish subclasses would end of breeding generic Fish objects, or they would have to redefine the whole breed method just because of that one line. On Jamie’s advice, I made a separate method, generateChild, that constructs the appropriate fish object, so that my subclasses could inherit the breed method and yet each breed their own type of fish.

To make it clearer which fish had bred, and which spaces it had bred into, I decided to use the four-parameter Fish constructor to give newborn fish their parent’s color. In other words, a red fish’s children will also be red, their children will be red, and so on.

Exercise: Breeding Fish

  1. Modify your Fish class to add a protected void generateChild method that takes a Location object as a parameter. Within the method, construct a new fish at the specified location, with a random direction. (See the two-parameter constructor if you're not sure how to generate a random direction.) Give your new fish the same color as the current fish.

    You might want to print a debugging statement, in which case you will first need to store your new fish in a local variable (for example, child).

        Debug.println(" New Fish created: " + child.toString());
    
  2. Add a new breed method to the Fish class that takes no parameters. (Should it be public, private, or protected? What should its return type be?)

    Using the nextLocation method and your modified act method as examples, implement the "revised pseudo-code for the breed method" above.

  3. Modify the act method to attempt to breed, and only move if the fish did not breed. You could implement this by storing the true/false result of the call to breed in a variable and testing the value of the variable, or by putting the call to breed in the if condition. These two alternatives are shown below.
    Alternative 1:
                        boolean successful = breed();
                        if ( ! successful)  // or if ( successful == false )
                            move();
                    
    Alternative 2:
                        if ( ! breed() )   // or if ( breed() == false )
                            move();
                    
  4. Test your programming, running it enough timesteps to be confident that fish are breeding and dying with the correct probabilities.
  5. Note: The check for whether a fish bred or not is not functionally necessary, because if a fish has bred into all its empty neighbors, it wouldn't be able to move anyway. The code could just call the two methods, one after the other.
                        breed();
                        move();    // inefficient!
                    
    Checking whether breeding took place before moving makes the code more efficient by eliminating the attempt to find (non-existent) empty neighbors after the fish has surrounded itself with offspring.

Breeding Subclasses

Finally, I edited my Fish subclasses to redefine the generateChild method, so that Fish produce Fish offspring, DarterFish produce DarterFish offspring, and SlowFish produce SlowFish offspring, etc..

Exercise: Breeding Subclasses

  1. Add an appropriate generateChild method to each of your Fish subclasses.
  2. In the constructor that initializes the instance variables, initialize the probabilities of breeding and dying to different probabilities.
  3. Test your programming, running it with enough types of fish, and over enough timesteps, to be confident that different types of fish are breeding and dying correctly.


Alyce Brady, Kalamazoo College