Details

Due date

This assignment is due on Monday, May. 6th, at 11:30PM.

Coverage

This assignment covers the material up to Lecture 12, focusing on dictionaries, CSV processing, and overall program decomposition to finish implementing an interactive game.

The csv and os.path libraries

This assignment involves programming with CSV files and the csv library. You can find all of the material needed to work with CSV in the Lecture Slides for Weeks 4-5 and readings (remember that these are still files, so what you learned for MP 3 material will still be relevant!). Several functions will also need to check whether a given filename string exists. To implement this, use the os.path.exists function that you saw in MP3.

Do not use any other functions in os or any other modules in this assignment.

What to hand in

You will be handing in 1 file for this assignment which is named mp4_pokemon.py.

Supporting files

The following CSV files are provided for you to use and test your solutions on:

  • pts_mini.csv
  • pokedex.csv
  • moves.csv
  • collected.csv
  • collected_example.csv

You can find the testing code here (test your solutions with pytest test_mp4_pokemon.py with this test file in the same directory as your solution)

  • Important: This testing program uses a pytest extension called mocker, which is a very useful pytest feature to "mock" (simulate) function calls in tests. You don't need to worry about knowing it, but you will likely need to install it for the tests to work. Use the same pip3 install syntax you've used for earlier assignments (e.g. pip3 install pytest-mock); as a reminder, you may need to restart VSCode after installing a new pip3 library.

We have also provided a library you will briefly use in Exercise 7 to represent "Move" objects for a working game. You do not need to read/understand Move.py aside from the instructions mentioned in Exercise 7 (we will learn more about "Objects" soon).

  • Move.py

You can find them in the mp4_starter.zip file.

We have also provided an interactive UI program (mp4_main.py) that works with your solutions soon. If you would like to add more features to this program, we are offering a creative showcase in Discord (your mp4_pokemon.py program should still work as described in the spec).

Your code (mp4_pokemon.py) should be submitted to CodePost as usual.

Miniproject: Pokemon Simulator

In this Miniproject, you will finish implementing an interactive Pokemon Simulator. This program uses the provided pokedex.csv and moves.csv files as datasets to work with, and you will manage an additional collected.csv file to add/remove Pokemon that are "collected" over time. You do not need to know anything about Pokemon for this assignment; everything is outlined clearly for your tasks.

The provided mp4_main.py will have various functionality already implemented relating to game UI, prompts, and utility functions that aren't the focus of this assignment's learning objectives. Most of the game logic specific to Pokemon is also factored out in the provided code (e.g. calculating "DP" damage points using move data and Pokemon types with simple heuristics and managing "buffs" and "debuffs" for special moves).

Your tasks will be to finish the functionality of the simulator using CSV file processing and dictionaries, along with clean program decomposition for a more complex project.

1. Utility Functions: load_data and clear_data

[30]

First, you'll implement 2 utility functions that work with CSV files and dictionaries. These will be used for some of the later functions in the Mini Project.

1. load_data

[10]

Write a function called load_data which takes a CSV filename string as an argument and returns a list of dictionaries corresponding to each row in the given file. Use the csv.DictReader object/methods to get the list of row dictionaries. For reference, our solution is 8-10 lines ignoring blank lines/comments. If the file doesn't exist, or if the file is empty, the function should return an empty list. For the first case, the function should print a message described below.

Note: As discussed in class, it is often poor practice to load all of a file's contents at once (e.g. with f.readlines() or f.read()) when you can instead iterate through the file line-by-line with for line in f or f.readline() to do what you need with each line. Later in this spec, we will see an example where the trade-off of storing a (known-to-be-small) dataset that we will need multiple times makes sense to load/save only once instead of re-loading each time.

Use os.path.exists(filename) to check that the given filename exists. If the file does not exist, print the message, <filename> not found. Aborting. and return [] (in practice, we would want to use exceptions but we will learn proper exception-handling soon). Examples of the expected behavior of your function are provided below (as a reminder, make sure to test your functions using spec examples before using pytest!).

