# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# + nbgrader={"checksum": "167dd6e1b2af5215e1d635028ac7213a", "grade": false, "grade_id": "cell-528ea9078565a6e1", "locked": true, "schema_version": 3, "solution": false, "task": false}
import csv
import json
import numpy as np
import utils

# + [markdown] deletable=false editable=false nbgrader={"checksum": "4367147b7cbcd58e93faeb3315a227b5", "grade": false, "grade_id": "cell-2f73855a3cbbf843", "locked": true, "schema_version": 3, "solution": false, "task": false}
# ### Exercise 1 (5 points)
#
# This exercise is a quiz with 14 multiple answer questions.
# In the cell below, we've declared a dictionary called `answers`.
#
# You should fill in that dictionary with your answers, using as keys the question number, like `question_x`, and as values the number from `1` to `4` that corresponds to the right answer.
# For each question, you should provide only one answer, i.e, the dict values should have type **int**.
#
# For example, if you want to answer Question 1 with choice number 2, then you do:
# ```
# answers["question_1"] = 2
# ```

# + deletable=false editable=false nbgrader={"checksum": "72eed0037d7c74f44f0586fa81aa98e1", "grade": false, "grade_id": "cell-845a4d3a7c6fd757", "locked": true, "schema_version": 3, "solution": false, "task": false}
answers = {}

# + [markdown] deletable=false editable=false nbgrader={"checksum": "3735e069c0527e617991de7ab227e666", "grade": false, "grade_id": "cell-4fee136a9be985d7", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 1
# In the terminal of a Unix based machine, which of these commands will take you to your home directory?
# 1. cd ..
# 2. ls ..
# 3. cd ~/
# 4. ls ~/

# + deletable=false nbgrader={"checksum": "a8005de2fcfd4047eee7c283923dc148", "grade": false, "grade_id": "cell-98b6eeed9f5eb544", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE

### BEGIN SOLUTION
answers["question_1"] = 3
### END SOLUTION

# answers["question_1"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "4d244463aed3c7cf08b11967d83befdd", "grade": false, "grade_id": "cell-844accc86543007c", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 2
# In the terminal of a Unix based machine, which of these commands would you use to see the entire content of a file named `data.csv`?
# 1. head -5 data.csv
# 2. tail -5 data.csv
# 3. cat data.csv
# 4. ls -la data.csv

# + deletable=false nbgrader={"checksum": "ba078e820e63715f12ab2cf278194d6b", "grade": false, "grade_id": "cell-f75169d418f280ba", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_2"] = 3
### END SOLUTION
# answers["question_2"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "ba7a3134a6de6971ba42fdd057736e3b", "grade": false, "grade_id": "cell-740c06fb3ff91e5a", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 3
# After running one of these commands in the terminal of a Unix based machine, you won't have a file named `data.csv` in your current directory. Which command is it?
# 1. cp dataset.csv data.csv
# 2. rm data.csv
# 3. touch data.csv
# 4. mv dataset.csv data.csv

# + deletable=false nbgrader={"checksum": "377bbdec02c2443067a9b40c872fe290", "grade": false, "grade_id": "cell-d0ee1b32bb06368b", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_3"] = 2
### END SOLUTION

# answers["question_3"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "11d55d9930a5f968393c74cd97486696", "grade": false, "grade_id": "cell-d057502c6df7557e", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 4
# In the terminal of a Unix based machine, if you want to see the size of the files in the current directory, in a human readable format, which command is the best option?
# 1. ls
# 2. ls .
# 3. ls -la
# 4. ls -lh

# + deletable=false nbgrader={"checksum": "845012ef9609a5595a5d2bc6c209646c", "grade": false, "grade_id": "cell-b48ab5ac85e1bf69", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_4"] = 4
### END SOLUTION

# answers["question_4"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "370a4e4f96d928b7dab1ba1040b08928", "grade": false, "grade_id": "cell-091fe9ea7062af04", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 5
# There are certain types of files that you usually don't want to have in your git repositories, like cache files, or local configuration files that contain passwords. In order to have git not tracking these files, you can use the `.gitignore` file. Say you're currently in the root directory of your project, and you have a `.gitignore` file there. In the terminal of a Unix based machine, which of these commands should you use to list the files and see the `.gitignore` file on that list?
# 1. ls -la
# 2. ls .
# 3. ls -lh
# 4. ls -l

# + deletable=false nbgrader={"checksum": "48b6852b18ec10a985646dc15592c640", "grade": false, "grade_id": "cell-d53190f9ce4dedbe", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_5"] = 1
### END SOLUTION

# answers["question_5"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "61a587e80919d4a08fadc1ae5e98335f", "grade": false, "grade_id": "cell-efd153569f17eddc", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 6
# Consider the following git commands: `add`, `commit`, `status`, `pull`, `push`. Which is the most natural order to use them?
# 1. `pull` > `status` > `add` > `commit` > `push`
# 2. `status` > `commit` > `add` > `push` > `pull`
# 3. `push` > `add` > `status` > `commit` > `pull`
# 4. `pull` > `push` > `status` > `add` > `commit`

