8
\$\begingroup\$

This below code implements a simulation of an ocean(as 2d-array of cells) which currently have Shark, fish or be Empty. User can run this simulation, as shown below:

> java Simtext 25 30 1

With this command, SimText will animate a 25x30 ocean array with a starveTime of 1 unit.


/* point.java */

package Project1;

/**
 * The Point class defines a location (x,y) in the Ocean.
 * 
 * @author mohet01
 *
 */
class Point {
    /**
     * Here top-left of Windows screen is considered as origin.
     * x is an x-coordinate of a location in an ocean 
     * y is an y-coordinate of a location in an Ocean
     */
    private int x;
    private int y;

    /**
     * Constructor creates a Point object below co-ordinates
     * @param x
     *          is an x-coordinate of a Critter location in an Ocean
     * @param y
     *          is an y-coordinate of a Critter location in an Ocean
     */
    public Point(int x, int y){
        this.x = x;
        this.y = y;
    }

    /**
     * This method returns the x-coordinate of Critter in an ocean
     * @return
     *          x-coordinate of a Critter location in an Ocean. 
     */
    public int getX(){
        return this.x;
    }

    /**
     * This method returns the y-coordinate of Critter in an ocean
     * @return
     *          y-coordinate of a Critter location in an Ocean. 
     */
    public int getY(){
        return this.y;
    }

}

/* Critter.java */

package Project1;
/**
 * The abstract class Critter defines a base class for any creature 
 * that can exist at a specific location in the ocean.
 * @author mohet01
 *
 */
abstract class Critter  {

    /**
     * Below data member defines a location of a Critter in an Ocean
     */

    Point location;


    public Critter(int x, int y){
        location = new Point(x,y);
    }

    public Point getLocation(){
        return location;
    }

    /**
     * This method computes the behavior of the Critter in the Ocean. 
     * Computes new value of location property of Critter.
     * No operation is performed as this is a base class.
     */
    public abstract Critter update(Ocean currentTimeStepSea);


}

/* Empty.java */

package Project1;
/**
 * The Empty class defines itself as an entity as it has some meaning/significance
 * being empty in an Ocean. Check update() method for more meaning.
 * @author mohet01
 *
 */
class Empty extends Critter{


    /**
     * Constructor will create a new location which is Empty
     * @param x
     *          is the x-coordinate of location which is Empty.
     * @param y
     *          is the y-coordinate of location which is empty.
     */

    public Empty(int x, int y){
        super(x,y);
    }

    /**
     * This method updates the Empty cell based on the behavior of it's
     * nearest neighborhood
     * @param currentTimeStepSea
     *                          Ocean in the current time step
     * @param nextTimeStepSea
     *                          Ocean that has to look like in next time Step 
     *                          
     */

    @Override
    public Critter update(Ocean currentTimeStepSea) {

        int neighborFishCount = 0;
        int neighborSharkCount = 0;

        //Check all the 8 neighbors of an Empty cell to count sharks and Fish
        neighborSharkCount = Utility.countSharkAsNeighbor(this, currentTimeStepSea);
        neighborFishCount = Utility.countFishAsNeighbor(this,currentTimeStepSea);

        //Update Empty Cell for next time step.


        if((neighborFishCount >= 2) && (neighborSharkCount <=1)){
            /*
             * 7) If a cell is empty, at least two of its neighbors are fish, and at most one
             * of its neighbors is a shark, then a new fish is born in that cell.
             * 
             */
            return new Fish(this.getLocation().getX(),this.getLocation().getY());

        }else if((neighborFishCount >= 2) && (neighborSharkCount >= 2)){
            /* 
             * 8) If a cell is empty, at least two of its neighbors are fish, and at least two
             * of its neighbors are sharks, then a new shark is born in that cell. (The new
             * shark is well-fed at birth, even though it hasn’t eaten a fish yet.)
             * 
             */
            return new Shark(this.getLocation().getX(),this.getLocation().getY(),0);
        }
        else{
            /* 
             * 6) If a cell is empty, and fewer than two of its neighbors are fish, then the
             * cell remains empty.
             */
            return this;
        }


    }



}

/* Fish.java */

package Project1;
/**
 * The Fish class defines the behavior of a Fish in an Ocean
 * @author mohet01
 *
 */
class Fish extends Critter{

    /**
     * Constructor will create a new location for Fish
     * @param x
     *          is the x-coordinate of location(which can be EMPTY) of Fish
     * @param y
     *          is the y-coordinate of location(which can be EMPTY) of Fish
     */

    public Fish(int x, int y){
        super(x,y);
    }


    /**
     * This method updates the Fish cell based on the behavior of it's
     * nearest neighborhood
     * @param currentTimeStepSea
     *                          Ocean in the current time step
     * @param nextTimeStepSea
     *                          Ocean that has to look like in next time Step 
     *                          
     */

    @Override
    public Critter update(Ocean currentTimeStepSea){

        int neighborSharkCount=0;
        neighborSharkCount = Utility.countSharkAsNeighbor(this, currentTimeStepSea);
        //Updating fish cell for current & next  time step
        if(neighborSharkCount ==1){
            /*
             * 4) If a cell contains a fish, and one of its neighbors is a shark, then the
             * fish is eaten by a shark, and therefore disappears.
             * 
             */
            return new Empty(this.getLocation().getX(),this.getLocation().getY());
        }
        else if(neighborSharkCount  > 1){
            /*
             *  5) If a cell contains a fish, and two or more of its neighbors are sharks, then
             * a new shark is born in that cell. Sharks are well-fed at birth; _after_ they
             * are born, they can survive an additional starveTime time steps without eating.
             * 
             */
            return new Shark(this.getLocation().getX(),this.getLocation().getY(),0);

        }
        else {
            /*
             * condition is (neighborSharkCount  < 1)
             * 3) If a cell contains a fish, and all of its neighbors are either empty or are
             * other fish, then the fish stays where it is.
             */
            return this;
        }
    }


}

