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:

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

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

  1. Copy the entries of the emojis list into a new result list.

  2. Multiply the result list by 2 to double all the entries, thus giving you six pairs of entries.

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

  1. The integer entered by the user is a valid list index (0 to 11).

  2. 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 play_one_turn, you should be sure to test some cases that return True and others that return False. When testing, you may want to strategically adjust your input lists so that you can more easily find matches during testing. Just be sure to remove these adjustments after you’re done 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!

  1. Initialize starting variables, including the number of turns, matches, and the status list.

  2. Build the cards list.

  3. While the game hasn’t yet ended…​

    1. Play one round of the game.

    2. Update the count of matches and turns as necessary.

    3. Prompt the user to press enter to continue (this can be a call to the input function where you ignore the return value).

    4. Call system("clear") to clear the terminal output so that the user can’t simply scroll up to cheat. :)

  4. Print the final results.

You may not want to add the call to system("clear") until after you’re done debugging all the other functionality. It may be helpful during debugging to be able to scroll back up and reference older output.

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:

  • Write a comment that describes the program as a whole using a comment at the top of the file.

  • All programs should have a main() function.

  • Use descriptive variable names.

  • Write code with readable line length, avoiding writing any lines of code that exceed 80 columns.

  • Write a comment for each function (except main) that describes its parameters, return value, and purpose.

  • (If applicable) Add comments to explain complex code segments.

Your program should meet the following requirements:

  1. Your program should implement the build_cards function with the same number of parameters, return type, and behavior as described above.

  2. Your program should implement the print_cards function with the same number of parameters, and behavior as described above.

  3. Your program should implement the handle_guess function with the same number of parameters, return type, and behavior as described above.

  4. Your program should implement the play_one_turn function with the same number of parameters, return type, and behavior as described above.

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

  1. Arrange the cards into a grid instead printing them all on a single line.

  2. When a player gets a match, don’t count it against their number of turns.

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