Loading [MathJax]/jax/output/HTML-CSS/jax.js

Tutorial on programming a memory card game


This post will be a not-so-detailed tutorial about creating a memory game with Python and pygame. The game we are about to implement is a fairly common game: we have some cards face down and we turn them face up two by two, trying to pair them up. If we manage to turn a pair face up, we remove those two cards from the table. If the cards don't match, we turn them face down again.

When I am coding a game, I like to set functional milestones: stages at which I can test whatever I have up to that point. When I follow this method, the whole process becomes much more interesting and engaging. It also makes it easier for me to control how the end product will turn out to be. In what follows I laid out a list of said milestones to be achieved. Each item is a new functionality to be added:
  1. Creates the screen and shows it with all the cards face down;
  2. Clicking a card flips it face up;
  3. Clicking a second card flips it up and then: if the two cards match they are removed from the table; if the cards don't match they are turned face down again;
  4. The player now has a time restriction; matching two cards awards the player with extra time and failing to match two cards penalizes the player;
  5. A final screen tells the player whether he lost or won. If the player won, display the score;
  6. A configuration file is used to set how many cards are initially on the table.
As we go along I will include the code I am describing; to show it just click the button that matches the description. The reason I am hiding all the code behind some buttons is twofold: on the one hand, those who want to try and implement by themselves what I describe won't be spoiled; on the other hand, I will be including so many snippets of code that having them all without hiding them would make this post too big. All the code and images is available in my GitHub page. Following that link you will also find snapshots of the game at the end of each major step.

Step 0: the face of the cards

Before starting to code the memory game I decided the front face of the cards would have different regular polygons. This meant it was very easy to create a series of images for the cards and it also made the game mildly difficult. Polygons with more sides start to become hard to distinguish when you have little time to look at them. To create all the polygons I wrote a small script that repeatedly asks for an integer and then draws different coloured polygons with the specified number of sides. With that script I created several images to use as my cards.

Step 1: show all the cards face down

In order to have a "table" with all the cards face down, we need to decide how many cards we are going to have and in what configuration (how many rows/columns). We also need an image to represent the back of the cards.
Obviously we started by importing everything necessary. We need to import the sys module so we can terminate the program when the user clicks the red x. The function load_image is a function that I (almost surely) copied from the internet; what it does is import the given image and then return its surface and rect. We can also specify if the image has a transparent background or not. After that we initialize some global variables that will be needed along the way. width and height are the number of columns and rows of cards we are going to have. WIDTH and HEIGHT are the dimensions of the window, in pixels. We also set the background colour, the padding (in pixels) between each card and the size (in pixels) of each card, which we assume to be a square.
The infinite cycle we also included is fairly standard and it keeps the game from freezing; as of now, the only action allowed is to close the game window.

Step 2: clicking a card turns it face up

To complete this step we will need to add a couple of things that handle some game logic: we need to shuffle the pairs of cards into their positions and we need to recognize when a card is clicked on.
We start by choosing the cards to be used and we distribute them on the table. Using the variables width, height we can know how many cards will be needed. All we have to do is open the directory where the images are and load randomly as many cards as needed. After we choose them, we store them in a dictionary and then create a matrix, with the same dimensions as the table, where we store which card is in which position. For that we start by shuffling all the cards in a vector and then we split the vector to create a matrix.
For this code to work we also need to import the modules random and os.
The game already knows what cards to put where, now we need to turn them face up. After we click the screen we need to recognize if we clicked a card and then turn it face up. As it will turn out to be very helpful, let us create a function board_to_pixels that receives a pair of integers (x,y), a card position on the card matrix card_list, and returns another pair of integers: the position, in pixels, of the top-left corner of the given card. This will be useful to create some rectangles that are going to be needed to update the screen and to redraw the cards.
With this auxiliary function we can now process the player clicks. For that matter, we create another branch in the if statement inside our main cycle. We can use a bit of maths and the divmod functoin to know which card was clicked. We will also ignore clicks between cards.
After all these changes, this is what we have:

