Mini game: Memory Puzzle

This is a really simple game, but it’s hard to win in short amount of time. Your task is to find all of the pairs of icons that look the same. There are totally (10*7)/2 pairs, this means the board’s width is 10 and height is 7.

Let’s get started.
First, we need to import some necessary modules.

import pygame, random, sys
from pygame.locals import *

and declare all instances that we are going to use later.

FPS = 30 # frame per second
WINDOW_WIDTH = 640 # size of the window's width in pixel
WINDOW_HEIGHT = 480 # size of the window's height in pixel
REVEAL_SPEED = 8 # speed boxes' sliding reveal and cover
BOX_SIZE = 40 # size of box width and height in pixel
GAP_SIZE = 10 # space gap between boxes in pixel
BOARD_WIDTH = 10 # the number of column icons
BOARD_HEIGTH = 7 # the number of row icons
X_MARGIN = int( (WINDOW_WIDTH - (BOARD_WIDTH*(BOX_SIZE+GAP_SIZE))) / 2)
Y_MARGIN = int( (WINDOW_HEIGHT - (BOARD_HEIGTH*(BOX_SIZE+GAP_SIZE))) / 2)

#            R    G    B
GRAY     = (100, 100, 100)
NAVYBLUE = ( 60, 60 , 100)
WHITE    = (255, 255, 255)
RED      = (255, 0  , 0)
GREEN    = ( 0 , 255, 0)
BLUE     = ( 0 , 0  , 255)
YELLOW   = (255, 255, 0)
ORANGE   = (255, 128, 0)
PURPLE   = (255, 0  , 255)
CYAN     = ( 0 , 255, 255)

BACKGROUND_COLOR = NAVYBLUE
BOX_COLOR = WHITE
HIGHLIGHT_COLOR = BLUE
LIGHT_BACKGROUND_COLOR = GRAY

DONUT = 'donut'
SQUARE = 'square'
DIAMOND = 'diamond'
LINES = 'lines'
OVAL = 'oval'

ALL_COLORS = [RED, GREEN, BLUE, YELLOW, ORANGE, PURPLE, CYAN]
ALL_SHAPES = [DONUT, SQUARE, DIAMOND, LINES, OVAL]
CLOCK, DISPLAY_SURFACE = None, None

We use instances instead of ‘magic numbers’. Magic number is a number in your code, for example, you create window game in pygame by using pygame.display.set_mode method:

window_surface = pygame.display.set_mode((400, 400))

This code create a Surface object, a special Surface to be exact, all things in your game will be displayed by this Surface. 400 is width and height of this Surface, but using those numbers make it hard to understand and if you want to change the window’s width and height you must find this exact line of code. These numbers are called ‘magic numbers’. So the appropriate and more effective way to do this is using instances.

WINDOW_WIDTH = 400
WINDOW_HEIGHT = 400
window_surface = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))

There’re totally 7 different colors and 5 shapes.

Next the main function, this function include all game logic, graphics, etc.