# + deletable=false nbgrader={"checksum": "a8979a8b2f20d6537de58d139a9931a2", "grade": false, "grade_id": "cell-44e7a38840f54f6c", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_6"] = 1
### END SOLUTION

# answers["question_6"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "04f1d2db9642f0748de053e1101f294e", "grade": false, "grade_id": "cell-c6c87fce1a31df28", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 7
# How do you change from the `master` branch to an existing `feature_dev` branch?
# 1. git checkout master
# 2. git checkout master feature_dev
# 3. git checkout -b feature_dev
# 4. git checkout feature_dev

# + deletable=false nbgrader={"checksum": "5fb0f4110d3cef129586777f60ce66b8", "grade": false, "grade_id": "cell-8f6345404f52ff67", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_7"] = 4
### END SOLUTION

# answers["question_7"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "652f57189a01dc5daee038d46b5800bb", "grade": false, "grade_id": "cell-400e482351eb011b", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 8
# Which of these commands should you use to find the hash of your previous commit?
# 1. git status
# 2. git log
# 3. git diff
# 4. git commit

# + deletable=false nbgrader={"checksum": "3bbc257f2b710438a5dee93f9c0a8a3d", "grade": false, "grade_id": "cell-67e3948e70b7e795", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_8"] = 2
### END SOLUTION

# answers["question_8"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "6c8d4e098816923277affd0a3b685496", "grade": false, "grade_id": "cell-789c29332373bbf7", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 9
# Which one of these is false:
# 1. You can use a tuple as a dictionary key.
# 2. You can add new elements to a tuple.
# 3. Tuples can have duplicated elements.
# 4. You can define a tuple with length 0.

# + deletable=false nbgrader={"checksum": "2b30872058c164c070e6649be30a029d", "grade": false, "grade_id": "cell-fd120313a3617c0f", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_9"] = 2
### END SOLUTION

# answers["question_9"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "cdf4dd23861044666949995370cb5483", "grade": false, "grade_id": "cell-dc30f487d5bed6b9", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 10
# If you want to incorporate the master branch into another branch called `dev`, which 2 commands should you run sequentially?
# 1. git checkout dev; git merge master
# 2. git merge master; git checkout dev
# 3. git checkout master; git merge dev
# 4. git merge dev; git checkout master

# + deletable=false nbgrader={"checksum": "7e4341b5628f92bc8d4a575eaf5373c6", "grade": false, "grade_id": "cell-03d49ee095b85596", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_10"] = 1
### END SOLUTION

# answers["question_10"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "72a9ca4cf9d162680806727c38a814a9", "grade": false, "grade_id": "cell-2ff278821108b8dc", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 11
# Which command should you use to bring back changes that you've stashed?
# 1. git stash
# 2. git stash pop
# 3. git revert
# 4. git revert apply

# + deletable=false nbgrader={"checksum": "ddeb5592f2b68aa14ccf500f8a84e406", "grade": false, "grade_id": "cell-3753c7fa82eba09c", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_11"] = 2
### END SOLUTION

# answers["question_11"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "1cf065a048e26b949cd95a75f7692465", "grade": false, "grade_id": "cell-1bd5d5dc872d9d60", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 12
# Which one of these files will you always find in the root directory of a Python package?
# 1. README.md
# 2. requirements.txt
# 3. \_\_init\_\_.py
# 4. setup.py

# + deletable=false nbgrader={"checksum": "f4677db1fa108269c15bc72f30562716", "grade": false, "grade_id": "cell-6c9b515dc4382694", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_12"] = 3
### END SOLUTION

# answers["question_12"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "c986764ebb3be36b2f687150dff854c7", "grade": false, "grade_id": "cell-0bc5861937b3cb7d", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 13
# How do you import function `my_function` from module `my_module`?
# 1. import my_function
# 2. import my_module
# 3. from my_function import my_module
# 4. from my_module import my_function

# + deletable=false nbgrader={"checksum": "0022a01f01eb96bdcbc2ff7e3f0b2822", "grade": false, "grade_id": "cell-63f33fece990776a", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_13"] = 4
### END SOLUTION

# answers["question_13"] = ...

# + [markdown] deletable=false editable=false nbgrader={"checksum": "603a53e60228a5db2f59ded6afe92199", "grade": false, "grade_id": "cell-783753cba7a2dd2c", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Question 14
# Which one of these should not raise an Error?
# 1. '4' + 'None'
# 2. 4 + None
# 3. 4 + 'None'
# 4. '4' + None

# + deletable=false nbgrader={"checksum": "3c39c91b708926bf459f1b8ef068a801", "grade": false, "grade_id": "cell-81aca48ed55e858b", "locked": false, "schema_version": 3, "solution": true, "task": false}
# YOUR CODE HERE
### BEGIN SOLUTION
answers["question_14"] = 1
### END SOLUTION

