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
-
So Long Neofetch and Thanks for the Info
Today is a day that every Linux user who enjoys bragging about their system(s) will mourn, as Neofetch has come to an end.
-
Ubuntu 24.04 Comes with a “Flaw"
If you're thinking you might want to upgrade from your current Ubuntu release to the latest, there's something you might want to consider before doing so.
-
Canonical Releases Ubuntu 24.04
After a brief pause because of the XZ vulnerability, Ubuntu 24.04 is now available for install.
-
Linux Servers Targeted by Akira Ransomware
A group of bad actors who have already extorted $42 million have their sights set on the Linux platform.
-
TUXEDO Computers Unveils Linux Laptop Featuring AMD Ryzen CPU
This latest release is the first laptop to include the new CPU from Ryzen and Linux preinstalled.
-
XZ Gets the All-Clear
The back door xz vulnerability has been officially reverted for Fedora 40 and versions 38 and 39 were never affected.
-
Canonical Collaborates with Qualcomm on New Venture
This new joint effort is geared toward bringing Ubuntu and Ubuntu Core to Qualcomm-powered devices.
-
Kodi 21.0 Open-Source Entertainment Hub Released
After a year of development, the award-winning Kodi cross-platform, media center software is now available with many new additions and improvements.
-
Linux Usage Increases in Two Key Areas
If market share is your thing, you'll be happy to know that Linux is on the rise in two areas that, if they keep climbing, could have serious meaning for Linux's future.
-
Vulnerability Discovered in xz Libraries
An urgent alert for Fedora 40 has been posted and users should pay attention.