Lake District Memory Game

The game screen appears here if your browser supports the Canvas API.

Attribution

Licensed under GNU GENERAL PUBLIC LICENSE Version 3.

Original Python code


# Memory Card Game - PyGame Zero
# This is licensed under GNU GENERAL PUBLIC LICENSE Version 3
# See : https://github.com/penguintutor/memorygame

import math
import time

# If running on a computer that doesn't include . in the Python Search Path
# Includes Raspbian on x86
import sys
sys.path.append('.')

import random
import pygame

from pgzero.actor import Actor

# Card is based on an Actor (uses inheritance)
class Card(Actor):

    # Status can be 'back' (turned over) 'front' (turned up) or 'hidden' (already used)
    status = 'back'


    def __init__(self, name, back_image, card_image):
        Actor.__init__(self, back_image, (0,0))
        self.name = name
        self.back_image = back_image
        self.card_image = card_image

    def turnOver(self):
        if (self.status == 'back'):
            self.status = 'front'
            self.image = self.card_image
        elif (self.status == 'front'):
            self.status = 'back'
            self.image = self.back_image
        # Attempt to turn over a hidden card - ignore
        else:
            return

    # Override Actor.draw
    def draw(self):
        if (self.status == 'hidden'):
            return
        Actor.draw(self)

    def hide(self):
        self.status = 'hidden'

    # When unhide set it to back image
    def unhide (self):
        self.status = 'back'
        self.image = self.back_image

    def isHidden (self):
        if self.status == 'hidden':
            return True
        return False

    # Is it turned to face forward
    def isFaceUp (self):
        if self.status == 'front':
            return True
        return False

    def reset (self):
        self.unhide()

    def toString(self):
        return self.name

    def setPosition(self, x, y):
        self.x = x
        self.y = y

    def equals (self, othercard):
        if self.name == othercard.toString():
            return True
        return False

class Timer():

    def __init__(self, start_count):
        self.start_count = start_count
        self.start_time = time.time()

    # start count down, with optional parameter to replace the start_count value
    # -1 is used as a "magic number", this method should only be called with positive number
    # if it isn't given a number then -1 indicates no new time give
    def start_count_down(self, new_time = -1):
        if (new_time >= 0):
            self.start_count = new_time
        self.start_time = time.time()

    def get_time_remaining(self):
        current_time = self.start_count + self.start_time - time.time()
        if (current_time <= 0):
            return 0
        return math.ceil(current_time)

# Status is tracked as a number, but to make the code readable constants are used
STATUS_NEW = 0                  # Game is ready to start, but not running
STATUS_PLAYER1_START = 1
STATUS_PLAYER1_CARDS_1 = 2
STATUS_PLAYER1_CARDS_2 = 3
STATUS_END = 50

# Number of seconds to display high score before allowing click to continue
TIME_DISPLAY_SCORE = 3

class GamePlay:

    # These are what we need to track
    score = 0



    status = STATUS_NEW

    # These are the cards that have been turned up.
    # Should be a positive number which is the index in the array (-1 is not set)
    cards_selected = [-1, -1]

    def __init__(self, count_down):
        self.count_down = count_down
        pass

    # If game has not yet started
    def isNewGame(self):
        if self.status == STATUS_NEW:
            return True
        return False

    def isGameOver(self):
        if self.status == STATUS_END:
            return True
        return False

    def setGameOver(self):
        # Add short timer for game over to ensure
        # player gets to see high score
        self.count_down.start_count_down(TIME_DISPLAY_SCORE)
        self.status = STATUS_END

    def isGameRunning(self):
        if (self.status >= STATUS_PLAYER1_START and self.status < STATUS_END):
            return True
        return False

    def startGame(self):
        self.score = 0
        self.status = STATUS_PLAYER1_START

    def setNewGame(self):
        self.status = STATUS_NEW

    def isPairTurnedOver(self):
        if (self.status == STATUS_PLAYER1_CARDS_2):
            return True
        return False

    # Return the index position of the specified card
    def getCard(self, card_number):
        return self.cards_selected[card_number]

    # Point scored, so add score and update status
    def scorePoint(self):
        self.score += 1
        self.status = STATUS_PLAYER1_START

    def getScore(self):
        return self.score

    # Not a pair - just update status
    def notPair(self):
        self.status = STATUS_PLAYER1_START

    # If a card is clicked then update the status accordingly
    def cardClicked(self, card_index):
        if (self.status == STATUS_PLAYER1_START):
            self.cards_selected[0] = card_index
            self.status = STATUS_PLAYER1_CARDS_1
        elif (self.status == STATUS_PLAYER1_CARDS_1):
            self.cards_selected[1] = card_index
            self.status = STATUS_PLAYER1_CARDS_2