# answers["question_14"] = ...

# + nbgrader={"grade": true, "grade_id": "cell-db5effd691135306", "locked": true, "points": 5, "schema_version": 3, "solution": false, "task": false}
# Run this cell to see if your answers are correct
utils.exercise_1(answers)


# + [markdown] deletable=false editable=false nbgrader={"checksum": "5a1644f5d02c318fa290091199a6277d", "grade": false, "grade_id": "cell-bc7df0ba9c5df93a", "locked": true, "schema_version": 3, "solution": false, "task": false}
# ### Exercise 2
#
# Consider a csv file with four columns: `city`, `latitude`, `longitude`, `country`.
#
# File `cities_locations.csv` is an example of such file and was included in the zip file you downloaded with this notebook, so you can preview it using some command line arguments.

# + [markdown] deletable=false editable=false nbgrader={"checksum": "7352381658a9e3baf1c0f4641e31d323", "grade": false, "grade_id": "cell-8589bdd23fda251c", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Part I (2 points)
#
# Implement a function that reads a file with the same format as the `cities_locations.csv` file, and stores the data in a dictionary with the following structure:
#
# ```
# {
#     "city_name": {
#         "idx": row number in the input file, not considering the header,
#                i.e, the first row with data values has idx=0 (type: int),
#         "lat": latitude value (type: float),
#         "lng": longitude value (type: float),
#         "country": country (type: str)
#     },
# }
#
# ```
#
# The function should be called `read_cities`, receives an argument called `filepath` (which is the file that the function should read the data from), and returns the dictionary that was created.

# + deletable=false nbgrader={"checksum": "1b9739d71d69631d1125cbc0ea142034", "grade": false, "grade_id": "cell-622c6adc26159244", "locked": false, "schema_version": 3, "solution": true, "task": false}
def read_cities(filepath):
    """
    Reads the file in filepath, parses it and returns the data in a dictionary.

    Parameters:
    filepath (str): Path to the input file to be parsed.

    Returns:
    cities_parsed_data (dict): Cities locations dataset stored as a dictionary, where the keys are city names
          and the values are data about the city.
    """
    
    # YOUR CODE HERE
### BEGIN SOLUTION
    with open(filepath, 'r') as f:
        reader = csv.reader(f, delimiter=',', quotechar='"')

        next(reader)

        data = {}    
        for i, line in enumerate(reader):
            data[line[0]] = {
                "idx": i,
                "lat": float(line[1]),
                "lng": float(line[2]),
                "country": line[3]
            }
    
    return data
### END SOLUTION


# + deletable=false editable=false nbgrader={"checksum": "92c2f0803719ff7b5e39576ce6f44188", "grade": true, "grade_id": "cell-6c24a4ad6f30ce55", "locked": true, "points": 2, "schema_version": 3, "solution": false, "task": false}
cities_locations_data = read_cities('cities_locations.csv')

assert len(cities_locations_data) == 10

assert 'Barcelona' in cities_locations_data
barcelona = cities_locations_data['Barcelona']
assert barcelona['idx'] == 5
assert barcelona['country'] == 'Spain'
np.testing.assert_almost_equal(barcelona['lat'], 41.3833, decimal=3)
np.testing.assert_almost_equal(barcelona['lng'], 2.1834, decimal=3)


from more_tests import test_exercise_2_I
test_exercise_2_I(read_cities)

# + [markdown] deletable=false editable=false nbgrader={"checksum": "6e5eefb2c73bcfeffa3d74a938d4f5be", "grade": false, "grade_id": "cell-84df9405332848ae", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Part II (3 points)
#
# We want to calculate the distance in km between each pair of cities from our dataset, and store this in a numpy array.
#
# In order to calculate these distances, you should use the **geodesic** function from the python package [geopy](https://pypi.org/project/geopy/).
#
# You'll need to read the package's docs, in order to install it in your local environment and to learn how to use the **geodesic** function. The version of geopy that is installed in our grader is `1.22.0`, so, to make sure that you'll have the same behaviour in your local environment and in our grader, make sure to install the same version.
#
# The matrix with the distances will have shape `(n_cities, n_cities)`, where `n_cities` is the number of cities in the dataset. Each city's position in the matrix's rows and columns should be the `idx` value from the dataset. 
#
# Implement a function called `create_dist_matrix` that receives as input a dictionary like the one we created in the first part of this exercise, and returns the numpy array representing the distances matrix.

# + deletable=false nbgrader={"checksum": "37419e5b69eba275af5f61e54dccd280", "grade": false, "grade_id": "cell-7854488ee93d67b6", "locked": false, "schema_version": 3, "solution": true, "task": false}
from geopy.distance import geodesic

