Let’s Make Our Own Python Game Part Two: Creating The Bingo Card (Python Lesson 50)

Advertisements

Hello everyone,

Michael here, and in today’s post, I’ll pick up where we left off last time. This time, we’ll create the BINGO board!

In case you forgot where we left off last time, here’s the code from the previous post:

Now let’s continue making our BINGO game. In today’s post, we’ll focus on generating the BINGO card.

Outlining the BINGO board

As you may recall from the previous post, all we really did was create one giant lime green square:

It’s a good start, but quite frankly, it won’t work for our BINGO board. How can we improve the look of our BINGO card? Add some borders!

Here’s the code to add the borders:

for xcoord in x:

for ycoord in y:
screen.blit(square1.surf, (xcoord, ycoord))
pygame.draw.rect(screen, pygame.Color("Red"), (xcoord, ycoord, 75, 75), width=3)

Pay attention to the last line in these nested for loops-the one that contains the pygame.draw.rect() method. What this method does is draw a square border around each square in the BINGO card. This method takes four parameters-the game screen (screen in this case), the color you want for your border (this could be a color name or hex code), a four-integer tuple that contains the x and y-coordinates for the square along with the square’s size, and the thickness of the border in pixels. Let’s see what we get!

The BINGO card already looks much better-now we need to fill it up!

  • Just a tip-for the border generation process to work, make the dimensions of the border the same as the dimensions of the square generated. In this case, we used 75×75 borders since the squares are 75×75 [pixels].

B-I-N-G-O

Now, what does every good BINGO card need. The B-I-N-G-O on the top row, of course!

Here’s how we can implement that:

font = pygame.font.Font('ComicSansMS3.ttf', 32) 

