Test Project 2

Initial Due Date: 2024-12-09 8:00AM
Final Due Date: 2024-12-16 4:15PM

Background

A test project is an assignment that you complete on your own, without the help of others. It is like a take-home exam. You may use the textbook, your notes, your previous assignments, the notes and examples on the course web page and the Python library documentation (linked from the course web page), but use of any other source, such as Google, is not permitted. You may not discuss this assignment with anyone except the course instructor, ASIs and peer drop-in course assistants. However, all of those parties may only give you limited answers (as you would expect during an exam).

To be clear, you may not work with, discuss, or in any way collaborate with anyone else.

Try to get as far as you can. If you are stuck, contact me. While I cannot help you solve the test project itself, I may be able to suggest general problem-solving strategies, help with conceptual difficulties about Python, and or direct you to relevant examples from class.

Memory Game

For this test project you will be implementing a text-based version of the game “Memory” (aka concentration), where the user has to find matching pairs of cards. See the Wikipedia page on the game for more information.

When the game starts, all of the cards are face down. In our version there are 16 cards, each indicated by a numbered square. The cards are laid out in four rows of four cards each:

1  2  3  4  
5  6  7  8  
9  10 11 12 
13 14 15 16 

“Underneath” each of these numbers is a letter. The user does not see these, but the program will keep track of them. For example, the board above may have the following letters underneath:

G  D  B  F  
H  B  C  D  
A  A  E  E  
F  H  G  C 

The game proceeds by having the user pick two squares to look under by entering the numbers of the squares separated by a space.

1  2  3  4  
5  6  7  8  
9  10 11 12 
13 14 15 16 
Guess two squares:

For example, to view squares 1 and 2 the user would type 1 2; to view 7 and 15 the user would type 7 15. The two cards that the user specified are then shown. For example, if the user entered 7 15 they would see:

1  2  3  4  
5  6  C  8  
9  10 11 12 
13 14 G  16 

If the user’s selected cards do NOT match, like 7 (C) and 15 (G), then the cards are displayed for 2 seconds and then turned back over leaving just the numbers. The user is then prompted for another choice:

1  2  3  4  
5  6  7  8  
9  10 11 12 
13 14 15 16 
Guess two squares:

If the user selects two that do match, for example 2 and 8 (both D) then those two are flipped over and they stay flipped over for the rest of the game. The user is then immediately prompted to enter an additional pair.

1  D  3  4  
5  6  7  D
9  10 11 12 
13 14 15 16 
Guess two squares:

If the user enters invalid numbers (outside the range 1-16, or cards that have already been turned over), the program notifies the user of the invalid input and prompts them again:

Guess two squares: 1 33
Invalid number(s).
Guess two squares:

The game ends once the user finds all matching pairs. When the game ends, the text “You win!” is displayed along with the finished board, the number of valid guesses, and the amount of time it took the player to solve the game (the time does not need to be rounded). Specifically:

You win!
G  D  B  F 
H  B  C  D  
A  A  E  E  
F  H  G  C
It took you 15 guesses and 99 seconds.

A Demo

Memory game demo

A snippet of the game is shown above. You can optionally try out the game for yourself on a remote server by doing the following. Note that to access this server from off-campus, you will need to be connected to the Middlebury VPN. Unfortunately you will need to request VPN access.

  1. Start Terminal from within Thonny via the “Tools -> Open System Shell” menu option.

  2. Connect to the basin.cs.middlebury.edu server via using the following command, replacing <username> with your Middlebury username. Answer yes to any question and then enter your Middlebury password (as a security measure nothing will show on the screen while you type your password).

    ssh <middlebury username>@basin.cs.middlebury.edu

    e.g. for me it is ssh mlinderman@basin.cs.middlebury.edu. For Windows users, if you get an error the “ssh” command is not found, you can download and use the Putty SSH program. Note that you want the “putty.exe” binary (further down on the page), not the package installer. Here are some additional instructions for using Putty.

  3. Once connected, in the Terminal window type the following and then Enter:

    ~mlinderman/courses/cs146/f24/tp2

    This should execute the example program in the terminal window. If you want to quit the game early, you can type Ctrl + c. To disconnect from the remote server, type exit at the prompt, then Enter.