def create_dist_matrix(cities_data):
    """
    Computes the geodesic distance between each pair of cities in the input dataset, and returns a matrix with the
    computed distances.

    Parameters:
    cities_data (dict): Dictionary with the city locations dataset.

    Returns:
    matrix (np.array): Matrix with shape (n_cities, n_cities) where the value in position (i, j) represents
        the geodesic distance in km between city_i and city_j.
    """
    
    # YOUR CODE HERE
### BEGIN SOLUTION
    n_cities = len(cities_data)
    dists = np.zeros((n_cities, n_cities))
    
    for city_i, data_i in cities_data.items():
        city_i_coords = (data_i['lat'], data_i['lng'])
        city_i_idx = data_i['idx']
        
        for city_j, data_j in cities_data.items():
            city_j_coords = (data_j['lat'], data_j['lng'])
            city_j_idx = data_j['idx']
            
            dist = geodesic(city_i_coords, city_j_coords).km
            dists[city_i_idx][city_j_idx] = dist
    
    return dists
### END SOLUTION


# + deletable=false editable=false nbgrader={"checksum": "048b6646312fa95b80ecaeed371ead78", "grade": true, "grade_id": "cell-56eb4a74f3544d03", "locked": true, "points": 3, "schema_version": 3, "solution": false, "task": false}
cities_locations_data = {
    'Rome': {'idx': 0, 'lat': 41.896, 'lng': 12.4833, 'country': 'Italy'},
    'Milan': {'idx': 1, 'lat': 45.47, 'lng': 9.205, 'country': 'Italy'},
    'Lisbon': {'idx': 2, 'lat': 38.7227, 'lng': -9.1449, 'country': 'Portugal'},
    'Porto': {'idx': 3, 'lat': 41.15, 'lng': -8.62, 'country': 'Portugal'},
    'Madrid': {'idx': 4, 'lat': 40.4, 'lng': -3.6834, 'country': 'Spain'},
    'Barcelona': {'idx': 5, 'lat': 41.3833, 'lng': 2.1834, 'country': 'Spain'}
}

dists = create_dist_matrix(cities_locations_data)

assert type(dists) == np.ndarray
assert dists.shape == (6, 6)

for i in range(dists.shape[0]):
    assert dists[i][i] == 0
    
np.testing.assert_almost_equal(dists[0][1], 476.93, decimal=1)
np.testing.assert_almost_equal(dists[0][5], 859.51, decimal=1)


from more_tests import test_exercise_2_II
test_exercise_2_II(create_dist_matrix)


# + [markdown] deletable=false editable=false nbgrader={"checksum": "e25aefb79021a3999b2ca66fb2418175", "grade": false, "grade_id": "cell-4ba93c4331a3e31c", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Part III (2 points)
#
# Create a function called `furthest_cities` that finds in our distances matrix, the pair of cities furthest away from each other.
#
# It should return a tuple of city indexes, where the first one is smaller than the second.

# + deletable=false nbgrader={"checksum": "f8ccfa4089fca0a46420a9b1bc78d915", "grade": false, "grade_id": "cell-29b0e001d3a16b34", "locked": false, "schema_version": 3, "solution": true, "task": false}
def furthest_cities(dist_matrix):
    """
    Finds the pair of cities that is furthest away from each other.

    Parameters:
    dist_matrix (np.array): Matrix that stores geodesic distances between cities.

    Returns:
    furthest_cities_idx (tuple): Indexes of the two cities. The first element in the tuple is the lower index. 
    """
    
    # YOUR CODE HERE
### BEGIN SOLUTION
    idx_1, idx_2 = sorted(np.argwhere(dist_matrix == dist_matrix.max())[0])
    return idx_1, idx_2
### END SOLUTION


# + deletable=false editable=false nbgrader={"checksum": "9de5194fdb6dff83f452eea6e3b9204e", "grade": true, "grade_id": "cell-7a082faf67626b85", "locked": true, "points": 2, "schema_version": 3, "solution": false, "task": false}
# Creating a random distances matrix for this test
np.random.seed(19)
dists = np.random.rand(5, 5) * 1000
for i in range(dists.shape[0]):
    dists[i][i] = 0
    for j in range(dists.shape[1]):
        if j > i:
            dists[i][j] = dists[j][i]

idx_1, idx_2 = furthest_cities(dists)
assert idx_1 == 1
assert idx_2 == 4


from more_tests import test_exercise_2_III
test_exercise_2_III(furthest_cities)


# + [markdown] deletable=false editable=false nbgrader={"checksum": "d4188a43cc2c4d05258b5eab9d07f937", "grade": false, "grade_id": "cell-17a275d699bf540a", "locked": true, "schema_version": 3, "solution": false, "task": false}
# ### Exercise 3
#
# In this exercise, we'll use object oriented programming concepts to model [Pokemons](https://en.wikipedia.org/wiki/Pok%C3%A9mon).
#
# In case you're not familiar with Pokemon, they are wild (imaginary) creatures. These are some examples.
#
# <img src="./images/pokemons.png" style="width:800px">
#
# When they're living in the wild, Pokemon can be captured by Pokemon Trainers. Once they belong to a Trainer, they will obey the Trainer's commands. They're usually sent out to non-lethal battles against other Pokémon, in order to gain experience and level up.
#
# <img src="./images/battle.png" style="width:300px">
#
# When they reach certain levels, they can undergo a form of metamorphosis and transform into a similar but stronger species of Pokémon: this is called evolution.
#
# <img src="./images/charmander_evolves.jpg" style="width:350px">

