This note discusses two aspects of the design of the GridWorld API. These observations may be helpful for teachers and students as they prepare for the AP CS Exam.
Critters are included in GridWorld to emphasize design. (For those of you
familiar with OO design patterns, the act
method is an example
of the “template method” design pattern. But you do not need to
know that pattern to use critters. Design patterns are definitely not a part
of the AP CS curriculum.)
The act
method calls five methods, and subclasses override
some or all of them to achieve some desired behavior. Ideally,
act
would have been declared final
, but that is not
in the AP CS subset.
Each of the five methods called from act
has postconditions
that restrict what it can do. The following table shows the postconditions as
they are stated in the GridWorld documentation.
getActors |
The state of all actors is unchanged. |
processActors |
(1) The state of all actors in the grid other than this critter and
the elements of actors is unchanged. (2) The location of
this critter is unchanged. |
getMoveLocations |
The state of all actors is unchanged. |
selectMoveLocation |
(1) The returned location is an element of locs , this
critter's current location, or null . (2) The state of
all actors is unchanged. |
makeMove |
(1) getLocation() == loc . (2) The state of all actors
other than those at the old and new locations is unchanged. |
Here, the state of an actor includes
Removing an actor changes its state: its location becomes
null
. (By the way, don't try to weasel out of that by calling
grid.remove(anActor)
—see Part B below.) Similarly, adding
an actor changes its state.
Note that getActors
, getMoveLocations
, and
selectMoveLocation
can't change the state of any actor.
The postcondition of processActors
is subtly worded to allow
adding new actors, since it only prohibits state change of actors in the
grid.
If a critter wants to change its own state, it must do so in
processActors
or makeMove
. If it wants to change
its location (or remove itself), it can only do so in makeMove
.
If a critter wants to change the state of another actor, it can
only do so in processActors
, and the actor must have been
included in the actors
parameter, or it must be newly added to
the grid. (There is a teensy exception: makeMove
removes the
actor at the target location, and it may add a single actor, but only to the
old location.)
Finally, selectMoveLocations
can only select from the
locations that were handed to it. The makeMove
method has even
less choice—it must move to the location given in the
parameter.
These conditions significantly restrict what a critter can do, and how it
can do it. For example, consider a BluesCritter
that chooses a
blue rock in the grid, eats the chosen rock's neighbors, and jumps to the
rock (thereby removing it from the grid). The first method called by
act
is the getActors
method. We must implement that
method to return a set that includes the blue rock's neighbors since
processActors
can only remove actors that are given to it. But
getActors
cannot update any instance variable of the
BluesCritter
. In particular, it cannot store the blue rock's
location. Instead, the processActors
method must leave some
trace of that location so that the getMoveLocations
method can
recall it. To finesse this, we can make the getActors
method
return all actors in the grid, so that processActors
can pick a blue rock, remember its location, and remove its neighbors.
When you design a particular critter, you need to distribute
responsibilities among the five methods that are called by act
.
For example, in the BluesCritter
class, if
processActors
sets an instance variable to the location of the
chosen blue rock, the getMoveLocations
relies on that setting.
Some people are concerned about the apparent fragility of such an
arrangement. What would happen if someone else called one of the methods, or
if a subclass of BluesCritter
changed one but not the other? In
the context of the AP CS Exam, students should not worry about such
issues unless they are specifically directed otherwise. These five methods
were intended to be used in the act
method, and not for any
other purpose.
To summarize, here are simple rules for your students when working with critters.
act
processActors
or
makeMove
processActors
Remember, these rules are only for critters, not for bugs or other actors.
Also keep in mind that not every actor can or should be represented as a
critter. In the context of GridWorld, a critter is not a warm and fuzzy
creature, but an actor that first processes actors and then makes a move. The
BluesCritter
doesn't fit that description very well, and it
would have been better to design it as a BluesActor
. Then you
can override act
without any tortured logic, simply moving to a
blue rock and removing its neighbors.
In a grid of actors, you must always use the
putSelfInGrid
/removeSelfFromGrid
in the
Actor
class, and never the put
/remove
method of the Grid
class. These methods ensure that the
Grid
reference in each Actor
object properly refers
to the grid containing the actor. In particular, don't try calling
getGrid().remove(this)
in an attempt to bypass the postcondition
of a Critter
method. The mere fact that the resulting code may
happen to “work” when you run it in the GridWorld environment
does not make it right.
Of course, if you have a grid of other objects that are not actors, then
use put
and remove
.
Finally, it is easy to be confused about valid and empty locations. There are two simple rules.
null
is never a location. Do not pass it to any of the
Grid
methods, not even isValid
.isValid
require a valid
location. What happens if you pass null
or an invalid location to a
grid method? If you know that the grid is a a
BoundedGrid
or UnboundedGrid
, then you can consult
the implementation and see that a NullPointerException
or
IllegalArgumentException
is thrown. But for a general
Grid
, you have no way of knowing what happens—someone
could have implemented the Grid
interface in a different way.
This work is licensed under a Creative Commons
Attribution-Noncommercial-Share Alike 3.0 license.