Specifications

  1. Your program must exactly implement the game play shown above:
    • The displayed board must look exactly like the board shown above with the columns lined up correctly and left justified (each column has a width of exactly 3 characters and there are no blank lines between rows).
    • It must display the flipped over entries for 2 seconds if the guess is incorrect. If the guess is correct it immediately shows the updated board and prompts for another guess.
    • We can’t delete previously printed text. Instead print a 100 blank lines before printing the board again (each time you do so in a turn) so it will appear to the user like the board is simply being updated.
    • The game should finish when all of the entries have been correctly guessed and are flipped over. You should then display the amount of time it took and the total number of valid guesses that the user made. A valid guess is any guess where the cards are flipped over (whether they match or don’t match).
    • The “underside” of the entries will be pairs of the letters ‘A’ through ‘H’ (for a 4x4 game). Each time the game is played you should get a different, random, configuration of the letters.
  2. Your game must be launched by invoking a function named play_game that does not take any parameters.
  3. When run, your program should start playing the game automatically. If your module is imported, however, the game should not start playing.
  4. You can assume that the user enters two numbers separated by a space. However, you must check that the user input is valid, specifically:
    • That the numbers are valid entries on the board (e.g. 1-16 for the default board size)
    • That the entries corresponding to the numbers entered have not already been correctly guessed (i.e., flipped over)
    • That the two numbers are different (e.g., “3 3” is not a valid input)
  5. You should define constants for the number of rows and columns at the top of your program, and use these constants throughout. Your submitted version should use 4 rows and 4 columns.

If you follow the above requirements and use satisfactory style, that will earn “Meets expectations”. If your implementation is clear, concise, readily understood, and maintainable and can easily generalize to other board sizes, solely by changing your two constants that specify the number of rows and columns, it will earn “Exemplary. This means you will need to use loops for initializing and printing the board. Any even number of squares should be supported, up to 50 squares (25 pairs of letters), with no more than 10 rows or 10 columns. For instance, if 5 rows and 8 columns are specified, the game should use 20 pairs of letters (‘A’ through ‘T’).

A note about testing

Gradescope is configured to test your game by actually playing it. There is effectively one test, “Play entire game (till victory) including checking invalid inputs” and as the name suggests it plays the game to completion (including trying out invalid inputs). It necessarily looks and waits for the expected output and input prompts. Thus your program must generate the same output exactly (including capitalization, punctuation, etc.). Pay close attention to the error messages for an indication of what might have failed. If you observe “Timeout” errors that indicates the test code could not find the output (the printed strings) it expected and was waiting to appear.

Guide

As always, I strongly suggest an incremental approach to developing your program (but not when it comes to reading the assignment, read it in its entirety first). Before you start programming, develop a design that describes what functions you will need, the parameters each function should take and how each function will work. You won’t turn this in, but it will save you a lot of time if you think through the design of the program first.

Before you start coding make sure to read through the entire assignment as there are numerous implementation suggestions that will help you along the way. As you start to code, work incrementally - pick one aspect of the game at a time and get it working before moving on. In particular work through the game play “in order”, continually testing with Gradescope, to ensure what you build matches the specifications (i.e., get the display working correctly, then handle valid guesses correctly, then handle invalid guesses..).

Representing state

As you’re thinking about how you will write your program, think about what information you need to store and update as the game progresses and how you are going to represent this information. For example, in the math game we need to keep track the current equation, the correct answer, the number guesses, etc. Some of the information was visible, e.g., the equation, and some was hidden, e.g., the answer. What information do we need to keep track of for this game? And how will it be stored? As a string, a list, a set, a dictionary, etc.? You should choose data structures that are convenient and efficient. Your choice of data structure will be part of the exemplary style assessment; for example, using a string to represent the entire board will not be clear, concise, readily understood, and maintainable.

Game play

Your program will necessarily have a loop to repeat the steps of each “turn”. In each turn:

  1. The current version of the board should be displayed
  2. A valid guess should be obtained from the user
  3. Check whether the guess is a match or not and act accordingly

This is a rough skeleton of each turn. I suggest making a more detailed listing of the game play in each turn during the planning phase.

Notes and suggestions