# + [markdown] deletable=false editable=false nbgrader={"checksum": "1d69525e5e0eb098c4b30fbd8b504951", "grade": false, "grade_id": "cell-1f2611d8d0e9a610", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Part I (4 points)
#
# Your first assignment is to implement a class that represents a `Pokemon`.
#
# You'll need to store the following information about a Pokemon:
# * `name`: the Pokemon's name.
# * `max_health`: the number of health points that this Pokemon has with full health.
# * `speed`: a measure of how fast this Pokemon is. Faster Pokemons usually attack first in battles.
# * `hp`: current number of health points. Pokemons may lose health points during battles.
# * `level`: Pokemon's current level. This is a measure of the Pokemon's experience. Pokemons in higher levels have more chances of winning battles.
#
# Some additional information:
# * All the stats points described above (`max_health`, `speed`, `hp`, `level`) should be measured with non-negative integers.
# * When a Pokemon is born, its `level` is always 1 and its `hp` is always the same as `max_health`, but its `name`, `max_health`and `speed` vary from Pokemon to Pokemon.
#
#
# Our Pokemon class should implement 4 methods, described below.
#
# ##### 1. Method `is_knocked_out`
#
# During battles, Pokemon take damage, which translates into losing health points.
# Method `is_knocked_out` receives no arguments, and checks whether the Pokemon is knocked out by checking if the Pokemon's `hp` is equal to 0.
# This method should return a bool.
#
#
# ##### 2. Method `level_up`
#
# When a Pokemon wins a battle, it will level up.
# Method `level_up` receives no arguments, and doesn't return anything.
#
# This method should:
# * Increase the Pokemon's `level` by one.
# * Increase the Pokemon's `max_health` by 20 points.
# * Increase the Pokemon's `speed` by 10%. But speed must be an integer, so round it down.
# * It shouldn't change the Pokemon's `hp`!
#
#
# ##### 3. Method `take_damage`
#
# As explained above, during battles, Pokemon take damage, which translates into losing health points.
# Method `take_damage` receives as argument the integer `damage_points`, and doesn't return anything.
#
# This method should decrease the Pokemon's `hp` by `damage_points`. Make sure that the Pokemon's `hp`doesn't fall below 0.
#
#
# ##### 4. Method `attack_damage`
#
# This method calculates the number of damage points that our Pokemon's attack will inflict on an enemy Pokemon, during a battle. More experienced Pokemon inflict more damage and have higher chances of having successful attacks than less experienced Pokemon. 
# Method `attack_damage` receives as argument an `enemy_pokemon` (which is another instance of the Pokemon class) and returns an integer representing the number of damage points.
#
# This method should compute the damage points in the following way:
# * Create a variable `level_diff` that stores the difference in levels of the two Pokemon. If our Pokemon is more experienced, `level_diff` should be positive. If our Pokemon is less experienced, `level_diff` should be 0.
# * Create a variable `max_level` that stores the maximum level between the levels of the two Pokemon.
# * Create a variable called `p_success`, that represents the probability of success of your Pokemon's attack, and set it to: $$0.5 + \frac{level\_diff}{2 * max\_level}$$
# * Create a variable called `attack_success`, that represents whether the attack was successful or not. Calculate its value by drawing one sample from a binomial distribution: use numpy's [binomial function](https://numpy.org/doc/stable/reference/random/generated/numpy.random.binomial.html), with parameters `n=1` and `p=p_success`. This function will output a 0 (which means the attack was not successful) or a 1 (which means the attack was successful). 
# * Create a variable called `damage_points`. Calculate its value by multiplying your Pokemon's `level` by `attack_success`.
# * Return `damage_points`.
#

# + deletable=false nbgrader={"checksum": "cd5a7394e782b0008d4629496b1f6809", "grade": false, "grade_id": "cell-25e4812b96a967d8", "locked": false, "schema_version": 3, "solution": true, "task": false}
class Pokemon:
    # YOUR CODE HERE
### BEGIN SOLUTION
    def __init__(self, name, max_health, speed):
        self.name = name
        self.max_health = max_health
        self.speed = speed
        self.hp = max_health
        self.level = 1

    def is_knocked_out(self):
        return self.hp == 0

    def level_up(self):
        self.level += 1
        self.max_health += 20
        new_speed = int(self.speed * 1.1)

    def take_damage(self, damage_points):
        self.hp = max(0, self.hp - damage_points)

    def attack_damage(self, enemy_pokemon):
        level_diff = max(0, self.level - enemy_pokemon.level)
        max_level = max(self.level, enemy_pokemon.level)
        p_success = 0.5 + level_diff / (2 * max_level)
        attack_success = np.random.binomial(1, p_success)

        damage_points = attack_success * self.level
        return damage_points