# These constants are used to simplify the game
# For more flexibility these could be replaced with configurable variables
# (eg. different number of cards for different difficulty levels)
NUM_CARDS_PER_ROW = 4
X_DISTANCE_BETWEEN_CARDS = 120
Y_DISTANCE_BETWEEN_CARDS = 120
CARD_START_X = 220
CARD_START_Y = 130
TIME_LIMIT = 60

TITLE = "Lake District Memory Game"
WIDTH = 800
HEIGHT = 600

cards_available = {
    'airafalls' : 'memorycard_airafalls',
    'ambleside' : 'memorycard_ambleside',
    'bridgehouse' : 'memorycard_bridgehouse',
    'derwentwater' : 'memorycard_derwentwater',
    'ravenglassrailway' : 'memorycard_ravenglassrailway',
    'ullswater' : 'memorycard_ullswater',
    'weatherstone' : 'memorycard_weatherstone',
    'windermere' : 'memorycard_windermere'
    }

card_back = "memorycard_back"

count_down = Timer(TIME_LIMIT)
game_status = GamePlay(count_down)


def draw():
    screen.fill((220, 220, 220))
    if (game_status.isNewGame()):
        screen.draw.text("Click mouse to start", fontsize=60, center=(WIDTH/2,HEIGHT/2), shadow=(1,1), color=(255,255,255), scolor="#202020")
    if (game_status.isGameOver()):
        screen.draw.text("Game Over\nScore: "+str(game_status.getScore()), fontsize=60, center=(WIDTH/2,HEIGHT/2), shadow=(1,1), color=(255,255,255), scolor="#202020")
    if (game_status.isGameRunning()):
        for card in all_cards:
            card.draw()
        screen.draw.text("Time remaining: "+str(count_down.get_time_remaining()), fontsize=40, bottomleft=(50,50), color=(0,0,0))
        screen.draw.text("Score: "+str(game_status.getScore()), fontsize=40, bottomleft=(600,50), color=(0,0,0))


# Mouse clicked
def on_mouse_down(pos, button):
    # Only interested in the left button
    if (not button == mouse.LEFT):
        return
    # If new game then this click is to start the game
    if (game_status.isNewGame()):
        game_status.startGame()
        # start the timer
        count_down.start_count_down(TIME_LIMIT)
        dealcards()
        return
    # If game over then this click is to get to new game screen
    if (game_status.isGameOver()):
        # Make sure the timer has reached zero (short delay to see score)
        if (count_down.get_time_remaining()<=0):
            game_status.setNewGame()
        return

    ## Reach here then we are in game play

    # First check for both already clicked and this is a click to test
    if (game_status.isPairTurnedOver()):
        if (all_cards[game_status.getCard(0)].equals(all_cards[game_status.getCard(1)])):
            # Add points and hide the cards
            game_status.scorePoint()
            all_cards[game_status.getCard(0)].hide()
            all_cards[game_status.getCard(1)].hide()
            # Check if we are at the end of this level (all cards done)
            if (end_level_reached()):
                dealcards()
        # If not match then turn both around
        else:
            all_cards[game_status.getCard(0)].turnOver()
            all_cards[game_status.getCard(1)].turnOver()
            game_status.notPair()
        return

    ## Otherwise we just turn over the next card if clicked
    for i in range (len(all_cards)):
        if (all_cards[i].collidepoint(pos)):
            # Ignore if card hidden, or has already been turned up
            if (all_cards[i].isHidden() or all_cards[i].isFaceUp()):
                return
            all_cards[i].turnOver()
            # Update status
            game_status.cardClicked(i)



def update():
    if (game_status.isNewGame()):
        pass
    elif (game_status.isGameOver()):
        pass
    else:
        if (count_down.get_time_remaining()<=0):
            game_status.setGameOver()



# Shuffle the cards and update their positions
# Do not draw as this is called before the screen is properly setup
def dealcards():
    # Create a temporary list of card indexes that is then shuffled
    keys = []
    for i in range (len(all_cards)):
        keys.append(i)
    random.shuffle(keys)

    # Setup card positions
    xpos = CARD_START_X
    ypos = CARD_START_Y
    cards_on_row = 0
    for key in keys:
        # Reset (ie. unhide if hidden and display back)
        all_cards[key].reset()
        all_cards[key].setPosition(xpos,ypos)
        xpos += X_DISTANCE_BETWEEN_CARDS

        cards_on_row += 1
        # If reached end of row - move to next
        if (cards_on_row >= NUM_CARDS_PER_ROW):
            cards_on_row = 0
            xpos = CARD_START_X
            ypos += Y_DISTANCE_BETWEEN_CARDS

# If reach end of level ?
def end_level_reached():
    for card in all_cards:
        if (not card.isHidden()):
            return False
    return True

# End of functions - start of initialization code

all_cards = []
# Create individual card objects, two per image
for key in cards_available.keys():
    # Add to list of cards
    all_cards.append(Card(key, card_back, cards_available[key]))
    # Add again (to have 2 cards for each img)
    all_cards.append(Card(key, card_back, cards_available[key]))

dealcards()