CS21 Lab 5: More Advanced Functions
Due Saturday, Oct 11, before midnight
Goals
-
More practice writing programs with multiple functions.
-
Perform input validation using indefinite
while
loops. -
Index into lists.
-
Write functions that mutate lists.
As you write programs, use good programming practices:
-
Use a comment at the top of the file to describe the purpose of the program (see example).
-
All programs should have a
main()
function (see example). -
Use variable names that describe the contents of the variables.
-
Write your programs incrementally and test them as you go. This is really crucial to success: don’t write lots of code and then test it all at once! Write a little code, make sure it works, then add some more and test it again.
-
Don’t assume that if your program passes the sample tests we provide that it is completely correct. Come up with your own test cases and verify that the program is producing the right output on them.
-
Avoid writing any lines of code that exceed 80 columns.
-
Always work in a terminal window that is 80 characters wide (resize it to be this wide)
-
In
vscode
, at the bottom right in the window, there is an indication of both the line and the column of the cursor.
-
Function Comments
All functions should include a comment explaining their purpose, parameters, return value, and describe any side effects. Please see our function example page if you are confused about writing function comments.
Memory (Matching Game)
For this week’s lab, you’ll be building a memory matching game. In this game, a collection of card pairs will be shuffled and shown to the user face down. The user will need to select cards in an attempt to find matching pairs. After revealing two cards, if the user finds a matching pair, they get a "match", and the pair of cards remains face up. Otherwise, the non-matching cards are turned face down and the user has to remember which cards were in those locations.
Here’s an example, where the face down cards are shown as blue squares
(":blue_square:"
) and the cards contain animal emojis. User input is shown
in bold:
$ python3 memory.py Turns remaining: 6 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 0 🐄 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 1 🐄 🐐 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 No match :( Press enter to continue. Turns remaining: 5 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 2 🟦 🟦 🦔 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 3 🟦 🟦 🦔 🐈 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 No match :( Press enter to continue. Turns remaining: 4 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 4 🟦 🟦 🟦 🟦 🐺 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 5 🟦 🟦 🟦 🟦 🐺 🐊 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 No match :( Press enter to continue. Turns remaining: 3 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 6 🟦 🟦 🟦 🟦 🟦 🟦 🐐 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 1 🟦 🐐 🟦 🟦 🟦 🟦 🐐 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Match! Press enter to continue. Turns remaining: 2 🟦 🐐 🟦 🟦 🟦 🟦 🐐 🟦 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 7 🟦 🐐 🟦 🟦 🟦 🟦 🐐 🐺 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 4 🟦 🐐 🟦 🟦 🐺 🟦 🐐 🐺 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Match! Press enter to continue. Turns remaining: 1 🟦 🐐 🟦 🟦 🐺 🟦 🐐 🐺 🟦 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 8 🟦 🐐 🟦 🟦 🐺 🟦 🐐 🐺 🐈 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Which card would you like to reveal? 3 🟦 🐐 🟦 🐈 🐺 🟦 🐐 🐺 🐈 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11 Match! Press enter to continue. Final match count: 3 🟦 🐐 🟦 🐈 🐺 🟦 🐐 🐺 🐈 🟦 🟦 🟦 0 1 2 3 4 5 6 7 8 9 10 11
Python Emojis
There’s an emoji
library for Python that makes printing an emoji relatively
easy. First, import the emojize
function from the library, and then call it
on the text version of a string containing an emoji. For example:
from emoji import emojize
s = "The :cat_face: says meow"
print(emojize(s))
When it encounters :cat_face:
, the emojize
function will replace the text
with the corresponding emoji, 🐱. The library supports a
a full list of emojis that you can use in
Python. For this lab, you can get creative with the ones you choose (as long
as they’re distinguishable from one another).
Representing Cards
For this lab, you’ll use two lists of length 12 to keep track of what’s happening with the cards in the game:
-
A
cards
list that contains the emoji strings for each of the cards (e.g., ":wolf:", ":crocodile:", etc.). One of the functions you write,build_cards
, will produce this list. -
A
status
list of strings that contains the game status for each card. Each entry in the list should only be one of"hidden"
,"selected"
, or"matched"
. This list will initially contain 12 entries that are all set to"hidden"
.
Cards that are "hidden"
should be displayed face down. As the user interacts
with cards, you can change their status. For example, if the user is in the
process of making a turn and they have temporarily flipped a card face up, you
can represent the card’s status as "selected"
. If the user matches two
cards, you can set the status of both cards to "matched"
.
Cards that are selected but not matched will reset to hidden at the end of turn. Cards that are matched stay matched and will remain visible for the remainder of the game.
Writing Helper Functions
To complete your memory game, you must write the following functions, and your
implementation must match the specifications given here. You may write
additional functions if you’d like. You should read the function specifications
all the way through and only start programming when you have a good
understanding of how main()
uses each of the other functions.
For each function that you write, provide a comment that describes its purpose, parameters, side effects, and return value. |
build_cards
build_cards(emojis)
: This function takes in a list of six emoji strings. You
may assume that the argument list will contain unique entries and that it will
have exactly six emoji strings in it, This function should ultimately produce
and return a list containing six pairs (12 total entries) of randomly shuffled
emoji strings.
The general sequence of events for this function is:
-
Copy the entries of the
emojis
list into a newresult
list. -
Multiply the
result
list by 2 to double all the entries, thus giving you six pairs of entries. -
Shuffle the
result
list and then return it.
The shuffle
function from the random
library will help you:
from random import sample, shuffle
# Given a list l:
l = [1, 2, 3, 4, 5, 6, 7]
# You can shuffle (randomly rearrange) the entries of a list using the shuffle
# function:
shuffle(l)
# Note that the shuffle function does not return a value! It uses list
# mutability to rearrange the order of the entries in the list that gets passed
# in to it.
After implementing this function, test it with an input list that contains six emoji strings. It should return to you a list containing exactly 12 emoji strings (six matching pairs). For example:
def main():
# This list has 6 entries
emojis = [":wolf:", ":bear:", ":horse_face:", ":crocodile:",
":hedgehog:", ":bird:"]
cards = build_cards(emojis)
print(cards)
You should see a different (randomized) output each time you run it. Here are some examples:
$ python3 memory.py [':horse_face:', ':wolf:', ':crocodile:', ':hedgehog:', ':bear:', ':bird:', ':bird:', ':wolf:', ':hedgehog:', ':horse_face:', ':crocodile:', ':bear:'] $ python3 memory.py [':bear:', ':crocodile:', ':wolf:', ':horse_face:', ':wolf:', ':hedgehog:', ':hedgehog:', ':crocodile:', ':bird:', ':bird:', ':horse_face:', ':bear:'] $ python3 memory.py [':bear:', ':horse_face:', ':bear:', ':bird:', ':wolf:', ':crocodile:', ':hedgehog:', ':bird:', ':crocodile:', ':hedgehog:', ':wolf:', ':horse_face:']
print_cards
print_cards(cards, status)
: This function takes two lists, each with 12
entries. The cards
list is the list of emoji strings that was produced by
build_cards
. The status
list is a list of 12 strings containing one of
"hidden"
, "selected"
, or "matched"
as described above in the
Representing Cards section.
This function doesn’t return anything or modify either of its input lists. It should print two rows of output based on the contents of the lists.
-
The first row should contain emojis, where:
-
emojis whose status is
"hidden"
use some sort of obvious symbol that indicates cards are face down. The examples above and below use":blue_square:"
for these face down cards. -
emojis whose status is not hidden (either
"selected"
or"matched"
) are face up and shown as they appear in thecards
list.
-
-
The second row should contain the index locations of each emoji in the list.
To create the output for each row, you can take advantage of the fact that both lists are the same length (12 entries) and iterate through every index of both lists to 1) check the status list at that index and then based on its value, either choose to display the corresponding card emoji or the "face down" emoji.
For example, suppose you had the following two lists:
cards = [":goat:", ":wolf:", ":bird:", ":wolf:", ":cat:", ":goat:", ...] status = ["matched", "selected", "hidden", "hidden", "hidden", "matched", ...]
You should expect to see output that looks like:
🐐 🐺 🟦 🟦 🟦 🐐 ... 0 1 2 3 4 5 ...
Rather than trying to neatly separate the emojis and indices using spaces, you
can use a single tab character \t
in between each entry to cleanly space them
out.
For example:
# Note the \t instead of spaces between letters.
print("A\tB\tC\tD")
Produces output that looks like:
A B C D
handle_guess
handle_guess(status)
: This function takes in a status
list of 12 strings,
where each string is one of "hidden"
, "selected"
, or "matched"
as
described above. It should prompt the user "Which card would you like to
reveal?"
and you may assume that the user will enter an integer value. You
should NOT assume that the integer will be valid according to the rules of the
game, so handle_guess
should not proceed until both of the following
conditions are met:
-
The integer entered by the user is a valid list index (0 to 11).
-
The card at the selected index is in a
"hidden"
state.
After meeting both conditions, handle_guess
should set the status of the card
at the selected index to "selected"
(by mutating the list) and return the
selected index number.
If condition 1 isn’t met, you can simply tell the user that their choice is invalid before prompting again.
If condition 2 isn’t met, you should print a message to let them know why.
Supposing you had a status list that looks like:
status = ["matched", "selected", "hidden", "hidden", "hidden", "matched", ...]
Here are some examples of what this function’s behavior might look like:
Which card would you like to reveal? 50 Sorry, 50 is not a valid choice. Which card would you like to reveal? -5 Sorry, -5 is not a valid choice. Which card would you like to reveal? 0 Sorry, card 0 is already visible. Try again. Which card would you like to reveal? 1 Sorry, card 1 is already visible. Try again. Which card would you like to reveal? 2 (At this point, because 2 is valid, the function would set status[2] to "selected" and return 2)
play_one_turn
play_one_turn(cards, status, turns)
: This function takes in a list of cards,
a status list, and the number of turns the player has remaining. It’s
responsible for playing one round of matching (asking the user to select two
cards) and checking to see if the selected cards match. If they do match, it
should return True
, otherwise it should return False
.
The overall sequence of events for this function is:
-
Print the number of remaining turns.
-
Print the cards so that user can see what they’re selecting.
-
Get the user’s first card selection.
-
Print the cards again, with the first selection now visible.
-
Get the user’s second card selection.
-
Print the cards again, with both selections now visible.
-
Check to see if the selected cards are a match.
-
If so, set both of their statuses to "matched" and return True.
-
If not, set both of their statuses back to "hidden" and return False.
-
When testing |
Writing a Main Function
In the beginning, you should use main
to incrementally test your individual
helper functions and then gradually build the complete program. The final
program should give the user six turns, and it should count the number of
matches the user achieves after all of their turns are done. At the end of the
game, it should print the final match count and show the final state of the
matched cards.
A finished program might have the primary steps in main
shown below. Think
about how to use the helper functions described above to implement each step.
Note that some helper functions may need to be called by other helpers and may
not appear in main
at all!
-
Initialize starting variables, including the number of turns, matches, and the status list.
-
Build the cards list.
-
While the game hasn’t yet ended…
-
Play one round of the game.
-
Update the count of matches and turns as necessary.
-
Prompt the user to press enter to continue (this can be a call to the
input
function where you ignore the return value). -
Call
system("clear")
to clear the terminal output so that the user can’t simply scroll up to cheat. :)
-
-
Print the final results.
You may not want to add the call to |
Think carefully about how each of your functions should be used. Each of the steps in the main function that we have outlined should only need a few lines of code — the helper functions handle most of the complexity.
Additionally, it is very important that you use incremental development. The outline of main is a good starting point — you should complete one step at a time, thoroughly test it to see if your program works, and only move on after getting it to work. The ninjas and instructors will ask you to backtrack and do incremental development if you do not follow this strategy. If you try to implement the whole program at once, you will waste a lot of time and energy trying to fix your program.
For testing certain functionality, it may be helpful to call functions with hard-coded lists that you define to test specific behavior.
Requirements
The code you submit for labs is expected to follow good style practices, and to meet one of the course standards, you’ll need to demonstrate good style on six or more of the lab assignments across the semester. To meet the good style expectations, you should:
|
Your program should meet the following requirements:
-
Your program should implement the
build_cards
function with the same number of parameters, return type, and behavior as described above. -
Your program should implement the
print_cards
function with the same number of parameters, and behavior as described above. -
Your program should implement the
handle_guess
function with the same number of parameters, return type, and behavior as described above. -
Your program should implement the
play_one_turn
function with the same number of parameters, return type, and behavior as described above. -
The main function should put all the pieces together and implement a fully playable game that allows the user to take at most six turns before printing the final results, as shown in the full example above.
Your solution should be contained within a main
function that you call at the end of your program.
(Optional) Extra Challenge
If you’d like to make your game more interesting, here are some fun things to
try. These are optional, but if you’re interested in trying them, copy your
memory.py
file to a new file named memory_fun.py
and add these fun things
to your memory_fun.py
file.
-
Arrange the cards into a grid instead printing them all on a single line.
-
When a player gets a match, don’t count it against their number of turns.
-
Allow for more than one players.
Do you have other ideas? Try them out! Make the game more fun for you. Just be
sure you put your ideas in memory_fun.py
. Your memory.py
program should be
the one that we will use to determine if you have met the requirements for the
lab.
Answer the Questionnaire
After each lab, please complete the short Google Forms questionnaire. Please select the right lab number (Lab 05) from the dropdown menu on the first question.
Once you’re done with that, you should run handin21
again.
Submitting lab assignments
Remember to run handin21
to turn in your lab files! You may run handin21
as many times as you want. Each time it will turn in any new work. We
recommend running handin21
after you complete each program or after you
complete significant work on any one program.
Logging out
When you’re done working in the lab, you should log out of the computer you’re using.
First quit any applications you are running, including your vscode editor, the browser and the
terminal. Then click on the logout icon ( or
) and choose "log out".
If you plan to leave the lab for just a few minutes, you do not need to log
out. It is, however, a good idea to lock your machine while you are gone. You
can lock your screen by clicking on the lock icon.
PLEASE do not leave a session locked for a long period of time. Power may go
out, someone might reboot the machine, etc. You don’t want to lose any work!