CS 111 Lab 6

Aaron Bauer

October 25, 2021

CS 111 f21 — Lab 6: Critter World

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

Logistics

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

There’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.

Critter World populated with the five standard critters

Background

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:

Getting Started

Each critter class in your program must have the following methods:

To 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.

Critters to Implement

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 (in 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 (in 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 (in 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 (in 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.

Testing Advice

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.

CHALLENGES

Hive Mind Chameleons (in hive.py) — 2 points

In 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.

No Friendly-Fire Tigers (in friendlyTiger.py) — 1 point

The 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.

(OPTIONAL) Critter Tournament (in 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!

What to Turn In

Submit the following files via the Lab 6 Moodle page. You do not need to submit any of the starter files.

Grading

This assignment is graded out of 30 points as follows:

Your 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.