Lab 3: Ants and Doodlebugs Simulation

Warning!! This is an involved assignment. You will need several days to complete it! You will want to complete the assignment step by step (as outlined below)!

"new" notationThis document provides an overview of the entire assignment. It has actually been broken into three smaller parts, and you should be following those, rather than this document. See the Canvas pages.

Provide a solution to Programming Project #4, at the end of chapter 8 of the textbook. The class Doodle should have the primary main() method. I.e., when I want to run your program, I will run that class. (Another way of looking at it: when you specify the "Main class" for your Eclipse project, it should be "Doodle".)

Here are skeletons of the five classes I used to solve this problem, Organism.java, Ant.java, Doodlebug.java, World.java and Doodle.java. There are an infinite number of ways to solve the problem. You are free to use as many or as few classes as you want. The only hard and fast requirement is that your primary "main()" method must be in a class named Doodle.

Note that my solution places each class in a separate file. This is good software engineering, but not strictly necessary. You could--but I don't recommend it--place all the class definitions in the file Doodle.java, and the system would work just fine. (You may recall that a Java file can contain any number of class definitions, but only one that is marked "public".)

This is a fairly involved assignment. Don't be scared by the number of classes. Just look at my comments in each and try to implement each method to do what my comments indicate it should. Don't be afraid to post questions on the course discussion site or to e-mail me.