if ycoord == 75:
if xcoord == 40:
text = font.render('B', True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))
elif xcoord == 115:
text = font.render('I', True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))
elif xcoord == 180:
text = font.render('N', True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))
elif xcoord == 255:
text = font.render('G', True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))
elif xcoord == 330:
text = font.render('O', True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

And how does all of this code work? Let me explain.

The B-I-N-G-O letters will usually go on the top row of the card. The line if ycoord == 75 will ensure that the letters are only drawn on the top row of the card since point 75 on the y-axis (on our gamescreen) corresponds to the top row of the card.

Since there are five letters in B-I-N-G-O, there are also five conditional statements that account for the five letters we’ll be drawing onto the top five squares. There are also five x-coordinates to account for (40, 115, 180, 255, 330).

But before we actually start drawing the text, I want you to take note of this line-font = pygame.font.Font('ComicSansMS3.ttf', 32). This line allows you to set the font you wish to use for text along with its pixel size-in this case, I wanted to use Comic Sans with a 32 pixel size (hey, this isn’t a business project, so I can have some fun with fonts). Unforntuantely, if you want custom fonts for your game, you’ll need to download a TFF (TrueType font) file and save it to your local drive.

  • Another tip-if the TFF file is saved in the same directory as your game file, you just need the TFF file name as the first parameter of the pygame.font.Font() method. Otherwise, you’ll need the whole filepath as the first parameter.

As for the five conditional statements, you’ll notice that they each have the same five lines. Let’s explain them one-by-one.

First off we have our text variable, which contains the text we want to write to the square. The value of this variable is stored as the results of the font.render() method, which takes four parameters-the text you wish to display, whether you want to antialias the text (True will antialias the text, which simply results in a smoother text appearance), a 3-integer tuple representing the color of the text in RGB form, and another 3-integer tuple representing the color of the background where you wish to apply the text-also in RGB form.

  • For a good look, be sure to make the backgound color the same as the square’s color.

Next we have our textRect variable, which represents the invisible rectangle (or square) that contains the text we will render.

Upon initial testing of the text rendering, I noticed that my text wasn’t being centered in the appropriate square. The testX and testY variables are here to fix it by using the simple formula (square size-rectangle width/height) // 2 (use width for the x-center point and height for the y-center point). What this does is help gather the textRect x-center and y-center to in turn help center the text within the square. However, these two variables alone won’t center the text correctly, and I’ll explain why shortly.

  • In Python, the // symbol indicates that the result of the division will be rounded down to the nearest whole number, which helps when dealing with coordinates and text centering.

Last but not least, we have our wonderful screen.blit() method. In this context, the screen.blit() method takes two parameters-the text you want to display (text) and a 2-integer tuple denoting the coordinates where you wish to place the text.

Simple enough, right? However, take note of the coordinates I’m using here-(xcoord + testX, ycoord + testY). What addind the testX and testY coordinates will do is help center the text within the square.

After all our text rendering, how does the game look now?

Wow, our BINGO game is starting to come together. And now, let’s generate some BINGO numbers!

B….1 to 15, I….16 to 30 and so on

Now that the B-I-N-G-O letters are visible on the top of our card, the next thing we should do is fill our card up with the appropriate numbers.

For anyone who’s played BINGO, you’ll likely be familiar with which numbers end up on which spots on the card. In any case, here’s a refresher on that:

  • B (1 to 15)
  • I (16 to 30)
  • N (31 to 45)
  • G (46 to 60)
  • O (61 to 75)

With these numbering rules in mind, let’s see how we can implement them into our code and show them on the card! First, inside the game while loop, let’s create five different arrays to hold our BINGO numbers, appropriately titled B, I, N, G, and O.

from random import seed, randint 

[meanwhile, inside the while game loop...]

    B = []
seed(10)
for n in range(5):
value = randint(1, 15)
B.append(value)

I = []
seed(10)
for n in range(5):
value = randint(16, 30)
I.append(value)

N = []
seed(10)
for n in range(5):
value = randint(31, 45)
N.append(value)

G = []
seed(10)
for n in range(5):
value = randint(46, 60)
G.append(value)

O = []
seed(10)
for n in range(5):
value = randint(61, 75)
O.append(value)

Also, don’t forget to include the line from random import seed, randint (I’ll explain why this is important) on the top of the script and out of the while game loop.

As for the array creation, please keep that inside the while game loop! How does the BINGO array creation work? First of all, I first created five empty arrays-B, I, N, G, and O-which will soon be filled with five random numbers according to the BINGO numbering system (B can be 1 to 15, I can be 16 to 30, and so on).

Now, you’ll notice that there are five calls to the [random].seed() method, and all of the calls take 10 as the seed. What does the seed do? Well, in Python (and many other programming languages), random number generation isn’t truly “random”. The seed value can be any positive integer under the sun, but your choice of seed value determines the sequence of random numbers that will be generated-hence why random number generation (at least in programming) is referred to as a deterministic algorithm since the seed value you choose determines the sequence of numbers generated.

  • If you don’t have a specific range of random number you want to generate, you’ll get a sequence of random numbers Python’s random number generator chooses to generate.
  • You simply need to write seed() to initialize the random seed generator-the random. part is implied.

After the random seed generators are set up, there are five different loops that append five random numbers to each array-the line for n in range(5) ensures that each array has a length of 5. Inside each loop I have utilized the [random].randint() function and passed in two integers as parameters to ensure that I only recieve random numbers in a specific range (such as 1 to 15 for array B).

Now, let’s display our numbers on the BINGO card! Here’s the code to run (and yes, keep it in the while game loop):

 if ycoord != 75:

if xcoord == 40:
for num in B:
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 115:
for num in I:
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 180:
for num in N:
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 255:
for num in G:
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 330:
for num in O:
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

Confused at what this code means? The last five lines in each if statement essentially do the same thing we were doing when we were rendering the B-I-N-G-O on the top row of the card (rendering and centering the text on each square). However, since we don’t want the numbers on the top row of the card, we include the main if statement if ycoord != 75 because this represents all squares that aren’t on the top row of the card.

Oh, one thing to note about rendering the numbers on the card-simply cast the number variable (num in this case) as a string/str because pygame won’t render anything other than text of type str.

With all that said, let’s see what our BINGO card looks like:

Well, we did get correct number ranges, but this isn’t the output we want. Time for some debugging!

D-E-B-U-G

Now, how do we fix this board to get distinct numbers on the card? Here’s the code for that!

First, let’s fix the BINGO number array creation process:

    B = []


value = sample(range(1, 15), 5)
for v in value:
B.append(v)

I = []

value = sample(range(16, 30), 5)
for v in value:
I.append(v)

N = []

value = sample(range(31, 45), 5)
for v in value:
N.append(v)

G = []

value = sample(range(46, 60), 5)
for v in value:
G.append(v)

O = []

value = sample(range(61, 75), 5)
for v in value:
O.append(v)

In this code, I first added the sample method to the from random import ... line as we’ll need this method to ensure we get an array of distinct random numbers.

   else:

y2 = [150, 225, 300, 375, 450]
if xcoord == 40:
for num, ycoord in zip(B, y2):
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 115:
for num, ycoord in zip(I, y2):
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 180:
for num, ycoord in zip(N, y2):
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 255:
for num, ycoord in zip(G, y2):
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

if xcoord == 330:
for num, ycoord in zip(O, y2):
text = font.render(str(num), True, (0,0,0), (50,205,50))
textRect = text.get_rect()
textX = (75 - textRect.width) // 2
textY = (75 - textRect.height) // 2
screen.blit(text, (xcoord + textX, ycoord + textY))

Remember the else block where we were rendering the text? Well, I made a few changes to the code. I first added another array of y-coordinates (y2) that is essentially the same as the y array without the number 75. Why did I remove the 75? I simply wanted to ensure that no numbers are drawn on the top row of the card, and 75 represents the y-coordinate that displays the top row of the card.

While iterating through our five BINGO number arrays aptly titled B, I, N, G and O, we’re also iterating through the y2 array to ensure that the correct numbers are rendered in the correct squares.

  • In case you’re wondering about the zip() line in our for loop, the zip() function allows us to iterate through multiple arrays at once in a for loop. However, the zip() function only works if the arrays you’re looping through are the same length.
  • If you want to iterate through multiple arrays of unequal lengths, include this import at the beginning of your script-from itertools import zip_longest. The zip_longest() function will allow you to iterate through multiple arrays of unequal length. Also remember to pip install the itertools package if you don’t have it on your laptop already.

Using our revised code, let’s see what our BINGO card looks like now!

Wow, the BINGO card already looks much better! However, if you’re familiar with BINGO, you know the center square in the N column is considered a “free space”. Let’s reflect this with the addition of one simple line of code:

N[2] = 'Free'

This line will replace the middle element in the N array with the word Free, which in turn will display the word Free on the center of the BINGO card:

Nice work!

Testing the card

Now that we’ve generated quite the good-looking BINGO card, the last thing we’ll need to do is test it to ensure we get a new card each time we open the game!

Here’s what our card currently looks like:

And let’s see what happens we we close and restart the game:

It looks like we got the same card. Now, playing with the same card every time would be a quite boring, right? How do we fix this bug? Here’s some code to do so (and keep in mind this is just one way to solve the problem):

possibleSeeds = []

value = sample(range(1, 10000), 5)
for v in value:
possibleSeeds.append(v)

meanwhile, inside the game loop

...

seed(possibleSeeds[0])

To solve the BINGO card generation bug, I used the same array generating trick I used for generating the BINGO arrays which is gather a specific number of integers from a specific integer range and use a loop to create a 5-element integer array. This time, I used integers ranging from 1 to 10000.

Inside the game loop, I set the random seed to the first element of the possibleSeeds array. Why did I do this? When I set my seed() to 10, I managed to see the same BINGO card each time I started the game because since the seed() value was fixed, the same sequence of random numbers are generated each time because using the same seed() each time you run a random number generator will give you the same sequence of random numbers each time it’s run. However, using the first element of the possibleSeeds array won’t give you the same random number sequence (and in turn, the same card) each time because the possibleSeeds array generates a sequence of five different integers with each iteration. Since you get a different random number sequence each time, the random number generation seed will be different each time, which in turn results in a different BINGO card generated each time the game is run.

  • Keep the seed() method inside the game loop, but keep the possibleSeeds array outside of the game loop because inserting the array into the game loop will generate random 5-integer sequences non-stop, which isn’t a desirable outcome.

Now, let’s see if our little trick worked. Let’s try running the game:

Now let’s close this window and try running the game again!

Awesome-we got a different BINGO card! How about another test run-third time’s the charm after all!

Nice work. Stay tuned for the next part of this game development series where we will create the mechanism to call out different BINGO “balls”.

Also, here’s a Word Doc file with our code so far (WordPress won’t let me upload PY files)-

Thanks for reading,

Michael

Leave a ReplyCancel reply