>>> from mp4_pokemon import load_data
>>> rows = load_data('pts_mini.csv')
>>> rows
[{'x': '0', 'y': '1'}, {'x': '2', 'y': '-1'}]
>>> rows = load_data('moves.csv')
>>> rows
[{'name': 'Energy Ball', 'type': 'grass', 'accuracy': '1', 'dp': '90',
  'buff': '', 'buff_amt': ''},
  {'name': 'Thief', 'type': 'dark', 'accuracy': '1', 'dp': '60',
  'buff': '', 'buff_amt': ''}, ... ]
>>> print(len(rows))
156
>>> rows = load_data('empty.csv')
>>> rows
[]
>>> rows = load_data('missing_file.csv')
missing_file.csv not found. Aborting.
>>> rows  # note: we are still returning [] in this case!
[]

clear_data

[10]

Write a function clear_data which takes a CSV filename string as an argument and clears all of the data in the file except for the first header row. You may assume that if the file exists, it is in CSV format and has a header column row.

If the given file doesn't exist, print the message, <filename> not found. Aborting. Otherwise, after clearing the non-header rows, print the message, <filename> data successfully cleared! replacing <filename> with the passed filename string in both cases.

Hint: You will need to read the file first to get the columns in the first line, and then overwrite that file with that first line (and nothing else). Do not use any loops in your solution and do not load the entire file contents at once. Also note that you can (and should) solve this problem without the csv module.

Important: We recommend testing this function on copies of the provided files (e.g. copying pts_mini.csv to pts_mini_copy.csv and so on) so that you don't accidentally erase the original contents when testing.

$ cat pts_mini_copy.csv  # print out contents of pts_mini_copy.csv
x,y
0,1
2,-1
$ python3
>>> from mp4_pokemon import load_data, clear_data
>>> rows = load_data('pts_mini_copy.csv')
>>> rows
[{'x': '0', 'y': '1'}, {'x': '2', 'y': '-1'}]
>>> clear_data('pts_mini_copy.csv')
mini_pts_copy.csv data successfully cleared!
>>> rows = load_data('pts_mini_copy.csv')
>>> rows
[]
>>> clear_data('missing_file.csv')
missing_file.csv not found. Aborting.
>>> quit()
$ cat pts_mini_copy.csv
x,y

2. load_pokedex and load_collected

[10]

Next, you'll write 2 functions called load_pokedex and load_collected which use your load_data function from above to return a list of row dictionaries from pokedex.csv and collected.csv, respectively.

For the provided pokedex.csv, your load_pokedex should return a list of 151 dictionaries (but do not hard-code 151 anywhere).

For the provided collected_example.csv (which we recommend you test with until you implement the rest of the functions for this assignment), your load_collected should return a list of 4 dictionaries (one per collected Pokemon in the example dataset). Note that because both of these files have different columns, the keys in the dictionaries are expected to be different between the two lists. Make sure you replace any instances of collected_example.csv with collected.csv in your submission.

>>> from mp4_pokemon import load_pokedex, load_collected
>>> pokedex = load_pokedex()
>>> len(pokedex)
151
>>> type(pokedex)
<class 'list'>
>>> first_pokemon = pokedex[0]
>>> type(first_pokemon)
<class 'dict'>
>>> first_pokemon
{'pid': '1', 'name': 'Bulbasaur', 'description': 'A strange seed...', 'stage': '1',
 'type': 'Grass', 'weakness': 'Fire', 'hp': '200',
 'move1': 'Vine Whip', 'move2': 'Growl', 'move3': 'Amnesia', 'move4': 'Magical Leaf'}
>>> first_pokemon['name']
'Bulbasaur'
>>> pokedex[150]['name']
'Mew'
>>> int(pokedex[150]['pid'])
151
>>> collected = load_collected()
>>> len(collected) # example if contents are same as collected_example.csv
4
>>> first_cid = 1
>>> first_collected = collected[first_cid - 1]
>>> first_collected
{'pid': '25', 'name': 'Pikachu', 'nickname': 'Sparky', 'level': '7',
 'type': 'Electric', 'weakness': 'Ground', 'hp': '160'}