/* Ocean.java */

package Project1;

/**
 * The Ocean class defines an object that models an ocean full of sharks and
 * fish.
 * @author mohet01
 *
 */
class Ocean {

    /**
     * Define any variables associated with an Ocean object here. These
     * variables MUST be private.
     * 
     */
    //width of an Ocean
    private int width;
    //height of an Ocean
    private int height;

    /*
     * I preferred, 2d array of references to Critter objects
     * rather than List. Reasons(correct me),
     * 1) To display an array of ocean, it adds more logic in paint() method.
     * 2) Checking 8 nearest neighbors of each Critter looks inefficient,
     * For example: for an ocean of SEEFE  
     *                              FEEFE a 2x2 ocean, If i maintain
     * a list of Critter for this 2x2 ocean, i need to traverse 
     * S->E->E->F->E->F to get my first nearest neighbor of Shark,
     * In contrast, With 2d array, I would just use modulo operation as
     * mentioned in update() method.  Let us see what happens!!!
     *  
     */
    private Critter[][] oceanMatrix;


    /**
     * Constructor that creates an empty ocean with below dimension
     *  
     * @param width
     *            is the width of the ocean.
     * @param height
     *            is the height of the ocean.
     * 
     */
    public Ocean(int width, int height){
        this.oceanMatrix = new Critter[height][width];
        this.width = width;
        this.height = height;
        for (int row = 0; row < height; row++) {
            for (int col = 0; col < width; col++) {
                oceanMatrix[row][col] = new Empty(row,col);
            }
        }
    }


    /**
     * This method adds Critter in an ocean.
     * @param object
     *              is the Critter object to be added in Ocean.
     */
    public void addCritter(Critter object){
        Point p = object.getLocation();
        int x = p.getX();
        int y = p.getY();
        /*
         * I understand that, location property make sense to be be moved 
         * to corresponding Critter<type> class as it's property, which i did, But 
         * also captured location property of a Critter Object in Ocean class(with
         * above 3 lines of code) which is redundant and not relevant, But 2d array
         * is more efficient than list, for checking neighbor in update() method.
         * Are we Breaking SRS????
         * So, Instead of List am using 2d array. Let us see what happens!!!
         */
        oceanMatrix[x][y] = object;
    }


    /**
     * This method returns either Critter Object reference
     * 
     * @param x
     *            is the x-coordinate of the cell whose contents are queried.
     * @param y
     *            is the y-coordinate of the cell whose contents are queried.
     */
    public Critter cellContents(int x, int y) {
        return oceanMatrix[x][y];
    }


    /**
     * getWidth() returns the width of an ocean Object.
     * 
     * @return 
     *          the width of the ocean.
     * 
     */
    public int getWidth() {
        return this.width;
    }

    /**
     * getHeight() returns the height of an Ocean object.
     * 
     * @return
     *          the height of the Ocean.
     */

    public int getHeight() {
        return this.height;
    }





    /**
     * timeStep() performs a simulation time step as described in README.
     * 
     * @return
     *          an ocean representing the elapse of one time Step.
     */

    public Ocean timeStep() {

        Ocean nextTimeStepSea = new Ocean(width, height);

        for (int row = 0; row < this.height; row++) {
            for (int col = 0; col < this.width; col++) {
                Critter creature = this.cellContents(row, col);
                nextTimeStepSea.addCritter(creature.update(this));
            }
        }
        return nextTimeStepSea;
    }





}

/* Shark.java */

package Project1;

/**
 * The Shark class defines behavior of a Shark in an Ocean.
 * @author mohet01
 *
 */
class Shark extends Critter{



    /**
     * Below data member is the number of simulation time steps that a Shark
     *  can live through without eating.
     */
    static int starveTime;

    /**
     * Below data member specifies the hunger of each shark you add to the 
     * ocean.
     */
    private int hungerLevel;  


    /**
     * Constructor will create a new location for Shark
     * @param x
     *          is the x-coordinate of location(which can be EMPTY) of Shark
     * @param y
     *          is the y-coordinate of location(which can be EMPTY) of Shark
     */
    public Shark(int x, int y, int hungerLevel){
        super(x,y);
        //Sharks are well-fed at birth
        this.hungerLevel = hungerLevel;
    }

    /*
     * This method provides the starvation time of Shark creature
     */
    public static int getStarvationTime(){
        return starveTime;
    }
    /**
     * isSharkStarving() checks the hunger level of shark, if reached to starveTime level
     * @param x
     *              is the x-coordinate of the cell whose contents are queried.
     * @param y 
     *              is the y-coordinate of the cell whose contents are queried.
     * @return the boolean value
     */
    private boolean isSharkStarving(){
        return (this.hungerLevel == (starveTime+1));  
    }