There are many ways of implementing this program. Here is one approach:

  1. Pick your board representation. You will need an approach for mapping “card numbers” to the underlying letter and keeping track of which numbers on the board have been correctly guessed. Enumerate the different actions in the game, e.g., “flipping card over” and map those actions to transformations to your chosen data structure(s). I encourage you to figure out your data structure(s) before writing any code.
  2. Write a function to generate a random board configuration using your chosen data structure. The random module has a function named shuffle that takes a list as an argument and randomly shuffles the elements in place. For example, try shuffling the list ['A', 'A', 'B', 'B'].
  3. Write a function to display the board. The board should be printed out as 4 lines. Recall that as game evolves you will be printing numbers for some positions and letters for others. It will be tempting to start by just printing numbers. Instead I encourage you to think about how you will print the current state of the board, i.e., what is visible for each card, for any state that might occur in the game.
  4. Obtain the input from the user and then, regardless of whether they match or not, show the updated version of the board (i.e., with the letters flipped over for the selected numbers), then revert back to the original board after the 2 second pause. Some suggestions:
    • In the time module there is a function named sleep that takes the number of seconds as a parameter and the program stops executing for that number of seconds.
    • When printing the “next” board, make sure to hide previous boards by printing a 100 blank lines as described in the specifications.
    • There are many ways of updating the board to reflect the current guess and then “undoing” that update if the guess is not right. Think about the best approach for your approach to storing the board.
  5. Update your game loop to determine when the pairs match versus when the pairs don’t match. To help you when you’re debugging, you can print out what the “underneath” letters are, though make sure to remove this printing before you submit.
  6. Add checks to make sure that the user enters valid numbers. You may assume that the user enters two integers separated by a space. You must however check to see if they are valid board spaces and whether or not they have already been successfully guessed. If the user’s input is not valid, then you should prompt the user to enter another selection without printing the board again. If you write a separate function that implements all of these different checks then your code will be much simpler.
  7. Add functionality to check if the user has won and print “You win!” when the game is won. Also print the number of valid guesses and time it took the user to solve the problem. At this point you should have a working version of the memory game. During testing, it may be easier if you make your game smaller (e.g. use just a 2 x 2 board or 2 x 4 board).

Some Python tips

To print multiple strings on the same line, use the optional keyword parameter end to specify what should be printed after each item (by default end is the newline character). See help(print). For example:

for i in range(3):
    print(i)
0
1
2
for i in range(3):
    print(i, end=" ")
0 1 2 
for i in range(3):
    print(i, end="")
012

Note that in both of the latter cases, changing end parameter means no newlines are printed at all. An alternate approach is to build up a string, and then print that string:

s = ''
for i in range(3):
    s += str(i)
s
'012'

The ljust method on Python strings is can be a helpful tool for implementing left-justification with correct spacing. ljsut will automatically pad a string out to the width specified in its argument, e.g.,

"a".ljust(3)
'a  '
"12".ljust(3)
'12 '

There are many ways to think about a grid, but one is as all combinations of sequences of rows and columns. Nested loops are natural way to implement all combinations of two sequences. A common pattern for iterating through all roles and columns is shown below. If we know the number of columns in each row, e.g., 4, we can compute the linear index from the current row and column as shown below.

for row in range(4):
    for col in range(4):
        print("Row:", row, "Col:", col, "Index:", row*4+col)
Row: 0 Col: 0 Index: 0
Row: 0 Col: 1 Index: 1
Row: 0 Col: 2 Index: 2
Row: 0 Col: 3 Index: 3
Row: 1 Col: 0 Index: 4
Row: 1 Col: 1 Index: 5
Row: 1 Col: 2 Index: 6
Row: 1 Col: 3 Index: 7
Row: 2 Col: 0 Index: 8
Row: 2 Col: 1 Index: 9
Row: 2 Col: 2 Index: 10
Row: 2 Col: 3 Index: 11
Row: 3 Col: 0 Index: 12
Row: 3 Col: 1 Index: 13
Row: 3 Col: 2 Index: 14
Row: 3 Col: 3 Index: 15

When you’re done

Before submitting your code, double check to make sure you have satisfied all of the requirements and your program runs properly in a newly started Python shell.

Make sure that your program is properly documented:

  • You should have a docstring comment at the very beginning of the file with your name, and section.
  • Each function should have an appropriate docstring (including arguments and return value if applicable).
  • Other miscellaneous inline/block comments if the code might otherwise be unclear.

In addition, make sure that you’ve used good coding style:

  • Use constants as noted above; avoid hard-coded or magic numbers in your code.
  • Avoid code duplication as much as possible and define functions to implement repeated computations.
  • Each function should perform one task. If a function definition is getting longer and longer, consider defining additional functions for subtasks.
  • Avoid global variables, i.e., variables used inside a function that were declared outside of the function (except for constants). Use parameters instead.
  • Use meaningful names for your functions, variables, and parameters.
  • Make your code readable by avoiding long lines that wrap, and using horizontal and vertical whitespace as appropriate.

Submit your program via Gradescope. Your program program file must be named tp2_memory.py. You can submit multiple times, with only the most recent submission (before the due date) graded. Note that the tests performed by Gradescope are limited. Passing all of the visible tests does not guarantee that your submission correctly satisfies all of the requirements of the assignment.

Gradescope will both import your file for testing and run it in the terminal so that make sure that, as specified, no code executes on import (i.e., your game should not start on import) but does start automatically when your program is run.

Grading

Assessment Requirements
Revision needed Some but not all tests are passing.
Meets Expectations All tests pass, the required functions are implemented correctly and your implementation uses satisfactory style.
Exemplary All requirements for Meets Expectations, your implementation can easily generalize to other board sizes, and your implementation is clear, concise, readily understood, and maintainable.