def main():
    global CLOCK, DISPLAY_SURFACE
    pygame.init() # initialize pygame
    CLOCK = pygame.time.Clock()
    DISPLAY_SURFACE = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT), 0, 32)

    mousex = 0
    mousey = 0
    pygame.display.set_caption('Memory Game') # set window's title

    main_board = get_randomized_board()  # create the board, random placed items
    revealed_boxes = generate_revealed_boxes_data(False) # there is no box revealed yet
    first_selection = None # store the x,y of the first click

    DISPLAY_SURFACE.fill(BACKGROUND_COLOR) # set background color
    start_game_animation(main_board) # start the game, show all items

    while True: # game loop
        mouse_clicked = False
        DISPLAY_SURFACE.fill(BACKGROUND_COLOR)
        draw_board(main_board, revealed_boxes)

        for event in pygame.event.get(): # check event
            if event.type == QUIT or (event.type == KEYUP and event.key == K_ESCAPE):
                pygame.quit()
                sys.exit()
            elif event.type == MOUSEMOTION: # mouse hover event
                mousex, mousey = event.pos
            elif event.type == MOUSEBUTTONUP: # mouse click event
                mousex, mousey = event.pos
                mouse_clicked = True

        box_x, box_y = get_box_at_pixel(mousex, mousey) # convert pixel coordinates to board coordinates
        if box_x != None and box_y != None:
            # the mouse is currently over a box
            if not revealed_boxes[box_x][box_y]: # if the box has not been revealed yet, highlight it
                draw_highlight_box(box_x, box_y)
            if not revealed_boxes[box_x][box_y] and mouse_clicked: # click on an item
                reveal_boxes_animation(main_board, [(box_x, box_y)]) # reveal that item
                revealed_boxes[box_x][box_y] = True # temporarily mark this item revealed

                if first_selection == None: # if this is the first box selected
                    first_selection = (box_x, box_y) # save it
                else: # otherwise check if two revealed boxes are the sae
                    # get shape and color of two boxes
                    icon1_shape, icon1_color = get_shape_and_color(main_board, first_selection[0], first_selection[1])
                    icon2_shape, icon2_color = get_shape_and_color(main_board, box_x, box_y)

                    if icon1_shape != icon2_shape or icon1_color != icon2_color: # they are not the same
                        pygame.time.wait(1000) # wait 1 second
                        cover_boxes_animation(main_board, [(first_selection[0], first_selection[1]), (box_x, box_y)]) # then cover them
                        # remark them as they have not been revealed yet
                        revealed_boxes[first_selection[0]][first_selection[1]] = False
                        revealed_boxes[box_x][box_y] = False
                    # if they are the same, they remain revealed
                    elif has_won(revealed_boxes): # check for the win
                        game_won_animation(main_board) # start animation (just change the background color)
                        pygame.time.wait(2000) # then wait 2 seconds

                        main_board = get_randomized_board() # create new board
                        revealed_boxes = generate_revealed_boxes_data(False) # cover all the items

                        draw_board(main_board, revealed_boxes) # draw the board
                        pygame.display.update() # update game screen
                        pygame.time.wait(1000) # then wait 1 second
                        start_game_animation(main_board) # then start the game again
                    first_selection = None # after two item selected, reset this variable to None so that the next click will be the first item

        pygame.display.update() # final update each frame
        CLOCK.tick(FPS) # set frame per second

Don’t panic, okey :> It’ll be crystal clear after I explain each line of code.
First you use global keyword to access two global instances CLOCK and DISPLAY_SURFACE. Then you inititialize the game using pygame.init() method. Next, you create the game window, mousex and mousey to save the location of the mouse when player hover or click on a box, set window’s title, create game board. revealed_box is a 2d array, returned from function generate_revealed_boxes_data, with False as a argument. This means all of the values of revealed_boxes are False, just like there is no box that has been revealed yet. If first_selection is None, this is means the next time player click on a box, that box will be the first box in a pair. Fill the window with BACKGROUND_COLOR and start the game.

The code in the game loop is already explained very clearly by comments, so check it out and free to ask me here if you’re stuck.

Finally, I’m about to explain all the functions that are used in main function. First, generate_revealed_box_data, this function returns a 2-d array, with all values of each row equal val.
The get_randomized_board function returns the game board, this function is like the main function, well commented so no need to explain more.
The get_box_at_plixel converts game (or pixel) coordinates into board coordinates. What’s this mean? This means if you click or hover on a box on the screen, this function will convert that pixel value, for example (400, 200) to board coordinates , (boxx, boxy). This function is the reverse of lefttop_box, which converts game board coordinates to pixel coordinates.
Then the draw_icon method, which takes 4 arguments, the game board, color, and boxx, boxxy. Convert boxx, boxy to pixel coordinates, then draw the icon (one of the five icons).
Function draw_boxes_cover, draw icons, and if coverage > 0, draw the coverage over the items, this creates the animation effect.
The reveal_boxes_animation and cover_boxes_animation makes use of draw_boxes_cover, with different coverage values.
draw_board function draw the game board, if the box has been revealed, just draw the icon, if it’s not, draw the box cover it.
start_game_animation, randomly reveal 8 boxes at a time. After quickly revealing those boxes will be covered it. This makes the game a little easier.
The game_won_animation function makes the background change to LIGHT_BACKGROUND_COLOR and back to BACKGROUND_COLOR multiple times.
has_won checks if all the values of revealed_box array are equal True.
Here’s the code of all functions that’re explained above.