### END SOLUTION


# + deletable=false editable=false nbgrader={"checksum": "30a15e31b34b903ef2bac34e7aee7ff8", "grade": true, "grade_id": "cell-92c4e256e70cfb10", "locked": true, "points": 4, "schema_version": 3, "solution": false, "task": false}
score = 0

try:
    pika = Pokemon(name='Pikachu', max_health=20, speed=5)
    assert pika.name == 'Pikachu'
    assert pika.max_health == 20
    assert pika.speed == 5
    assert pika.hp == pika.max_health
    assert pika.level == 1
    assert not pika.is_knocked_out()
except AssertionError:
    pass
else:
    score += 1

try:
    pika.level_up()
    assert pika.max_health == 40
    assert pika.hp == 20
    assert pika.speed == 5
    assert pika.level == 2
except AssertionError:
    pass
else:
    score += 1


try:
    enemy = Pokemon(name='Squirtle', max_health=10, speed=10)
    np.random.seed(42)
    assert pika.attack_damage(enemy) == 2
    assert pika.attack_damage(enemy) == 0
    assert pika.attack_damage(enemy) == 2

    np.random.seed(13)
    assert pika.attack_damage(enemy) == 0
    assert pika.attack_damage(enemy) == 2
    assert pika.attack_damage(enemy) == 0
except AssertionError:
    pass
else:
    score += 1

try:
    pika.take_damage(5)
    assert pika.hp == 15

    pika.take_damage(20)
    assert pika.hp == 0
    assert pika.is_knocked_out()
except AssertionError:
    pass
else:
    score += 1

if score == 0:
    raise AssertionError("Not enough correct answers to score points :(")

score


# + [markdown] deletable=false editable=false nbgrader={"checksum": "e0ebdd61ba40c6e30a477b7635fb98e1", "grade": false, "grade_id": "cell-038a08738a966958", "locked": true, "schema_version": 3, "solution": false, "task": false}
# #### Part II (2 points)
#
# Now we'll implement a battle between two Pokemon.
#
# Write a function called `battle` that receives as arguments `p1` and `p2`, both instances of the Pokemon class, and returns the Pokemon that wins the battle and the Pokemon that loses the battle (in this order!).
#
# A battle is a sequence of attacks. In the first turn, the Pokemon who attacks is the fastest (if they have the same speed, `p1` attacks first); the slowest Pokemon attacks in the second turn, and then the fastest Pokemon attacks again. No Pokemon is allowed to attack twice, they always switch places as the attacker.
#
# In an attack, the attacker inflicts as many damage points as indicated by the `attack_damage` method. Remember that this method has a random component, so you have to explicitly call it everytime the Pokemon attacks.
# The defender may suffer damage points. In order to record that, you should use the `take_tamage` method.
#
# The battle ends as soon as one of the Pokemon is knocked out.
#
# Grader Tip: If you see a `KeyboardInterrupt` error on the grader feedback, that is because your cell is taking too long to run, which is probably due to an infinite loop.

# + deletable=false nbgrader={"checksum": "bc39e7bc81faeb80e6fead24e25e871e", "grade": false, "grade_id": "cell-6ee8d592008072c6", "locked": false, "schema_version": 3, "solution": true, "task": false}
def battle(p1, p2):
    """
    Represents a battle between two Pokemon, where each Pokemon attacks at a time.

    Parameters:
    p1 (Pokemon): A Pokemon fighting in the battle.
    p2 (Pokemon): The other Pokemon fighting in the battle.

    Returns:
    winner (Pokemon): The winner Pokemon
    loser (Pokemon): The loser Pokemon
    """    
    # YOUR CODE HERE
### BEGIN SOLUTION
    if p1.speed >= p2.speed:
        p_attack, p_defense = p1, p2
    else:
        p_attack, p_defense = p2, p1

    while (not p_attack.is_knocked_out()) and (not p_defense.is_knocked_out()):
        damage_points = p_attack.attack_damage(p_defense)
        p_defense.take_damage(damage_points)

        tmp_p = p_attack
        p_attack = p_defense
        p_defense = tmp_p
    
    if p2.is_knocked_out():
        winner, loser = p1, p2
    else:
        winner, loser = p2, p1

    return winner, loser
### END SOLUTION


# + deletable=false editable=false nbgrader={"checksum": "2477c539ead021083fe26398848fbf7b", "grade": true, "grade_id": "cell-b055a1cab77f20ad", "locked": true, "points": 2, "schema_version": 3, "solution": false, "task": false}
pika = Pokemon(name='Pikachu', max_health=20, speed=5)
squirtle = Pokemon(name='Squirtle', max_health=10, speed=10)

