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:

  1. one call to make_answers

  2. one call to make_status

  3. one call to show_status that shows the initial status of all ??

  4. 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:

  1. A non-negative integer

  2. Within the range of valid list indices in the status board

  3. 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.

  1. choice is a string representation of a non-negative integer.

  2. The choice is within the range of valid list indices in the status board.

  3. 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 :

  1. Make the shuffled list of pairs of the items provided.

  2. Make a initially blank game status board.

  3. 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.

  4. 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 (logout icon or other logout icon) 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 xlock 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!