CS21 Lab 7: Simon

Due 11:59pm Saturday, March 22nd
For this lab you will write one program, simon.py, that plays the memory game of Simon. First, run update21, if you haven't already, to create the cs21/labs/07 directory. Then cd into your cs21/labs/07 directory and create the python program for lab 7 in this directory (handin21 looks for your lab 7 assignments in your cs21/labs/07 directory):

$ update21
$ cd ~/cs21/labs/07
$ pwd

For this lab we will continue using the Zelle graphics library to play the classic game of Simon. You will practice top-down design by writing your program as several functions that implement the game.

Playing the Game

The game of Simon is a memory game played on a board with four colored buttons. One round of the game consists of the computer lighting up one or more of the lights in a sequence. The user then tries to replicate the sequence by pressing the buttons in the same order that was shown to the user. The game ends as soon as the user clicks a color that is not next in the sequence. The user wins the round by pressing all the buttons in the same order. The computer then repeats the same sequence, adding one more random button to the sequence. To get a feel for the game, you can play Simon online.

Here is a video of someone playing the verison of the game written for this lab.

Designing our Simon game

We're going to guide you closely on this lab using the principles of top-down design, describing the functions you should be creating. In later labs, this part of the solution design will be up to you.

Let's think about the major steps your program will need to accomplish in the main() function:

  1. Create a graphics window.
  2. Create the game board.
  3. Let the player know the game is about to begin.
  4. While entered pattern is correct, repeat:
    1. Pick a random square to add to the sequence
    2. Show the sequence of flashing lights.
    3. Get button presses from the user.
    4. Let the player know if they won or lost.

Now, let's look at all these steps and see if they need further refinement.

Create a graphics window. A window of 500x500 is a good size to create. Give it the title "Simon". This is just a line of code and doesn't even need to be a function. In fact, it is often bad style to create a separate function for a single line of code.

Create the game board. There are enough steps to creating the game board it should be a function. This function should take the GraphWin object you created in the previous step as a parameter. We'll draw the board as four equal squares. To help with drawing the board, you may want to use the setCoords() method of the GraphWin class. This will change the coordinate system to make the rest of the implementation easier for you. It takes four arguments: setCoords(xll, yll, xur, yur). Where the lower-left corner of the new coordinate system is (xll, yll) and the upper-right corner of the new coordinate system is (xur, yur). For example, win.setCoords(0,2,2,0) will give the following coordinate system:

A good choice of colors for the squares are "green4", "red4", "yellow4", and "blue4". These are the ones I used, but feel free to choose other colors if you wish. Your function should take the four rectangle objects you created and return them as a list. This list of rectangle objects will be used in several of our other functions.

Let the player know the game is about to begin.

We'll do this with a Text object. We'll need to display several messages during the game, for example, to tell the player the game is about to begin, if they successfully completed a round, or if they have lost the game. Let's write a helper function to do this. This function should have two parameters. One parameter should be the window object to show the message. The other parameter should be a text string, the message to display to the user. The function should work as follows: It should create a text object, center the text in the middle of the board, set the text large enough to read, and set the color to white so it is easy to read. It should show the text and then wait for the user to click on the window. Once the user clicks on the window, it should get rid of the message and the function should return.

Pick a random square to add to the sequence. It's a good idea to keep the sequence of squares to light up as a list. Adding a random square to the list is just a couple lines of code.

Show the sequence of flashing lights. To simulate the flashing lights of the board game, you can change the color of the rectangle to a brighter version of that color, pause with the sleep() function, and then change it back to its original color. I suggest "green", "red", "yellow", and "blue" as the bright colors to change briefly to (and then change back to the already mentioned "green4", "red4", "yellow4", and "blue4" default colors). This function should take two parameters. The first one is a list of the rectangle objects, and the second parameter should be a list containing the sequence of rectangles to flash (perhaps as indexes to the rectangle list). This function should go through the sequence, flashing each rectangle in turn.

Flashing a rectangle is complicated enough to break out into its own function, called from the function described in the previous paragraph. It should handle the flashing of a rectangle by changing to a bright version of its color, pausing for a period of time, and then back to its original color. This additional function should take three parameters. The first one is a list of the four rectangle objects, the second one is an index of the list representing the rectangle to flash, and the third parameter specifies for how long to flash the rectangle.

Let's look a little more closely as to how to implement this function. The first parameter is a list of rectangle objects. You know the order of those rectangles (since you created them). The second parameter is the index of the rectangle to flash (0-3). Inside your function you can create two lists, one for the default colors and one for bright colors, in the same order as your rectangle list. To flash the rectangle you would implement something like the following pseudocode:

function flashRect(rectList, rectIndex, flashTime)
   normalColors =  ['green4', 'red4', 'yellow4', 'blue4']
   brightColors = ['green', 'red', 'yellow', 'blue']
   change color of rectList[rectIndex] to brightColors[rectIndex]
   wait for flashTime seconds
   change color of rectList[rectIndex] to normalColors[rectIndex]

Get button presses from the user. This is complicated enough to make into a function. The function should have three parameters. The first one is the GraphWin window object, as we'll need to get mouse clicks from the user. The second parameter is the list of rectangle objects, and the third parameter is the sequence of rectangles the user should press. The function should return True if the sequence was matched by the correct mouse clicks and it should return False as soon as the user clicks the wrong rectangle.

You might have noticed that in the description above, we have glossed over how to tell if the user has clicked the right rectangle. As you may have guessed, this is complicated enough to make into its own function. This function should have just one parameter, a GraphWin object. It should return an integer, representing the index of the rectangle that was clicked (from our rectangle list). This function works by getting the point of the mouse click from the user and seeing what rectangle that point lies in. The image of the board drawn with coordinates should help you determine which points lie within each rectangle.

Let the player know if they won or lost. We can do this by using the helper function we created for displaying text messages earlier.

Implementation Tips

Think about the data structures you are using in this program and how they are used as inputs to the functions. One import data structure is the list of the four rectangle objects. Another is a list of rectangles to flash. Think about the best way to represent this list. Hint: you already have a list of rectangle objects so you can take advantage of that fact.

Incrementally test your code. Write placeholder versions of all the functions you plan to implement that just return some constant value. As you work through the lab you will fill in the functions with working implementations. But you should always be able to run your program without it just plain crashing because some function is not defined. First have a simple main() which calls your createBoard() and then call getMouse() to have the program wait. Try printing the output from the getMouse() function to make sure you understand how the setCoords() method has changed the coordinate system. Do not move on till this is working.

Then add some code to your main() function to test your showMessage() function. Display a message to the user saying "Click to Start" to see that your program is working as you expect. It is the time for more testing!

Do this for every stage of you program. Every time you write a new function, run your code with debugging print statements to verify that your program is working how you think it should. It is much easier to debug your code incrementally.

There are several opportunities for extending this lab.
  1. For every round, shorten the time each rectangle lights up.
  2. Add a text object showing the current round of the game.
  3. Try different variations of the game. One variation is where you have to enter the sequence backwards from what is shown. Another variation is where the colors of the squares changes after each round.
Once you are satisfied with your programs, hand them in by typing handin21 at the Linux prompt.

You may run handin21 as many times as you like, and only the most recent submission will be recorded. This is useful if you realize, after handing in some programs, that you'd like to make a few more changes to them.