def generate_revealed_boxes_data(val):
    # create a 2-D array of True or False values
    revealed_boxes = []
    for i in xrange(BOARD_WIDTH):
        revealed_boxes.append([val]*BOARD_HEIGTH)
    return revealed_boxes


def get_randomized_board():
    # create game board
    # first we create an array saving all the possible combinations of shapes and colors
    icons = []
    for color in ALL_COLORS:
        for shape in ALL_SHAPES:
            icons.append((shape, color))
    random.shuffle(icons)
    num_icons_used = int(BOARD_WIDTH*BOARD_HEIGTH/2) # then we divide it by 2 because we just need half of them
    icons = icons[:num_icons_used]*2 # then we create a copy of current items, so we have even number of pairs of item
    random.shuffle(icons) # shuffle it

    # and convert into 2-D dimensional array
    board = []
    for x in xrange(BOARD_WIDTH):
        column = []
        for y in xrange(BOARD_HEIGTH):
            column.append(icons[0])
            del icons[0]
        board.append(column)
    return board

def split_into_groups(group_size, list):
    # split a list into group
    result = []
    for i in xrange(0, len(list), group_size):
        result.append(list[i:i+group_size])
    return result

def lefttop_box(boxx, boxy):
    # convert board coordinates to pixel coordinates
    left = boxx*(BOX_SIZE+GAP_SIZE) + X_MARGIN
    top = boxy*(BOX_SIZE+GAP_SIZE) + Y_MARGIN
    return (left, top)


def get_box_at_pixel(x, y):
    # get the box on board where player click on
    for boxx in xrange(BOARD_WIDTH):
        for boxy in xrange(BOARD_HEIGTH):
            left, top = lefttop_box(boxx, boxy)
            box_rect = pygame.Rect(left, top, BOX_SIZE, BOX_SIZE)
            if box_rect.collidepoint(x, y):
                return (boxx, boxy)
    return (None, None)


def draw_icon(shape, color, boxx, boxy):
    # draw icon
    # these two variables are used to centralize the icon
    quarter = int(BOX_SIZE * 0.25)
    half = int(BOX_SIZE * 0.5)

    left, top = lefttop_box(boxx, boxy) # convert board coordinates to pixel coordinates
    if shape == DONUT:
        pygame.draw.circle(DISPLAY_SURFACE, color, (left+half, top+half), half-5)
        pygame.draw.circle(DISPLAY_SURFACE, BACKGROUND_COLOR, (left+half, top+half), quarter-5)
    elif shape == SQUARE:
        pygame.draw.rect(DISPLAY_SURFACE, color, (left+quarter, top+quarter, BOX_SIZE-half, BOX_SIZE-half))
    elif shape == DIAMOND:
        pygame.draw.polygon(DISPLAY_SURFACE, color, ((left+half, top), (left+BOX_SIZE-1, top+half), (left+half, top+BOX_SIZE-1),(left, top+half)))
    elif shape == LINES:
        for i in xrange(0, BOX_SIZE, 4):
            pygame.draw.line(DISPLAY_SURFACE, color, (left, top+i), (left+i, top))
            pygame.draw.line(DISPLAY_SURFACE, color, (left+i, top+BOX_SIZE-1), (left+BOX_SIZE-1, top+i))
    elif shape == OVAL:
        pygame.draw.ellipse(DISPLAY_SURFACE, color, (left, top + quarter, BOX_SIZE, half))


