Lab 5: Packages, Testing, and Inheritance with Large Systems

Submit by Friday, Oct 3 at 11:59 PM
This lab is not fully battle tested. Please report any errors/ambiguities/pointless exercises in suffering to your lab TA or to me directly: hug@cs.berkeley.edu

Table of Contents

Pre-lab

None this week.

Introduction

Don't forget to submit a "hello.txt" along with your lab that contains the magic word of the day. Ordinarily you'd need to either finish the lab before Wednesday at 2 if you didn't want ot attend, but since this lab was released on Tuesday, that's not enough time, so this week the magic word is fere, and it is "clorus".

In this lab, you'll create a package named creature that will implement two creatures (or more, if you'd like) that will inhabit a world simulated by the huglife package. Along the way we'll learn how to debug small pieces of a much larger system, even if those small pieces happen to live inside a package (a very good idea, by the way, for project 1).

Start the lab by booting up the HugLife simulator. To do this, use the following commands:

  $ make
  $ java huglife.HugLife samplesolo

This starts up a world I've created called samplesolo. You should see a little red square wandering around randomly.

Your job for this assignment is to add two classes to the creatures directory: Plip.java and Clorus.java. Eventually these two types of creatures will also inhabit the world, and unlike this red guy, they actually do something interesting.

These classes will extend the huglife.Creature package, which provides a template that all creatures should follow.

How the Simulator Works

Creatures live on an NxN grid, with no wraparound. Each square may be empty, impassible, or contain exactly one creature. Each tic, exactly one creature takes a single action. Creatures choose actions in a round-robin fashion.

