October 25, 2021
Post questions to the Moodle Forum!
In this lab you will practice creating classes that operate as components within a larger program. The larger program in question is the Critter World simulation that pits different critters against each other in a free-for-all struggle. You will create classes for a set of critters each with different behavior. You will practice
To begin, download the starter files in lab6.zip and unzip them to their own folder. These files (which you will not need to modify at all) are
color.py
: sets up variables for different colors. You will access these by including import color
at the top of your file and then writing color.BLACK
(for example)attack.py
: sets up variables for the different attacks critters can use. You will access these by including import attack
at the top of your file and then writing attack.POUNCE
(for example)direction.py
: sets up variables for the different directions in the Critter World grid. You will access these by including import direction
at the top of your file and then writing direction.NORTH
(for example)critter_gui.py
: defines a class that creates and manages the user interface for Critter Worldcritter_model.py
: defines a class that manages the behavior of critters in Critter Worldcritter_main.py
: defines functions that actually run Critter WorldThere’s also a configuration file (critter_world_config.json
) that Critter World uses as input to control how many of each critter are present, the size of the grid world, and various other settings.
Take a look at the current state of Critter world by running the file critter_main.py
from VS Code.
At the beginning, you will see an empty grid with no critters. The “Go” button starts the simulation; initially there is nothing to simulate so the move count will just count up with nothing happening. The real simulation gets going once you have some critters on your board.
On each round of the simulation, each critter is asked which direction it wants to move. On each round, each critter can move one square to the north, south, east, west, or stay at its current location. Critters move around in a world of finite size, but the world is toroidal (going off the end to the right brings you back to the left and vice versa; going off the end to the top brings you back to the bottom and vice versa).
The critter world is divided into cells that have integer coordinates. The initial configuration file specifies 40 cells across and 30 cells up and down. The upper-left cell has coordinates (0,0), increasing x values moves you right and increasing y values move you down (similar to images in lab 5).
This program may be confusing at first because you are not writing the main component (i.e., critter_main.py
and the other files that will use your critter objects), therefore your code will not be in control of the overall program’s execution. Instead, you are defining a series of objects that become part of a larger system. For example, you might find that you want to have one of your critters make several moves all at once—you won’t be able to do that. The only way a critter can move is to wait for the simulator to ask it for a move. Although this experience can be frustrating, it is a good introduction to the kind of programming we do with objects.
As the simulation runs, critters may collide by moving onto the same location. When two critters collide, they fight to the death. The winning animal survives and the losing animal is removed from the simulation. The following table summarizes the possible fighting choices each animal can make and which animal will win in each case. To help you remember which beats which, notice that the starting letters and win/loss ratings of “roar, pounce, scratch” correspond to those of “rock, paper, scissors.” If the critters make the same choice, the winner is chosen at random.
Critter #2 | ||||
---|---|---|---|---|
ROAR | POUNCE | SCRATCH | ||
Critter #1 | ROAR | random winner | #2 wins | #1 wins |
POUNCE | #1 wins | random winner | #2 wins | |
SCRATCH | #2 wins | #1 wins | random winner |
Please post your questions on the Moodle forum or bring them to class.
Suggested Timeline:
Each critter class in your program must have the following methods:
fight(self, oppInfo)
: returns this critter’s attack when it collides with another crittergetColor(self)
: returns this critter’s colorgetMove(self, info)
: returns this critter’s move for the roundgetChar(self)
: returns the character (one-letter string) that represents this critterfightOver(self, won, oppAttack)
: method called after each fight, returns nothingTo start out, let’s add the stationary, yet mighty Stone critter to our world. Read through the code below and the comments on each method. Stones don’t move (getMove
always returns direction.CENTER
), and always use the ROAR
attack.
import color, attack, direction
class Stone():
# we can omit a __init__ method if there's nothing we need to initialize
# @param oppInfo The critter info of the current opponent.
# @returns Your attack: attack.ROAR, attack.POUNCE, or attack.SCRATCH
def fight(self, oppInfo):
return attack.ROAR
# Give your color.
# @returns Your current color.
def getColor(self):
return color.GRAY
# Give your move this round.
# @param info your critter info
# @returns A cardinal direction or staying put:
# (direction.NORTH, direction.SOUTH, direction.EAST, direction.WEST, direction.CENTER)
def getMove(self, info):
return direction.CENTER
# Give the letter that represents you.
# @returns Whichever character represents this critter.
def getChar(self):
return 'S'
# End of fight shenanigans.
# @param won Boolean; true if won fight, false otherwise.
# @param oppAttack Opponent's choice of fight strategy (ROAR, etc)
# @returns Nothing.
def fightOver(self, won, oppAttack):
# we don't do anything in fightOver method here, because a
# Stone doesn't do anything with this information
# we have to have the method because the simulation expects every critter class to have one
# and we'll get a syntax error if we have the def line with nothing indented after it
# so we use the pass instruction since it literally means "do nothing"
# leave this method as just pass unless the critter needs to change its behavior based on these parameters
pass
def __str__(self):
return self.__class__.__qualname__
Add this code to a file called stone.py
, then run critter_main
. No Stones appear! This is because you need to edit the configuration file (critter_world_config.json
) to tell Critter World to create some Stones. Specifically, add an entry to the "critter_files"
section to tell the simulation to use "stone.py"
as the file for Stones and an entry to the "critter_pops"
section to tell it to create 25 Stones:
{
"critter_files": {
"Stone": "stone.py"
},
"critter_pops": {
"Stone": 25
},
"width": 40,
"height": 30,
"quickfight": false,
"verbose": false
}
Now you should see some stones in your world when you run critter_main
. Of course, Stones don’t move, so even when you press the Go button, nothing happens, but we’ll do more interesting critters in due time.
The direction
module defines nine constants for the various directions (NORTH
, NORTHEAST
, NORTHWEST
, SOUTH
, SOUTHEAST
, SOUTHWEST
, EAST
, WEST
CENTER
), and the attack
module defines three constants for the three types of fighting (ROAR
, POUNCE
, SCRATCH
). You make these available to your code with import direction
and import attack
.
The following are the four critter classes you will implement. Each class must have only one constructor and that constructor must accept exactly the parameter(s) described in the table. For random moves, each possible choice must be equally likely (use random.choice(choices)
to return a random element from a list choices
). Put each definition in its own file with the specified name.
Several of these critters have movement behavior that follows some regular pattern. For example, the Mouse moves in a zig-zag while the Elephant moves in a square. A critter can keep track of how many simulation turns have occurred to help it know what move to make next or when to change direction. Initialize this count to 0 in the constructor (self.count = 0
) and then add one to it every time getMove
is called. Note that getColor
may be called multiple times per step, and is therefore a bad method in which to keep track of the number of steps that have passed. Since the critters need to alternate their movement or make a change every n
turns, think about how you could use self.count % 2
and self.count % n
to accomplish this.
Note: the only critter that uses the fightOver
method is Chameleon, since it changes its attack based on its most recent fight. The other critters can leave it as doing nothing like Stone does.
Read the Testing Advice section for information on how to test your work.
mouse.py
)constructor | def __init__(self, color) |
fighting behavior | always attack.SCRATCH |
color | the color passed to the constructor |
movement behavior | alternates between direction.EAST and direction.SOUTH in a zigzag pattern (first direction.EAST , then direction.SOUTH , then direction.EAST , then direction.SOUTH , …) |
character | 'M' |
The Mouse constructor accepts a parameter representing the color in which the Mouse should be drawn. This color should be returned each time the getColor
method is called on the Mouse (so you probably want to store it in an instance variable). For example, a Mouse constructed with a parameter value of color.RED
will return color.RED
from its getColor
method and will therefore appear red on the screen. Of course, it is the main program that will construct these mice, so you will have no control over what color each Mouse is displayed as.
Remember, the getMove
method is called by the main program each time the critter needs to move; you can use this information (and perhaps an instance variable) to keep track of whether it is time to move East or to move South.
tiger.py
)constructor | def __init__(self) |
fighting behavior | always attack.ROAR |
color | alternates between color.ORANGE and color.BLACK (first color.ORANGE , then color.BLACK , then …) |
movement behavior | moves 3 turns in a row in one random direction (direction.NORTH , direction.SOUTH , direction.EAST , or direction.WEST ), then chooses a new random direction and repeats |
character | 'T' |
Remember, the getMove
method is called by the main program each time the critter needs to move; you can use this information (and perhaps an instance variable) to keep track of whether it is time to choose a new random direction.
elephant.py
)constructor | def __init__(self, steps) |
fighting behavior | if opponent displays as a Tiger (with the character 'T' ), then attack.ROAR ; otherwise attack.POUNCE |
color | color.GRAY |
movement behavior | first go direction.SOUTH steps times, then go direction.WEST steps times, then go direction.NORTH steps times, then go direction.EAST steps times (a clockwise square pattern), then repeats |
character | 'E' |
The Elephant constructor accepts a parameter representing the distance the Elephant will walk in each direction before changing direction. For example, an Elephant constructed with a parameter value of 8 will walk 8 steps south, 8 steps west, 8 steps north, 8 steps east, and repeat. You can assume that the value passed for steps is at least 1.
For the fight
method, you need to determine your opponent’s display character. Every time a critter’s fight
method is called, it is passed a parameter oppInfo
of type CritterInfo
that provides useful information about the current opponent, including its display character. In particular, a critter’s CritterInfo
contains the following instance variables and the method getNeighbor
:
x -- the current critter's x coordinate y -- the current critter's y coordinate width -- the width of the simulation world height -- the height of the simulation world char -- the current critter's display character color -- the current critter's display color getNeighbor(direction) -- a method that, when called with a parameter representing one of the direction constants, returns the name of the class (NOT the display character) of the critter in that location (i.e. the location that is one space in the given direction of the current critter.)
Thus, in the fight
method, the code oppInfo.char
represents the opponent critter’s display character, and oppInfo.color
its color. Slightly less useful now, the code oppInfo.getNeighbor(direction.NORTH)
would return the string 'Stone'
if there is a stone to the north of your opponent, and the string '.'
if there is no neighbor to the north.
Note that every time a critter’s getMove
method is called, it is passed a parameter info
also of type CritterInfo
; this time, the information being passed is about the current (self
) critter, not of an opponent. Once again, some of the information (for example, knowing what neighbors you have in your immediate vicinity) may help you make movement choices.
chameleon.py
)constructor | def __init__(self) |
fighting behavior | the attack used by this Chameleon’s last opponent (in its very first fight, the Chameleon uses a random attack) |
color | the color used by this Chameleon’s last opponent (all Chameleons start as color.GREEN) |
movement behavior | the Chameleon waits for its prey–stays still unless it sees a critter it can move to |
character | 'C' |
The Chameleon will try to emulate the appearance of its most recent (losing) opponent by displaying the opponent’s color. (You can get this information from the fight
method’s oppInfo
parameter.) As its fighting behavior, a Chameleon will use the attack of its most recent opponent. You can obtain information about the outcome of each fight from the fightOver
method, which is called at the end of each and every fight, and tells each critter whether it won or lost the fight (won
is True
if the former, False
if the latter) and what its opponent’s attack was (oppAttack
is one of attack.ROAR
, attack.POUNCE
, or attack.SCRATCH
).
Finally, a Chameleon’s movement strategy is based on its surroundings. It moves to fight any neighbor it sees and otherwise stays still. You can find out which critters (if any) are to your North, South, East, and West through the info
parameter (of type CritterInfo
, described above) that is passed to the getMove method. For example, info.getNeighbor(direction.NORTH)
will return the class name (as a string) of the critter to the North, and '.'
if there is no critter in that direction.
Before you can test your critters, you will need to understand the Critter World configuration file, critter_world_config.json
. After you have implemented all five critters, your file should look something like this:
{
"critter_files": {
"Stone": "stone.py",
"Tiger": "tiger.py",
"Mouse": "mouse.py",
"Elephant": "elephant.py",
"Chameleon": "chameleon.py"
},
"critter_pops": {
"Stone": 25,
"Tiger": 25,
"Mouse": 25,
"Elephant": 25,
"Chameleon": 25
},
"width": 40,
"height": 30,
"quickfight": false,
"verbose": false
}
This file follows a format called JSON, which is commonly used for structured data like this. Here’s a breakdown of what each part means: - "critter_files"
: determines which critters are present and which Python file is used for each critter. The critter name on the left side of the colon must match the class name in the Python file. - "critter_pops"
: determines how many of each critter there are. - "width"
: determines how many cells wide the grid world is - "height"
: determines how many cells tall the grid world is - "quickfight"
: when set to true
, no GUI interface will be displayed, and just the final results will be printed to the terminal. - "verbose"
: when set to true
, a lot of information about the internal simulation will be printed to the terminal
As you work on each critter, you will need to add entries to "critter_files"
and "critter_pops"
for that critter, so you can test your work. The config file must match the JSON format exactly. This means anything that isn’t a number or a Boolean must be inside double quotes, and that entries must be separated with commas. Fortunately, VS Code understands the JSON format and can highlight most problems. Pay attention to those and read any error messages in the terminal carefully. critter_main.py
tries to give you an informative message for any error it encounters in reading the configuration file.
There are a couple useful ways to use the configuration file to test specific critters. First, you can look at a single critter in isolation to get a better sense if its movement is correct. For example, the configuration below would run a simulation with just a single Tiger, making it much easier to tell if it is choosing a random direction every three moves (compared to trying to pick that out with a whole bunch of critters all moving at once). Instead of clicking the “Go” button, you might use the “Tick” button to advance one simulation turn at a time.
{
"critter_files": {
"Tiger": "tiger.py"
},
"critter_pops": {
"Tiger": 1
},
"width": 40,
"height": 30,
"quickfight": false,
"verbose": false
}
Another way to use the configuration file is to set "verbose"
to true
(note the lack of capiltalization in JSON, unlike Python). This will cause information about movement and fighting to be printed to the terminal each simulation turn. This can be overwhelming with a lot of critters, but if you set up the configuration file to have just a few (and maybe make the width and height smaller so they’re more likely to fight), you can use the output to double check fighting behavior. If you have your own print
statements in your critter classes, they will show up in this output in the right place.
hive.py
) — 2 pointsIn a new file called hive.py
implement an enhanced version of the Chameleon critter to have the following behavior:
constructor | def __init__(self) |
fighting behavior | the attack that beats the most used attack against the Chameleons so far (not just this particular Chameleon) |
color | the color used by this Chameleon’s last opponent (all Chameleons start as color.GREEN) |
movement behavior | if the Chameleons have lost the majority of their fights so far, then avoid enemy critters. Otherwise, move towards opponents. |
character | 'C' |
For both fighting and movement, this version of the Chameleon relies on a hive mind where information is shared among all Chameleons. For fighting, a Chameleon will use the attack that beats (and is thus different from) the most past opponents of Chameleons; that is, the Chameleon species needs to keep counts on how often each attack is used against a Chameleon (think: static (i.e., non-instance) variables) and play the counter-attack to the most-used one.
A hive mind Chameleon’s movement strategy is based on the Chameleon species’ success rate: if they are not faring well (if Chameleons have lost at least half their fights) then they will try to avoid other critters, that is, move away from adjacent critters if possible, or stay in the same spot if surrounded. If Chameleons are winning over half their fights then they become aggressive and move towards other critters.
friendlyTiger.py
) — 1 pointThe version of the Tiger critter you implemented above is just as willing to fight other Tigers as it is to fight any critter. Copy your Tiger
class to a new file called friendlyTiger.py
. In that file, change the getMove
method so that these Tigers will never fight each other.
To test your code, edit the configuration file to have just "Tiger": 100
in the "critter_pops"
section and "Tiger": "friendlyTiger.py"
in the "critter_files"
section. When you run with this configuration file, none of the Tigers should ever fight and their population should stay at 100 alive.
tournament.py
)Submit a critter of your own design to compete in a Critter World tournament. Credit will only be given for critters with creative and non-trivial behavior. A very simple critter or one that too closely resembles one of the standard critters (or another extension you submitted) will receive partial or zero bonus points for this extension.
Your critter’s fighting behavior may want to utilize the parameter sent to the fight
method, oppInfo
, which tells you how your opponent displays themselves (oppInfo.char
is their display character, oppInfo.color
is their display color, and oppInfo.getNeighbor(critter.CENTER)
is their class name; for a Mouse these might be 'M'
, Color(r=133, g=132, b=118)
, and "Mouse"
, respectively.)
You can make your critter return any character you like from its getChar
method and any color you like from the getColor
method. In fact, critters are asked what display color and character to use on each round of the simulation, so you can have a critter that displays itself differently over time. Keep in mind that the getChar
character is also passed to other critters when they fight your critter; you may wish to strategize to try to fool opponents.
We will host the Critter World tournament in class consisting of battles in the following format: two critter classes will be placed into the simulator world along with the other standard critters (Stone, Elephant, Tiger, and Mouse), with 25 of each critter type. The simulator will be started and run until no significant activity is occurring or until 1000 moves have passed, whichever comes first. The student whose critter class has the higher score (other critters defeated + your critters alive) wins the battle. The winner of the tournament will gain fame and fortune!
Submit the following files via the Lab 6 Moodle page. You do not need to submit any of the starter files.
Four Python files, one for each of the critters you implemented: mouse.py
, tiger.py
, elephant.py
, and chameleon.py
OPTIONALLY files called hive.py
, friendly_tiger.py
, and/or tournament.py
if you did any of the CHALLENGES or the optional tournament
This assignment is graded out of 30 points as follows:
mouse.py
– 5 points
getChar
– 0.5 pointsgetColor
– 1.5 pointsgetAttack
– 0.5 pointsgetMove
– 2.5 pointstiger.py
– 6 points
getChar
– 0.5 pointsgetColor
– 2 pointsgetAttack
– 0.5 pointsgetMove
– 3 pointselephant.py
– 6.5 points
getChar
– 0.5 pointsgetColor
– 1 pointgetAttack
– 2 pointsgetMove
– 3 pointschameleon.py
– 7 points
getChar
– 0.5 pointsgetColor
– 2 pointsgetAttack
– 2 pointsgetMove
– 2.5 points.py
file you submit – 1 pointYour critters will be evaluated by automated script that tests for correct behavior. If you submit code that crashes the autograder (because your code crashes or you used different class or function names than those specified in this writeup), we will attempt to fix any issues and re-grade. You will lose points for each issue we need to fix.
Style will be evaluated by us reading your code.
Acknowledgments: This assignment description is modified from Adam Eck’s Critter Tournament lab.