np.random.seed(19)
winner, loser = battle(pika, squirtle)

assert winner.name == "Pikachu"
assert loser.name == "Squirtle"
assert winner.hp == 15
assert loser.hp == 0

from more_tests import test_exercise_3_II
test_exercise_3_II(battle, Pokemon)


# + [markdown] deletable=false editable=false nbgrader={"checksum": "a35c24076dd08dddea31eb5ef3c5ffa5", "grade": false, "grade_id": "cell-3deddd603f411b0a", "locked": true, "schema_version": 3, "solution": false, "task": false}
# ### Exercise 4
#
# Consider that you have a matrix, that represents a field, where you can go for a walk.
#
# In order to refer to a specific position in the matrix, we'll use the notation `(i,j)`, where `i` represents the row number, `j` represents the column number, using 0-indexing (the first row has index 0).
#
# You start walking at some position in the matrix, and the value on that position will guide you towards your next position. The possible values in the matrix are:
# * `l`: means that you'll move to the column on your **left**, in the same row
# * `r`: means that you'll move to the column on your **right**, in the same row
# * `u`: means that you'll move to the row **above**, in the same column
# * `d`: means that you'll move to the row **below**, in the same column
#
#
# You'll be able to keep walking until you're guided to a position outside the boundaries of the field.
#
# Let's see an example. This is a field.
#
# <code>[['r', 'r', 'd', 'l', 'u'],
#  ['r', 'd', 'l', 'd', 'd'],
#  ['u', 'r', 'd', 'r', 'l'],
#  ['l', 'd', 'd', 'u', 'r']]
# </code>
#
# If you start your walk in position (0,0), your path will be:
# * `(0,0)`
# * `r` => `(0,1)`
# * `r` => `(0,2)`
# * `d` => `(1,2)`
# * `l` => `(1,1)`
# * `d` => `(2,1)`
# * `r` => `(2,2)`
# * `d` => `(3,2)`
# * `d` => `end`
#
# In red you can see the positions where you passed.
#
# <code>[[<font color='red'>'r'</font>, <font color='red'>'r'</font>, <font color='red'>'d'</font>, 'l', 'u'],
#  ['r', <font color='red'>'d'</font>, <font color='red'>'l'</font>, 'd', 'd'],
#  ['u', <font color='red'>'r'</font>, <font color='red'>'d'</font>, 'r', 'l'],
#  ['l', 'd', <font color='red'>'d'</font>, 'u', 'r']]
# </code>
#
# The length of your path is the number of positions where you have been in the field.
# The path in the example has length 8.
#
# ***
#
# #### Part I (1 point)
#
# Write a function, named `walk` that receives as arguments:
# * a numpy matrix that represents a **field**, and
# * a tuple `(i, j)` that represents a **starting position**,
#
# and returns the length of the path that you get if you start walking at the starting position and continue until you leave the field.
#
# You can assume that the field is not empty, i.e, it has at least one position, and that the starting position is within the field.
#
#
# #### Part II (1 point)
#
# For an extra point, add the following validations to your function.
#
# If you find an unknown instruction while walking your path, raise a **ValueError** with message: `Invalid instruction found on position (i, j)`, where `i` and `j` are the row and column numbers of the position where you found the unknown instruction. If the field has unknown instructions, but you don't stumble upon them while walking, no exception should be raised.
#
# And be careful! Your field might tell you to start walking in circles! If during your walk, you find yourself in a position where you have been before, you should raise a **ValueError** with message: `"I'm running in circles"`
#
# Grader Tip: If you see a `KeyboardInterrupt` error on the grader feedback, that is because your cell is taking too long to run, which is probably due to an infinite loop.

# + deletable=false nbgrader={"checksum": "b4e74fd27b964661b9ff2f960c8d4e07", "grade": false, "grade_id": "cell-5a7eaacbb7097d35", "locked": false, "schema_version": 3, "solution": true, "task": false}
def walk(field, start):
    """
    Returns the length of the path that begins on a starting position and continues according to the
    field's instructions.

    Parameters:
    field (np.array): Matrix with instructions that describes a field where one can go for a walk.
    start (tuple):    Position where to start walking.
                      The first value is the row index and the second is the column index.

    Returns:
    walk_length (int): Length of the walk.
    
    Raises:
    ValueError: if an invalid instruction is found.
    ValueError: if the walk has a loop.
    """
    
    # YOUR CODE HERE
### BEGIN SOLUTION
    i, j = start
    max_i, max_j = field.shape

    path = []
    while (0 <= i < max_i) and (0 <= j < max_j):
        if (i, j) in path:
            raise ValueError("I'm running in circles") 
        
        path.append((i, j))
        instruction = field[i][j]

        if instruction == 'r':
            j += 1
        elif instruction == 'l':
            j -= 1
        elif instruction == 'u':
            i -= 1
        elif instruction == 'd':
            i += 1
        else:
            raise ValueError(f"Invalid instruction found on position ({i}, {j})")

    return len(path)