There is a global queue of all creatures in the world, waiting their turn to take an action. When a Creature is next in line to move, the world simulator tells that creature who its four neighbors are, and requests a choice of action from the creature (more specifically, the world simulator calls the Creature's chooseAction method, which takes as an argument a collection of all four neighbors).

Based on the identity of the four neighbors, the Creature chooses one of exactly five actions: MOVE, REPLICATE, ATTACK, STAY, or DIE. MOVE, REPLICATE, and ATTACK are directional actions, and STAY and DIE are stationary actions. For example, if the acting Creature sees a creature to its LEFT that it can eat, it might choose to ATTACK LEFT. If a creature takes a directional action, it also specifies either a direction or a location. One of your main tasks for this lab is to write the code that makes Creature decisions. Actions are returned as objects of type Action, which are fully described in Action.java.

The chosen Action is given to the simulator which enacts the changes to the world grid (much like how the Game class in 2048 did all the drawing for you).

Unlike 2048, you'll be responsible for writing the code that tracks the state of each Creature. For example, after the acting Creature eats another Creature, the acting Creature might become stronger, die, change colors, etc.

This will be accomplished by a callback to the creature, which (as described below) is required to provide move(), replicate(), attack(Creature c), and stay() methods that describe how the acting Creature's physical state will evolve after each of these respective actions. For example, if your creature chooses to replicate upwards by returning new Action(Action.ActionType.REPLICATE, Direction.TOP), then the game simulator is guaranteed to later call the .replicate() method of the creature that made this choice. There is no die() method since the Creature is simply removed from the world entirely.

Experimenting with the sample creature

Open up Occupant.java, Creature.java, and SampleCreature.java, which you'll find in the directory for huglife package.

Try making some changes to the sample creature, and see how your changes affect how things change when you run the HugLife simulator. As one of your experiments, you might have the SampleCreature react in some observable way when it sees a wall. You can do this by requesting a list of all neighbors of type "impassible" from the getNeighborsOfType method.

Important: After you've experimented to your hearts content, use the hw checkout command to revert your project directory to its original state. If you don't know how to do this, see the documentation for the homework command. If you get stuck on this, get a neighbor or a lab TA to help you. Maybe someone will even write something on the board (I've intentionally not given you the exact command here).

Developing the Plip class

Basic Plip functionality

Now it's time to add a new type of creature to the world. Go into the creatures directory, and you'll see there is a class file named Plip there, waiting to be filled out.

Plips will be lazy (but motile) photosynthesizing creatures that mostly stand around and grow and replicate, but will flee if they happen to see their mortal enemy, the Clorus.

Let's start with just a few of the properties that we'll eventually need for our Plip class.

It would be theoretically possible to test our Plip class by sticking them on a HugLife world grid and watching what they do (with gjdb or print statements) as they run amok. However, this would be a terrible idea. Instead, it's better to perform testing on the Plip class directly. On project 1, you'll have similar basic units (in that case called Rows and Tables), and you'll probably do much better if you test these basic classes independently instead of in the context of the larger system.

To test the Plip class, which is part of the creatures package, we can create a test class TestPlip that is also part of the creatures package. You'll see that a skeleton containing a few simple tests is provided.

One way to run this test file is as follows:

  $ make
  $ java creatures.TestPlip

Try it out and you'll see that our test fails. Now after all that reading you can finally do something! Modify the Plip class according to the specifications above until all tests pass. Make sure you don't forget to rerun make in between calls to the test. Later in this lab we'll discuss how to set up a Makefile to avoid having to do call make in between calls to our tests.

Once you're done, you're well on your way to having a fully functional Plip.

The Plip replicate method

Do not start this part until your Plip class passes all the provided tests. Once you've done so, we'll work on adding the correct replication property to our Plips, namely:

The replicate test doesn't do anything yet. Fill in the test for the replicate method. Make sure to test that the returned Plip is not the same Plip as the Plip whose replicate method was called. You can use the JUnit assertNotSame method for this purpose (do not confuse assertNotEquals with assertNotSame. see the JUNit documentation if the distinction is unclear)!

The Plip chooseAction method

All that's left is giving the Plip a brain. To do this, you'll be filling out the chooseAction method as follows.

These rules must be obeyed in this strict order! Example: If it has energy greater than 1, it will ALWAYS replicate before trying to run from a clorus.

Writing testChoose

Uncomment the @Test annotation tag for the testChoose method. This will allow the testChoose method to run when you invoke the $ java creatures.TestPlip command. The existing test checks the first rule, namely that if there are no empty spaces next to the plip, then it should stay (if you checked out the lab before 1:50 PM on Wednesday, you'll need to fix the test so that line 46 ends with new Action(Action.ActionType.STAY)); instead of new Action(Action.ActionType.MOVE));).

Add tests for the choose method to your TestPlip class. Everything might look complicated (e.g. we're using a Map, and we haven't talked about them at all!). However, if you use SampleCreature as a guide, I'm hoping that pattern matching will be enough to figure out the syntax.

You might find it useful to look at the code for the Action class to see the various constructors for Actions.

Don't worry (yet) about testing the 25% rule if a Clorus is nearby. This isn't possible since you haven't created a Clorus class yet, and thus you won't be able to create a HashMap that involves Cloruses. Also this test is pretty tricky to write.

Later, once you write Clorus, you might find it interesting to come back and try to write a randomness test. One possibility is to might simply test that both choices are possible by making many calls and ensuring that each happens at least once. Performing a statistical test is probably a bit too much for lab today (though you're welcome to try).

If you're totally stuck and there are no lab assistants available to help, you can see sample tests at this link.

Writing ChooseAction

After writing a set of tests that you feel happy about, edit the Plip class so that it makes the right choices. You'll want to look carefully at the SampleCreature class as a guide.

Optional: Setting up a Makefile to make testing easier

This section is optional. Feel free to skip to the next part of the lab by using this link. If you need easy access to the Makefile for hw6, it can be found at this link.

As we saw above, the creatures package provides a class for testing plips that we ran with:

  $ make
  $ java creatures.TestPlip

Having to alternate between these two commands to make and test test can get a bit annoying to do over and over (and is error prone), so instead, let's open and edit our Makefile (scary!). You'll see it's pretty minimal. The first line of the file just tells the makefile where to find source code. Don't worry about this one.

The second line is:

porcupine: $(SRCS)

This line defines a named rule (with name porcupine) that is only allowed to run under the condition given on the right hande side of the colon, namely that all files in $(SRCS) exist. Since we just defined $(SRCS) based on what files exist a line ago, this check will never fail (unless I guess somehow the files get deleted in between the time $(SRCS) is created on the 1st line and this rule is exected on the second).

Finally, the makefile tells the javac compiler to compile all of the $(SRCS).

Let's add a new rule to the Makefile called crecheck, and specify that when this rule is invoked, then make should run the java command on creatures.testPlip. That is, you'll want to add the following to your makefile:

crecheck: 
    java creatures.TestPlip

Make sure you use a tab instead of spaces. Makefiles need tabs. This is insane, but we can't do anything about it except accept it. Note that we provide no dependency on the right hand side of the colon, meaning that this rule has no required files.

Try using make crecheck and you'll see that your code runs. However, this isn't good enough; if you changed your code, the makefile would run an out of date test!

We want the makefile to only run the tests as long as our .class files are up to date. To do this, we could modify the makefile to check all of the .class files to make sure they're newer than their respective .java files, but this would be a mess.

A more common solution is to create a blank file called sentinel and tell Java to make sure that sentinel is newer than all of our java files. To do this, we'd add something to the right hand side of our crecheck rule as follows:

crecheck: sentinel
    java creatures.TestPlip

However, if you try running make now, you'll see that it complains that it has no rule to make sentinel. What make is doing is looking for any rules named 'sentinel:' that will tell it how to make sentinel.

Let's give it one. Rename the porcupine to sentinel, and add the line touch sentinel to the bottom of the sentinel: rule, so that we have five nonblank lines of your Makefile that now read:

sentinel: $(SRCS)
    javac $(SRCS)
    touch sentinel

crecheck: sentinel
    java creatures.TestPlip

To understand what this all means, you need to know that the touch command creates a blank file with the given name (in this case sentinel).

Thus, the overall effect is that if someone requests the crecheck rule, the makefile will look and see that crecheck: requires a file called sentinel. Make will then inspect the existing sentinel file, and if it either doesn't exist or it is older than ANY file in $(SRCS), then the sentinel: rule will execute, resulting in recompilation and the generation of a blank sentinel file with the current date. Finally, the crecheck: rule will run, and the test will execute. Neat!

Running the Plip class

Assuming your tests worked, you can now see how your Plips fare in the real HugLife world. Use the commands:

  $ make
  $ java huglife.HugLife sampleplip

You should see your plips happily growing along. If something goes wrong, it's probably because your tests are not thorough enough. If this is the case, using the error messages, add new tests to TestPlip until something finally breaks. And if you're stuck, feel free to use the reference TestPlip solution linked above in this page. If you feel very confident in your Plip class and the HugLife simulator is still failing, it could be a bug in the simulator. Let me know by email.

Introducing the Clorus

We'll now add another Creature and corresponding test to the creatures package. This time, we'll be implementing the Clorus, a fierce blue colored box that enjoys nothing more than snacking on hapless Plips.

This time, you'll create your TestClorus and Clorus classes from scratch (using what you've got so far as a guide).

The Clorus should obey the following rules exactly:

As before, write a TestClorus class. You probably don't need to test the move(), stay(), or color() methods, but you're welcome to. Instead, it's probably only necessary to test the Choose() action. Your tests for TestClorus should involve at least one of each type of action.

Once you've written tests, write the Clorus class itself, again from scratch.

Showtime

We did it.

Now it's time to watch Cloruses and Plips battling it out. Use the following command to kick off a Manichaean struggle that will end either in eternal harmony or in a lonely immortal wandering the wastes forever.

  $ make
  $ java huglife.HugLife strugggz

If you did everything right, it should hopefully look cool. You might consider tweaking the HugLife simulator parameters, including the world size and the pause time between simulation steps. Be warned that world sizes about ~50x50 are probably going to run fairly slowly.

Enrichment

There's a hell of a lot one could do to improve the simulation. Possibilities include:

Let me know if you do anything you think is cool.