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 samepip3 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 cid
th 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 topip3 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.