Program a game of bingo with ReportLab and Panda3D for Python
Panda3D
Now that you have a set of bingo cards, it's time to play the game. However, it's no fun choosing one of your friends to sit out and call the numbers, so I created an automated, visually interesting bingo caller (Figure 2) that uses the Panda3D library, a 3D rendering and graphics environment (Listing 2). The program also uses the eSpeak speech synthesizer to call the numbers.
eSpeak
eSpeak is an open source text-to-speech engine that you can install with your distribution's package manager. Once installed, open a terminal and type:
espeak "Hello World!"
Your computer should greet you verbally. When it's not convenient to display a debug message, it may be helpful for your project to literally tell you what is happening.
Listing 2
bingoCaller.py
001 from direct.showbase.ShowBase import ShowBase 002 003 from panda3d.core import WindowProperties 004 from panda3d.core import TextNode 005 from panda3d.core import NodePath 006 from panda3d.core import Point3 007 from panda3d.core import DynamicTextFont 008 009 from direct.interval.LerpInterval import LerpPosInterval 010 from direct.interval.IntervalGlobal import * 011 012 from direct.task import Task 013 014 import pprint 015 import random 016 import os 017 import sys 018 import thread 019 020 class bingo ( ShowBase ): 021 def __init__ ( self ): 022 ShowBase.__init__ ( self ) 023 wp = WindowProperties() 024 wp.setFullscreen(1) 025 wp.setSize(1280, 720) 026 base.openMainWindow() 027 base.win.requestProperties(wp) 028 base.graphicsEngine.openWindows() 029 base.disableMouse() 030 self.auto = False 031 self.calledTiles = list() 032 033 base.camera.setPos( 40, -85, 15 ) 034 035 font = loader.loadFont ( "TitanOne-Regular.ttf" ) 036 font.setPixelsPerUnit ( 240 ) 037 038 self.text= TextNode('text') 039 self.text.setText( "BINGO" ) 040 self.text.setTextColor(1,1,1,1) 041 self.text.font = font 042 043 self.text3d = NodePath(self.text) 044 self.text3d.reparentTo(self.render) 045 self.text3d.setScale(.8) 046 self.text3d.setPos( 39 , -75 , 17 ) 047 048 self.initTiles() 049 self.accept ( "q" , sys.exit ) 050 self.accept ( "c" , self.callTile , [ True ] ) 051 self.accept ( "a" , self.autoCall ) 052 self.accept ( "space" , self.stopAuto ) 053 self.accept ( "r" , self.reset ) 054 055 def autoCall ( self ): 056 self.auto = True 057 taskMgr.doMethodLater ( 5 , self.callTile , "Call Bingo" , extraArgs = [] ) 058 059 def stopAuto ( self ): 060 self.auto = False 061 062 def getCam ( self ): 063 pprint.pprint ( base.camera.getPos() ) 064 065 def initTiles ( self ): 066 self.tiles = list() 067 self.tiles3d = dict() 068 069 bingoWord = "BINGO" 070 total = 0 071 for char in bingoWord: 072 for i in range ( 15 ): 073 self.tiles.append ( char + str ( i + total + **1 ) ) 074 total += 15 075 076 font = loader.loadFont ( "TitanOne-Regular.ttf" ) 077 font.setPixelsPerUnit ( 240 ) 078 079 oldLetter = "" 080 for tile in self.tiles: 081 self.text= TextNode('text') 082 self.text.setText( tile ) 083 self.text.setTextColor(1,1,1,1) 084 self.text.font = font 085 086 self.text3d = NodePath(self.text) 087 self.text3d.reparentTo(self.render) 088 self.text3d.setScale(.8) 089 if tile [ 0 ] == "B": x = 25 090 elif tile [ 0 ] == "I": x = 28 091 elif tile [ 0 ] == "N": x = 31 092 elif tile [ 0 ] == "G": x = 34 093 elif tile [ 0 ] == "O": x = 37 094 095 if oldLetter != tile [ 0 ]: 096 z = 20 097 oldLetter = tile [ 0 ] 098 099 self.text3d.setPos( x , -50 , z ) 100 self.text3d.setTwoSided(True) 101 z -= 1 102 self.tiles3d [ tile ] = self.text3d 103 random.shuffle ( self.tiles ) 104 105 def callTile ( self , manual = False ): 106 if self.auto == False and manual == False: return 107 108 if len ( self.tiles) > 0: tile = self.tiles.pop() 109 else: return 110 111 startPos = self.tiles3d [ tile ].getPos() 112 newPos = Point3 ( startPos [ 0 ] + 17 , startPos [ 1 ] , startPos [ 2 ] ) 113 114 i = LerpPosInterval ( self.tiles3d [ tile ] , 2 , Point3 ( 39.4 , -82 , 14.7 ) ) 115 park = LerpPosInterval ( self.tiles3d [ tile ] , 2 , newPos ) 116 117 Sequence ( i , Wait ( 1 ) , park ).start() 118 thread.start_new_thread ( self.speak , ( tile , ) ) 119 120 self.calledTiles.append ( tile ) 121 if self.auto == True: self.autoCall() 122 123 def speak ( self , string ): 124 os.system ( "espeak " + string ) 125 126 def reset ( self ): 127 resetParallel = Parallel() 128 for obj in self.calledTiles: 129 pos = self.tiles3d [ obj ].getPos() 130 newPos = Point3 ( pos [ 0 ] - 17 , pos [ 1 ] , pos [ 2 ] ) 131 resetParallel.append ( LerpPosInterval ( self.tiles3d [ obj ] , 2 , newPos ) ) 132 if obj not in self.tiles: self.tiles.append ( obj ) 133 resetParallel.start() 134 random.shuffle ( self.tiles ) 135 136 game = bingo() 137 game.run()
When you are working in a 3D environment, you have to shift your thinking a little bit. When working on a computer, most of us are only worried about two dimensions when trying to get something to show up in the right place onscreen. In three dimensions, though, you add depth (the distance from the camera or viewpoint) and height off the ground.
Think of your 3D canvas like your living room. Imagine you are sitting on a couch, viewing a coffee table, a TV, and all of the decorations that make up your home. If you move to a different place in the room, the objects look different. You can also put objects on a shelf or table to change their height. All of these factors have to be considered when working in a 3D environment. Luckily Panda3D hides a lot of the inner workings and makes it easy to set everything up.
Bingo Caller
As with any Python program, you have to import the appropriate libraries. Panda3D splits all of its functions into sublibraries, so you will have a number of imports just for those (Table 1).
Table 1
bingoCaller.py Imports
Line No. | Import | Function |
---|---|---|
1 |
ShowBase |
Main Python interface to Panda3D |
3-7 |
panda3d.core |
|
3 |
WindowProperties |
Controls the window showing the Panda3D project |
4 |
TextNode |
Creates text objects |
5 |
NodePath |
Panda3D internal object references |
6 |
Point3 |
Represents a 3D point |
7 |
DynamicTextFont |
Loads TTF fonts |
9 |
direct.interval.LerpInterval |
Lerps are the Panda3D movement controllers |
10 |
direct.interval.IntervalGlobal |
Allows things to happen over a period of time |
12 |
direct.task |
Recurrent tasks after a specific period of time |
14 |
pprint |
Prints nicely formatted strings (mainly for debugging) |
15 |
random |
Gets numbers in an arbitrary order |
16 |
os |
Calls functions in the underlying operating system |
17 |
sys |
Used for sys.exit to close the program on request |
18 |
thread |
Runs multiple portions of the program concurrently |
As with any Python program, __init__
is called automatically when a class is instantiated. Here it is used to set up the Panda3D environment, starting with calling ShowBase.__init__ ( self )
(line 22), which gives the bingo
class all of the variables, functions, and set up associated with ShowBase
or Panda3D.
By default Panda3D opens a normal desktop window. To get the application to run fullscreen, though, you need to do a little bit of setup. Line 23 creates a WindowProperties
object, which allows you to call wp.setFullscreen
(line 24) to request a fullscreen window and wp.setSize
to request the screen resolution.
As mentioned earlier, Panda3D opens a window by default, but in this case, you need to force it to open now with base.openMainWindow
(line 26) and then apply the WindowProperties
object created above with base.win.requestProperties
(line 27). Finally, you ask the graphics engine to draw the windows onscreen with base.graphicsEngine.openWindows
(line 28).
The next line calls base.disableMouse
. Panda3D includes by default a set of built-in controls to let you explore the Panda3D world. The mouse controls your orientation (where you are looking), and keyboard keys move you forward, back, left, and right. In this case, though, you want to control the camera position automatically. If you do not disable the mouse, the camera commands will be ignored.
If you were writing an interactive game, you could allow the player to trigger an in-game animation sequence to introduce the next level. Once you disable the mouse, you can do whatever you want to move the user around, make them look in a certain direction, and so on. Once you have told your part of the story, you use enableMouse
to return control and allow the user to keep exploring.
Lines 30 and 31 set some variables that are used later: self.auto
is a flag that indicates whether numbers are currently being called, and self.calledTiles
is a list of bingo numbers that have been called. More on these a little later.
Line 33 sets the initial camera position with base.camera.setPos
. Just like setting up a camera in the real world, the coordinates are relative to the objects set in the scene. You need the camera to be a little ways back so that it can see everything.
Line 35 loads the custom font; loader.loadFont
makes a TTF file available for converting into a 3D object. The internal resolution of the font just loaded is set by setPixelsPerUnit
on line 36. By default, the resolution is a fairly low value (Figure 3), which is fine if the text will be far away. However, the text will be moved very close to the camera, so the value needs to be turned up to get a sharper line when up close.
To create a 3D text object, in this case "BINGO" for the top of the screen, TextNode
(line 38) contains the text to render with setText
(line 39). The setTextColor
in line 40 is what it sounds like, but its arguments are a little different: Instead of arguments for red, green, blue, and alpha being mapped from 0 to 255, they are mapped from 0 to 1. All 1
entries get you white. Finally self.text.font = font
assigns the font loaded on line 35.
So far this is only a 2D text object. The next set of lines puts it in three dimensions. A node
is an object in Panda 3D's internal library, so NodePath(self.text)
(line 43) gets the address of the text node just created. On line 44, then, the reparentTo
assigns it to self.render
. In Panda3D anything attached to self.render
is rendered as a 3D object. Now that it is in the 3D realm, a setScale
(line 45) sets the text size, and setPos
(line 46) positions it in the 3D world in front of the camera.
Line 48 calls initTiles
, which creates the numbers that fly around the screen, but more on that a little further along.
The last section is lines 49-53, where a few self.accept
lines set up keyboard input. The first argument is the key to trigger a response, and the second argument is the function to call when it is pressed. The optional third argument is a list of parameters to pass to the function when the key is pressed. Line 50 says, "watch for the c key to be pressed, and when it is, run self.callTile
and give it the parameter True
".
Automatic Calling
The autoCall
function enables automatic bingo calling. To start, self.auto
is set to True
(line 56), then taskMgr.doMethodLater
(line 57) sets up a function to be called in the future. The argument list is how long to wait (five seconds), what to call (self.callTile
), an internal label (Call Bingo
), and any extra arguments (empty list=none). Lines 59 and 60 stop the auto calling by setting self.auto
to False
.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Wine 10 Includes Plenty to Excite Users
With its latest release, Wine has the usual crop of bug fixes and improvements, along with some exciting new features.
-
Linux Kernel 6.13 Offers Improvements for AMD/Apple Users
The latest Linux kernel is now available, and it includes plenty of improvements, especially for those who use AMD or Apple-based systems.
-
Gnome 48 Debuts New Audio Player
To date, the audio player found within the Gnome desktop has been meh at best, but with the upcoming release that all changes.
-
Plasma 6.3 Ready for Public Beta Testing
Plasma 6.3 will ship with KDE Gear 24.12.1 and KDE Frameworks 6.10, along with some new and exciting features.
-
Budgie 10.10 Scheduled for Q1 2025 with a Surprising Desktop Update
If Budgie is your desktop environment of choice, 2025 is going to be a great year for you.
-
Firefox 134 Offers Improvements for Linux Version
Fans of Linux and Firefox rejoice, as there's a new version available that includes some handy updates.
-
Serpent OS Arrives with a New Alpha Release
After months of silence, Ikey Doherty has released a new alpha for his Serpent OS.
-
HashiCorp Cofounder Unveils Ghostty, a Linux Terminal App
Ghostty is a new Linux terminal app that's fast, feature-rich, and offers a platform-native GUI while remaining cross-platform.
-
Fedora Asahi Remix 41 Available for Apple Silicon
If you have an Apple Silicon Mac and you're hoping to install Fedora, you're in luck because the latest release supports the M1 and M2 chips.
-
Systemd Fixes Bug While Facing New Challenger in GNU Shepherd
The systemd developers have fixed a really nasty bug amid the release of the new GNU Shepherd init system.