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
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.)- Analysis: Why did Pat decide to make the
die
methodprotected
, and notpublic
orprivate
?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 theFish
class, 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
move
that checks whether the fish should die, and calls thedie
method if it should. You may use your code in theSlowFish
class 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
Fish
class to add aprotected void generateChild
method that takes aLocation
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());Add a new
breed
method to theFish
class that takes no parameters. (Should it bepublic
,private
, orprotected
? What should its return type be?)Using the
nextLocation
method and your modifiedact
method as examples, implement the "revised pseudo-code for the breed method" above.- Modify the
act
method to attempt to breed, and only move if the fish did not breed. You could implement this by storing thetrue
/false
result of the call to breed in a variable and testing the value of the variable, or by putting the call tobreed
in theif
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();- 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
generateChild
method to each of yourFish
subclasses.- 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.