>>> first_collected['name']
'Pikachu'
>>> int(first_collected['pid'])
25  # not the same as the collected id (cid)!

3. display_pokedex and display_collected

[20]

Next, you'll implement the functions to display the Pokedex and the user's currently-collected Pokemon.

3.a. display_pokedex

Finish display_pokedex to print each Pokemon in the given pokedex dictionary list with the format '#<pid>: <name> (<pokemon type>)', replacing <pid> with the Pokemon's pid, <name> with the Pokemon's name, and <pokemon type> with the Pokemon's type.

Each row in the list of dictionaries should be printed as described, and you should not make assumptions about the number of rows in the list of dictionaries (though there will be 151 Pokemon printed for the dataset we're using).

For example, the first and last rows printed should match the following:

>>> from mp4_pokemon import load_pokedex, display_pokedex
>>> pokedex = load_pokedex()
>>> display_pokedex(pokedex)
#1: Bulbasaur (Grass)
... (other Pokemon)
#151: Mew (Psychic)

3.b. display_collected

Finish display_collected to print each Pokemon in the passed list of dictionaries (which you can assume came from collected.csv) with the format '<cid>: <name> "<nickname>" (<pokemon type>)', replacing <cid> with the collected Pokemon's cid ('collected id', not the 'pid' corresponding to the "Pokedex" id) which is determined by the row number for that Pokemon in the CSV file, <name> with the Pokemon's name, <nickname> with the Pokemon's nickname, and <pokemon type> with the Pokemon's type.

Each row in the list of dictionaries should be printed as described, and you should not make assumptions about the number of rows in list. We strongly recommend testing your function with the provided collected_example.csv file to make sure your output matches that of below before using your own collected.csv file that is updated with your other functions. Note: In the past, students have used pid instead of cid, so make sure you remember that the cid is based on the row number in the source file which should be computed with a counter in your loop ( this was discussed in Lecture 12!)

Expected output for collected_example.csv:

>>> from mp4_pokemon import load_collected, display_collected
>>> collected = load_collected()
>>> display_collected(collected)
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Magikarp "Finny" (Water)
3: Mew "Mu" (Psychic)
4: Paras "PARAS" (Bug)

4. add_pokemon

[15]

Next, you'll finish implementing the add_pokemon function to add a new Pokemon to the collected.csv dataset given a Pokemon dictionary, which in the finished program, comes from one of the rows of the Pokedex list of dictionaries. Replace the TODO comment in this function to create an entry holding the following information: pid (pokedex id, e.g. 25), name (e.g. 'Pikachu'), nickname (e.g. 'Sparky'), level (e.g. 7), Pokemon's type (e.g. 'Electric'), Pokemon's weakness (e.g. 'Ground'), and Pokemon's HP (e.g. 160), which correspond to the COLLECTED_COLUMNS (the ordered list of columns in collected.csv).

It's up to you to choose how to construct and append the row, but you must use csv.writer (a list of string values) or csv.DictWriter (a dictionary) and otherwise may not change the other contents of collected.csv. The row should be appended to the end of the file with the correct values for the columns as shown (these are the same ones as COLLECTED_COLUMNS):

pid,name,nickname,level,type,weakness,hp

For example, the first row in the collected_example.csv dataset added Pikachu with the following values (in order):

25,Pikachu,Sparky,7,Electric,Ground,160

Some example calls you can test with are provided below:

>>> from mp4_pokemon import load_collected, load_pokedex
>>> from mp4_pokemon import display_collected, add_pokemon
>>> collected = load_collected()
>>> pokedex = load_pokedex()
>>> display_collected(collected)
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Magikarp "Finny" (Water)
3: Mew "Mu" (Psychic)
4: Paras "PARAS" (Bug)
>>> pikachu_pid = 25
>>> pikachu = pokedex[pikachu_pid - 1]
>>> pikachu
{'pid': '25', 'name': 'Pikachu', 'description': "El's favorite Pokemon!...",
 'stage': '1', 'type': 'Electric', 'weakness': 'Ground', 'hp': '160',
 'move1': 'Growl', 'move2': 'Thunderbolt', 'move3': 'Quick Attack', 'move4': ''}
>>> add_pokemon(pikachu)
>>> add_pokemon(pikachu)
Do you want to give a name to your new Pikachu (y for yes)? y
What nickname do you want to give? Sparkster
>>> bulbasaur_pid = 1
>>> bulbasaur = pokedex[bulbasaur_pid - 1]
>>> add_pokemon(bulbasaur)
Do you want to give a name to your new Bulbasaur (y for yes)? no
>>> collected = load_collected()
>>> display_collected(collected)
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Magikarp "Finny" (Water)
3: Mew "Mu" (Psychic)
4: Paras "PARAS" (Bug)
5: Pikachu "Sparkster" (Electric)
6: Bulbasaur "BULBASAUR" (Grass)

5. abandon_pokemon

[20]

Next, you'll finish implementing the abandon_pokemon to support functionality to remove a Pokemon from the collected.csv dataset based on a user's inputted cid which represents the row number to remove. Again, we have given you the UI, but you will practice removing a single row in an existing CSV file, without changing the contents of the other rows. This function has a few edge cases we'll want to consider.

First, if the given cid is less than 1 or greater than the length of the collected list of dictionaries, the function should print:

Invalid cid #.

Otherwise, remove the Pokemon at the given row number (cid), remembering that 1 (not 0) represents the first row. This means you will need to remove the row from collected.csv without changing the original contents of the other rows or the file's header columns.

Since you have access to the collected list of Pokemon dictionaries in this function, you don't need to reload the collected.csv file, but you will need to update the list appropriately (hint: you may find the del keyword useful here, which was also covered in Lecture 11; the list.remove method will be less helpful since it requires a value, not an index to remove) and rewrite the contents to collected.csv (as a hint, you need to use 'w' as the file option here). We strongly recommend you starting this exercise with a breakpoint at the end of the starter code in this function, and explore the variables in your debugger pane. Don't forget you can use VSCode's Debug Console to interact with variables in the current function (this was shown in Lecture 12).

Hint: We strongly recommend that you write to a temporary file collected_temp.csv to make sure your output is as expected (collected.csv without removed Pokemon row) since you will be overwriting the file. Once you have ensured your function works correctly, change this to collected.csv. Our solution adds about 10 lines to the started function, ignoring comments.

An example of using the function in interactive testing mode is provided below:

>>> from mp4_pokemon import load_collected, display_collected
>>> from mp4_pokemon import abandon_pokemon
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Magikarp "Finny" (Water)
3: Mew "Mu" (Psychic)
4: Paras "PARAS" (Bug)
Which Pokemon do you want to say goodbye to (Enter #)? 2
Successfully said goodbye to Magikarp!
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Mew "Mu" (Psychic)
3: Paras "PARAS" (Bug)
Which Pokemon do you want to say goodbye to (Enter #)? 0
Invalid cid #.
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Mew "Mu" (Psychic)
3: Paras "PARAS" (Bug)
Which Pokemon do you want to say goodbye to (Enter #)? 3
Successfully said goodbye to Paras!
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Mew "Mu" (Psychic)
Which Pokemon do you want to say goodbye to (Enter #)? 1
# This really hurt, but good to test first Pokemon case...
Successfully said goodbye to Pikachu!
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Mew "Mu" (Psychic)
Which Pokemon do you want to say goodbye to (Enter #)? 0
Invalid cid #.
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Mew "Mu" (Psychic)
Which Pokemon do you want to say goodbye to (Enter #)? 2
Invalid cid #.
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Mew "Mu" (Psychic)
Which Pokemon do you want to say goodbye to (Enter #)? -1
Invalid cid #.
>>> abandon_pokemon()
------------------------------
Your collected Pokemon:
------------------------------
1: Mew "Mu" (Psychic)
Which Pokemon do you want to say goodbye to (Enter #)? 1
Successfully said goodbye to Mew!
>>> collected = load_collected()
>>> display_collected(collected)
No Pokemon collected yet.
>>> abandon_pokemon()
No Pokemon collected yet.

6. rename_pokemon

[20]

Next, you will finish the implementation for the rename_pokemon function, which allows a user to rename one of their collected Pokemon. In this function, you will practice updating a single row in a CSV file without changing the contents of the other rows. The prompting for this function is factored out for you in mp4_main.py's prompt_rename_pokemon.

This function takes three arguments: a cid int (1 represents the first row in the collected list), a new_name string, and the list of collected Pokemon dictionaries. Your task is to update the cidth row in the list to update the nickname of the row dictionary to be set to the passed new_nickname. Use this list to rewrite the collected.csv file with the updated row.

The other keys for that Pokemon dictionary and the rest of the rows should be unchanged.

Upon success, the following should be printed, replacing <old nickname> with the old nickname value for the chosen Pokemon's row (no < or > should be in the printed result):

Successfully renamed <old nickname> to <new nickname>!

An example of the intended functionality is provided below:

>>> from mp4_pokemon import load_collected, display_collected, rename_pokemon
>>> collected = load_collected()
>>> display_collected(collected)
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparky" (Electric)
2: Magikarp "Finny" (Water)
3: Mew "Mu" (Psychic)
4: Paras "PARAS" (Bug)
>>> rename_pokemon(1, 'Sparkster', collected)
Successfully renamed Sparky to Sparkster!
>>> rename_pokemon(4, 'Shroomy', collected)
Successfully renamed PARAS to Shroomy!
>>> collected = load_collected() # Reload to make sure collected.csv is updated
>>> display_collected(collected)
------------------------------
Your collected Pokemon:
------------------------------
1: Pikachu "Sparkster" (Electric)
2: Magikarp "Finny" (Water)
3: Mew "Mu" (Psychic)
4: Paras "Shroomy" (Bug)

For reference, our solution is about 11 lines long, ignoring blank lines/comments.

7. load_moves

[20]

At this point, you've got all of the functionality for managing a Pokedex (pokedex.csv) and allowing a player to collect, rename, and abandon their own Pokemon (collected.csv)! These last two functions will wrap up the application to load information about Pokemon "moves" which can be used in an interactive game play. Note that each Pokemon has between 1 and 4 moves, as specified in the move1-move4 columns (if a row has an empty value for any move column, that Pokemon has fewer than 4 moves).

Finish the load_moves function which takes no arguments but should generate a dictionary mapping move name strings to Move objects using moves.csv. For each row in the CSV file, construct a Move object with the row's 'name', 'type', 'accuracy' ("accuracy rate"), 'dp', 'buff', and 'buff_amt' values (in order). Objects are constructed by Move constructor; the provided Move class will convert the values as needed, so any number values from moves.csv row dicts that are in strings can be passed as-is to get the same result (you can see the documentation for the constructor if you'd like using help(Move) as long as you imported it). An example of constructing move objects are provided below. Note that we have not learned about objects yet, but the Move object is useful here since it provides functionality to print different types of moves (DP vs. Buff moves) without you having to do anything more than using the Move(...) constructor.

>>> from Move import Move  # Imports the Move constructor from provided Move.py
>>> # Syntax: Move(name, type, acc_rate, dp, buff_type, buff_amt)
>>> ergy_ball_move = Move('Energy Ball', 'Grass', '1', '90', '', '')
>>> ergy_ball_move
<Move.Move object at 0x10090b520>
>>> print(ergy_ball_move)
Energy Ball: (Type: Grass, DP: 90)
>>> growl_move = Move('Growl', 'Normal', 1, '', 'Attack', '-1')
>>> print(growl_move)
Growl: (Type: Normal, Debuff: -1 Attack)
>>> duck_move = Move('duck', 'fire', 1, '', 'Defense', -2) 
>>> duck_move
<Move.Move object at 0x000002C7A9B249D0>
>>> print(duck_move)
duck: (Type: fire, Debuff: +2 Defense)

An example of using the load_moves function in interactive testing mode is provided below (for the provided moves.csv file, you should expect 156 entries in the result dictionary):

>>> from mp4_pokemon import load_moves
>>> moves_dict = load_moves()  # a {str -> Move} dict
>>> len(moves_dict)
156
>>> type(moves_dict)
<class 'dict'>
>>> ergy_ball = moves_dict['Energy Ball']
>>> ergy_ball
<Move.Move object at 0x7fc128971ac0>
>>> print(ergy_ball)  # The provided Move object formats this for you
Energy Ball: (Type: Grass, DP: 90)
>>> flee = moves_dict['Flee']  # 'Flee' not defined in moves.csv, thus not a key in move_dict
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Flee'

Requirement: Use csv.DictReader. Dot not use your load_data in this function.

8. generate_move_list

You'll now write a function which generates and returns a list of the moves that are "known" by a Pokemon based on the dictionary passed (this dictionary corresponds to a row from pokedex.csv). The function should look through the move1-move4 keys in the passed dictionary and return a list of all values (move name strings) for the name values that are non-empty. You can assume that the passed dictionary has all four move1-move4 keys.

Hint: Use a loop to check the value of each move1-move4 key. If the move is an empty string '' then do not append the move to the list.

This is a very small function, our solution is only 8 lines long. Below is an example of the expected functionality from generate_move_list.

>>> from mp4_pokemon import load_pokedex, generate_move_list
>>> pokedex = load_pokedex()  # the list of rows in pokedex.csv
>>> bulbasaur = pokedex[0]  # bulbasaur is our "pokemon" in this example
>>> bulbasaur_moves = generate_move_list(bulbasaur) # here we generated the list of "moves"
>>> bulbasaur_moves
['Vine Whip', 'Growl', 'Amnesia', 'Magical Leaf']
>>> caterpie = pokedex[9]
>>> caterpie
{'pid': '10', 'name': 'Caterpie', 'description': 'Its short feet...', ...,
 'move1': 'Tackle', 'move2': 'Bug Bite', 'move3': '', 'move4': ''}
>>> caterpie_moves = generate_move_list(caterpie) # only move1 and move2 are non-empty
>>> caterpie_moves
['Tackle', 'Bug Bite']

Final Reminders

Here are a few final reminders to check before submitting your assignment:

  • Make sure you are testing your functions iteratively (one after the other) using the provided examples in the interpreter, before using pytest. If you are unsure about interpreting an error, you are allowed to (and encouraged to) share screenshots on the #mp4-questions channel on Discord, as long as you aren't posting your code. For full credit, your program should not produce any errors. If any errors are reported, you have some debugging to do.
  • Make sure you are running the provided test_mp4_pokemon.py test suite as described above. For full credit, your program should not produce any errors. Remember to pip3 install pytest-mock as mentioned at the beginning of this spec.
  • Use pycodestyle to test your solution for common PEP8 issues (covered in last week's lectures/Discord instructions). You can ignore trailing whitespace errors (the invisible whitespace at the end of lines) but should fix other reported errors.
  • Make sure you are following Python language conventions taught in class/the CS 1 Code Quality Guide. Some common conventions students have misused in the past for this assignment include:
    • Not using lowercase_snake_casing for function and variable names (no upper-case characters!)
    • Make sure you are using spaces, not tabs (you can configure this in VSCode Settings if needed)
    • Make sure you don't have any unused/unnecessary variables in functions
    • No variables should be outside the scope of a function (these are what we refer to as global variables)
    • When using loops, make sure you are not using them unnecessarily (Read each function's requirements carefully!)
    • Do not use any advanced Python features that we have not taught; the reason for this requirement is 1. students are not expected to have prior experience and 2. students who do have some experience have often misused advanced Python without understanding trade-offs where simpler implementations are more appropriate. Advanced features include list comprehension, any modules that are not random, functional Python (e.g. map), etc. If you want to discuss these trade-offs/ask about exceptions, you should reach out to El (not TAs); they are happy to talk about this!
  • And finally, double-check this spec once more to make sure you've followed any missed requirements!
  • And have fun! Feel free to ask about any creative extensions if you finish early.