Step 3: matches are removed from the table

If we want the matches to be removed from the table we will need to know if there is a card face up and which one it is. Having that in mind, we will add some auxiliary variables that will help us check if the second card to be turned face up is a match or not. We also need to modify the processing of the mouse clicks to take this into consideration.
This new game cycle removes the matches, which is what we wanted, but there is a funny behaviour that wasn't supposed to happen: if we click a spot where there used to be a card, that card will show again! I illustrate this behaviour in the following image, where a pair of triangles has already been found and then I clicked the region in red.
To avoid this situation we need to store all positions of all cards that have already been found. When we click a region of a card, we check if that card has already been removed. We do that by adding a new variable found_cards where we store all positions that have been found. We add a small check so we ignore these clicks and then change the code to store the cards found when we find them. The final code for this step is:

Step 4: adding a timer

At this point we will add a timer for the game. Whenever the player finds a match a time bonus is awarded and whenever the player fails to find a match, a time penalty is applied. For the player to keep track of the time left we will also include a time bar. We will draw it to the right of the table and it will empty itself as the time goes by. We will create a function to draw the bar and we will create two global variables BONUSTIME and PENALTYTIME to store, in milliseconds, the time bonus and the time penalty. We will also change the main loop to stop whenever the time is up. As a default, we set BONUSTIME = 3000 and PENALTYTIME = 600
If we set the time bar to be drawn to the right, the function that draws the bar only needs the percentage of time left. That information is all we need, as long as we also have a variable controlling the width of the time bar. We will also need to update the variable WIDTH to reflect the extra pixels needed to draw the bar:
Before going into the main loop we also need to define the moment the game is supposed to end. We also need to score all the bonuses and penalties the player has gotten, so we can update the time left. For that we will create two variables end and score in which we store the time at which the game ends and all the bonuses and penalties the player got. We then change the main loop to stop whenever we run out of time:
All that is left is changing the score whenever we match two cards or fail at doing so, making use of the variables BONUSTIME and PENALTYTIME...
and updating the time bar at each time step. To do so we need to calculate the percentage of time left and then call the function that draws the bar.
If we play our game now we will see the new time bar! We will also notice, however, that when two cards are face up the time bar freezes for a little. This happens because of the line pygame.time.delay(800), which stops pygame for 800ms. A way to circumvent this problem is by creating an auxiliary variable wait that tells the program to keep running BUT to ignore all user input. After that waiting time, we process the two cards that are facing up and then let the game flow as usual.
These are the changes that are due:
  • When we flip a second card face up, we store its position and define a waiting time;
  • When we are inside the main loop check if it is time for us to end the pause;
  • When we are in the waiting time, do not let the user click any more cards;
  • When the waiting time is over, turn the two cards face down or remove them from the table.
Taking this into consideration, the code could end up like this: (where the changes are in between ### --> and ### <--)
If we put together all of these changes we get a game that fulfills all requirements up to step 4. This is the final code for this step:

Step 5: scoreboard

To add a victory/defeat screen is relatively easy. We can go about it by adding a bit of code after the main game loop:

Step 6: configuration file

In this step we will create a configuration file so we can easily change the number of cards on the table (through the width and height variables), as well as change the bonus and penalty times, through BONUSTIME and PENALTYTIME. After a bit of googling I found the module configparser would be of great help; we import it and then define a function with the purpose of reading the configuration file and parsing it; if the file doesn't exist, we create it with default values. We called this function parse_configurations. We include its implementation, as well as the section where the global variables for the whole game are initialized; notice we removed the lines regarding width, height, BONUSTIME and PENALTYTIME.
The whole game, in no more than 230 lines, is thus:
This concludes the post, where I gave you the code for a working game with Python and pygame! If you make any modifications or have any suggestions and/or questions, please use the comment section below!

  - RGS

Tutorial on programming a memory card game Tutorial on programming a memory card game Reviewed by Unknown on April 19, 2018 Rating: 5

No comments:

Powered by Blogger.