Simon was a very popular electronic game of memory skill invented by Ralph H. Baer and Howard J. Morrison. It creates a series of tones and lights and requires its user to repeat the sequence. If the user succeeds, the series becomes progressively longer and therefore, more complex.
It was so simple, and that’s perhaps why it was so ingenious.
Colors are very important in this game, that’s why it makes perfect sense to create a class representing them.
The console is split into four quadrants (red, blue, green, and yellow), and each lights up and plays a different tone when pressed. Each round, you have to remember the previous round’s pattern, and then pay attention as our good friend Simon will add one more ‘sound’ to the mix.
import pygame, configparser, sys, random, enum, time
This code is based on Making Games with Python & Pygame, Simulate by Al Sweigart, al@inventwithpython.com, http://inventwithpython.com/pygame, Creative Commons BY-NC-SA 3.0 US and it is released under the same license.
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
BRIGHTBLUE = ( 0, 50, 255)
NAVYBLUE = ( 0, 0, 128)
ROYALBLUE = ( 65, 105, 225)
WHITE = (255, 255, 255)
BRIGHTRED = (255, 0, 0)
RED = (155, 0, 0)
BRIGHTGREEN = ( 0, 255, 0)
GREEN = ( 0, 155, 0)
BRIGHTBLUE = ( 0, 0, 255)
BLUE = ( 0, 0, 155)
BRIGHTYELLOW = (255, 255, 0)
YELLOW = (155, 155, 0)
DARKGRAY = ( 40, 40, 40)
PINK = (255, 192, 203)
BRIGHTPINK = (255, 20, 147)
BORDERCOLOR = BRIGHTBLUE
BGCOLOR = PINK
MESSAGECOLOR = WHITE
global game
class Color(enum.Enum):
""" We identify each of these names with a particular color, button, and sound."""
YELLOW = 'Yellow'
BLUE = 'Blue'
RED = 'Red'
GREEN = 'Green'
def allColors():
""" It will return the four game's main colors."""
return [Color.YELLOW, Color.BLUE, Color.RED, Color.GREEN]
def getRectangle(self):
""" It will return the Rectangle object associated with the color. """
if self == Color.GREEN:
return game.greenButtonRect
elif self == Color.RED:
return game.redButtonRect
elif self == Color.BLUE:
return game.blueButtonRect
elif self == Color.YELLOW:
return game.yellowButtonRect
def getFlashColor(self):
""" It will return the bright version of the color. """
if self == Color.GREEN:
return BRIGHTGREEN
elif self == Color.RED:
return BRIGHTRED
elif self == Color.BLUE:
return BRIGHTBLUE
elif self == Color.YELLOW:
return BRIGHTYELLOW
def getSound(self):
""" It will return the sound associated with the color. """
if self == Color.GREEN:
return game.sound1
elif self == Color.RED:
return game.sound2
elif self == Color.BLUE:
return game.sound3
elif self == Color.YELLOW:
return game.sound4
class Game: # It represents the game itself
def __init__(self):
self.running = True # It indicates that the game is still running.
pygame.init() # It initializes all imported pygame modules.
self.fpsClock = pygame.time.Clock() # A pygame.time.Clock object helps us to make sure our program runs at a certain FPS.
self.config = configparser.ConfigParser() # We create an instance of the main configuration parser.
We will use the ConfigParser module to manage a user-editable configuration file for our application. It provides a structure similar to what’s found in Microsoft Windows INI files. Our config file slidepuzzle.ini is quite simple:
[BASIC]
# The file consists of sections, each of which contains keys with values.
WINDOWWIDTH = 640
WINDOWHEIGHT = 480
FPS = 30
BUTTONSIZE = 200
BUTTONGAPSIZE = 20
TIMEOUT = 4
self.config.read("simon.ini") # We read the configuration file.
self.font = pygame.font.Font(pygame.font.get_default_font(), 20) # pygame.font.Font allows us to render fonts in the game.
self.WINDOWWIDTH = int(self.config['BASIC']['WINDOWWIDTH'])
WINDOWHEIGHT = int(self.config['BASIC']['WINDOWHEIGHT'])
self.screen = pygame.display.set_mode((self.WINDOWWIDTH, WINDOWHEIGHT)) # It sets the display mode and creates an instance of the pygame.Surface class.
pygame.display.set_caption('Simon Says') # It sets the current window caption.
self.fps = int(self.config['BASIC']['FPS']) # This is our frame rate. It is expressed in frames per second or FPS. It is the frequency or rate at which consecutive images or frames are captured or displayed in the game.
self.timeout = int(self.config['BASIC']['TIMEOUT']) # It is the number of seconds before our user loses the game if he or she does not press any buttons when he or she is supposed to repeat the sound pattern.
self.MOUSEBUTTONUP = False
self.BUTTONSIZE = int(self.config['BASIC']['BUTTONSIZE']) # It indicates the size of the buttons.
BUTTONGAPSIZE = int(self.config['BASIC']['BUTTONGAPSIZE']) # It indicates the gap between two buttons.
XMARGIN = int((self.WINDOWWIDTH - (2 * self.BUTTONSIZE) - BUTTONGAPSIZE) / 2)
This is basic mathematics:
XMARGIN (the margin on the left) + BUTTONSIZE (the first button) + BUTTONGAPSIZE (the gap between the two buttons) + BUTTONSIZE (the second button; each row has two buttons) + XMARGIN (the margin on the right) = WINDOWWIDTH, and therefore, XMARGIN = ( WINDOWWIDTH - (2 * BUTTONSIZE) - BUTTONGAPSIZE ) / 2.
YMARGIN = int((WINDOWHEIGHT - (2 * self.BUTTONSIZE) - BUTTONGAPSIZE) / 2)
Playing sounds that are stored in sound files is very simple in pygame. First, we create a pygame.mixer.Sound object by calling the pygame.mixer.Sound() constructor function. It takes one string parameter, which is just the filename of the sound file. Pygame can reproduce WAV, MP3, and OGG files. To play this sound, we only need to call the Sound object’s play() method.
self.sound1 = pygame.mixer.Sound('resources/sound1.ogg') # We create four Sound objects.
self.sound2 = pygame.mixer.Sound('resources/sound2.ogg')
self.sound3 = pygame.mixer.Sound('resources/sound3.ogg')
self.sound4 = pygame.mixer.Sound('resources/sound4.ogg')
self.myPattern = [] # This game's attribute stores the pattern of colors that the user will need to repeat.
# Next, we are going to create the Rect objects for each of the four buttons.
self.yellowButtonRect = pygame.Rect(XMARGIN, YMARGIN, self.BUTTONSIZE, self.BUTTONSIZE)
self.blueButtonRect = pygame.Rect(XMARGIN + self.BUTTONSIZE + BUTTONGAPSIZE, YMARGIN, self.BUTTONSIZE, self.BUTTONSIZE)
self.redButtonRect = pygame.Rect(XMARGIN, YMARGIN + self.BUTTONSIZE + BUTTONGAPSIZE, self.BUTTONSIZE, self.BUTTONSIZE)
self.greenButtonRect = pygame.Rect(XMARGIN + self.BUTTONSIZE + BUTTONGAPSIZE, YMARGIN + self.BUTTONSIZE + BUTTONGAPSIZE, self.BUTTONSIZE, self.BUTTONSIZE)
self.userInputTurn = False # It indicates whether it is the user's turn to listen (False) or repeat (True) the round's pattern.
self.currentColor = 0 # This is the pattern's current index or color. It helps us to know which button or color the user should press next.
self.score = 0 # It indicates the length of the pattern. Each successful round, we will increase the pattern with a new color.
self.lastClickTime = 0 # This is the timestamp of the last time that the player has pushed a button.
The method gameLoop controls the overall flow for the entire game program. There are three tasks: 1. Check all events. 2. Process these events and take action accordingly. 3. Draw the current state of the game so the player can see what is going on.
def gameLoop(self):
while self.running: # If the player is actually playing...
if self.userInputTurn: # We will draw the board/game with a different message depending on whether it is the user's turn to repeat the pattern or just to listen to the pattern.
self.drawGame("It is your turn! Score: " + str(self.score))
else:
self.drawGame("Listen and Repeat. Score: " + str(self.score))
self.check_events() # It checks all the events.
if not self.userInputTurn: # If it is not the user's turn...
self.myPattern.append(random.choice((Color.YELLOW, Color.BLUE, Color.RED, Color.GREEN)))
self.score += 1 # ... we add a new color to the mix and increase the user's score by one.
for color in self.myPattern: # We loop for each color in the pattern and then we animate the corresponding button, play its sound, and wait for a little while.
self.animation(color)
pygame.time.wait(100)
self.userInputTurn = True # Finally, it is the user's turn to repeat the sound sequence.
else:
# It is the user's turn to repeat the sound sequence. Let's process the events.
if self.MOUSEBUTTONUP: # The user has clicked on one of the mouse's buttons.
colorButton = self.getButtonClicked(self.pos[0], self.pos[1]) # If the user has pressed on one of the game's buttons, getButtonClick returns its color. Otherwise, it returns None. getButtonClicked takes the X, Y parameters of the mouse's click.
if self.currentColor != 0 and time.time() - self.timeout > self.lastClickTime: # If it is not the player's first button click (the first sound in the sequence) and the user has taken too long to press on one of the game's buttons, the player has lost. It is game over.
self.running = False
self.gameOver("Game Over. Time Out.")
elif colorButton is not None and colorButton!=self.myPattern[self.currentColor]: # If the user has pressed on one of the game's buttons, but it is the wrong one, then it is also game over.
self.running = False
self.gameOver("Game Over. Wrong Button.")
elif colorButton is not None and colorButton==self.myPattern[self.currentColor]: # If the user has pressed on the right button...
self.animation(colorButton) # We animate the corresponding button and play its sound.
pygame.time.wait(100)
self.currentColor += 1 # We update the pattern's index or current color.
self.lastClickTime = time.time() # And the last click's timestamp.
if self.currentColor == len(self.myPattern): # The user has guessed correctly this round's sound sequence.
self.userInputTurn = False # The user's turn to repeat the sequence has ended.
self.currentColor = 0 # And reset the pattern's index or current color.
self.reset_keys() # We reset the keys as we have already processed the key's associated event.
def animation (self, color):
""" It creates an animation for the button corresponding to "color" and plays its sound."""
flashColor = color.getFlashColor()
rectangle = color.getRectangle()
sound = color.getSound()
sound.play()
self.animationButton(color, rectangle, flashColor)
def animationButton(self, color, rectangle, flashColor):
""" It is an auxiliary method to create animations for our buttons."""
origSurf = self.screen.copy() # It creates a copy of the display Surface.
flashSurf = pygame.Surface((self.BUTTONSIZE, self.BUTTONSIZE)) # It creates a new Surface object. Its size is the size of a button.
flashSurf = flashSurf.convert_alpha()
textSurf = self.font.render(color.name, True, WHITE) # It creates a Surface object for writing the color's name in white.
textRect = textSurf.get_rect() # It creates a Rectangle object for writing the color.
textRect.center = rectangle.left + self.BUTTONSIZE/2, rectangle.top + self.BUTTONSIZE/2 # It positions the rectangle's center in the middle of the button.
r, g, b = flashColor
for alpha in range(0, 255, 10):
self.screen.blit(origSurf, (0, 0)) # It draws (and blends) the new Surface object (origSurf) onto our main display Surface (self.screen).
flashSurf.fill((r, g, b, alpha)) # On top of that, the bright color version of the button is drawn over the button. The alpha value of the bright color starts off at 0 for the first frame of animation, but then on each frame after, the alpha value is increased by 10. It will look like the button is slowly, but surely, brightening up.
self.screen.blit(flashSurf, rectangle.topleft) # It draws the flashSurf surface onto the game's main surface.
pygame.draw.rect(self.screen, flashColor, (rectangle.left, rectangle.top, self.BUTTONSIZE, self.BUTTONSIZE), 4) # It draws a "flashColor" border for our button.
self.screen.blit(textSurf, textRect) # It draws the text onto the game's main surface.
pygame.display.update() # It will update the full display surface to the screen.
self.fpsClock.tick(self.fps) # It makes sure that the animation could be seen by the user.
self.screen.blit(origSurf, (0, 0))
def check_events(self): # Pygame handles all its event messaging through an event queue. This method checks all these events.
for event in pygame.event.get(): # It gets events from the queue.
if event.type == pygame.QUIT:
self.running = False
sys.exit()
if event.type == pygame.MOUSEBUTTONUP:
self.MOUSEBUTTONUP = True
self.pos = event.pos
def reset_keys(self): # Reset all the keys
self.MOUSEBUTTONUP = False
def drawGame(self, message=None): # It draws the actual state of the game.
self.screen.fill(BGCOLOR) # It fills the game's main surface with the predefined background color.
self.drawButtons() # It draws the four buttons.
self.drawMessage(message) # It draws the message at the top of the window.
pygame.display.update() # It will update the full display surface to the screen.
self.fpsClock.tick(self.fps) # It just sets up how fast our game should run.
def drawButtons(self): # It draws the four buttons.
pygame.draw.rect(self.screen, YELLOW, self.yellowButtonRect)
pygame.draw.rect(self.screen, BLUE, self.blueButtonRect)
pygame.draw.rect(self.screen, RED, self.redButtonRect)
pygame.draw.rect(self.screen, GREEN, self.greenButtonRect)
def drawMessage(self, message):
""" This method handles the drawing of "message" at the top of the window. """
textSurf = self.font.render(message, True, WHITE, RED) # It creates the Surface object for the message.
textRect = textSurf.get_rect() # It creates the Rectangle object for the message.
textRect.topleft = (self.WINDOWWIDTH - 450, 5) # It positions the Rectangle object at the top of the window.
self.screen.blit(textSurf, textRect) # It draws the message onto the game's main surface.
def getButtonClicked(self, x, y):
# It takes the x and y pixel coordinates where the user has clicked as parameters. It returns the color of the button where the user has clicked or None.
if self.yellowButtonRect.collidepoint( (x, y) ):
return Color.YELLOW
elif self.blueButtonRect.collidepoint( (x, y) ):
return Color.BLUE
elif self.redButtonRect.collidepoint( (x, y) ):
return Color.RED
elif self.greenButtonRect.collidepoint( (x, y) ):
return Color.GREEN
return None
def gameOver(self, message):
self.sound1.play() # We play all four beeps at the same time.
self.sound2.play()
self.sound3.play()
self.sound4.play()
self.drawMessage(f"{message} Score: {str(self.score)}")
pygame.time.wait(1000)
origSurf = self.screen.copy().convert_alpha() # It creates a copy of the display Surface.
r, g, b = BRIGHTPINK
for alpha in range(0, 255, 10):
self.screen.fill(BGCOLOR) # It fills the game's main surface with the predefined background color.
origSurf.fill((r, g, b, alpha)) # It fills our copy of our main display Surface with the new background color.
self.screen.blit(origSurf, (0, 0)) # It draws (and blends) the new Surface object (origSurf) onto our main display Surface (self.screen).
self.drawButtons() # It draws the four buttons.
pygame.display.update() # It will update the full display surface to the screen.
self.fpsClock.tick(self.fps) # It just sets up how fast our game should run.
pygame.time.wait(2000)
if __name__ == '__main__':
game = Game() # The main function is quite simple. We create an instance of our Game class.
game.gameLoop() # We call the game's game_loop function.