    /**
     * This method updates the shark cell based on the behavior of it's
     * nearest neighborhood
     * @param currentTimeStepSea
     *                          Ocean in the current time step
     * @param nextTimeStepSea
     *                          Ocean that has to look like in next time Step 
     *                          
     */
    @Override
    public Critter update(Ocean currentTimeStepSea){

        boolean gotTheFish = false;
        //Check all the 8 neighbors of a Shark Cell for fish
        gotTheFish = Utility.checkFishAsNeighbor(this, currentTimeStepSea);

        //Updating Shark Cell
        if(gotTheFish){
            /*
             * 1) If a cell contains a shark, and any of its neighbors is a fish, then the
             * shark eats during the time step, and it remains in the cell at the end of the
             * time step.  (We may have multiple sharks sharing the same fish.  This is fine;
             * they all get enough to eat.)
             */
            return this; //return currentTimeStep Shark

        }else{
            /* 
             * 2) If a cell contains a shark, and none of its neighbors is a fish, it gets
             * hungrier during the time step.  If this time step is the (starveTime + 1)th
             * time step the shark has gone through without eating, then the shark dies
             * (disappears).  Otherwise, it remains in the cell.
             * 
             */
            this.hungerLevel++;
            if(isSharkStarving()){
                return new Empty(this.getLocation().getX(), this.getLocation().getY());
            }
            else{
                return this; //return currentTimeStep Shark
            }
        }



    }



}

/* Utility.java */

package Project1;

/**
 * The Utility class provides some utility functions which are used
 * by multiple classes like Fish, Shark etc...
 * @author mohet01
 *
 */
final class Utility {


    /**
     * Don't let anyone instantiate this class.
     */
    private Utility() {}




    /**
     * This method checks the existence of at-least one fish as nearest 
     * neighbor surrounding Creature's  cell
     * @param sea
     *          is the ocean of currentTimeStep
     * @return 
     *          returns true on at-least one fish existence otherwise false
     * 
     */
    public static boolean checkFishAsNeighbor(Critter creature, Ocean sea){
        //get Creatures location
        Point p = creature.getLocation();
        int x = p.getX();
        int y = p.getY();
        int row,col;

        for(int i = x-1;i <= x+1; i++){
            for(int j = y-1; j <= y+1; j++){
                /*
                 * Problem statement says(as per link): 
                 * http://www.cs.berkeley.edu/~jrs/61bf06/hw/pj1/readme
                 * You can also refer to locations such as (4, 0) or (-4, 3), 
                 * which are both the same as (0, 0) in a 4x3 ocean.
                 * so modulo is being performed for given i & j
                 */
                row = mod(i, sea.getHeight());
                col = mod(i, sea.getWidth());
                if(sea.cellContents(row, col) instanceof Fish)      
                    return true;
            }
        }
        return false;
    }


    /**
     * This method counts number of Shark  as nearest neighbor surrounding
     * Creature's  cell
     * @param sea
     *          is the ocean of currentTimeStep
     * @return 
     *          returns number of Shark surrounding creature
     * 
     */

    public static int countSharkAsNeighbor(Critter creature, Ocean sea){
        int neighborSharkCount = 0;
        //get Creatures location
        Point p = creature.getLocation();
        int x = p.getX();
        int y = p.getY();
        int row,col;

        for(int i = x-1;i <= x+1; i++){
            for(int j = y-1; j <= y+1; j++){
                /*
                 * Problem statement says(as per link): 
                 * http://www.cs.berkeley.edu/~jrs/61bf06/hw/pj1/readme
                 * You can also refer to locations such as (4, 0) or (-4, 3), 
                 * which are both the same as (0, 0) in a 4x3 ocean.
                 * so modulo is being performed for given i & j
                 */
                row = mod(i, sea.getHeight());
                col = mod(i, sea.getWidth());
                if(sea.cellContents(row, col) instanceof Shark)     
                    neighborSharkCount++;
            }
        }
        return neighborSharkCount;
    }



    /**
     * This method counts number of Fish  as nearest neighbor surrounding
     * Creature's  cell
     * @param sea
     *          is the ocean of currentTimeStep
     * @return 
     *          returns number of Fish surrounding creature
     * 
     */

    public static int countFishAsNeighbor(Critter creature, Ocean sea){
        int neighborFishCount = 0;
        //get Creatures location
        Point p = creature.getLocation();
        int x = p.getX();
        int y = p.getY();
        int row,col;

        for(int i = x-1;i <= x+1; i++){
            for(int j = y-1; j <= y+1; j++){
                /*
                 * Problem statement says(as per link): 
                 * http://www.cs.berkeley.edu/~jrs/61bf06/hw/pj1/readme
                 * You can also refer to locations such as (4, 0) or (-4, 3), 
                 * which are both the same as (0, 0) in a 4x3 ocean.
                 * so modulo is being performed for given i & j
                 */
                row = mod(i, sea.getHeight());
                col = mod(i, sea.getWidth());
                //if((sea.cellContents(row, col).getClass().getName()).equals(Fish.class))
                if(sea.cellContents(row, col) instanceof Fish)
                    neighborFishCount++;
            }
        }
        return neighborFishCount;
    }


    /**
     * This method performs the modulo operation using euclidean divison
     * 
     * @param n
     *            is the numerator
     * @param d
     *            is the denominator
     * @return 
     *            Remainder 
     */

    private static int mod(int n, int d) {
        if (n >= 0)
            return n % d;
        else
            return d + ~(~n % d);
    }

}

package Project1;

import java.util.Random;



/* SimText.java */

/* DO NOT CHANGE THIS FILE (except as noted). */
/* (You may wish to make temporary or insert println() statements   */
/* while testing your code. when you're finished testing and debugging, */
/* though, make sure your code works with the original version of this file. */

/**
 * The SimText class is a program that runs and animates a simulation of Sharks
 * and Fish.
 * 
 * The SimText program takes up to four parameters. The first two specify the
 * width and height of the ocean. The third parameter specifies the value of
 * starveTime. For example, if you run
 * 
 * java SimText 25 25 1
 * 
 * then SimText will animate a 25x25 ocean with a starveTime of 1 unit. If you run
 * "java SimText" with no parameters, by default SimText will animate a 50x25
 * ocean with a starveTime of 3. With some choices of parameters, the ocean
 * quickly dies out; with others, it teems forever.
 * @author mohet01
 *
 */

