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

Let’s Make Our Own Python Game Part One: Getting Started (Python Lesson 49)

Advertisements

Hello loyal readers,

I hope you all had a wonderful and relaxing holiday with those you love-I know I sure did. I did promise you all new and exciting programming content in 2024, so let’s get started!

My first post of 2024 (and first several posts of 2024 for that matter) will do something I haven’t done on this blog in its nearly 6-year existence-game development! Yup, that’s right, we’ll learn how to make a simple game using Python’s pygame package. And yes, this game will include graphics (so we’re making something way cooler than a simple text-based blackjack game or something like that).

Let’s begin!

Setting ourselves up

Before we even start to design our game, let’s install the pygame package using the following line either on our IDE or command prompt-pip install pygame.

Next, let’s open our IDE. You could technically use Jupyter notebook to start creating the game, but for something like game creation that utilizes graphics (and likely lots of code) I’d suggest an IDE like Spyder.

Now, where do we begin?

To start, here are the first three lines of code we should include in our script:

import pygame
from pygame.locals import *
import sys

What game will I teach you how to program? Well, in this series of posts, we’ll learn to make our own BINGO clone.

Why BINGO? Well, compared to many other games I could possibly teach you to program, BINGO seemed like a relatively easy first game to learn to develop as it doesn’t involve multiple levels, much scoring, health tracking, or final bosses (though we could certainly explore games that involve these concepts later on).

Let’s start coding!

First off, since we are programming a BINGO game, we’ll need to draw squares. 30 of them, to be precise, as simple BINGO games utilize a 5×5 card along with five squares at the top that contain the letters B, I, N, G, and O.

Seems simple enough to understand right? Let’s see how we code it!

class Square(pygame.sprite.Sprite):

def __init__(self):

super(Square, self).__init__()


self.surf = pygame.Surface((75, 75))


self.surf.fill((50, 205, 50))

self.rect = self.surf.get_rect()



pygame.init()



screen = pygame.display.set_mode((800, 600))



square1 = Square()

First of all, to draw the BINGO squares, we’ll first need to create a Square class and pass in the pygame.sprite.Sprite parameter into it like so-class Square(pygame.sprite.Sprite).

What is the Sprite class in pygame? For those who are familiar with fanatsy works (e.g. Shrek, Lord of the Rings), a sprite is a legendary mythical creature such as a pixie, fairy, or elf (among others). In pygame a sprite simply represents a 2D image or animation that is displayed on the game screen-like the squares we’ll need to draw for our BINGO board.

The next line-the one that begins with super-allows the Square class to inherit all of the methods and capabilites of the Sprite class, which is necessary if you want the squares drawn on the game screen.

The following three lines set the drawing surface (and in turn, the size) of the square, set the color of each square on the gameboard using RGB color coding (yes, you can make the squares different colors, but I’m keeping it simple and coloring all the squares lime green), and get the rectangular area of each square, respectively.

The next two lines initiate the instace of the game-using the line pygame.init()-and set the size of the screen (in pixels). In this case, we’ll use an 800×600 pixel screen.

The last line initiates a square object for us to draw. The interesting thing to note here is that even though we’ll ultimately need to draw 30 squares for our BINGO board, we only need one square object since we can draw that same square object 30 different times in 30 different places.

Even with all this code, we’ll still need to actually draw the squares onto our game screen-this code just ensures that we have the ability to do just that (it doesn’t actually take care of the graphics drawing).

Let’s run the game!

Now that we have created the squares for our BINGO game and imported the necessary packages, let’s figure out how to get our game running! Check out this chunk of code that helps us do just that!

gameOn = True


while gameOn:


for event in pygame.event.get():

if event.type == KEYDOWN:


if event.key == K_BACKSPACE:

gameOn = False




elif event.type == QUIT:

gameOn = False

First, we have our boolean variable gameOn, which indicates whether or not our game is currently running (True if it is, False if it isn’t).

The while loop that follows is a great example of event handling (I think this is the first time I mention it on this blog), which is the process of what your program should do in various scenarios, or events. This while loop will keep running as long as gameOn is true (in other words, as long as the game is running).

You’ll notice two event types that will shut the game down, KEYDOWN and QUIT. In the case of KEYDOWN, the game will shut down only if the backspace key is pressed. In the case of the QUIT event, the game will quit if the user presses the X close button on the window. However, something to note about the QUIT event is that pressing X alone doesn’t quit the game-I know because I tried using the X button to quit the game and ran into an unresponsive window that I ended up force-quitting. Don’t worry, I’ll explain how to quit the game properly later in this post.

Drawing the squares

Now that we have a means to keep our game running (or close it if we so choose), let’s now draw the squares onto the gameboard. Here’s the code to do so:

screen.blit(square1.surf, (40, 75))

screen.blit(square1.surf, (115, 75))
screen.blit(square1.surf, (180, 75))
screen.blit(square1.surf, (255, 75))
screen.blit(square1.surf, (330, 75))
screen.blit(square1.surf, (40, 150))
screen.blit(square1.surf, (115, 150))
screen.blit(square1.surf, (180, 150))
screen.blit(square1.surf, (255, 150))
screen.blit(square1.surf, (330, 150))
screen.blit(square1.surf, (40, 225))
screen.blit(square1.surf, (115, 225))
screen.blit(square1.surf, (180, 225))
screen.blit(square1.surf, (255, 225))
screen.blit(square1.surf, (330, 225))
screen.blit(square1.surf, (40, 300))
screen.blit(square1.surf, (115, 300))
screen.blit(square1.surf, (180, 300))
screen.blit(square1.surf, (255, 300))
screen.blit(square1.surf, (330, 300))
screen.blit(square1.surf, (40, 375))
screen.blit(square1.surf, (115, 375))
screen.blit(square1.surf, (180, 375))
screen.blit(square1.surf, (255, 375))
screen.blit(square1.surf, (330, 375))
screen.blit(square1.surf, (40, 450))
screen.blit(square1.surf, (115, 450))
screen.blit(square1.surf, (180, 450))
screen.blit(square1.surf, (255, 450))
screen.blit(square1.surf, (330, 450))

pygame.display.flip()

Even though you’ll only need to create one square object, you’ll need to draw that object 30 different times since the BINGO board will consist of 30 squares drawn in a 6×5 matrix. To draw the squares, you’ll need to use the following method-screen.blit(square1.surf, (x-coordinate, y-coordinate). The screen.blit(...) method drawes the squares onto the screen and it takes two parameters-the square1.surf, which is the surface of the square and a two-integer tuple stating the coordinates where you want the square placed (x-coordinate first, then y-coordinate).

After the 30 instances of the screen.blit() method, the pygame.display.flip() method is called, which simply updates the game screen to display the 30 squares. You might’ve thought the screen.blit() method already accomplishes this, but this method simply draws the squares while the pygame.display.flip() method updates the game screen to ensure the squares are present.

Quitting the game

As I mentioned earlier in this post, I’ll show you how to properly quit the game. Here are the two lines of code needed to do so:

pygame.quit()

sys.exit()

To properly end the pygame session, you’ll need to include these two commands in your code. Why do you need them both? Wouldn’t one command or the other work?

You need both commands because the pygame.quit() method simply shuts down the active pygame module while the sys.exit() method proprely shuts down the entire window.

And now, let’s see our work!

Now that we’ve got the basic game outline set up, let’s see our work by running our script!

As you see here, we simply have one giant lime-green square. However, that lime green square consists of the 30 squares we drew earlier-the squares are simply drawn on top of each other, hence why the output looks like one big square. Don’t worry, in the next post we’ll make this square look more like a BINGO board!

A small coding improvement

As you noticed earlier in this post, I was calling the screen.blit() method 30 times while drawing the squares. However, there is a much better way to accomplish this:

x = [40, 115, 180, 255, 330]

y = [75, 150, 225, 300, 375, 450]

for xcoord in x:
for ycoord in y:
screen.blit(square1.surf, (xcoord, ycoord))

In this example, I placed all possible x and y coordinates into arrays and drew each square by looping through the values in both arrays. Here’s the output of this improved approach:

As you see, not only did we improve the process for drawing the squares onto the game screen, but we also got the same result we did when we were calling the screen.blit() method 30 times.

For your reference, the code

Just in case you want it, here’s the code we used for our game development in this post (and we will certainly change it throughout this series of posts). I’m copying the code here since WordPress won’t let me upload .PY files:

import pygame

from pygame.locals import *
import sys

class Square(pygame.sprite.Sprite):
def __init__(self):
super(Square, self).__init__()

self.surf = pygame.Surface((75, 75))

self.surf.fill((50, 205, 50))
self.rect = self.surf.get_rect()

pygame.init()

screen = pygame.display.set_mode((800, 600))

square1 = Square()

gameOn = True

while gameOn:
for event in pygame.event.get():

if event.type == KEYDOWN:

if event.key == K_BACKSPACE:
gameOn = False

# Check for QUIT event
elif event.type == QUIT:
gameOn = False

x = [40, 115, 180, 255, 330]
y = [75, 150, 225, 300, 375, 450]

for xcoord in x:
for ycoord in y:
screen.blit(square1.surf, (xcoord, ycoord))

# Update the display using flip
pygame.display.flip()

pygame.quit()
sys.exit()

Thanks for reading, and I look forward to having you code along with me in 2024!

Michael