**CS 111 w20 --- Lab 6: Critter World (pair programming)** *Due: Wednesday, February 26, at 9pm* 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 - Writing code that is one piece of a bigger program - Defining new classes that meet a specification - Using instance variables to keep track of necessary information - Using a JSON configuration file to control a program (##) Logistics To begin, download the starter files in [lab6.zip](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 World - `critter_model.py`: defines a class that manages the behavior of critters in Critter World - `critter_main.py`: defines functions that actually run Critter World 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](critter_world.png) (##) 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 |
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: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON { "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. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JSON { "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. # Extensions ## (OPTIONAL CHALLENGE) Hive Mind Chameleons (in `hive.py`) 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. ## (OPTIONAL CHALLENGE) No Friendly-Fire Tigers (in `friendlyTiger.py`) 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 FUN) 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](https://moodle.carleton.edu/mod/assign/view.php?id=507499). 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` - OPTIONAL files called `hive.py`, `friendly_tiger.py`, and/or `tournament.py` if you did any of the corresponding extensions - A plain text file called `feedback.txt` with the following information (you get points for answering these questions!): - How many hours you spent outside of class on this homework. - The difficulty of this homework for this point in a 100-level course as: too easy, easy, moderate, challenging, or too hard. - What did you learn on this homework (very briefly)? Rate the educational value relative to the time invested from 1 (low) to 5 (high). # Grading This assignment is graded out of 30 points as follows: - Submitting `feedback.txt` -- 3 points - `mouse.py` -- 5 points + Correct `getChar` -- 0.5 points + Correct `getColor` -- 1.5 points + Correct `getAttack` -- 0.5 points + Correct `getMove` -- 2.5 points - `tiger.py` -- 6 points + Correct `getChar` -- 0.5 points + Correct `getColor` -- 2 points + Correct `getAttack` -- 0.5 points + Correct `getMove` -- 3 points - `elephant.py` -- 6 points + Correct `getChar` -- 0.5 points + Correct `getColor` -- 0.5 points + Correct `getAttack` -- 2 points + Correct `getMove` -- 3 points - `chameleon.py` -- 6 points + Correct `getChar` -- 0.5 points + Correct `getColor` -- 1.5 points + Correct `getAttack` -- 1.5 points + Correct `getMove` -- 2.5 points - Style -- 4 points + Appropriate use of instance variables -- 2 points + Good variable names -- 1 point + Comments with name, date, and purpose at the top of each `.py` file you submit -- 1 point 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](http://www.cs.oberlin.edu/~aeck/Fall2018/CSCI150/Labs/Lab10/index.html).