public final class SimText {



    /**
     * Don't let anyone instantiate this class.
     */
    private SimText(){}

    /**
     * Default parameters. (You may change these if you wish.)
     * 
     */
    // Default ocean width
    private static int width = 50; 
    // Default ocean height
    private static int height = 25; 
    // Default shark starvation time
    private static int starveTime = 3; 
    //Default Shark hunger Level
    private static int hungerLevel = 0;


    /**
     * The {@code MAGIC_NUMBER} is a prime number that is used in LCG 
     * expressions to select random location in an Ocean. 
     */
    private static final int MAGIC_NUMBER = 78887;

    private static final int LOC_MULTIPLE_OF_NUMBER = 8;


    /**
     * paint() prints an Ocean.
     */
    public static void paint(Ocean sea) {
        if (sea != null) {
            int width = sea.getWidth();
            int height = sea.getHeight();

            // Draw the ocean 
            for (int x = 0; x < width + 2; x++) {
                System.out.print("-");
            }

            System.out.println();

            for (int row = 0; row < height; row++) {
                System.out.print("|");
                for (int col = 0; col < width; col++) {
                    Critter creature = sea.cellContents(row, col);
                    System.out.print(creature.getClass().getName().substring(9, 10));  
                }
                System.out.println("|");
            }
            for (int x = 0; x < width + 2; x++) {
                System.out.print("-");
            }
            System.out.println();
        }

    }




    /**
     * main() reads the parameters and performs the simulation and animation.
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {
        Ocean sea;

        /**
         * Read the input parameters.
         */

        if (args.length > 0) {
            try {
                width = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                System.out
                        .println("First argument to SimText is not an number");
            }
        }

        if (args.length > 1) {
            try {
                height = Integer.parseInt(args[1]);
            } catch (NumberFormatException e) {
                System.out
                        .println("Second argument to SimText is not an number");
            }
        }

        if (args.length > 2) {
            try {
                starveTime = Integer.parseInt(args[2]);
            } catch (NumberFormatException e) {
                System.out
                        .println("Third argument to SimText is not an number");
            }
        }

        Shark.starveTime = starveTime;

        /**
         * Create the initial ocean.
         */

        sea = new Ocean(width, height);


        /**
         * Visit each cell (in a roundabout order); randomly place either Fish,
         * or Shark or Empty Object in each location. 
         * 
         */
        // Create a "Random" object with seed 0
        Random random = new Random(0); 
        int x = 0;
        int y = 0;
        for (int row = 0; row < height; row++) {
            x = (x + MAGIC_NUMBER) % height; 
            if ((x & LOC_MULTIPLE_OF_NUMBER) == 0) {
                for (int col = 0; col < width; col++) {
                    y = (y + MAGIC_NUMBER) % width; 
                    if ((y & LOC_MULTIPLE_OF_NUMBER) == 0) {
                        // Between -2147483648 and 2147483647
                        int r = random.nextInt(); 
                        if (r < 0) { 
                            sea.addCritter(new Fish(x,y)); 
                        } else if (r > 1500000000) { 
                            sea.addCritter(new Shark(x,y,hungerLevel)); 
                        }
                    }
                }
            }
            else{
                sea.addCritter(new Empty(x,y));
            }
        }



        /**
         * Perform time steps forever.
         *
         */
        // Loop forever
        while (true) { 
            paint(sea);
            // For fun, you might wish to change the delay in the next line.
            Thread.sleep(100); // Wait ten seconds (10000 milliseconds)
            sea = sea.timeStep(); // Simulate a timestep
        }



    }/* end main() */

}

My main goal for this implementation is to understand OOP programming principles Abstraction and Encapsulation

With the given 8 rules of Simulation in Part I of this link mentioned below,

The ocean is rectangular, but the edges are connected together in a topological donut or torus. This means that the top (North) and bottom (South) edges are considered adjacent, so if you start at the top edge and go up, you'll be at the bottom edge (just like in the video game Asteroids). Similarly, the East and West edges are connected (just like in Pac Man).
enter image description here

Note that the origin is in the upper left; the x-coordinate increases as you move right, and the y-coordinate increases as you go down. (This conforms to Java's graphics commands, though you won't need to use them directly in this project.) You can also refer to locations such as (4, 0) or (-4, 3), which are both the same as (0, 0) in a 4x3 ocean. (More generally, the coordinates in an ixj ocean are taken modulo i for the x-coordinate, which is horizontal, and modulo j for the y-coordinate, which is vertical.) Any pair of integers will give you a valid position in the grid by "wrapping around" at the edges.

