Simulate
How to Play Simulate
Simulate is a clone of the game Simon. There are four colored buttons on the screen. The buttons light up in a certain random pattern, and then the player must repeat this pattern by pressing the buttons in the correct order. Each time the player successfully simulates the pattern, the pattern gets longer. The player tries to match the pattern for as long as possible.
Source Code to Simulate
This source code can be downloaded from http://invpy.com/simulate.py. If you get any error messages, look at the line number that is mentioned in the error message and check your code for any typos. You can also copy and paste your code into the web form at http://invpy.com/diff/simulate to see if the differences between your code and the code in the book.
You can download the four sound files that this program uses from:
- http://invpy.com/beep1.ogg
- http://invpy.com/beep2.ogg
- http://invpy.com/beep3.ogg
- http://invpy.com/beep4.ogg
The Usual Starting Stuff
1. # Simulate (a Simon clone) 2. # By Al Sweigart al@inventwithpython.com 3. # http://inventwithpython.com/pygame 4. # Creative Commons BY-NC-SA 3.0 US 5. 6. import random, sys, time, pygame 7. from pygame.locals import * 8. 9. FPS = 30 10. WINDOWWIDTH = 640 11. WINDOWHEIGHT = 480 12. FLASHSPEED = 500 # in milliseconds 13. FLASHDELAY = 200 # in milliseconds 14. BUTTONSIZE = 200 15. BUTTONGAPSIZE = 20 16. TIMEOUT = 4 # seconds before game over if no button is pushed. 17. 18. # R G B 19. WHITE = (255, 255, 255) 20. BLACK = ( 0, 0, 0) 21. BRIGHTRED = (255, 0, 0) 22. RED = (155, 0, 0) 23. BRIGHTGREEN = ( 0, 255, 0) 24. GREEN = ( 0, 155, 0) 25. BRIGHTBLUE = ( 0, 0, 255) 26. BLUE = ( 0, 0, 155) 27. BRIGHTYELLOW = (255, 255, 0) 28. YELLOW = (155, 155, 0) 29. DARKGRAY = ( 40, 40, 40) 30. bgColor = BLACK 31. 32. XMARGIN = int((WINDOWWIDTH - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2) 33. YMARGIN = int((WINDOWHEIGHT - (2 * BUTTONSIZE) - BUTTONGAPSIZE) / 2)
Here we set up the usual constants for things that we might want to modify later such as the size of the four buttons, the shades of color used for the buttons (the bright colors are used when the buttons light up) and the amount of time the player has to push the next button in the sequence before the game times out.
Setting Up the Buttons
35. # Rect objects for each of the four buttons 36. YELLOWRECT = pygame.Rect(XMARGIN, YMARGIN, BUTTONSIZE, BUTTONSIZE) 37. BLUERECT = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN, BUTTONSIZE, BUTTONSIZE) 38. REDRECT = pygame.Rect(XMARGIN, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE) 39. GREENRECT = pygame.Rect(XMARGIN + BUTTONSIZE + BUTTONGAPSIZE, YMARGIN + BUTTONSIZE + BUTTONGAPSIZE, BUTTONSIZE, BUTTONSIZE)
Just like the buttons in the Sliding Puzzle games for "Reset", "Solve" and "New Game", the Simulate game has four rectangular areas and code to handle when the player clicks inside of those areas. The program will need Rect objects for the areas of the four buttons so it can call the collidepoint() method on them. Lines 36 to 39 set up these Rect objects with the appropriate coordinates and sizes.
The main() Function
41. def main(): 42. global FPSCLOCK, DISPLAYSURF, BASICFONT, BEEP1, BEEP2, BEEP3, BEEP4 43. 44. pygame.init() 45. FPSCLOCK = pygame.time.Clock() 46. DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) 47. pygame.display.set_caption('Simulate') 48. 49. BASICFONT = pygame.font.Font('freesansbold.ttf', 16) 50. 51. infoSurf = BASICFONT.render('Match the pattern by clicking on the button or using the Q, W, A, S keys.', 1, DARKGRAY) 52. infoRect = infoSurf.get_rect() 53. infoRect.topleft = (10, WINDOWHEIGHT - 25) 54. # load the sound files 55. BEEP1 = pygame.mixer.Sound('beep1.ogg') 56. BEEP2 = pygame.mixer.Sound('beep2.ogg') 57. BEEP3 = pygame.mixer.Sound('beep3.ogg') 58. BEEP4 = pygame.mixer.Sound('beep4.ogg')
The main() function will implement the bulk of the program and call the other functions as they are needed. The usual Pygame setup functions are called to initialize the library, create a Clock object, create a window, set the caption, and create a Font object that will be used to display the score and the instructions on the window. The objects that are created by these function calls will be stored in global variables so that they can be used in other functions. But they are basically constants since the value in them is never changed.
Lines 55 to 58 will load sound files so that Simulate can play sound effects as the player clicks on each button. The pygame.mixer.Sound() constructor function will return a Sound object, which we store in the variables BEEP1 to BEEP4 which were made into global variables on line 42.
Some Local Variables Used in This Program
60. # Initialize some variables for a new game 61. pattern = [] # stores the pattern of colors 62. currentStep = 0 # the color the player must push next 63. lastClickTime = 0 # timestamp of the player's last button push 64. score = 0 65. # when False, the pattern is playing. when True, waiting for the player to click a colored button: 66. waitingForInput = False
The pattern variable will be a list of color values (either YELLOW, RED, BLUE, or GREEN) to keep track of the pattern that the player must memorize. For example, if the value of pattern was [RED, RED, YELLOW, RED, BLUE, BLUE, RED, GREEN] then the player would have to first click the red button twice, then the yellow button, then the red button, and so on until the final green button. As the player finishes each round, a new random color is added to the end of the list.
The currentStep variable will keep track of which color in the pattern list the player has to click next. If currentStep was 0 and pattern was [GREEN, RED, RED, YELLOW], then the player would have to click the green button. If they clicked on any other button, the code will cause a game over.
There is a TIMEOUT constant that makes the player click on next button in the pattern within a number of seconds, otherwise the code causes a game over. In order to check if enough time has passed since the last button click, the lastClickTime variable needs to keep track of the last time the player clicked on a button (Python has a module named time and a time.time() function to return the current time. This will be explained later).
It may be hard to believe, but the score variable keeps track of the score. Inconceivable!
There are also two modes that our program will be in. Either the program is playing the pattern of buttons for the player (in which case, waitingForInput is set to False), or the program has finished playing the pattern and is waiting for the user to click the buttons in the correct order (in which case, waitingForInput is set to True).
Drawing the Board and Handling Input
68. while True: # main game loop 69. clickedButton = None # button that was clicked (set to YELLOW, RED, GREEN, or BLUE) 70. DISPLAYSURF.fill(bgColor) 71. drawButtons() 72. 73. scoreSurf = BASICFONT.render('Score: ' + str(score), 1, WHITE) 74. scoreRect = scoreSurf.get_rect() 75. scoreRect.topleft = (WINDOWWIDTH - 100, 10) 76. DISPLAYSURF.blit(scoreSurf, scoreRect) 77. 78. DISPLAYSURF.blit(infoSurf, infoRect)
Line 68 is the start of the main game loop. The clickedButton will be reset to None at the beginning of each iteration. If a button is clicked during this iteration, then clickedButton will be set to one of the color values to match the button ( YELLOW, RED, GREEN, or BLUE).
The fill() method is called on line 70 to repaint the entire display Surface so that we can start drawing from scratch. The four colored buttons are drawn with a call to the drawButtons() (explained later). Then the text for the score is created on lines 73 to 76.
There will also be text that tells the player what their current score is. Unlike the call to the render() method on line 51 for the instruction text, the text for the score changes. It starts off as 'Score: 0' and then becomes 'Score: 1' and then 'Score: 2' and so on. This is why we create new Surface objects by calling the render() method on line 73 inside the game loop. Since the instruction text ("Match the pattern by…") never changes, we only need one call to render() outside the game loop on line 50.
Checking for Mouse Clicks
80. checkForQuit() 81. for event in pygame.event.get(): # event handling loop 82. if event.type == MOUSEBUTTONUP: 83. mousex, mousey = event.pos 84. clickedButton = getButtonClicked(mousex, mousey)
Line 80 does a quick check for any QUIT events, and then line 81 is the start of the event handling loop. The XY coordinates of any mouse clicks will be stored in the mousex and mousey variables. If the mouse click was over one of the four buttons, then our getButtonClicked() function will return a Color object of the button clicked (otherwise it returns None).
Checking for Keyboard Presses
85. elif event.type == KEYDOWN: 86. if event.key == K_q: 87. clickedButton = YELLOW 88. elif event.key == K_w: 89. clickedButton = BLUE 90. elif event.key == K_a: 91. clickedButton = RED 92. elif event.key == K_s: 93. clickedButton = GREEN
Lines 85 to 93 check for any KEYDOWN events (created when the user presses a key on the keyboard). The Q, W, A, and S keys correspond to the buttons because they are arranged in a square shape on the keyboard.
The Q key is in the upper left of the four keyboard keys, just like the yellow button on the screen is in the upper left, so we will make pressing the Q key the same as clicking on the yellow button. We can do this by setting the clickedButton variable to the value in the constant variable YELLOW. We can do the same for the three other keys. This way, the user can play Simulate with either the mouse or keyboard.
The Two States of the Game Loop
97. if not waitingForInput: 98. # play the pattern 99. pygame.display.update() 100. pygame.time.wait(1000) 101. pattern.append(random.choice((YELLOW, BLUE, RED, GREEN))) 102. for button in pattern: 103. flashButtonAnimation(button) 104. pygame.time.wait(FLASHDELAY) 105. waitingForInput = True
There are two different "modes" or "states" that the program can be in. When waitingForInput is False, the program will be displaying the animation for the pattern. When waitingForInput is True, the program will be waiting for the user to select buttons.
Lines 97 to 105 will cover the case where the program displays the pattern animation. Since this is done at the start of the game or when the player finishes a pattern, line 101 will add a random color to the pattern list to make the pattern one step longer. Then lines 102 to 104 loops through each of the values in the pattern list and calls flashButtonAnimation() which makes that button light up. After it is done lighting up all the buttons in the pattern list, the program sets the waitingForInput variable to True.
Figuring Out if the Player Pressed the Right Buttons
106. else: 107. # wait for the player to enter buttons 108. if clickedButton and clickedButton == pattern[currentStep]: 109. # pushed the correct button 110. flashButtonAnimation(clickedButton) 111. currentStep += 1 112. lastClickTime = time.time()
If waitingForInput is True, then the code in line 106's else statement will execute. Line 108 checks if the player has clicked on a button during this iteration of the game loop and if that button was the correct one. The currentStep variable keeps track of the index in the pattern list for the button that the player should click on next.
For example, if pattern was set to [YELLOW, RED, RED] and the currentStep variable was set to 0 (like it would be when the player first starts the game), then the correct button for the player to click would be pattern[0] (the yellow button).
If the player has clicked on the correct button, we want to flash the button the player clicked by calling flashButtonAnimation() then, increase the currentStep to the next step, and then update the lastClickTime variable to the current time (The time.time() function returns a float value of the number of seconds since January 1st, 1970, so we can use it to keep track of time.)
114. if currentStep == len(pattern): 115. # pushed the last button in the pattern 116. changeBackgroundAnimation() 117. score += 1 118. waitingForInput = False 119. currentStep = 0 # reset back to first step
Lines 114 to 119 are inside the else statement that started on line 106. If the execution is inside that else statement, we know the player clicked on a button and also it was the correct button. Line 114 checks if this was the last correct button in the pattern list by checking if the integer stored in currentStep is equal to the number of values inside the pattern list.
If this is True, then we want to change the background color by calling our changeBackgroundAnimation(). This is a simple way to let the player know they have entered the entire pattern correctly. The score is incremented, currentStep is set back to 0, and the waitingForInput variable is set to False so that on the next iteration of the game loop the code will add a new Color value to the pattern list and then flash the buttons.
121. elif (clickedButton and clickedButton != pattern[currentStep]) or (currentStep != 0 and time.time() - TIMEOUT > lastClickTime):
If the player did not click on the correct button, the elif statement on line 121 handles the case where either the player clicked on the wrong button or the player has waited too long to click on a button. Either way, we need to show the "game over" animation and start a new game.
The (clickedButton and clickedButton != pattern[currentStep]) part of the elif statement's condition checks if a button was clicked and was the wrong button to click. You can compare this to line 108's if statement's condition clickedButton and clickedButton == pattern[currentStep] which evaluates to True if the player clicked a button and it was the correct button to click.
The other part of line 121’s elif condition is ( currentStep != 0 and time.time() - TIMEOUT > lastClickTime). This handles making sure the player did not "time out". Notice that this part of the condition has two expressions connected by an and keyword. That means both sides of the and keyword need to evaluate to True.
In order to "time out", it must not be the player’s first button click. But once they’ve started to click buttons, they must keep clicking the buttons quickly enough until they’ve entered the entire pattern (or have clicked on the wrong pattern and gotten a "game over"). If currentStep != 0 is True, then we know the player has begun clicking the buttons.