### END SOLUTION


# + deletable=false editable=false nbgrader={"checksum": "348dc635f3ec83c5c1bceebf581d4504", "grade": true, "grade_id": "cell-2eb102f666a51257", "locked": true, "points": 1, "schema_version": 3, "solution": false, "task": false}
# test field with single position
field = np.array([['d']])
assert walk(field, (0, 0)) == 1


# test field with successful walk
field = np.array([
    ['d', 'l', 'd'],
    ['d', 'u', 'l'],
    ['d', 'r', 'd'],
    ['d', 'd', 'd'],
])
assert walk(field, (1, 2)) == 7
assert walk(field, (2, 2)) == 2


# test another field with successful walk
field = np.array([
    ['r', 'r', 'd', 'l', 'u'],
    ['r', 'd', 'l', 'd', 'd'],
    ['u', 'r', 'd', 'r', 'l'],
    ['l', 'd', 'd', 'u', 'r'],
])
assert walk(field, (0, 0)) == 8

# + deletable=false editable=false nbgrader={"checksum": "d19aad7b1614392a2e58fd3c20b33b61", "grade": true, "grade_id": "cell-8da0084a934a316d", "locked": true, "points": 1, "schema_version": 3, "solution": false, "task": false}
# test field with loop
field = np.array([
        ['r', 'r', 'd', 'l', 'u'],
        ['r', 'd', 'l', 'd', 'd'],
        ['u', 'r', 'd', 'r', 'l'],
        ['l', 'd', 'd', 'u', 'r'],
    ])
try:
    walk(field, (1, 3))
    raise AssertionError("Should raise a ValueError when running in circles!")
except ValueError as e:
    assert str(e) == "I'm running in circles"
else:
    raise AssertionError("Should have raised the right error type!")

# test another field with loop
field = np.array([
    ['d', 'l', 'l'],
    ['d', 'w', 'u'],
    ['d', 'u', 'u'],
    ['r', 'r', 'u'],
])
try:
    walk(field, (3, 1))
    raise AssertionError("Should raise a ValueError when running in circles!")
except ValueError as e:
    assert str(e) == "I'm running in circles"
else:
    raise AssertionError("Should have raised the right error type!")

# test field with error
field = np.array([
    ['d', 'l', 'l'],
    ['d', 'w', 'u'],
    ['d', 'u', 'u'],
    ['r', 'r', 'u'],
])
try:
    walk(field, (2, 1))
    raise AssertionError("Should raise a ValueError when an invalid instruction is found!")
except ValueError as e:
    assert str(e) == "Invalid instruction found on position (1, 1)"
else:
    raise AssertionError("Should have raised the right error type!")

# test another field with error
field_with_error = np.array([
    ['r', 'x', 'd', 'l', 'u'],
    ['r', 'd', 'l', 'd', 'd'],
    ['u', 'r', 'd', 'r', 'l'],
    ['l', 'd', 'd', 'u', 'r'],
])
try:
    walk(field_with_error, (0, 0))
    raise AssertionError("Should raise a ValueError when an invalid instruction is found!")
except ValueError as e:
    assert str(e) == "Invalid instruction found on position (0, 1)"
else:
    raise AssertionError("Should have raised the right error type!")

# + [markdown] nbgrader={"grade": false, "grade_id": "cell-3444297cdaee0f61", "locked": true, "schema_version": 3, "solution": false, "task": false}
# ---

# + [markdown] nbgrader={"grade": false, "grade_id": "cell-6eec9aa841594049", "locked": true, "schema_version": 3, "solution": false, "task": false}
# ### Last but not least, submit your work!

# + [markdown] nbgrader={"grade": false, "grade_id": "cell-a51ce822b44b6cad", "locked": true, "schema_version": 3, "solution": false, "task": false}
# To submit your work, fill your slack ID in the `slack_id` variable (as a string).
#
# Example: `slack_id = "x-men"`
#
# Help: if you forgot your slack ID, [read this](https://moshfeu.medium.com/how-to-find-my-member-id-in-slack-workspace-d4bba942e38c).

# + nbgrader={"grade": false, "grade_id": "cell-af941850b12aa77e", "locked": false, "schema_version": 3, "solution": true, "task": false}
# Submit your work!

#slack_id = 

### BEGIN SOLUTION
slack_id = "ADMINSLU18_3"
### END SOLUTION

# + nbgrader={"grade": true, "grade_id": "cell-92bbc7cc332a69f5", "locked": true, "points": 0, "schema_version": 3, "solution": false, "task": false}
from submit import submit
assert isinstance(slack_id, str)

slu = 18_3
submit(slack_id, slu)

# + [markdown] nbgrader={"grade": false, "grade_id": "cell-0d112b8c1200b7cd", "locked": true, "schema_version": 3, "solution": false, "task": false}
# ---

StarcoderdataJupyterScriptsDedupFiltered