+1 (315) 557-6473 

Python Program to Simulate Populations Using OOP And ABM Assignment Solution.


Instructions

Objective
Write a program to simulate populations using OOP and ABM in python.

Requirements and Specifications

Create a notebook showing the simulation of populations using OOP and ABM.
Describe the methods used in the markdown section too
Source Code
# Homework Assignment #3 (Individual)
## Simulating populations using OOP and ABM (135 pts total, 135 pts == 100%)
### <p style="text-align: right;"> &#9989; Put your name here.</p>
### <p style="text-align: right;"> &#9989; Put your _GitHub username_ here.</p>
<img src="https://i.ibb.co/mNpTrnt/Rock-paper-scissors.png" width=300px align='left' style="margin-right: 20px" >
## Rock-Paper-Scissors(-Lizard-Spock)
### Goal for this homework assignment
By now, you have learned OOP and ABM through the assignments of assembling Zoo and Superbugs, respectively. Let us use what you learned to build a simple model of population competition between three different species in a simple model built after [Rock-Paper-Scissors](https://en.wikipedia.org/wiki/Rock_paper_scissors).
**This assignment is due roughly two weeks from now at 11:59 pm on Sunday, October 24.** *Note*: Due to the proximity of the standard Friday homework deadline to the midterm, we're giving you an extra couple days, but keep in mind there will NOT be helproom hours during the weekend, so try to start early enough to get help with the assignment!
When you're finished, it should be uploaded into the "Homework Assignments" submission folder for Homework #3. Submission instructions can be found at the end of the notebook. **The distribution of points can be found in the section headers**.
**At the end of the assignment, if everything went as intended, you should have a population evolution plot like the one below.**
<img src="https://i.ibb.co/DpYsq1d/populations.png" alt="Populations" border="0" width=600px>
This plot should help you to determine if your code is headed in the right direction!
---
## Part 1: Add to your Git repository to track your progress on your assignment (5 points)
As usual, for this assignment, you're going to add it to the `cmse202-f21-turnin` repository you created in class so that you can track your progress on the assignment and preserve the final version that you turn in. In order to do this you need to
**&#9989; Do the following**:
1. Navigate to your `cmse202-f21-turnin` repository and create a new directory called `hw-03`.
2. Move this notebook into that **new directory** in your repository, then **add it and commit it to your repository**.
1. Finally, to test that everything is working, "git push" the file so that it ends up in your GitHub repository.
**Important**: Make sure you've added your Professor and your TA as collaborators to your "turnin" respository with "Read" access so that we can see your assignment (you should have done this in the previous homework assignment)
**Also important**: Make sure that the version of this notebook that you are working on is the same one that you just added to your repository! If you are working on a different copy of the noteobok, **none of your changes will be tracked**!
If everything went as intended, the file should now show up on your GitHub account in the "`cmse202-f21-turnin`" repository inside the `hw-03` directory that you just created. Periodically, **you'll be asked to commit your changes to the repository and push them to the remote GitHub location**. Of course, you can always commit your changes more often than that, if you wish. It can be good to get into a habit of committing your changes any time you make a significant modification, or when you stop working on the project for a bit.
**Do this**: Before you move on, put the command that your instructor should run to clone your repository in the markdown cell below.
``` bash
# Put the command for cloning your repository here!
```
---
## Part 2: Load necessary modules
Execute the next two Code cells to load python packages for math and visualization functions.
import random
import numpy as np
import math
import matplotlib.pyplot as plt
import time
from IPython.display import display, clear_output
---
## Part 3: Problem Statement
You are creating a simple ecosystem containing three types of populations (you can imagine these to be animals, bacteria or something similar) on the computer.
1. **In your environment, there are three different animals: "Rock Animals", "Paper Animals" and "Scissors Animals".** ==> You will create a generic `Animal` class and derived classes for each of the specific animals.
2. **In our model, animals will not move from where they start.**
3. **The animals reproduce.** ==> All derived objectcs will have a method of reproduce/duplicate/clone over a given period. Their offspring will appear in a neigboring spot.
4. **Each animal has a certain energy level. If an animal reproduces, it will first lose 1 unit of energy and split into two: it will keep half of its energy for itself and give its offspring the other half.** ==> You will write a `reproduce` method that will find an empty neighboring spot in the world, creates a clone of itself (same animal type) and assigns it to that empty spot.
5. **The environment has a boundary.** ==> No animals can exist outside of this boundary.
6. **Animals will be able to consume each other in a cyclical way modeled after Rock-Paper-Scissors.** ==> You will write code that allows them to "eat" a certain kind of different animal only. "Rock 'eats' (beats) scissors", "scissors beats paper" and "paper beats rock".
7. **Note here** your main tasks are creating a generic `Animal` object and an object for each animal type that derives from `Animal`. The code to verify the created classes and run simulations is already functioning. No need to modify them. You are encouraged to take a look of those code to ensure your objects will be compatible with them before you create the objects.
---
## Part 3.1 Animal object (15 pt)
**Step by step. Let's start with creating an `Animal` object.** The object should contain the attributes of
- Energy: An energy level represented by a positive floating point number. The class should initialize this number to a starting energy that is given as an argument to the class.
- **Important**: Add Docstrings to explain your code. Without any Docstrings, **your score will be compromised**. (5 pt for docstrings, 10 pt for the code)
- You can find an outline of the class below. You will need to implement `__init__` and `get_energy()`.
  - The `__init__` method has one argument (besides `self`) called `starting_energy`. You should make sure that this value is stored as an attribute called `self.energy` and that `get_energy()` returns the current value of the attribute.
- Note that the class given below contains more stub functions (`get_rgb`, `get_kind_name`, `can_eat`, `clone`, `eat`, and `reproduce`). You do not have to change them at this point and do not need to provide docstrings for them right now.
- Finally, **create a Animal object called `animal`** and set its initial energy to 100 .
So, to summarize, in the cell below, do this:
1. Implement the class constructor `__init__` as outlined above
2. Make sure that the constructor initializes an attribute called `self.energy` and sets its value to `starting_energy`
3. Implement the function `get_energy()` to return the current energy
4. Add Docstrings to the overall class, the constructor and to `get_energy()`
### Modify the code below as explained.
### Start with the class skeleton provided and implement the constructor and `get_energy()`.
### Also add Docstrings to these two functions and to the class overall (no need for docstrings for the other stub functions).
class Animal:
    def __init__(self, starting_energy = 100):
        self.energy = starting_energy
    def get_energy(self):
        return self.energy
    def get_rgb(self):
        pass # to be implemented by derived classes
    def get_kind_name(self):
        pass # to be implemented by derived classes
    def can_eat(self, other_animal):
        pass # to be implemented by derived classes
    def clone(self):
        pass # to be implemented by derived classes
    def eat(self, env, neighbor_spots):
        pass # will be implemented later
    def reproduce(self, env, neighbor_spots):
        pass # will be implemented later
### IMPLEMENT THIS: Add code here to create an Animal object called `animal` here and set its initial energy to 100.
- Now do this: Now that your class is defined, create an instance of the `Animal` class with an energy of 100.
### In this cell, do this: create an instance of the `Animal` class with an energy of 100.
animal = Animal(100)
### Once your code is implemented, running the following cell should print "The animal has an energy of 100."
energy = animal.get_energy()
print("The animal has an energy of {0}.".format(energy))
---
### &#128721; STOP
**Pause to commit your changes to your Git repository!**
Take a moment to save your notebook, commit the changes to your Git repository using the commit message "version 1 of Animal", and push the changes to GitHub.
---
---
## Part 3.2 Specific animal objects derived from Animal (25 pt)
In this section you will create one of three classes. Each of the classes will be derived from the `Animal` class you defined previously. We will start with one of these classes first.
- Create a class called `Rock_Animal` and derive it from `Animal`. This means that `Rock_Animal` should **inherit** the `Animal` class.
- Implement (and therefore override the version from the `Animal` class) the following methods from Animal in your new derived object, `Rock_Animal`:
  - `get_rgb(self)`: This method should return a tuple made of three floating point numbers defining the RGB color that this animal will be represented by. For example, for red you would return `(1.,0.,0.)`. Choose a color you like.
    - You will use two more colors later on. You are free to choose three different colors, such as just red, green and blue, but there are other color palettes out there that might be more suitable for anyone who might be color blind. There are many resources on the internet on this, one example that might be useful is [this web page](https://davidmathlogic.com/colorblind/). The color scale you choose will not be graded, of course.
  - `get_kind_name(self)`: This method should return the name of the kind of animal. You can choose any name you like, but in this case something like `"rock"` might be a good idea.
  - `can_eat(self, other_animal)`: This method should return a boolean value (`True` or `False`). Its argument `other_animal` will be another instance of a class derived from `Animal`. If the other animal is one that we can eat it should return `True`, otherwise it should return `False`. You might want to call `get_kind_name()` from this function and check the name of the other animal. If it is `"scissors"`, we can eat it, otherwise we cannot ("rock beats scissors").
  - `clone(self)`: This method should return a *new* instance of the `Rock_Animal` class, initialized to half the energy the current we currently have. It should also reduce the current energy of the `Rock_Animal` itself by half. (Thus simulating "splitting" the animal into two pieces of the same kind with half the energy as the original one/providing half of its energy to its offspring.) [Note that it is not necesary to deal with the part where animals lose "1 unit of energy" before they reproduce in `clone()` itself as this will be taken care of later by the `reproduce()` function.]
    - ==> So clone should reduce the current energy by half then create a **new** `Rock_Animal` and inititalize that new animal to the new, halved, energy level. This new `Rock_Animal` should then be returned by the `clone` function.
  - (Note: We will deal with defining the `eat()` and `reproduce()` functions from the `Animal` class later on. These functions will be part of the `Animal` class, but you can ignore them for now. Specifically, they should not be part of your derived class.)
### put your code of creating the derived class `Rock_Animal` in this cell
class Rock_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (1.0, 0, 0) # red
  def get_kind_name(self):
    return "rock"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors then this can eat it
    return other_animal.get_kind_name() == "scissors"
  def clone(self):
    new_animal = Rock_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
---
## Part 3.3 Create two more animal classes (25 pt)
Now, create two more classes, one of them called `Paper_Animal` and one of them called `Scissors_Animal`. They should look like `Rock_Animal` previously with these differences:
- Each of them should have a different color from each other and different from `Rock_Animal`.
- Their names returned by `get_kind_name()` should be different for each class. Use `"scissors"` for `Scissors_Animal` and `"paper"` for `Paper_Animal`.
- The `can_eat()` function needs to be updated to follow the "rock-paper-scissors" rules:
  - rock beats ("can eat") scissors
  - scissors beats ("can eat") paper
  - paper beats ("can eat") rock
- Each of the `clone()` functions needs to return an animal object of the same kind. So make sure that `clone()` for `Scissors_Animal` actually returns a `Scissors_Animal` instance. The rules for energy splitting are the same as before.
### put your code of creating the derived classes `Paper_Animal` and `Scissors_Animal` in this cell (10 pt)
class Paper_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (252.0/255.0, 219.0/255.0, 3.0/255.0) # yellow
  def get_kind_name(self):
    return "paper"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors then this can eat it
    return other_animal.get_kind_name() == "rock"
  def clone(self):
    new_animal = Paper_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
class Scissors_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (3.0/255.0, 157.0/255.0, 252.0/255.0) # blue
  def get_kind_name(self):
    return "scissors"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors then this can eat it
    return other_animal.get_kind_name() == "paper"
  def clone(self):
    new_animal = Scissors_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
- Now run the cell below. It should finish without error. You can see it testing some of the assumption we made above and some of the intital definitions as a "unit tests", each one a test for a small logical unit of code. If everything works as expected, none of the `assert` statements will produce any output. There is a `print` statement at the end of the cell that, once reached, will just print a message that everything worked. Each `assert` line below checks a certain assumption. If your classes are implemented as expected, none of them should fail. If one *does* fail, try to go back and correct your code above accordingly.
# test 01 - create objects
rock = Rock_Animal(100)
paper = Paper_Animal(100)
scissors = Scissors_Animal(100)
# test 02 - are the classes derived from `Animal`?
assert isinstance(rock, Animal), "Rock_Animal is not derived from Animal"
assert isinstance(paper, Animal), "Paper_Animal is not derived from Animal"
assert isinstance(scissors, Animal), "Scissors_Animal is not derived from Animal"
# test 03 - make sure the energy is 100
assert abs(rock.get_energy() - 100.) < 1e-5, "The energy of the rock animal should be 100"
assert abs(paper.get_energy() - 100.) < 1e-5, "The energy of the paper animal should be 100"
assert abs(scissors.get_energy() - 100.) < 1e-5, "The energy of the scissors animal should be 100"
# test 04 - make sure that the objects have an attribute called `energy` and that the value is the same as the value returned by `get_energy()`
assert rock.energy == rock.get_energy(), "rock: self.energy is not an attribute of the class or its value is different from what `get_energy()` returns"
assert paper.energy == paper.get_energy(), "paper: self.energy is not an attribute of the class or its value is different from what `get_energy()` returns"
assert scissors.energy == scissors.get_energy(), "rock: self.energy is not an attribute of the class or its value is different from what `get_energy()` returns"
# test 05 - make sure that the rgb colors are in a valid form
assert len(rock.get_rgb()) == 3, "The RGB color for `rock` should have 3 entries"
assert (rock.get_rgb()[0] >= 0.) and (rock.get_rgb()[0] <= 1.), "The R component ([0]) of the color for `rock` needs to be a value between 0 and 1"
assert (rock.get_rgb()[1] >= 0.) and (rock.get_rgb()[1] <= 1.), "The G component ([1]) of the color for `rock` needs to be a value between 0 and 1"
assert (rock.get_rgb()[2] >= 0.) and (rock.get_rgb()[2] <= 1.), "The B component ([2]) of the color for `rock` needs to be a value between 0 and 1"
assert len(paper.get_rgb()) == 3, "The RGB color for `paper` should have 3 entries"
assert (paper.get_rgb()[0] >= 0.) and (paper.get_rgb()[0] <= 1.), "The R component ([0]) of the color for `paper` needs to be a value between 0 and 1"
assert (paper.get_rgb()[1] >= 0.) and (paper.get_rgb()[1] <= 1.), "The G component ([1]) of the color for `paper` needs to be a value between 0 and 1"
assert (paper.get_rgb()[2] >= 0.) and (paper.get_rgb()[2] <= 1.), "The B component ([2]) of the color for `paper` needs to be a value between 0 and 1"
assert len(paper.get_rgb()) == 3, "The RGB color for `scissors` should have 3 entries"
assert (scissors.get_rgb()[0] >= 0.) and (scissors.get_rgb()[0] <= 1.), "The R component ([0]) of the color for `scissors` needs to be a value between 0 and 1"
assert (scissors.get_rgb()[1] >= 0.) and (scissors.get_rgb()[1] <= 1.), "The G component ([1]) of the color for `scissors` needs to be a value between 0 and 1"
assert (scissors.get_rgb()[2] >= 0.) and (scissors.get_rgb()[2] <= 1.), "The B component ([2]) of the color for `scissors` needs to be a value between 0 and 1"
# test 06 - make sure the colors are different
assert rock.get_rgb() != paper.get_rgb(), "Rock and Paper colors are the same!"
assert rock.get_rgb() != scissors.get_rgb(), "Rock and Scissors colors are the same!"
assert paper.get_rgb() != scissors.get_rgb(), "Paper and Scissors colors are the same!"
# test 07 - make sure get_kind_name() returns a string
assert isinstance(rock.get_kind_name(), str), "rock.get_kind_name() does not return a string"
assert isinstance(paper.get_kind_name(), str), "paper.get_kind_name() does not return a string"
assert isinstance(scissors.get_kind_name(), str), "scissors.get_kind_name() does not return a string"
# test 08 - make sure get_kind_name() return *different* strings
assert rock.get_kind_name() != paper.get_kind_name(), "Rock and Paper kind names are the same!"
assert rock.get_kind_name() != scissors.get_kind_name(), "Rock and Scissors kind names are the same!"
assert paper.get_kind_name() != scissors.get_kind_name(), "Paper and Scissors kind names are the same!"
# test 09 - make sure that the "rock paper scissors" rules are followed when calling can_eat()
assert not rock.can_eat(rock), "rock *can* eat rock"
assert not rock.can_eat(paper), "rock *can* eat paper"
assert rock.can_eat(scissors), "rock *can not* eat scissors"
assert paper.can_eat(rock), "paper *can not* eat rock"
assert not paper.can_eat(paper), "paper *can* eat paper"
assert not paper.can_eat(scissors), "paper *can* eat scissors"
assert not scissors.can_eat(rock), "scissors *can* eat rock"
assert scissors.can_eat(paper), "scissors *can not* eat paper"
assert not scissors.can_eat(scissors), "scissors *can* eat scissors"
# test 10 - make sure that clone() returns an object of the same kind
assert isinstance(rock.clone(), Rock_Animal), "rock.clone() does not return an instance of Rock_Animal"
assert isinstance(paper.clone(), Paper_Animal), "paper.clone() does not return an instance of Paper_Animal"
assert isinstance(scissors.clone(), Scissors_Animal), "scissors.clone() does not return an instance of Scissors_Animal"
# test 11 - make sure that the energy levels returned are correct (should be 25 now)
assert abs(rock.clone().get_energy() - 25.) < 1e-5, "The energy of the animal retruned by rock.clone() should be 25"
assert abs(paper.clone().get_energy() - 25.) < 1e-5, "The energy of the animal retruned by paper.clone() should be 25"
assert abs(scissors.clone().get_energy() - 25.) < 1e-5, "The energy of the animal retruned by scissors.clone() should be 25"
# If the code gets here, everything works as excepted
print("All checks passed, everything is working as expected.")
The cell above uses `assert` to check assumptions. In the following markdown cell:
1. Explain how `assert` works in python and how it can help make sure your code does the right thing. This is a new concept that you might have to do a bit of documentation reading and Google searching to understand. Remember, learning new skills independently is one of the skills we work to hone in 202!
2. Also explain why the last test (number 09) checks for an energy of 25 instead of 50. (The initial energy is 100 and `clone()` returns an object that has half of the energy of the initial object.) (10 pt)
Assert uses two arguments. The first one is a condition, and the second one is a message that will be displayed. Assert checks if the given coindition is True, and if it's now, it will display an error with the error-message being the message passed as the second argument. This function is very useful to test the code because is an assert fails, the execution of the code stops.
In thest **11**, the code checks if the animals has an energy of 25, because they have been cloned two times. The first time was in **test 10**, when checking that the clone function returns an animal of the same type. After this test, the energy of each animal is reduced to 50. Then, in **test 11**, the animals are cloned again, so their energy is reduced to 25.
The tests related to energy use `abs()` and a difference between the expected and returned value and check if it is smaller than a very small number. See if you can find a reason why it doesn't just use the `==` operator here. Explain the reason in the markdown cell below. (5 pt)
Because we are working with floating-point variables, and computers does not returns exact calculations. So, for example, checking if the resulting energy is equal to 25.0 could cause the test to fail, because in reality, the resulting energy could be something like 25.0000000001 which is different to 25.0. So, using the function **abs()** is better.
---
### &#128721; STOP
**Pause to commit your changes to your Git repository!**
Take a moment to save your notebook, commit the changes to your Git repository using the commit message "Three Animal kinds", and push the changes to GitHub.
---
---
## Part 4 Run the model (30 pt)
We will now define a class called `Environment` which will be used to run the simulation. This class is pre-defined and you will not need to change it. It makes use of the "animal" classes you wrote previously.
## Run this cell to define the Environment class
class Environment:
    def __init__(self, animal_kinds = [Rock_Animal, Paper_Animal, Scissors_Animal], num_animals_initial=1000, size_x=100, size_y=100, padding=20, inital_animal_energy=100):
        self.size_x = size_x
        self.size_y = size_y
        self.inital_animal_energy = inital_animal_energy
        self.animal_grid = [ ([None] * self.size_x) for row in range(self.size_y) ]
        self.population_history = []
        self.population_colors = dict()
        for i in range(0,num_animals_initial):
            # choose a random space on the grid
            place_x = random.randint(padding, self.size_x-1-padding)
            place_y = random.randint(padding, self.size_y-1-padding)
            # if it is not empty, keep choosing new random spaces until we find an empty one
            while self.has_animal(place_x, place_y):
                place_x = random.randint(padding, self.size_x-1-padding)
                place_y = random.randint(padding, self.size_y-1-padding)
            # now choose one of the animal classes provided
            Chosen_Animal_Class = random.choice(animal_kinds)
            # instantiate the class to create an object and add it to the grid
            new_animal = Chosen_Animal_Class(inital_animal_energy)
            self.add_animal(place_x, place_y, new_animal)
        self.update_population_history()
    def update_population_history(self):
        new_entry_dict = dict()
        # make entries for all previous kinds that might have existed but are maybe now extinct
        for h in self.population_history:
            for kind_name in h.keys():
                if kind_name not in new_entry_dict:
                    new_entry_dict[kind_name]=0
        for x in range(0,self.size_x):
            for y in range(0,self.size_y):
                if self.has_animal(x,y):
                    this_animal = self.get_animal(x,y)
                    kind_name = this_animal.get_kind_name()
                    if kind_name not in self.population_colors:
                        self.population_colors[kind_name] = this_animal.get_rgb()
                    if kind_name not in new_entry_dict:
                        new_entry_dict[kind_name]=0
                    new_entry_dict[kind_name]+=1
        self.population_history.append(new_entry_dict)
    def add_animal(self, x, y, animal):
        self.animal_grid[y][x] = animal
    def remove_animal(self, x, y):
        self.animal_grid[y][x] = None
    def has_animal(self, x, y):
        return self.animal_grid[y][x] is not None
    def get_animal(self, x, y):
        return self.animal_grid[y][x]
    def is_inside_of_environment(self, x, y):
        if x < 0 or x >= self.size_x: return False
        if y < 0 or y >= self.size_y: return False
        return True
    def number_total(self):
        num=0
        for x in range(0,self.size_x):
            for y in range(0,self.size_y):
                if self.has_animal(x,y):
                    num+=1
        return num
    def find_all_neighbors(self, x, y):
        neighbor_spots = []
        for other_x in [x-1, x, x+1]:
            for other_y in [y-1, y, y+1]:
                if self.is_inside_of_environment(other_x, other_y) and (not ((other_x==x) and (other_y==y))):
                    neighbor_spots.append( (other_x, other_y) )
        return neighbor_spots
    def life_cycle(self):
        # make sure to go through the animals in random order
        # this makes a shuffled list of animals on the grid in random order
        def shuffled_animal_list():
            animals = []
            for x in range(0,self.size_x):
                for y in range(0,self.size_y):
                    if self.has_animal(x,y):
                        the_animal = self.get_animal(x,y)
                        neighbor_spots = self.find_all_neighbors(x, y)
                        animals.append( (the_animal, neighbor_spots) )
            # shuffle the list and return it
            random.shuffle(animals)
            return animals
        # give all animals a chance to eat one of their neighbors
        animals = shuffled_animal_list()
        for the_animal, neighbor_spots in animals:
            the_animal.eat(self, neighbor_spots)
        # give all animals a chance to reproduce into empty spots next to them
        animals = shuffled_animal_list()
        for the_animal, neighbor_spots in animals:
            the_animal.reproduce(self, neighbor_spots)
        # update the statistics for plotting
        self.update_population_history()
    def draw_grid(self, ax):
        color_grid = np.zeros((self.size_x, self.size_y, 3))
        max_energy = self.inital_animal_energy
        for x in range(0,self.size_x):
            for y in range(0,self.size_y):
                this_rgb = (0.0, 0.0, 0.0)
                if self.has_animal(x,y):
                    the_animal = self.get_animal(x,y)
                    this_rgb = the_animal.get_rgb()
                    this_energy_mod = the_animal.get_energy()/max_energy
                    if this_energy_mod > 1.: this_energy_mod=1.
                    #this_rgb = ( this_rgb[0]*(this_energy_mod*0.8+0.2), this_rgb[1]*(this_energy_mod*0.8+0.2), this_rgb[2]*(this_energy_mod*0.8+0.2) )
                color_grid[x, y, 0] = this_rgb[0]
                color_grid[x, y, 1] = this_rgb[1]
                color_grid[x, y, 2] = this_rgb[2]
        ax.imshow(color_grid)
    def draw_population(self, ax):
        kinds = self.population_history[0].keys()
        for kind in kinds:
            this_population_array = []
            for history_entry in self.population_history:
                this_population_array.append(history_entry[kind])
            ax.plot(range(len(this_population_array)), this_population_array, label=kind, color=self.population_colors[kind])
        ax.set_ylabel("Population")
        ax.legend()
- This is a lot of code with sparse documentation. Let's try to run it first using the next cell. **Make sure this cell actually plots something.** (5pts)
env = Environment(animal_kinds = [Rock_Animal, Paper_Animal, Scissors_Animal],
        num_animals_initial=200,
        size_x=100, size_y=100,
        padding=15,
        inital_animal_energy=10000)
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111)
env.life_cycle()
env.draw_grid(ax)
- The output should show a plot like this:
<img src="https://i.ibb.co/M1sxN2m/example01.png" alt="Example Plot" border="0" width=300px>
We will now do a bit of code review of the `Environment()` class. **All of the following written answers need to be at least a few sentences long in order to receive full credit.**
**Explain what is shown in the plot.** (5 pt)
In the plot shown above, a set of different kind of animals are created in an environment. Each animal has a different color and that are the colors shown in the figure. Each dot is a different animal, where red dots are Rock Animals, Yellow dots are Paper Animals, and green dots are Scissors Animals.
Try to read and understand what the `Environment()` class does and how it works.
**In the next cell, explain what the `Environment` constructor does in some detail.** (You may need to remind yourself what part of the class the "constructor" is) (5 pt)
The Envinroment constructor created a grid of size (x,y), and also receives a list with the kind of animals availables. Then, given an initial number of animals (1000 by default), it creates 1000 random animals between Rock, Paper and Scissors animals. It then places then randomly in the grid, and finally, it recalculates the count for each kind of animal using the function update_population_history()
**Describe what the following 4 functions do, respectively: add_animal(), remove_animal(), has_animal(), get_animal().** (5 pt)
Again, make sure to provide the necessary detail here, including what their arguments are and what they do.
**add_animal()**: Given a position (x,y) and an Animal instance, it places the animal in the grid at the position (x,y)
**remove_animal()**: Given a position (x,y), it removes the Animal at that position from the grid.
**has_animal()**: Given a position (x,y), the function checks if there is an Animal object in the grid at that position, by checking the the value in that position is different to None. The functions returns True if there is an animal, or False if there is no animal in that position.
**get_animal()**: The function returns the object in the grid at the given position (x,y). If there is an Animal, it returns the Animal Object. If not, it returns None
**Describe what the `life_cycle()` method does.** (5 pt)
This function simulates one life cycle in the environment. First, it shuffles the animals in the grid. Then, for each animal (let's call this Animal **x**), it checks the neighboring positions of the animal, and if there are animals that **x** can eat (let's call this animal **y**), then **x** eats **y**.
Then, the function shuffles the animals again. It iterates through the animals again, and for each animal **x**, the function calls the **reproduce** function, making the animal reproduce, creating a new animal with half the energy and placing it into the grid.
**Explain what the `update_population_history()` tries to achieve and how it does its job.** (5 pt)
This function iterates through the animals and counts how many of each kind are in the grid. The function does this by creating a dictionary for each kind of animal. The key for this dictionary if the 'kind name' and the value is a counter, initialized at zero. Then, for each animal of that kind, the counter is increased by 1.
**Finally, describe if the simulation step performed will actually work in the current version of your code. If it does not, explain what is missing.** (5 pt)
It won't because the **eat** and **reproduce** functions are not added. So, the code will just simulate life cycles without changes.
---
### &#128721; STOP
**Pause to commit your changes to your Git repository!**
Take a moment to save your notebook, commit the changes to your Git repository using the commit message "Animals in an environment", and push the changes to GitHub.
---
---
## Part 5 Finalize the `Animal` class (25 pt)
Here, we will implement the two missing stub functions of the `Animal` class: `eat()` and `reproduce()`
```python
    def eat(self, neighbor_spots):
        pass # will be implemented later
    def reproduce(self, neighbor_spots):
        pass # will be implemented later
```
Replace the `eat()` stub function with this code:
```python
    def eat(self, env, neighbor_spots):
        # select only the neighboring spots that contain animals we can actually eat
        neighboring_spots_with_animals_we_can_eat = []
        for spot in neighbor_spots:
            if env.has_animal(spot[0],spot[1]):
                # only look if it has an animal, then get the other animal
                other_animal = env.get_animal(spot[0],spot[1])
                if self.can_eat(other_animal):
                    neighboring_spots_with_animals_we_can_eat.append( spot )
        if len(neighboring_spots_with_animals_we_can_eat)==0:
            # no animals there.. cannot eat
            return
        # an animal I can eat! eat it and gain its energy - Highlander rules
        # choose a spot randomly
        chosen_spot = random.choice(neighboring_spots_with_animals_we_can_eat)
        # get the other animal and its energy
        # TODO: << ... retrieve the "other" animal using `env.get_animal()` from the x-y coordinates given in `chosen_spot`, get its energy and store it in the variable `other_energy` ... >>
        # eat the animal!
        # TODO: << ... use the appropriate member function of the `Environment` `env` in order to remove the other animal in the spot given by `chosen_spot` ... >>
        # increase our own energy
        # TODO: << ... make sure to increase our own energy by the amount stored in `other_energy` ... >>
```
Replace the `reproduce()` stub function with this code:
```python
    def reproduce(self, env, neighbor_spots):
        # can we reproduce? only possible if energy > 1
        if self.energy <= 1:
            return
        # find an empty neighboring spot to reproduce to
        # make a list of neighboring spots as (x,y) tuples
        open_neighbor_spots = []
        for spot in neighbor_spots:
            if not env.has_animal(spot[0],spot[1]):
                # if it doesn't have an animal in it already, it is available!
                open_neighbor_spots.append( spot )
        if len(open_neighbor_spots)==0:
            # no space left, we cannot reproduce
            return
        # spend one unit of energy in order to reproduce
        # TODO: << ... Lower our own energy level by `1` ... >>
        # choose one of the spots
        the_neighboring_spot = random.choice(open_neighbor_spots)
        # make a new animal by cloning ourself
        # TODO: << ... use our own `clone()` method to create a copy (and split our energy in half). Store that clone in a variable called `the_new_animal` ... >>
        # TODO: << ... Use the correct member function of the `Environment` class given in `env` to add the cloned animal (`the_new_animal`) to the environment at the x-y coordinates given by `the_neighboring_spot` ... >>
```
**IMPORTANT NOTE**: The code given above requires you to add some pieces at the points indicated by `TODO`.
**Paste the previous version of `Animal` from the top of the notebook into the cell below, add the two implementations of the stub functions `eat()` and `reproduce()` given above and write the missing code replacing the `TODO` comment lines.** (15 pt)
## In this cell, paste the previous version of `Animal` from the top of the notebook into the cell below,
## add the two implementations of the stub functions `eat()` and `reproduce()` given above and finish the code by
## replacing the `TODO` comment lines with the missing parts of the implementation.
class Animal:
    def __init__(self, starting_energy = 100):
        self.energy = starting_energy
    def get_energy(self):
        return self.energy
    def get_rgb(self):
        pass # to be implemented by derived classes
    def get_kind_name(self):
        pass # to be implemented by derived classes
    def can_eat(self, other_animal):
        pass # to be implemented by derived classes
    def clone(self):
        pass # to be implemented by derived classes
    def eat(self, env, neighbor_spots):
        # select only the neighboring spots that contain animals we can actually eat
        neighboring_spots_with_animals_we_can_eat = []
        for spot in neighbor_spots:
            if env.has_animal(spot[0],spot[1]):
                # only look if it has an animal, then get the other animal
                other_animal = env.get_animal(spot[0],spot[1])
                if self.can_eat(other_animal):
                    neighboring_spots_with_animals_we_can_eat.append( spot )
        if len(neighboring_spots_with_animals_we_can_eat)==0:
            # no animals there.. cannot eat
            return
        # an animal I can eat! eat it and gain its energy - Highlander rules
        # choose a spot randomly
        chosen_spot = random.choice(neighboring_spots_with_animals_we_can_eat)
        # get the other animal and its energy
        # TODO: << ... retrieve the "other" animal using `env.get_animal()` from the x-y coordinates given in `chosen_spot`, get its energy and store it in the variable `other_energy` ... >>
        other_animal = env.get_animal(chosen_spot[0], chosen_spot[1])
        other_energy = other_animal.get_energy()
        # eat the animal!
        # TODO: << ... use the appropriate member function of the `Environment` `env` in order to remove the other animal in the spot given by `chosen_spot` ... >>
        env.remove_animal(chosen_spot[0], chosen_spot[1])
        # increase our own energy
        # TODO: << ... make sure to increase our own energy by the amount stored in `other_energy` ... >>
        self.energy += other_energy
    def reproduce(self, env, neighbor_spots):
        # can we reproduce? only possible if energy > 1
        if self.energy <= 1:
            return
        # find an empty neighboring spot to reproduce to
        # make a list of neighboring spots as (x,y) tuples
        open_neighbor_spots = []
        for spot in neighbor_spots:
            if not env.has_animal(spot[0],spot[1]):
                # if it doesn't have an animal in it already, it is available!
                open_neighbor_spots.append( spot )
        if len(open_neighbor_spots)==0:
            # no space left, we cannot reproduce
            return
        # spend one unit of energy in order to reproduce
        # TODO: << ... Lower our own energy level by `1` ... >>
        self.energy -= 1
        # choose one of the spots
        the_neighboring_spot = random.choice(open_neighbor_spots)
        # make a new animal by cloning ourself
        # TODO: << ... use our own `clone()` method to create a copy (and split our energy in half). Store that clone in a variable called `the_new_animal` ... >>
        the_new_animal = self.clone()
        # TODO: << ... Use the correct member function of the `Environment` class given in `env` to add the cloned animal (`the_new_animal`) to the environment at the x-y coordinates given by `the_neighboring_spot` ... >>
        env.add_animal(the_neighboring_spot[0], the_neighboring_spot[1], the_new_animal)
### IMPLEMENT THIS: Add code here to create an Animal object called `animal` here and set its initial energy to 100.
animal = Animal(100)
**You now NEED to re-define the derived classes, since their base class has changed. Copy&paste your previous versions of `Rock_Animal`, `Paper_Animal`, and `Scissors_Animal` in the cell below. No changes to the derived classes should be necessary.**
### You now NEED to re-define the derived classes, since their base class has changed.
### Copy&paste your previous versions of `Rock_Animal`, `Paper_Animal`, and `Scissors_Animal`
### in the cell below. No changes to the derived classes should be necessary.
### put your code of creating the derived classes `Paper_Animal` and `Scissors_Animal` in this cell (10 pt)
class Rock_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (1.0, 0, 0) # red
  def get_kind_name(self):
    return "rock"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors then this can eat it
    return other_animal.get_kind_name() == "scissors"
  def clone(self):
    new_animal = Rock_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
class Paper_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (252.0/255.0, 219.0/255.0, 3.0/255.0) # yellow
  def get_kind_name(self):
    return "paper"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors then this can eat it
    return other_animal.get_kind_name() == "rock"
  def clone(self):
    new_animal = Paper_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
class Scissors_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (3.0/255.0, 157.0/255.0, 252.0/255.0) # blue
  def get_kind_name(self):
    return "scissors"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors then this can eat it
    return other_animal.get_kind_name() == "paper"
  def clone(self):
    new_animal = Scissors_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
**At this point, you should be able to run the full simulation of your environment. Run the cell below to see a simulation using 5 "life cycle" time steps. Make sure your code is working correctly and the simulation keeps updating.** (10pt)
env = Environment(
        animal_kinds = [Rock_Animal, Paper_Animal, Scissors_Animal],
        num_animals_initial=200,
        size_x=100, size_y=100,
        padding=15,
        inital_animal_energy=10000)
## iterate over 5 time steps
for d in range(5):
    clear_output(wait=True)
    fig = plt.figure(figsize=(16, 8))
    ax = fig.add_subplot(121)
    bx = fig.add_subplot(122)
    ## call the roaming method and then draw here
    env.life_cycle()
    env.draw_grid(ax)
    env.draw_population(bx)
    plt.show()
    time.sleep(0.001)
Your output should look somewhat like this:
<img src="https://i.ibb.co/4mKY9K3/Expected-Output-5.png" alt="Example Plot" border="0" width=600px>
**Congratulations, you now have a fully working simulation!**
---
### &#128721; STOP
**Pause to commit your changes to your Git repository!**
Take a moment to save your notebook, commit the changes to your Git repository using the commit message "Full Simulation", and push the changes to GitHub.
---
---
## Part 6. Running experiments. (10 pt)
- You will now run a couple of experiments using the existing code.
- **First, copy the previous code cell and make it run for more than 5 steps (start with around 100-200 steps, increase the number if necessary).** (5pt)
### your code here
env = Environment(
        animal_kinds = [Rock_Animal, Paper_Animal, Scissors_Animal],
        num_animals_initial=200,
        size_x=100, size_y=100,
        padding=15,
        inital_animal_energy=10000)
# <.... add the remaining code here ....>
## iterate over 5 time steps
for d in range(200):
    clear_output(wait=True)
    fig = plt.figure(figsize=(16, 8))
    ax = fig.add_subplot(121)
    bx = fig.add_subplot(122)
    ## call the roaming method and then draw here
    env.life_cycle()
    env.draw_grid(ax)
    env.draw_population(bx)
    plt.show()
    time.sleep(0.001)
- **Run the model several times as-is. Then in a few more runs try to experiment with the settings (try `num_animals_initial` and `inital_animal_energy` first)**
- **Summarize the observation in your words.** What do you observe? How do the three species interact with each other? What effect (if any) do the settings mentioned above have? (5 pt)
From what it can be seen in the figures, the populations of the three animals oscillates because they keep eating others and also reproducing.
---
### &#128721; STOP
**Pause to commit your changes to your Git repository!**
Take a moment to save your notebook, commit the changes to your Git repository using the commit message "Running experiments", and push the changes to GitHub.
---
---
## Part 7. Possible improvement. (20 pt) - EXTRA CREDIT [The total assignment score is capped at 105%]
<img src="https://i.ibb.co/F6Nkzqv/RPSLS.webp" width=300px align='left' style="margin-right: 20px" >
- **Try to improve your code by making 5 instead of 3 animal species.** Make them follow the rules of "Rock-Paper-Scissors-Lizard-Spock" (pictured the the left). The arrows show which sign beats which other sign. Your animal species should be able to set up accordingly. Try to implement new versions of the respective classes derived from animal and run a simulation with five instead of three species.
- (Note: This game variant is also described on the [Rock Paper Scissors Wikipedia page](https://en.wikipedia.org/wiki/Rock_paper_scissors#Additional_weapons) under the "Additional weapons" heading.)
**Implement the 5 animal classes `Rock_Animal, Paper_Animal, Scissors_Animal, Lizard_Animal, Spock_Animal`, then initialize the environment below like this:**
```python
    env = Environment(
        animal_kinds = [Rock_Animal, Paper_Animal, Scissors_Animal, Lizard_Animal, Spock_Animal],
        num_animals_initial=200,
        size_x=100, size_y=100,
        padding=15,
        inital_animal_energy=10000)
```
## Add your code here (10 pt)
class Rock_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (1.0, 0, 0) # red
  def get_kind_name(self):
    return "rock"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors or lizard then this can eat it
    return other_animal.get_kind_name() == "scissors" or other_animal.get_kind_name() == "lizard"
  def clone(self):
    new_animal = Rock_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
class Paper_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (252.0/255.0, 219.0/255.0, 3.0/255.0) # yellow
  def get_kind_name(self):
    return "paper"
  def can_eat(self, other_animal):
    # If the other_animal name is rock or spock then this can eat it
    return other_animal.get_kind_name() == "rock" or other_animal.get_kind_name() == "spock"
  def clone(self):
    new_animal = Paper_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
class Scissors_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (3.0/255.0, 157.0/255.0, 252.0/255.0) # blue
  def get_kind_name(self):
    return "scissors"
  def can_eat(self, other_animal):
    # If the other_animal name is paper or lizard then this can eat it
    return other_animal.get_kind_name() == "paper" or other_animal.get_kind_name() == "lizard"
  def clone(self):
    new_animal = Scissors_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
class Lizard_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (9.0/255.0, 219.0/255.0, 2.0/255.0) # green
  def get_kind_name(self):
    return "lizard"
  def can_eat(self, other_animal):
    # If the other_animal name is paper or spock then this can eat it
    return other_animal.get_kind_name() == "paper" or other_animal.get_kind_name() == "spock"
  def clone(self):
    new_animal = Lizard_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
class Spock_Animal(Animal):
  def __init__(self, energy):
    super().__init__(energy)
  def get_rgb(self):
    return (219.0/255.0, 2.0/255.0, 158.0/255.0) # purple
  def get_kind_name(self):
    return "spock"
  def can_eat(self, other_animal):
    # If the other_animal name is scissors or rock then this can eat it
    return other_animal.get_kind_name() == "scissors" or other_animal.get_kind_name() == "rock"
  def clone(self):
    new_animal = Spock_Animal(self.get_energy()/2)
    self.energy /= 2
    return new_animal
- Run the model for around 100-200 steps and see what happens. Re-run it a few times. How do the results differ from 3-species version? (5 pt)
# Run the model for around 100-200 steps and see what happens. Re-run it a few times. Put your code here.
env = Environment(
        animal_kinds = [Rock_Animal, Paper_Animal, Scissors_Animal, Lizard_Animal, Spock_Animal],
        num_animals_initial=200,
        size_x=100, size_y=100,
        padding=15,
        inital_animal_energy=10000)
## iterate over 5 time steps
for d in range(200):
    clear_output(wait=True)
    fig = plt.figure(figsize=(16, 8))
    ax = fig.add_subplot(121)
    bx = fig.add_subplot(122)
    ## call the roaming method and then draw here
    env.life_cycle()
    env.draw_grid(ax)
    env.draw_population(bx)
    plt.show()
    time.sleep(0.001)
Similar to what happened with three kind of animals, the populations oscillates.
- Run one model for even more steps (around 500 to 1000 more steps depending on how long you are willing to watch it for). Describe if there are any additional observations. (5 pt)
# Run one model for even more steps (around 500 to 1000 depending on how long you are willing to watch it for). Put your code here.
# Run the model for around 100-200 steps and see what happens. Re-run it a few times. Put your code here.
env = Environment(
        animal_kinds = [Rock_Animal, Paper_Animal, Scissors_Animal, Lizard_Animal, Spock_Animal],
        num_animals_initial=200,
        size_x=100, size_y=100,
        padding=15,
        inital_animal_energy=10000)
## iterate over 5 time steps
for d in range(1000):
    clear_output(wait=True)
    fig = plt.figure(figsize=(16, 8))
    ax = fig.add_subplot(121)
    bx = fig.add_subplot(122)
    ## call the roaming method and then draw here
    env.life_cycle()
    env.draw_grid(ax)
    env.draw_population(bx)
    plt.show()
    time.sleep(0.001)
This case is very interesting, because it can be seen that for a large number of steps, the populations oscillates but in the end, the populations stabilizes.
---
### &#128721; STOP
**Pause to commit your changes to your Git repository!**
Take a moment to save your notebook, commit the changes to your Git repository using the commit message "Assignment complete", and push the changes to GitHub.
---
---
## Assignment wrap-up
Please fill out the form that appears when you run the code below. **You must completely fill this out in order to receive credit for the assignment!**
from IPython.display import HTML
HTML(
"""
<iframe
 src="https://forms.office.com/Pages/ResponsePage.aspx?id=MHEXIi9k2UGSEXQjetVofddd5T-Pwn1DlT6_yoCyuCFUOEhVVUZBVklNQ1NBUFhKNFNHWkpSMFZIQS4u"
 width="800px"
 height="600px"
 frameborder="0"
 marginheight="0"
 marginwidth="0">
 Loading...
</iframe>
"""
)
### Congratulations, you're done!
Submit this assignment by uploading it to the course Desire2Learn web page. Go to the "Homework Assignments" folder, find the dropbox link for Homework #3, and upload it there.
&#169; Copyright 2021, Department of Computational Mathematics, Science and Engineering at Michigan State University