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:
- have a 1 in 7 chance of breeding,
- breed into all its empty neighboring locations if it does breed,
- attempt to move to an empty neighboring location when it does not breed,
- never move backwards (unchanged from previous version), and
- have a 1 in 5 chance of dying after it has bred or moved.
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
- 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.
- Analysis: Pat chose to represent the probabilities as
doublevalues. 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.)- Analysis: Why did Pat decide to make the
diemethodprotected, and notpublicorprivate?Review the set of methods available to a
BoundedGridin either the Grid Package class documentation or this quick reference.Add a
protected void diemethod with no parameters to theFishclass, and implement it to remove the fish from the grid environment.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
movethat checks whether the fish should die, and calls thediemethod if it should. You may use your code in theSlowFishclass as an example.- 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
Modify your
Fishclass to add aprotected void generateChildmethod that takes aLocationobject 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());Add a new
breedmethod to theFishclass that takes no parameters. (Should it bepublic,private, orprotected? What should its return type be?)Using the
nextLocationmethod and your modifiedactmethod as examples, implement the "revised pseudo-code for the breed method" above.- Modify the
actmethod to attempt to breed, and only move if the fish did not breed. You could implement this by storing thetrue/falseresult of the call to breed in a variable and testing the value of the variable, or by putting the call tobreedin theifcondition. 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();- Test your programming, running it enough timesteps to be confident that fish are breeding and dying with the correct probabilities.
- 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
- Add an appropriate
generateChildmethod to each of yourFishsubclasses.- In the constructor that initializes the instance variables, initialize the probabilities of breeding and dying to different probabilities.
- 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.