(Hint: programming will be a lot easier if you write helper functions that do the wrapping around for you, and use them in all your methods, so you don't have to think about it again.)

There are two kinds of entities in this ocean: sharks and fish. The sharks and fish breed, eat, and die in the ocean. Each cell of the grid can be occupied by a single shark or fish, or it can be empty.

Part I: Simulating Sharks and Fish

An ocean is described by its size and the initial placement of sharks and fish in the ocean. It is also described by a parameter called the "starveTime" for a shark. This is the number of simulation timesteps that a shark can live through without eating.

The simulation proceeds in timesteps. A "timestep" is a transition from one ocean to the next. (Don't confuse timesteps with oceans; every timestep starts with one ocean and ends with another.) The rules for how the ocean looks at the end of a timestep depend only on the occupants of the cells at the beginning of the timestep. Therefore, to obtain correct behavior, you will often be working with two copies of the ocean simultaneously; one representing the ocean at the beginning of the timestep, and the other representing the ocean at the end of the timestep. (If you are foolish enough to try to implement a timestep using just a single Ocean object, you will modify the values of cells whose old values are still needed to compute the new values for other cells, and thus you will compute the wrong answer.)

The contents of any particular cell at the end of a timestep depend only on the contents of that cell and its eight neighbors at the beginning of the timestep. The "neighbors" are the eight adjacent cells: the cells immediately to the north, south, east, and west, as well as the four diagonal neighbors. Here are the rules:

  1. If a cell contains a shark, and any of its neighbors is a fish, then the shark eats during the timestep, and it remains in the cell at the end of the timestep. (We may have multiple sharks sharing the same fish. This is fine; they all get enough to eat.)

  2. If a cell contains a shark, and none of its neighbors is a fish, it gets hungrier during the timestep. If this timestep is the (starveTime + 1)th timestep the shark has gone through without eating, then the shark dies (disappears). Otherwise, it remains in the cell. An example demonstrating this rule appears below.

  3. If a cell contains a fish, and all of its neighbors are either empty or are other fish, then the fish stays where it is.

  4. If a cell contains a fish, and one of its neighbors is a shark, then the fish is eaten by a shark, and therefore disappears.

  5. If a cell contains a fish, and two or more of its neighbors are sharks, then a new shark is born in that cell. Sharks are well-fed at birth; after they are born, they can survive an additional starveTime timesteps without eating. (But they will die at the end of starveTime + 1 consecutive timesteps without eating.)

  6. If a cell is empty, and fewer than two of its neighbors are fish, then the cell remains empty.

  7. If a cell is empty, at least two of its neighbors are fish, and at most one of its neighbors is a shark, then a new fish is born in that cell.

  8. If a cell is empty, at least two of its neighbors are fish, and at least two of its neighbors are sharks, then a new shark is born in that cell. (The new shark is well-fed at birth, even though it hasn't eaten a fish yet.

My question:

Does this implementation breaks the rule of proper abstraction and encapsulation, if it runs for long run?

Note: Am new to Java programming, and learning as per the guidance in course. This course does not encourage to use existing java packages.

\$\endgroup\$
7
  • \$\begingroup\$ The class you are following is nearly a decade old. Things move very fast in IT and I would suggest that you pick a more recent class. There are certainly hundreds available. You can Google for them or look at MOOC (coursera, edX, etc.) which will even grade your work. \$\endgroup\$
    – toto2
    Nov 2, 2014 at 21:43
  • \$\begingroup\$ @toto2 Instead, Please let me know, what is wrong with this course? Of-course am taking some specialization courses in parallel with coursera. \$\endgroup\$ Nov 5, 2014 at 2:45
  • \$\begingroup\$ @Pimgd Do you think the way starveTime member variable is set in Shark class is valid? Do you think am setting this this variable in SimText class before Shark class is loaded. Somehow, i feel this is wrong approach. i did not assign starveTime property to every Shark object because it is common value. \$\endgroup\$ Nov 5, 2014 at 2:47
  • \$\begingroup\$ Java itself changes over time, but mostly best practices. Whatever they teach at university is probably 5-10 years behind from industry. So if you take an 8 year-old class, you're not even in the right century. \$\endgroup\$
    – toto2
    Nov 5, 2014 at 2:58
  • \$\begingroup\$ @toto2 may be you can go thru this link and let me know, what is obsolete? I feel this is building block for any OOP programmer? I understand there are lot of wrappers added over it using design patterns and extra java collection and concurrent packages etc... \$\endgroup\$ Nov 5, 2014 at 3:58

2 Answers 2

10
\$\begingroup\$

Ahhh, tile based 2D grid layouts.

It's an OO problem for sure. How can you keep things separate if objects on tiles need to know what tile they're on and their neighboring tiles and what's on their neighboring tiles?

I did a similar exercise and I still need to think carefully about it.

I do feel the best solution is the following:

Create an enum Direction.

Create a class Cell, give it a Map<Direction, Cell> neighbors.

Give a Cell a possible CellObject (like Fish and Shark).

Give a Cell a Point so it knows where it is. (see below in "how do we simulate a timestep")

Remove a Critter's x and y. It's not supposed to know those things. Sharks don't have GPS.

This is something I can do due to my design; it's not something you should do before thinking hard about what a position means and how to identify an object's position. In my case, I place the responsibilities all on cells. As CellObjects gain more responsibility, my design falls apart. There is no clean way to make an object on a grid navigate through the grid without telling it about the grid. However, take a look at yourself: without a map OR your memory, you too must rely on your own senses to navigate a maze. And in all cases, you do this via looking at your surroundings (the Cell). Thus in my design, CellObjects never gain much responsibility. They're just for holding state. Simply put; you know you are "here", but you don't know your longitude and latitude. That's why you don't have x or y. To put x or y on an object like that is to pull it away from the actual world - you're not "there" any more, but you're a map marker. Map markers can access the map, yes, map markers can pathfind, yes, but map markers cannot eat fish.

And lastly, give Game a 2D array ([x][y]) for storing cells. This array is for iterating cells. The array is not for searching for things. That's what Cell's neighbors are for.

And have the game just go through the cells, asking how many neighbors have Shark.class and Fish.class instances on them. Add the relevant changes to a list, then make the relevant changes.

This solves your problems where your Ocean is recreated each frame. This solves your problems where Critters of various sorts need to hassle with x and y. This solves your problem where adding a Critter to an Ocean means you need to hassle with x and y. This localizes your game rules in one location.

The hassles you gain are that you now need two iterations. First iteration is to find out what needs to happen to all the cells. Second iteration is to apply the changes. I think you'll want to make a new 2D array of objects to place on the cells. The alternative is storing a set of commands per critter; Whilst this does make more sense, it doesn't scale. ... in a functional programming language it would: one would create a set of delayed function calls, then execute them all after another.


But that's my solution. What about yours?

Well, you have a few issues.

  • Duplicated information. Both Critter and Game know where they are.
  • Ocean is recreated every timestep. This might be okay for your assignment; but for real life simulations, this sort of thing kills performance. Bad performance results in scaled down models, which means something meant to simulate global populations can only handle a single sea.
  • Empty critter. This just doesn't make sense. Isn't it the Cell that's empty?
  • Fish needs to know about Shark. Shark needs to know about Fish. What happens when I add an extra step to the chain and BigFish need to eat SmallFish? All the classes change.

How would you go about solving these issues?

Personally, I take a whiteboard and start by drawing the domain model classes. These are things that are mentioned in a problem statements. You have the disadvantage of a biased problem statement. Here's the bias:

(If you are foolish enough to try to implement a timestep using just a single Ocean object, you will modify the values of cells whose old values are still needed to compute the new values for other cells, and thus you will compute the wrong answer.)

But I digress. One starts by identifying objects and concepts.

The first paragraph ("The ocean is rectangular, but the edges...") talks at length about an Ocean of sorts. It goes into detail on how there's a grid in it. The second paragraph continues on stating that the grid has x and y coordinates, how you need to support values that are out of bounds due to the shape of the ocean...

We have thusly identified an object Ocean and a concept grid. The difference between objects and concepts is that objects are described as what they are and what they can do, and concepts are described as a set of requirements. This difference is important. You see, objects WILL end up in your eventual result. Concepts might be integrated - you'll see them back in multiple pieces.

The third paragraph is a hint regarding the implementation of the grid concept. They're leading you with an implementation. This lead may or may not help you. If you were to use a 2D array, helper methods to access this array do seem to be a smart idea. But don't worry about the implementation for now; focus on the requirements. Focus on the objects and concepts that have to go into the eventual application.

The fourth paragraph ("There are two entities in this ocean...") goes on to describe two more objects, Shark and Fish. It also provides the key sentence "Each cell of the grid can be occupied by a single shark or fish, or it can be empty.". Via this sentence, you can see that apparently a grid has Cells. But all a grid was was an Ocean. So an Ocean has Cells. And each of these Cells can have a Fish or a Shark or be empty.

Let's stop here for a moment.

Ocean has many Cells. Cells are arranged in a grid. A Cell has nothing, Fish, or Shark.

That's the core design right now. No more, no less. These are just facts as stated in the problem statement.

The fifth paragraph goes on to describe identity for an Ocean. It also introduces a strange design requirement: Ocean decides how long a Shark gets to live before it starves. I have not commented on this because the customer said so so we implemented it. It also introduces a concept timesteps.

The sixth paragraph proceeds to explain what a timestep is. Apparently, it's a transition from one Ocean to the next.

There is a hole here.

An ocean is described by its size and the initial placement of sharks and fish in the ocean.

and

A "timestep" is a transition from one ocean to the next.

If you combine these two ideas, you could come to the conclusion "an Ocean holds a single state, and when a simulation advances via a timestep, we get to the next Ocean, which is a new Ocean". This would be a wrong conclusion. They have stated that an Ocean is described by its size and the initial placement (of things). Thus, to state that you'd get a new Ocean would be to imply that the initial placement of various objects has changed. This is not the case.

My conclusion would be that an Ocean changes over time. I hold this conclusion to be correct, for it is an accurate representation of the real world - the placement of objects changes over time(steps).

The rest of the sixth paragraph continues to force non-functional implementation details on to you: "Therefore, to obtain correct behavior, you will often be working with two copies of the ocean simultaneously; one representing the ocean at the beginning of the timestep, and the other representing the ocean at the end of the timestep."

They're basically saying there's no correct way to simulate the ocean without using two instances.

At this point, I would just have made a Timestep, added a Ocean before, an Ocean after and called it a day. I wouldn't even have called it bad design. It would have been what the problem statement said, it's probably what your professor/teacher is looking for (they wouldn't keep using this word timestep if they weren't, remember, identifying objects and concepts).

But I have more experience with these kinds of things. That experience isn't really relevant, but it does enable me to make the following observation:

Recreating an Ocean for each timestep is contradictory to how real life works.

And why does this happen? What could have gone wrong that they are required to make a new Ocean for each timestep?

I'll tell you what went wrong.

They defined an Ocean as...

The ocean is rectangular, but the edges are connected together in a topological donut or torus. This means that the top (North) and bottom (South) edges are considered adjacent...

And then went on to show a picture of a grid.

They're not talking about an Ocean.

They're talking about a grid.

You can't have the same grid represent two different moments in time, for the grid will have changed.

You need two grids to make a timestep work.

The last part of the sixth paragraph hammers this point home:

(If you are foolish enough to try to implement a timestep using just a single Ocean object, you will modify the values of cells whose old values are still needed to compute the new values for other cells, and thus you will compute the wrong answer.)

You are not modifying the Ocean. An Ocean is defined by its size and the initial placement of its contained objects. You are modifying the grid.

This ... mistake in naming (of which you will encounter many more during your studies, as naming is hard), combined with a willingness to help you (by providing hints) is steering you away from the correct solution. At the very least, you're being steered away from alternatives that could be better.

Oh well. Let's move on.

The seventh paragraph ("The contents of any particular cell...") describes the workings of Cells. It says Cells have 8 neighbors, and even goes on to enumerate (whenever a problem statement describes a list of values, chances are it's an enumeration, and chances are it can become an enum) these directions.

The rules themselves are pretty basic; it describes how cells will behave.

I'm not going to describe their analysis; I don't see anything particularily interesting in there. It's basically describing a series of conditionals. It doesn't contain any objects or concepts.

So, what do we have?

Ocean has many Cells. Cells are arranged in a grid. A Cell has nothing, Fish, or Shark. During simulation, the contents of a Cell may change per timestep depending on the contents of its neighbors in the directions (N W E S NW NE SW SE) and its own contents.

The problems:

  • How do we make the grid?

  • How do we simulate the timestep?

These are your only two problems. All of your other problems can be stowed onto one of these. Handling coordinates is part of the grid, handling issues like a fish being shared between sharks is easier.

How do we make the grid?

Well, the grid is made of Cells, and Cells have neighbors. They also have contents. These contents are either empty, Fish, or Shark.

You have multiple choices here. You can make a 2D array. You can create a mesh network (what I did, linking Cells together via neighbors). I bet you can create a 1D array and use multiplication to access the proper indexes.

How to decide the correct implementation here is simply a matter of weighing pro's and cons. Personally, I suck at that. I don't know what the pros and cons are, and I don't know which ones are relevant.

So what I do instead, is that I mentally create the scenario where I have implemented one of these, and then I visualize what I would have to do to make a specific use-case work.

In a 2D array, how would I count the neighbors?

Well, that'd be a bother. I'd have to do math with X/Y.

In a mesh network, how would I count the neighbors?

Easy, use a for loop.

In a 1D array, how would I count the neighbors?

Wat. That's some evil math right there.

It seems there's two choices. Here I have to say that I'm biased. My original own assignment only used N W E S neighbors. I was able to make the argument that if we ever needed diagonal movement, it would be a pain to add in a 2D array. It would be easy to add in a mesh network. So I opted for the mesh network. This is where you use experience to make a choice. Possibly even implement both in a proof of concept, and see the pros and cons, and pick the one you like.

How do we simulate the timestep?

Again, multiple options.

  • Just iterate over the cells and alter directly.
  • Iterate over the cells and return a value that will be used to fill a new grid.
  • Iterate over the cells and return a value that says what SHOULD be done.

Huh. I only seem to be getting to iteration.

So the choices are actually "Alter directly", "Return new value", "Return descriptive action".

Each has their own pro's and cons.

Alter directly

Bugs. As pointed out by the question statement. Don't do this.

Return new value

Seems... strange. But it works. It's also easy to do.

Return descriptive action.

This seems like the best option. But it's hard to do. In a functional programming language, this would be the best option. You could return a delayed function call and execute the whole timestep IN SYNC. It's the best option because it simulates real life. Nay, it's the best option because it simulates the reality you wish to achieve.

... but we're not using a functional programming language, so new values are a better option.

Because I opt for both a mesh network and new values, I decide to make a 2D array so I can set the Cell values properly. I also need the array to iterate over the Cells, since all options require iteration. If you read back up, you'll see that I define the array last.

That's because I'm the least sure of this part.

Now that I think about it, I don't think Cell needs a Point at all. I only added it because your Critter had a Point.

The final result?

Ocean has many Cells. Cells are arranged in a grid. A Cell has nothing, Fish, or Shark. During simulation, the contents of a Cell may change per timestep depending on the contents of its neighbors in the directions (N W E S NW NE SW SE) and its own contents. The grid consists of Cells linked together. When simulating a timestep, the new value for each Cell is created based of the old state. Then all the Cells values are set to their new value.

One question remains.

Those 8 rules, where do they go?

This one I don't have good argumentation for. It just goes in the same location as the cell iteration. It fits easiest there. And since Ocean already knows the Cells... I fit it in Ocean.

Ocean gains a method "simulateTimestep", and the result is that the content of all the Cells change.

I said before that you need 2 grids to make a timestep work. Yet I only have one. What happened? It turns out that the reason why you need two grids is because if the values change, you might get incorrect results. And you'll see that I do have two sets of values; one as the currently active grid, and one as the set of values I got back from iterating the cells during a timestep.

With all that said about your design, let's take a look at your code.


Documentating with javadoc

/**
 * Here top-left of Windows screen is considered as origin.
 * x is an x-coordinate of a location in an ocean 
 * y is an y-coordinate of a location in an Ocean
 */
private int x;
private int y;

Document per entity. So this should be split between the two variables.

/**
 * Here top-left of Windows screen is considered as origin.
 * x is an x-coordinate of a location in an ocean 
 */
private int x;

/**
 * Here top-left of Windows screen is considered as origin.
 * y is an y-coordinate of a location in an Ocean
 */
private int y;

Duplicating the comment here might be bad, but to be honest, when you have comments like this, I rather like that both x and y mention the origin as "top-left". I feel I spend too much time trying to get my IDE to give me documentation on various combined variables, and this alleviates this partly.

Using Random

int r = random.nextInt(); 
if (r < 0) { 
    sea.addCritter(new Fish(x,y)); 
} else if (r > 1500000000) { 
    sea.addCritter(new Shark(x,y,hungerLevel)); 
}

Take a look at Random.nextInt(int n). It allows you to specify the upper bound of the result. Like this, you can get a chance for this; random.nextInt(100) will give you a value between 0 and 99. You can use this to assign understandable probabilities to your distribution of sharks/fish/empty tiles.

\$\endgroup\$
10
  • \$\begingroup\$ Before suggesting the best solution, I would be more interested to know the issues in current solution. Best solution would come out on it's own. So, I need suggestions to correct my perception. \$\endgroup\$ Oct 31, 2014 at 14:39
  • 1
    \$\begingroup\$ @overexchange It's very hard for me to correctly identify the problems with your object hierarchy. I think that if you start with the parts that are just plain wrong (Empty Critter) then the whole system comes tumbling down. You seem to have made something that works, rather than something that is of good quality design (but I don't blame you, good design is hard, that's why my answer has so many edits - I spent all that time thinking - nearly 2 hours of trying to figure out what's wrong and how it could be better). \$\endgroup\$
    – Pimgd
    Oct 31, 2014 at 15:06
  • \$\begingroup\$ @overexchange see my updated answer for a full reasoning behind my thinking. \$\endgroup\$
    – Pimgd
    Oct 31, 2014 at 16:19
  • \$\begingroup\$ If i follow your from-scratch change in design, this is against the spirit of assignment given as per the provided link. I Need to be in alignment with the skeleton code given in the assignment. \$\endgroup\$ Nov 4, 2014 at 3:18
  • \$\begingroup\$ Can you also help me go thru this link which is related. \$\endgroup\$ Nov 4, 2014 at 3:46
3
\$\begingroup\$

It's not bad for a beginner.

  • Either store the position (Point) in the Critters, or have the Critter's location stored in the Ocean, but do not do both at the same time. You DO NOT EVER want to have any information duplicated. Any code that duplicates information very quickly becomes unmaintainable. I think that storing the positions in the Ocean rather than the Critters is the better option.

  • The method Critter.update(Ocean) which returns a Critter is awkward. This is very much related to the point discussed above. Instead I would define a method updateLife(Collection<Critter> neighbors) which returns a LifeStatus (alive/dead). For a Fish, that method just checks if there is a Shark amongst the neighbors:

    public enum LifeStatus {
       ALIVE, DEAD;
    }
    
    @Override
    public LifeStatus updateLife(Collection<Critter> neighbors) {
        boolean isEaten = neighbors.stream()
            .filter(neighbor -> neighbor instanceof Shark)
            .findAny();
        return (isEaten) ? DEAD : ALIVE;
    }
    

    and for a Shark

    @Override
    public LifeStatus updateLife(Collection<Critter> neighbors) {
        boolean doesEatFish = neighbors.stream()
            .filter(neighbor -> neighbor instanceof Fish)
            .findAny();
        hungerLevel = doesEatFish ? 0 : (hungerLevel + 1);
        return isStarving() ? DEAD : ALIVE;
    } 
    

    By the way, it is difficult to come with a good solution for a given problem. I have a lot of experience and I still needed some time to come up with this solution.

  • You don't need the Empty class. For example, the List<Critter> above is just empty if there is no fish or shark nearby.

  • You should make width and height final in Ocean.

  • Having a class which contains a bunch of static methods (Utililty) is usually a sign of poor OO design. Most of those methods should be non-static methods in Ocean.

  • I would change private static int width = 50; to private final static int DEFAULT_OCEAN_WIDTH = 50;.

  • The part where you generate the random starting configuration for the ocean is too complex and difficult to read. If you leave the logic as such, you should create some LCG class because 1) it's duplicated code for x and y, and 2) it's messy, so it's better to isolate the messiness in another class, or at least another method.

  • Again on the random initialization: you should not use your LCG and just stick to random.nextInt(width) which returns what you want. It's quite confusing that you use your own LCG for some things, but then you use java.util.Random for others.

  • How long your code runs has nothing to do with code design. In reply to

    Does this implementation breaks the rule of proper abstraction and encapsulation, if it runs for long run?

    I don't have much to say related to abstraction/encapsulation because this is quite a small system with very few classes.

  • (Minor point) There are two ways to store a 2-D matrix: dense representation or sparse representation. Dense representation stores all values in a 2-D array matrix[n][m], which is basically what you did.

    Sparse representation stores the matrix as Map<Point2D, Value>. Sparse representation is used when most values in a matrix are null since it saves memory. My guess is that most squares in the ocean are empty, so a sparse representation would be more appropriate. However, that is really not important for your homework.

\$\endgroup\$
16
  • \$\begingroup\$ I just noticed that you asked this question twice before. You got a smorgasbord of (sometimes contradictory) advice. Hopefully you won't get discouraged. \$\endgroup\$
    – toto2
    Nov 1, 2014 at 1:24
  • \$\begingroup\$ My advice is the only one you should consider and the others are completely wrong. ;-) \$\endgroup\$
    – toto2
    Nov 1, 2014 at 1:24
  • \$\begingroup\$ Please do not mention the answers using existing java packages as mentioned in the note section of this query.I would need answer that would conclude/suggest things on improving what is currently written. I prefer your above answer. \$\endgroup\$ Nov 2, 2014 at 21:01
  • 1
    \$\begingroup\$ It's poor OO design because those methods belong with a class. However, it does happen sometimes that a method does not really belong anywhere and I will put it in some utility class, but that is quite rare. Most of the methods in your utility class belong in other classes. Having a bunch of static method in a utility class is basically equivalent to a non-OO C program. \$\endgroup\$
    – toto2
    Nov 2, 2014 at 21:58
  • 1
    \$\begingroup\$ It's Critter[][]. It's not just an array, but a 2-D array which indicates the positions of the critters. So the information is duplicated. \$\endgroup\$
    – toto2
    Nov 2, 2014 at 21:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.