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()
Figure 2: The initial layout of the bingo caller display. Tiles on the left have not yet been called.

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.

Figure 3: This text was rendered with a pixels-per-unit size of 8. As you can see, the tile number N40 is barely recognizeable.

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

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • DIY Scoreboard

    We look at a broadcast video system network that uses Python code to control a video router and check out another program that creates a scoreboard.

  • Panda3D

    Several free game engines are available for Linux users, but programming with them is often less than intuitive. Panda3D is an easy-to-use engine that is accessible enough for newcomers but still powerful enough for the pros at Disney Studios.

  • Top Coder

    Springtime is application time! Mike Schilli, who has experience with job application procedures at companies in Silicon Valley, digs up a question asked at the Google Engineering interview and explains a possible solution in Go.

  • Nerf Target Game

    A cool Nerf gun game for a neighborhood party provides a lesson in Python coding with multiple processors.

  • Tutorials – WM Tiling

    Regular window managers are so 2016 – install a tiling WM and work faster, smarter, and cooler.

comments powered by Disqus
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.

Learn More

News