CS21 Lab 5: More Functions/Memory Game
Due Saturday, October 14, before midnight
Goals
The goals for this lab assignment are:
-
Write and test functions using mutable lists
-
Design an interactive game of memory
-
Practice while loops and input validation
Work through the following sections and ask if you have questions!
Programming Tips
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 have a top-level comment! Please see our function example page if you are confused about writing function comments.
1. Overview
The game of memory is a card/tile matching game consisting of multiple pairs of matching words or pictures. In the solo version of the game, a player turns over two cards and determines if they are a match. If the cards match, they can be removed from play by keeping them face up for the remainder of the game. If the cards do not match, they are turned back over, hiding the pictures and the player picks another pair to see if they are a match. This process continues until all matches have been found.
You will write a simplified version of this memory game and track the number of turns it takes to find all the matches.
2. Displaying the cards
For our python version of this game, we will being using fruit emoji as our playing cards. An example game after finding all the matches will look as follows.
0 1 2 3 4 5 6 7 8 9 10 11 🍌 🥝 🍉 🍇 🍌 🍎 🍎 🥝 🍉 🍊 🍇 🍊
In main
, we have defined the initial list of items that we will use to make pairs.
items = ['🍎', '🍇', '🍌', '🍊', '🥝', '🍉']
Your first task is to make two new lists. The first list, will contain two copies of each item, and be shuffled once. This list represents the answers, or what is behind each card displayed to the user. Once created and shuffled, you will not need to modify this list for the remainder of the program.
The second list is the same length as the first, and displays the current status of the game. Each card of this list is either hidden and displayed as ??
or previously matched, in which case it is displayed by the corresponding element in the answer list.
At the start of the game, the status list should contain all ??
, e.g.,
0 1 2 3 4 5 6 7 8 9 10 11 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??
After the first successful match, you can display an updated version showing all currently matched cards by the answer, and remaining unmatched cards as a ??
, e.g.,
0 1 2 3 4 5 6 7 8 9 10 11 🍌 ?? ?? ?? 🍌 ?? ?? ?? ?? ?? ?? ??
2.1. Making the answer key
To generate the first list, write a function make_answers(items)
. The input parameter items
is a list of strings, representing the set of strings/emojis that you wish to match. Each string in items
is unique and you need to create a matching pair for each item in items
.
You can create a new list of strings by concatenating two lists of strings using the +
sign. Note that concatenating two lists of strings generates a new list of strings, not one big string.
l1 = ["a"]
l2 = ["b"]
l3 = l1 + l2 # The list ['a', 'b']
l4 = l3 + l3 # The list ['a', 'b', 'a', 'b']
Once you have created your list of pairs, you can shuffle it once using the shuffle()
function from the random
library.
To use shuffle
, place the line
from random import shuffle
at the top of your program, outside the definition of main
. To shuffle a list lst
, use the syntax shuffle(lst)
. The shuffle
function does not return a new list. Instead, it rearranges and modifies the contents of lst
lst = [0, 1, 2, 3, 4]
shuffle(lst)
print(lst) # will display a shuffled version of lst
# e.g. [4, 1, 0, 3, 2]
After making your shuffled list of pairs, return this new list of pairs. Test your make_answers
function in main
, by calling the function, saving the returned result as a variable and printing the result. Do not continue with the next step until make_answers
is working as described. Your function should work with any size list of items, not just the sample list provided in main()
.
2.2. Making the current status list
Write a function make_status(answers)
that creates and returns an initially blank board to display to the user. The answers
parameter is a list of strings, typically created by make_answers
and stored in main()
. You should not modify the answers
list directly. Instead, create a list of ??
strings that is of the same length as the parameter answers
and return the newly created list. Recall from above that you can use list concatenation and that ["??"] + ["??"]
creates a new list ["??", "??"]
. You can also use lst.append("??")
multiple times on an initially empty list.
Again, test your make_status
function in main() and save the result as a second variable distinct from the answers list.
2.3. Displaying the status list nicely
For your third function, write the function show_status(status)
that doesn’t return anything, but prints a display of the current game status. Your function should print the numeric index for each card position in the game and then the current status ??
or a previously matched card after a match has been made. You can test this function in main
on both the status list and the answer list. They should print the following:
show_status(status)
, where status
is the current status board, initially blank
0 1 2 3 4 5 6 7 8 9 10 11 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??
show_status(answers)
, where answers
is the initial shuffled set of cards
0 1 2 3 4 5 6 7 8 9 10 11 🍌 🥝 🍉 🍇 🍌 🍎 🍎 🥝 🍉 🍊 🍇 🍊
Your implementation just needs to print two strings. You can use the string accumulator pattern twice to create these strings. First, create a string representing the positions of items in the list. Second, create a string representing the concatenation of the items in the list. You may need to add some spaces in between to get the two rows to line up a little, but the alignment does not need to be perfect.
2.4. Display Checkpoint
Before proceeding, ensure that all three functions above are working correctly. Your main`
function should have:
-
one call to
make_answers
-
one call to
make_status
-
one call to
show_status
that shows the initial status of all??
-
another call to
show_status
that displays the answer key.
You can keep the printing of the answer key in your main function as you continue to develop and test your program. In the real game though, the answer key should not be shown to the user when starting the game.
3. Picking a card
Once we can create and display a game board, it is time to start implementing the game logic and allowing the user to pick cards.
Ultimately, we want to write a function get_choice(status)
which displays the current status of the game and prompts the user to enter a valid numeric slot to pick. A valid choice for the user should be:
-
A non-negative integer
-
Within the range of valid list indices in the status board
-
A position that has not been previously picked or matched.
Once the user picks a valid card position, the get_choice
function should return the integer id of the user’s pick.
You will need to write some small helper functions to aid in the implementation of your get_choice
function.
3.1. Check if a string is a number
We provide a is_a_number(txt)
in the f23_memory
module.
from f23_memory import is_a_number
This function has one input parameter, txt
, which is a string type. The is_a_number
function will return a Boolean value True
if there is at least one character in the string, and all the characters in the string txt
are digits between 0
and 9`inclusive. Otherwise, the function should return `False
.
txt |
is_a_number(txt) |
"" |
False |
"0" |
True |
"123" |
True |
"123xyz" |
False |
You will use is_a_number
to help get a valid choice as outlined below.
3.2. Checking if a choice is valid
Write a function is_valid(choice, status)
. This function has two parameters: an string choice
and a list of the current board status, status
. The function is_valid
should return a Boolean True
or False
. Return True
if all of the following conditions are met.
-
choice
is a string representation of a non-negative integer. -
The choice is within the range of valid list indices in the status board.
-
The item at the chosen position in
status
is??
meaning it has not been previously picked/match.
If the current board is as shown below
0 1 2 3 4 5 6 7 8 9 10 11 🍌 ?? ?? ?? 🍌 ?? ?? ?? ?? ?? ?? ??
then the return value of is_valid
should be as follows
choice |
is_valid(choice, status) |
"one" |
False |
"20" |
False |
"2" |
True |
"11" |
True |
"4" |
False |
You should use is_a_number
to check if choice
is a number before trying to convert it to an int.
Don’t assume that status is always of length 12. Again, try a few tests calls to is_valid
in main
before continuing.
3.3. Putting together the get_choice
function.
With is_valid
complete, you can now implement get_choice(status)
. This function takes a list of strings status
representing the current game board as input, prompts the user to pick a valid choice, and returns the integer index of the valid choice once the user has picked an appropriate index. You should use is_valid
to check if a user response is acceptable. If not, repeatedly prompt the user for a valid choice. Don’t forget to convert a valid response from a string to an integer before returning from get_choice
.
3.4. Picking a Card Checkpoint
In main
, make a single call to get_choice
and print the returned result. Ensure that get_choice
and is_valid
are working properly before proceeding. Note you do not need to check is_valid
directly in main
since get_choice
should be calling is_valid
.
4. Playing one turn
Once you can make the board, display the status, and single card (see the checkpoints above), implement the logic to take a single turn of the memory game in a function take_turn(answers, status)
. The list answers
is the list of shuffled strings representing the complete answer key. The list status
is the current status, containing a mix of matched pairs and unknown ??
positions. The function take_turn
will prompt the user for two valid index picks. After each pick you should flip the card at that position to reveal the hidden element (see flip_card
below). If the cards match, print a message congratulating the user and return the value True
indicating that take_turn
successfully found a match.
If the two user picks are not a match, print an encouraging message to try again, flip the two unmatched cards back to ??
and return False
indicating that no match was found this time.
The implementation of take_turn
should use the functions show_status
and get_choice
as described above and the helper function flip_card
described below.
4.1. Flipping a card and updating status
When a user picks a new card to reveal, you should update the current status
list to revel the last picked card. Write a function flip_card(answers, status, n)
that can flip a card at an integer position n
. The list answers
is the list of shuffled strings representing the complete answer key. The list status
is the current status, containing a mix of matched pairs and unknown ??
positions. The function flip_card
should update the status list at position n
to either be revealed or hidden.
If the current board is as shown below
0 1 2 3 4 5 6 7 8 9 10 11 🍌 ?? ?? ?? 🍌 ?? ?? ?? ?? ?? ?? ??
Then flip_card(answers, status, 2)
would change the ??
in position 2 to the corresponding answer at position 2, e.g.,
0 1 2 3 4 5 6 7 8 9 10 11 🍌 ?? 🍉 ?? 🍌 ?? ?? ?? ?? ?? ?? ??
A call to flip_card
should modify the list status
, but not the list answers
. Calling flip_card(answers, status, 2)
again after the 🍉 is revealed will hide the status, reverting the status list back to
0 1 2 3 4 5 6 7 8 9 10 11 🍌 ?? ?? ?? 🍌 ?? ?? ?? ?? ?? ?? ??
Because input validation will happen before any call to flip_card
, this function can assume that answers
and status
are the same length, and n
is a valid integer index.
5. Writing the final game
With all your helper functions implemented and tested, it is time to write a final version of main
that plays the entire game. Your main
function should do the following :
-
Make the shuffled list of pairs of the
items
provided. -
Make a initially blank game status board.
-
Repeatedly take a single turn and keep track of total matches made and total turns. Your program should print the turn number before each call to take_turn.
-
Stop the game when the user has found all the matches and print the total number of turns it took to find all the matches.
5.1. OPTIONAL: Using clear_screen
for better gameplay
Once your game is complete, you can add a call to clear_screen()
before taking a turn. The clear_screen
function will clear the screen, preventing you from scrolling back and seeing previous turns. This makes the game more interesting, and requires you to rely on your memory. To pause the game at the end of one turn and the beginning of the next, you can use an input
statement to prompt the user to press enter to continue, e.g.,
input("Press enter to continue") #pauses game until user presses enter
Omitting the clear_screen
during your initial development can make it easier to design and debug your solution.
Example output
It may help to view example output showing all the turns from beginning to the end of a sample game.
Answer the Questionnaire
Each lab will have a short questionnaire at the end. Please edit
the Questions-05.txt
file in your cs21/labs/05
directory
and answer the questions in that file.
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, like 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!