def get_shape_and_color(board, boxx, boxy):
    # get shape and color of box with given coordinate
    return board[boxx][boxy][0], board[boxx][boxy][1]


def draw_boxes_cover(board, boxes, coverage):
    # draw boxes with coverage
    # this create animation revealing and covering effect
    for box in boxes:
        left, top = lefttop_box(box[0], box[1])
        pygame.draw.rect(DISPLAY_SURFACE, BACKGROUND_COLOR, (left, top, BOX_SIZE, BOX_SIZE))
        shape, color = get_shape_and_color(board, box[0], box[1])
        # first we draw the icon
        draw_icon(shape, color, box[0], box[1])
        # then draw coverage on top of it
        if coverage > 0:
            pygame.draw.rect(DISPLAY_SURFACE, BOX_COLOR, (left, top, coverage, BOX_SIZE))
    pygame.display.update() # update the window
    CLOCK.tick(FPS) # slow down the animation


def reveal_boxes_animation(board, boxes_to_reveal):
    # the box width will decrease each iteration until they reach 0
    for coverage in xrange(BOX_SIZE, -(REVEAL_SPEED)-1, -REVEAL_SPEED):
        draw_boxes_cover(board, boxes_to_reveal, coverage)


def cover_boxes_animation(board, boxes_to_cover):
    # then they increase until they reach BOX_SIZE again
    for coverage in xrange(0, BOX_SIZE+REVEAL_SPEED, REVEAL_SPEED):
        draw_boxes_cover(board, boxes_to_cover, coverage)


def draw_board(board, revealed):
    # draw the game board
    for boxx in xrange(BOARD_WIDTH):
        for boxy in xrange(BOARD_HEIGTH):
            left, top = lefttop_box(boxx, boxy) # convert board coordinates to game coordinates to draw
            if not revealed[boxx][boxy]: # if the box has not been revealed yet
                # draw a box to cover it
                pygame.draw.rect(DISPLAY_SURFACE, BOX_COLOR, (left, top, BOX_SIZE, BOX_SIZE))
            else: # otherwise, draw the icon
                shape, color = get_shape_and_color(board, boxx, boxy)
                draw_icon(shape, color, boxx, boxy)


def draw_highlight_box(boxx, boxy):
    # draw a blue rect when player hover a box
    left, top = lefttop_box(boxx, boxy)
    pygame.draw.rect(DISPLAY_SURFACE, HIGHLIGHT_COLOR, (left-5, top-5, BOX_SIZE+10, BOX_SIZE+10), 4)


def start_game_animation(board):
    covered_boxes = generate_revealed_boxes_data(False)
    boxes = []
    for x in xrange(BOARD_WIDTH):
        for y in xrange(BOARD_HEIGTH):
            boxes.append((x,y))
    random.shuffle(boxes)
    box_groups = split_into_groups(8, boxes)

    draw_board(board, covered_boxes)
    for box_group in box_groups:
        reveal_boxes_animation(board, box_group)
        cover_boxes_animation(board, box_group)


def game_won_animation(board):
    covered_boxes = generate_revealed_boxes_data(True)
    color1 = LIGHT_BACKGROUND_COLOR
    color2 = BACKGROUND_COLOR

    for i in xrange(13):
        color1, color2 = color2, color1
        DISPLAY_SURFACE.fill(color1)
        draw_board(board, covered_boxes)
        pygame.display.update()
        pygame.time.wait(300)


def has_won(revealed_boxes):
    for i in revealed_boxes:
        if False in i:
            return False
    else:
        return True

Game play:

Donate (I’ll very appreciate your donation, it will help a lot): https://www.paypal.me/TruongLoc

Advertisements