Part 4

In Part 4 of Candy Crush we will add these features:

  • add new tiles to fill the gaps
  • make it a match 3 game, or match square game
  • add a score
  • add bombs that help you clear the screen.

One thing: now that we are on part 4 and you have a bit of practice thinking like a developer we are going to ask you to start creating your own algorithms and Python code.

Don’t worry if you get stuck, we’ve supplied some solutions at the end.

Adding new tiles

The screen starts to look quite empty as you match and remove tiles. If our game objective is to clear the screen then that’s OK, but let’s keep the player busy by adding tiles as gaps appear at the top of the screen.

First have a think about a good algorithm to achieve this. You can do this on a piece of paper or in the code with comments, let’s try this…

Add this method and comments:

def new_tiles():
    # Put the steps of your algorithm here.
    # Go on, have a think first... before you scroll down
    # and see a solution.
    #
    # What are the steps we need to do to add new tiles?

Here’s one algorithm that might work:

def new_tiles():
    # Check for gaps starting at the top of the screen
    # Can we just check the first row?
    # Place a new tile into any gaps we find
    # The existing code will make them fall down the screen?

How did this compare with your algorithm? Which one do you want to try?

Now can you use what you’ve already practiced to turn your algorithm into Python code? If you get stuck you can always scroll down to see some example code, but do try to do it yourself first.

Make it a match 3 game

Right now you only have to match 2 tiles to remove them from the screen. This is too easy. Let’s make it so that you have to match 3 tiles.

Have a look at the matching code and see if you can work out how to achieve this.

You may well get this error on your first attempt:

IndexError: list index out of range

If you do, then take a look at the line above the matching code that reads:

for x in range(TILESW-1):

Why do you think we put in TILESW-1 and not just TILESW ? Could this help you fix the error?

Matching squares

Instead of matching a row of 3 tiles we could get the player to match a set of 2x2 tiles instead. Once you’ve worked out how to move to a match 3 game then this change shouldn’t be too hard to achieve.

As with the change to match 3 above, you might see that IndexError error but on the line for y .... The fix is very similar.

Adding a score

Let’s add a score so that the player gets some sense of achievement from playing the game.

We’ll need to add a variable to the start of the game, so add the following line near the top of your code:

score = 0

Now we need to draw the score, where shall we place it on the screen? Shall we place it over the top of tiles or make space for it on a blank row with no tiles? You decide.

In order to display text you’ll need to use the function screen.draw.text like this:

screen.draw.text("Score: %s" % score, bottomleft=(0, HEIGHT), fontsize=60)

Now on to changing the score… Inside any function that changes the score we need to add this line at the top of the function:

global score

Now you get to decide when and how to change the score. Clearly we should increase it when the user gets a match, but by how much? We could reduce it when they move, maybe?

What’s global do?

You might not have seen global before. It tells Python that when we use score in this function we want to use the one defined outside the function (in global scope), not one private to this function’s scope.

By default in Python (and many other programming languages) if you create a variable in a function then it is assumed that this is private to that function. This is a good thing as it stops code in a function messing up code outside the function.

Here’s an example (create a new Python script if you want to see it in action):

def fac(i):
    """Compute the factorial of i, e.g. fac(5) is 5*4*3*2*1."""
    f = 1
    for a in range(i, 0, -1):
        f = f * a
    return f

a = 5
print(fac(a))
a = a + 1
print(fac(a))

If Python didn’t use a private scope in the function fac above then our choice of the variable named a for the loop would overwrite the variable a outside of the function, creating some really odd bugs.

Put another way: the variable a in the function fac is completely different to the variable a outside the function, even though they have the same name.

Other score ideas

If you’ve followed the match 3 and match square code above we could support both and give a higher score for matching squares

We could add a time limit and count the score down from 100, ending the game at zero.

You are the game creator, so you decide!

Bombs away

As we did for matching 3 tiles, let’s first have a think about how we want bombs to work. The general idea is that matching 3 bombs (or a square) will clear more than just the tiles that the bombs occupy, but how many extra tiles? What shape?

As well as deciding on the effect of matching bombs we also need to make bombs appear in the game and spot when they are matched. Oh and we need to draw a bomb tile too.

So to start with draw yourself a bomb tile. You could find one on the internet or make your own using a free graphics program such as GIMP. The tile needs to be 32x32 pixels so that it is the same size as the others.

Placing the bombs

Now let’s look at how we can place the bombs on the board.

The simplest way is to name the tile 9.png and then change the random range in the first for loop (the one that fills up the board array) to include 1-9, then bombs will appear. Try this. I think you’ll agree there are way too many bombs!

This is because the code random.randint(1,9) returns a random number in the range 1 to 9 with no preference, all are equally likely. We can make 9 (the bomb) less likely…

First let’s split out the random tile code into a new function. Add this function above the for loop:

def random_tile():
    return random.randint(1,9)

And change the code inside the loop to read:

tiles = [random_tile() for _ in range(TILESW)]

Now we can play with random_tile to get the effect we want. Let’s try another random function random.choice(), this takes a list and gives us a random element. If we fill the list with more of our regular tiles and just one bomb tile then we should get less tiles.

Change the function to the following:

def random_tile():
    tiles = [1,2,3,4,5,6,7,8]
    return random.choice( tiles + tiles + [9] )

Here we use double the number of regular tiles and one bomb. Try it to see the effect. You could add more + tiles if you want to have less bombs.

Matching bombs

If you don’t change any other code you’ll be able to match 3 bombs and they’ll be removed as with any other tile. However we want bombs to do more damage than that.

So take a look at the function check_matches() and see if you can figure out how to (a) spot bomb matches and (b) remove more than just the bombs.

Try to think through how you’ll achieve this… remember you can always scroll down to see a solution, but do try first yourself.

What’s next?

Well done! You’ve made it to the end of the Candy Crush Tutorial! You are now thinking like a programmer and have many of the skills required to create your own games.

All you need to do now is come up with some ideas to try out…


Solutions

Code for adding new tiles

NEW_TILE_PROB = 0.1 # 10% chance of adding a new tile each time

def add_new_tiles():
    for x in range(TILESW):
        if board[0][x] is None and random.random() < NEW_TILE_PROB:
            board[0][x] = random.randint(1,8)

Code for match 3 game

Here is the new check_matches function with changes to make it a match 3 game:

def check_matches():
    for y in range(TILESH):
        for x in range(TILESW-2):
            if board[y][x] == board[y][x+1] == board[y][x+2]:
                board[y][x] = None
                board[y][x+1] = None
                board[y][x+2] = None

Code for match squares

def check_matches():
    for y in range(TILESH-1):
        for x in range(TILESW-1):
            if board[y][x] == board[y][x+1] == board[y+1][x] == board[y+1][x+1]:
                board[y][x] = None
                board[y][x+1] = None
                board[y+1][x] = None
                board[y+1][x+1] = None

Code for matching bombs

def check_matches():
    global score
    for y in range(TILESH):
        for x in range(TILESW-2):
            if board[y][x] is not None and board[y][x] == board[y][x+1] == board[y][x+2]:
                if board[y][x] == 9:
                    # A bomb, so blank out whole row
                    for x2 in range(TILESW):
                        board[y][x2] = None
                else:
                    board[y][x] = None
                    board[y][x+1] = None
                    board[y][x+2] = None

                score += 50