Oh, and as usual, don't try to do the whole thing all at once. You might try reaching each of these milestones in order:

  1. Get World's constructor(s) working and it's toString method. Just create a world and print it. (Use the main() I've provided in the World class as a starting point.) I chose to have my toString method precede each line with a line number to make it easier to see if the ants and doodlebugs were moving properly. Click here to see an example.

    To make sure the World is being created properly, count the ants and doodlebugs and make sure the right number are there. You may want to get World.isEmptyCell(x,y) working properly first, as you'll want to use it in the constructor (to make sure you are only putting organisms where the cells are empty). Note the code in the constructor that controls whether the random number generator is seeded or not.

    Make sure that you complete the implementation of the Organism constructor--you'll need it for the next phase. Make sure that your Ant and Doodlebug constructors call it!!!
    If you are in the lab, show me your output.

  2. Get the Ants moving properly. I have provided a working version of World.moveAllAnts(). You'll notice that function relies on Ant.move(),which in turn relies on Organism.moveToEmpty. You will have to finish Organism.moveToEmpty()to get things running properly. When you run Doodle, you should see your ants moving with each frame. Check carefully to make sure Ants are not disappearing (for example, by moving off the grid, or "onto" another ant or doodlebug).

    To get this to work, you should complete the implementation of several methods that I have provided skeletons of. In each case, check the header comments of the methods and comments within the bodies of the methods for hints as to what you should have the method do. See Organism.moveToEmpty(), World.isEmptyCell(x,y). (Also, you did finish the Organism constructor in the previous assignment, right?). Think about how these methods can make use of each other. Use the Organism.hasMoved flag to ensure that each ant moves only once as you iterate across the cells of a map. You will need to clear all those flags (setting them back to false) as the first action of World.moveAllAnts(). If you want, you might create a (private?) helper function in World that clears the flags of all Ants and Doodlebugs. Your World.moveAllAnts could then call that function.

    To simplify your task you might consider creating a World function that returns a Point, representing the location of an empty cell that is adjacent to a cell with given coordinates. I.e., World.getEmptyAdjacentLoc(x,y). The function could return null if there is not empty cell adjacent to location (x,y). The Point class is defined in the java.awt library.

    You can see sample output from my implementation at this phase here.

    Run the simulation for several steps. If you are in the lab, demonstrate to me or a TA that your ants are moving properly. Cut and paste into your lab document three consecutive time frames, highlighting one of the moving ants. In your lab document you'll want the pasted text to be in a fixed-width font.

  3. Get the Doodlebugs moving. For now, have them move only into empty spaces. Use the same strategy as for the Ants--i.e., use Doodlebug.move() , World.moveAllDoodles() and Organism.moveToEmpty(). You'll have to uncomment one of the lines in Doodle.main() to activate Doodlebug movement.
    Cut and paste into your lab document three consecutive time frames, highlighting one of the moving doodlebugs. In your lab document you'll want the pasted text to be in a fixed-width font.
  4. Get the Ants breeding. Each Ant should breed if it has survived for three time steps (see its howLongSinceBreeding and Ant.BREEDING_FREQ). You'll have to complete Ant.doBiology(), and at least partially implement World.doAllBiology() (which should probably invoke the former), as well as World.addAdjacent(). Examine the code provided, especially the comments, to understand the purpose of each method. Think about how these methods can make use of each other. Complete their coding.
    Run Doodle to see if all is well. After a dozen or so generations, the Ants should occupy a lot of the cells of the grid not held by Doodlebugs. (Because we haven't yet implemented Doodlebug mortality, nor Doodelbug breeding.)

    The top half of this document is sample output from my implementation at this stage.

    Run the simulation for 12 steps. Cut and paste a copy of your output into your lab document.

  5. Get the Doodlebugs breeding. Don't worry about eating or starving yet. Modify Doodlebug.doBiology() to breed (when appropriate--see Organism.howLongSinceBreeding) and World.doAllBiology() to call it. Run Doodle to see if all is well. Run the simulation for 16 steps or so. Are Doodlebugs breeding every eight turns?
    The bottom half of this document is sample output from my implementation at this stage.

  6. Get the Doodlebugs eating. They should move onto adjacent ants. See Doodlebug.moveToEatIfPossible() and/or Doodelbug.moveToAntIfAdjacent(), and consider how to interface them with Doodlebug.move().
    Run Doodle to see if all is well. Cut and paste the output of two consecutive time steps where a doodlebug eats an ant. Highlight the ant in the first generation, and the doodlebug in the second generation. Here is sample output demonstrating eating.

  7. Turn on doodlebug starvation. You will have to modify Doodlebug.doBiology() to have doodle's die/disappear if they haven't eaten recently enough.
    Run Doodle. Watch closely to see that unfed doodlebugs are starving, as appropriate. (You may find it helps your debugging to do as I demonstrated in class and have the printed single character representation of a doodlebug being a digit equal to the number of generations since it has eaten.) Collect and print output of two generations where a doodle starves. Highlight the doodle in the first generation, and where the doodle is NOT in the second generation. Here is sample output demonstrating starving and all biology.

  8. Final touch-up! Good job! It's kind of fun to run the simulation multiple times (with the random number generator seed removed so you get a different starting world each time). Sometimes the doodlebugs win, sometimes the ants, but usually neither.

To Submit

Submit to the dropbox for this assignment your final project (your java files are sufficient) as well as your lab document.

Two Dimensional Arrays

It may have been a while since you worked with 2-dimensional arrays. If so, you might want to review pages 387-390 of your textbook.

The World Constructor

In the constructor for World you'll use a loop to generate all the ants, and another to generate all the doodlebugs. Each time you go through these loops you'll generate a random x and y coordinate as the position of the organism. Make sure the cell at those coordinates is empty (you might use the World.isEmpty() method!) If it is, go ahead and generate an Ant or Doodlebug (as appropriate) and put it there. Your code might look something like:

public World(int numAnts, int numDoodles) {
   ...loop to generate all ants...{
      cells[y][x] = new Ant(x,y,this);
   }
	
Notice the use of this in the invocation of the constructor. You need this because the Ant constructor wants a reference to the World that the Ant resides in. The this keyword refers to the World object that is currently being constructed. This, by the way, is an example of a data structure using circular referencing. The World refers to each of its organisms, and each organism refers to the World it belongs in.

A Note on Random Number Generation

To generate random numbers in this program, use the myRand method in World. It uses a "random number generator" stored as an instance variable and initialized in the World constructor. The rest of this section describes how to create a random number generator (which you'll need to finish the World constructor) and explains how the generate is used in myRand (you don't have to implement myRand, but you should look at my code to understand what it does).

While the book suggests that you use the Math.random() method to generate random numbers, I suggest you use java.util.Random objects, instead. (Full documentation can be found be found here.) It can be easier to debug programs using these because you can generate repeatable results. I.e., if you see an error, you can just run the program again and the error should appear at the same time and place. Here's how to set one up:

import java.util.Random;

....

private myRndNumGenerator = new Random(123456L);

....

You may not recall the syntax of "123456L". The 'L' indicates that the value is a long, not an int. The number 123456 is called the "seed". (You can use any number as the seed.) This determines what sequence of random numbers this generator will create. If you omit the seed from the constructor, you will get a different sequence of random numbers each time. The most common method to invoke on a Random is nextInt(x), with an integer parameter, x. The method returns a random integer between 0 and x-1, inclusive.

Here is an example. The following code is executed twice:

import java.util.Random;
public class DemoRandom {
 public static void main(String args[]){
   Random generator1 = new Random();
   Random generator2 = new Random(345612L);
   System.out.print("Sequence generated without a seed: ");
   for (int i=0; i<10; i++)
     System.out.print(generator1.nextInt(10)+" ");
   System.out.print("\nSequence generated with a seed: ");
   for (int i=0; i<10; i++)
     System.out.print(generator2.nextInt(10)+" ");
   System.out.println(""); 
 }
}
This yielded the following output:
Sequence generated without a seed: 7 8 8 2 3 6 9 3 2 9 
 Sequence generated with a seed: 8 4 2 6 6 8 1 2 8 3
and...
Sequence generated without a seed: 3 5 6 3 3 4 3 9 8 1 
 Sequence generated with a seed: 8 4 2 6 6 8 1 2 8 3  

Note that the "generated with a seed" sequences are the same in both cases, while the sequences generated with a non-seeded generator differ.