Home-built shooting game with Nerf targets and a Raspberry Pi

Ready, Aim, Fire

© Lead Image © Tithi Luadthong, 123rf.com

© Lead Image © Tithi Luadthong, 123rf.com

Article from Issue 242/2021
Author(s):

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

Last Halloween, I was asked to put together a Nerf game for a local neighborhood party. Not wanting to do just the same old thing, I got together with a couple of local makers, and we built a set of electronic targets to create a real-life tower defense game! Although COVID put a damper on this year's plans, we still managed to get most of the hardware together to expand the experience for a second round.

The original game had three targets: an Arduino Uno [1] brain, an audio amplifier, and a loudspeaker. Each target was placed inside a wooden structure that we called a "tower" (Figure 1). The object of the game was to shoot at the targets with a gun firing Nerf darts. A confetti canon was also built into the tower to announce when the tower "fell," which means that the target on the tower had sustained a predefined number of hits from the Nerf gun. We built two identical tower sets, one for each end of the field. Each system operated independently. The game monitors were responsible for powering down the system when the other team won.

Figure 1: The original Nerf targets: The blue disks are worth one point; the red disk in the center tower is worth three points.

The larger targets are worth one point each, and the smaller center target is worth three points. Games can be selected to run between 10 and 100 hits. A "traffic light" health gauge on the center tower gave an approximate value of how many hits remained before the game ended (see Figure 1).

When we deployed the system a second time, we arranged the hardware a little differently. Each target now has a single piezo sensor and its own NodeMCU-based (open source firmware) Arduino. Targets are powered by 3.7V LiPo batteries, so they are self-contained. Hits are transmitted to a server running on a Raspberry Pi on the edge of the playing field. Score announcements are still made by audio because outdoor video displays are unwieldy and expensive.

Arduino Code

The schematic for the second version of the Nerf tower is shown in Figure 2. Each node has only a single piezo, and all of the sound production now comes from the Raspberry Pi, which greatly simplifies the nodes compared to the original design. The nodes themselves are massively simplified. The main processor in this version is an ESP8266 on a WiFi Kit 8 [2]. (See the box entitled "More Inputs.")

Figure 2: Schematic for the second version of the Nerf tower.

More Inputs

The ESP8266 only has a single analog input. Although it possible to wire additional piezos in parallel, it is also possible to add a little bit of external hardware and expand the analog capability as well. Consider the LM339 quad comparator chip. It has four comparators that operate independently. Each comparator has a positive and a negative input. When the voltage on the positive input is higher than the voltage on the negative input, the output is active. It should be noted that the output of each comparator switches between ground and high impedance rather than V+. So if you want to use this with your Arduino, you'll need to turn on the pull-up resistor on whatever pin you connect it to.

This circuit connects the piezo to the positive input. The negative input will have a potentiometer with the lower end grounded, the top end connected to V+, and the wiper connected to the positive input of the comparator. The potentiometer then is the trigger level, or the voltage that needs to be exceeded by the piezo (how hard a hit) to enable the output.

With this one chip, I can add up to four piezo sensors to my target. The downside to this approach is that each piezo requires an associated variable resistor to tune its sensitivity. The variable resistor can't be adjusted remotely like the analog input can.

Each target is a self contained wireless node. A NodeMCU-based processor connects to WiFi and transmits hits to a central game server. The Arduino code is simpler, because all it does is report hits. All of the game logic lives in the game server itself. Each node also accepts a few commands to control game play or calibrate the node.

The code for the Arduino is shown in Listing 1. There are several different things needed to set up a WiFi connection so that's what most of these includes are for (lines 1-7). Arduino.h provides info about the hardware this sketch is currently running on (line 8), and U8g2lib.h is a library to draw on the attached OLED screen on the back of the board.

Listing 1

Arduino Code

001 #include <ESP8266WiFi.h>
002 #include <ESP8266WiFiGeneric.h>
003 #include <ESP8266WiFiMulti.h>
004 #include <ESP8266WiFiSTA.h>
005 #include <ESP8266WiFiType.h>
006 #include <WiFiClient.h>
007 #include <WiFiUdp.h>
008 #include <Arduino.h>
009 #include <U8g2lib.h>
010
011 #ifdef U8X8_HAVE_HW_SPI
012 #include <SPI.h>
013 #endif
014 #ifdef U8X8_HAVE_HW_I2C
015 #include <Wire.h>
016 #endif
017
018 const char* ssid = "YOURNETWORKNAME";
019 const char* password = "YOURNETWORKPASSWORD";
020 const char* server = "IP_OF_SERVER";
021
022 int iTrigger = 333;
023 int iHits = 0;
024 unsigned long ulNextHit = 0;
025
026 WiFiClient client;
027
028 U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ 16, /* clock=*/ 5, /* data=*/ 4);
029
030 void setup(void) {
031   int iConnectCount = 0;
032
033   u8g2.begin();
034
035   WiFi.mode ( WIFI_STA );
036   WiFi.begin ( ssid , password );
037
038   u8g2.clearBuffer();    // clear the internal memory
039   u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
040   u8g2.drawStr(0,10,"Connecting to AP");  // write something to the internal memory
041   u8g2.sendBuffer();    // transfer internal memory to the display
042
043   while ( WiFi.status() != WL_CONNECTED )
044   {
045     delay ( 500 );
046     u8g2.clearBuffer();
047     u8g2.drawStr(0,10,"Connecting to AP");
048     u8g2.setCursor ( 0 , 20 );
049     u8g2.print ( iConnectCount );
050     iConnectCount ++;
051     u8g2.sendBuffer();
052   }
053
054   if (!client.connect( server , 9000 )) {
055     u8g2.clearBuffer();  // clear the internal memory
056     u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
057     u8g2.drawStr(0,10,"Connection Failed");  // write something to the internal memory
058     u8g2.sendBuffer();
059     delay(5000);
060     return;
061   }
062
063   u8g2.clearBuffer();
064   u8g2.setFont(u8g2_font_ncenB08_tr);
065   u8g2.drawStr ( 0 , 10 , "Connected to AP" );
066   u8g2.setCursor ( 0 , 20 );
067   u8g2.print(WiFi.localIP());
068   u8g2.sendBuffer();
069 }
070
071 void loop(void) {
072   int iSensor = 0;
073
074   iSensor = analogRead ( A0 );
075
076   u8g2.clearBuffer();
077   u8g2.setCursor ( 0 , 10 );
078   u8g2.print ( iSensor );
079   u8g2.setCursor ( 30 , 10 );
080   u8g2.print ( iTrigger );
081   u8g2.setCursor ( 70 , 10 );
082   u8g2.print ( iHits );
083   u8g2.setCursor ( 10 , 20 );
084   if ( ulNextHit > millis() ) u8g2.print ( ulNextHit - millis() );
085   u8g2.sendBuffer();
086
087   if ( iSensor > iTrigger && millis() > ulNextHit )
088   {
089     iHits += 1;
090     ulNextHit = millis() + 1000;
091
092     if ( client.connected() == false )
093     {
094       client.connect ( server , 9000 );
095     }
096     client.print ( "H:" );
097     client.println ( iHits );
098   }
099
100   if ( client.available() > 0 )
101   {
102     String received = client.readStringUntil ( ';' );
103     switch ( received [ 0 ] )
104     {
105       case 'R': iHits = 0;break;
106       case 'T': iTrigger = received.substring ( 1 ).toInt();break;
107       case 'D': ulNextHit = millis() + received.substring ( 1 ).toInt();break
108     }
109   }
110 }

Lines 11-16 are a unique kind of if statement. ifdef is an if statement that is interpreted by the compiler itself rather than the C code in other parts of the sketch. In this case, if U8x8_HAVE_HW_SPI is defined (line 11) then include <SPI.h> (line 12). endif on line 13 closes the block. Lines 14-16 work the same way, but I'm checking for I2C and including Wire.h instead.

Any variables that are defined outside of functions are global – they can be accessed by any function in the program. On lines 18-20, I'm defining constants. These constants can't be changed once they are set up. In this case, it is the network SSID (line 18), network password (line 19), and the IP of the game server (line 20).

On line 22, iTrigger is the value that the analog input must go above to trigger a hit. I default to 333, but this can be changed by the server once it connects. Line 23 sets up iHits, which is how many hits this particular node has registered. The game server tracks hits for scoring purposes, but this value is used to update the display, so it is easy to confirm that the sensor is working. Line 24 sets up ulNextHit, which generally is assigned from the millis() function plus a delay. Once millis() is greater than this value, then the timer has expired.

Line 26 creates client as an instance of WiFiClient. This client's purpose is to interact with all of the network commands. Finally, line 28 sets up all of the connections for the OLED display attached to the board.

Just like in any other Arduino program, setup runs once. The setup function (lines 30-69) defines some variables, connects to the WiFi, sets up the display, and then connects to the game server.

The main loop appears in lines 71-110. Each cycle through the main loop updates the display with some internal readings, checks to see if the sensor has been hit, and, finally, sees if there are any characters waiting to receive and process.

Line 72 initializes iSensor; then I do an analogRead on A0 to get the voltage coming off the piezo disk [3]. On lines 76-85, I update the display with three values: the most recent analog reading from A0, the current value of iTrigger, and iHits. This hit counter is only for this node, not game wide. Then, if ulNextHit is greater than millis(), I display the remaining time out value – how long until this node will respond to a hit again.

If iSensor is greater than iTrigger, then this target has been hit. I also check that millis() is greater than ulNextHit. If it's not, then this target is currently disabled. If all that checks out, I increment the hit counter with iHits += 1 (line 89) and disable the target for one second with (line 90):

<C>ulNextHit = millis() + 1000;<C>

Line 92 checks to make sure that the client is still connected to the game server, and if not, line 94 re-establishes the connection. Finally, I send the string H: and the number of hits this node has recorded.

Line 100 checks client.available() to see if there are any characters waiting. If the value is greater than zero, the client has received a message and needs to process it. Line 102 reads incoming data into received until it finds a semicolon.

The switch statement on lines 103-108 checks the first character of the string received [ 0 ] to see if it's a command. If it's an R, then I should reset the hit counter with iHits = 0. If it's a T, I use received.substring to get the rest of the string, use toInt to make it an integer, and set iTrigger with this value. That changes how sensitive the piezo disk is. Finally, if it's a D then I set ulNextHit to millis() plus the integer value of whatever the rest of the received string equals. This sets the delay before any new hits will be registered. A delay of zero will re-enable the target immediately.

The Server

The Raspberry Pi server receives information from the Arduino clients and manages the game. The server software is written in Python and shows a text user interface in a terminal (Figure 3). All of the game logic lives in the server software. Whenever a hit is received from the wireless nodes, the server announces "Red target hit!" or "Blue target hit!" Targets are assigned to colors in the server interface. Similarly, the length of the game (number of hits) is also adjusted there. The server can also adjust each node's input sensitivity and enable a global disable on all the node's targets. Disabling the targets is handy for pausing the game for additional instructions.

Figure 3: The user interface presented by the server is text only, so it will run in any terminal. The connections are simulated with netcat, as noted by the 127.0.0.1 address.

I've utilized multiple threads for this server; each task is split off to its own process and deposits input into a queue as it is received. Then the main process handles input from the threads as it arrives and takes care of the human interface. Each thread can block or wait as long as it needs to, because it is running independently of the main program.

Listing 2 shows the server code. The external libraries imported for the game appear in lines 1-7. socket accesses the computer's networking hardware and allows for network communication. thread allows programs to separate into sub-processes. curses controls character cell displays (like terminal windows). time lets you access the system clock and other timing functions. os allows you to talk to the operating system that the program is running on. Queue creates first-in/first-out thread-safe channels to communicate between different threads.

Listing 2

Server Code

001 import socket
002 import thread
003 import pprint
004 import curses
005 import time
006 import os
007 import Queue
008
009 class MySocket:
010   """demonstration class only
011     - coded for clarity, not efficiency
012   """
013
014   def __init__(self, sock=None):
015     if sock is None:
016       self.sock = socket.socket(
017           socket.AF_INET, socket.SOCK_STREAM)
018     else:
019       self.sock = sock
020     self.receivedData = ""
021
022   def connect(self, host, port):
023     self.sock.connect((host, port))
024
025   def send(self, msg):
026     totalsent = 0
027     while totalsent < len ( msg ):
028       sent = self.sock.send(msg[totalsent:])
029       if sent == 0:
030         raise RuntimeError("socket connection broken")
031       totalsent = totalsent + sent
032
033   def receive(self):
034     chunks = []
035     bytes_recd = 0
036     chunk = self.sock.recv(64)
037     if chunk == b'':
038       raise RuntimeError("socket connection broken")
039     self.receivedData += chunk.strip()
040     if ";" in self.receivedData:
041       output = self.receivedData [ :self.receivedData.index ( ";" ) ]
042       self.receivedData = self.receivedData [ self.receivedData.index ( ";" ) + 1 : ]
043       return output
044     else:
045       return None
046
047 class nodeClass:
048   def __init__ ( self , address , port , socket , displayLine ):
049     self.address = address
050     self.port = port
051     self.socket = socket
052     self.hits = 0
053     self.trigger = 333
054     self.delay = 0
055     self.team = None
056     self.displayLine = displayLine
057
058   def statusLine ( self , screen ):
059     if time.time() < self.delay:
060       delayLeft = int ( ( self.delay - time.time() ) * 1000 )
061     else:
062       delayLeft = "ACTIVE"
063
064     colSize = int ( ( curses.COLS-1 ) / 6 )
065     screen.addstr ( self.displayLine + 1 , 0 , self.address )
066     screen.addstr ( self.displayLine + 1 , colSize , str ( self.port ) )
067     screen.addstr ( self.displayLine + 1 , colSize * 2 , str ( self.hits ) )
068     screen.addstr ( self.displayLine + 1 , colSize * 3 , str ( self.trigger ) )
069     screen.addstr ( self.displayLine + 1 , colSize * 4 , str ( delayLeft ) )
070     screen.addstr ( self.displayLine + 1 , colSize * 5 , str ( self.team ) )
071
072   def changeTrigger ( self , delta ):
073     self.trigger += delta
074     self.socket.send ( "T:" + str ( self.trigger ) + ";" )
075
076   def clearHits ( self ):
077     self.hits = 0
078
079   def setDelay ( self , delay ):
080     self.delay = time.time() + delay
081     self.socket.send ( "D:" + str ( delay * 1000 ) + ";" )
082
083 class gameServer:
084   def __init__ ( self ):
085     self.screen = curses.initscr()
086     curses.noecho()
087     curses.cbreak()
088     self.screen.keypad ( True )
089     self.screen.nodelay ( True )
090
091     self.allNodes = list()
092     self.nodeIndex = 0
093     self.cursorIndex = 0
094     self.oldCursor = -1
095
096     self.redScore = 0
097     self.blueScore = 0
098     self.scoreGoal = 10
099
100     self.announceRed = 0
101     self.announceBlue = 0
102
103     self.q = Queue.Queue()
104     thread.start_new_thread ( self.startListening , () )
105
106     colSize = int ( ( curses.COLS ) / 6 )
107     self.screen.addstr ( 0 , 0 , " " * ( curses.COLS ) , curses.A_REVERSE )
108     self.screen.addstr ( 0 , 0 , "IP" , curses.A_REVERSE  )
109     self.screen.addstr ( 0 , colSize , "PORT" , curses.A_REVERSE )
110     self.screen.addstr ( 0 , colSize * 2 , "HITS" , curses.A_REVERSE  )
111     self.screen.addstr ( 0 , colSize * 3 , "TRIGGER" , curses.A_REVERSE  )
112     self.screen.addstr ( 0 , colSize * 4 , "DELAY" , curses.A_REVERSE  )
113     self.screen.addstr ( 0 , colSize * 5 , "TEAM" , curses.A_REVERSE )
114     self.checkScore()
115
116     while 1:
117       if self.q.empty() == False:
118         data = self.q.get ( False )
119         #print ( data [ 1 ] + " from " + data [ 0 ] [ 0 ] + " port " + str ( data [ 0 ] [ 1 ] ) )
120         if data [ 1 ] [ 0 ] == "H":
121           for nd in self.allNodes:
122             if nd.address == data [ 0 ] [ 0 ] and nd.port == data [ 0 ] [ 1 ]:
123               nd.hits += 1
124               nd.delay = time.time() + 1
125               if nd.team == "RED":
126                 self.redScore += 1
127                 thread.start_new_thread ( self.speak , ( "RED" , "HIT" ) )
128               elif nd.team == "BLUE":
129                 self.blueScore += 1
130                 thread.start_new_thread ( self.speak , ( "BLUE" , "HIT" ) )
131               self.checkScore()
132
133       for nd in self.allNodes:
134         nd.statusLine ( self.screen )
135
136
137       key = self.screen.getch()
138       if key != -1:
139         if key == curses.KEY_UP:
140           self.cursorIndex -= 1
141           if self.cursorIndex < 0: self.cursorIndex = 0
142         elif key == curses.KEY_DOWN:
143           self.cursorIndex += 1
144           if self.cursorIndex > len ( self.allNodes ) - 1:
145             self.cursorIndex = len ( self.allNodes ) - 1
146         elif key == curses.KEY_LEFT:
147           self.allNodes [ self.cursorIndex ].changeTrigger ( -10 )
148         elif key == curses.KEY_RIGHT:
149           self.allNodes [ self.cursorIndex ].changeTrigger ( 10 )
150         elif key == ord ( 'r' ):
151           self.allNodes [ self.cursorIndex ].team = "RED"
152         elif key == ord ( 'b' ):
153           self.allNodes [ self.cursorIndex ].team = "BLUE"
154         elif key == ord ( 'x' ):
155           self.allNodes [ self.cursorIndex ].clearHits()
156         elif key == ord ( '1' ):
157           for nd in self.allNodes:
158             nd.setDelay ( 10 )
159         elif key == ord ( '3' ):
160           for nd in self.allNodes:
161             nd.setDelay ( 30 )
162         elif key == ord ( '6' ):
163           for nd in self.allNodes:
164             nd.setDelay ( 60 )
165         elif key == ord ( "-" ):
166           self.scoreGoal -= 10
167           if self.scoreGoal < 10: self.scoreGoal = 10
168           self.checkScore()
169         elif key == ord ( "=" ):
170           self.scoreGoal += 10
171           self.checkScore()
172
173       if self.cursorIndex != self.oldCursor:
174         self.screen.addstr ( self.oldCursor + 1 , curses.COLS-1 , " " )
175         self.screen.addstr ( self.cursorIndex + 1 , curses.COLS-1 , "<" )
176         self.oldCursor = self.cursorIndex
177
178       self.screen.refresh()
179
180   def checkScore ( self ):
181     screenXhalf = int ( curses.COLS / 2 )
182     screenXquarter = int ( screenXhalf / 2 )
183
184     self.screen.addstr ( curses.LINES - 5 , screenXquarter - 3 , "RED SCORE" )
185     self.screen.addstr ( curses.LINES - 4 , screenXquarter , str ( self.redScore ) )
186     self.screen.addstr ( curses.LINES - 5 , screenXhalf + screenXquarter - 4 , "BLUE SCORE" )
187     self.screen.addstr ( curses.LINES - 4 , screenXhalf + screenXquarter , str ( self.blueScore ) )
188     self.screen.addstr ( curses.LINES - 3 , screenXhalf - 6 , "SCORING GOAL" )
189     self.screen.addstr ( curses.LINES - 2 , screenXhalf , str ( self.scoreGoal ) )
190
191     if self.redScore >= int ( self.scoreGoal * .5 ) and self.announceRed < 5:
192       self.screen.addstr ( 15 , 5 , "Red Team 50%" )
193
194       self.announceRed = 5
195       thread.start_new_thread ( self.speak , ( "RED" , "SCORE" ) )
196     elif self.redScore >= int ( self.scoreGoal * .75 ) and self.announceRed < 7:
197       self.announceRed = 7
198       thread.start_new_thread ( self.speak , ( "RED" , "SCORE" ) )
199     elif self.redScore >= int ( self.scoreGoal * .9 ) and self.announceRed < 9:
200       self.announceRed = 9
201       thread.start_new_thread ( self.speak , ( "RED" , "SCORE" ) )
202     elif self.redScore >= self.scoreGoal and self.announceRed < 10:
203       self.announceRed = 10
204       thread.start_new_thread ( self.speak , ( "RED" , "WINNER" ) )
205
206     if self.blueScore >= int ( self.scoreGoal * .5 ) and self.announceBlue < 5:
207       self.announceBlue = 5
208       thread.start_new_thread ( self.speak , ( "BLUE" , "SCORE" ) )
209     elif self.blueScore >= int ( self.scoreGoal * .75 ) and self.announceBlue < 7:
210       self.announceBlue = 7
211       thread.start_new_thread ( self.speak , ( "BLUE" , "SCORE" ) )
212     elif self.blueScore >= int ( self.scoreGoal * .9 ) and self.announceBlue < 9:
213       self.announceBlue = 9
214       thread.start_new_thread ( self.speak , ( "BLUE" , "SCORE" ) )
215     elif self.blueScore >= self.scoreGoal and self.announceBlue < 10:
216       self.announceBlue = 10
217       thread.start_new_thread ( self.speak , ( "BLUE" , "WINNER" ) )
218
219
220   def speak ( self , team , message ):
221     team = team.lower()
222     if message == "HIT":
223       os.system ( "espeak \"" + team + " target hit!\"" )
224     if message == "SCORE" and team == "red":
225       self.screen.addstr ( 16 , 5 , "Speaking score" )
226       time.sleep ( 1.5 )
227       os.system ( "espeak \"" + team + " base " + str ( ( 10 - self.announceRed ) * 10 ) + "percent\"" )
228     if message == "SCORE" and team == "blue":
229       time.sleep ( 1.5 )
230       os.system ( "espeak \"" + team + " base " + str ( ( 10 - self.announceBlue ) * 10 ) + "percent\"" )
231
232     if message == "WINNER":
233       time.sleep ( 1.5 )
234       os.system ( "espeak \"" + team + " base has fallen, game over!\"" )
235
236   def startListening ( self ):
237     serversocket = socket.socket ( socket.AF_INET , socket.SOCK_STREAM )
238     serversocket.bind ( ( socket.gethostname() , 9000 ) )
239     serversocket.listen ( 5 )
240     while 1:
241       ( clientsocket , address ) = serversocket.accept()
242       thread.start_new_thread ( self.node , ( clientsocket , address , self.q ) )
243
244   def node ( self , client , address , q ):
245     sock = MySocket ( client )
246     self.allNodes.append ( nodeClass ( address [ 0 ] , address [ 1 ] , sock , self.nodeIndex ) )
247     self.nodeIndex += 1
248
249     while 1:
250       data = sock.receive()
251       if data != None:
252         q.put ( ( address , data ) )
253
254 gs = gameServer()

send (lines 25-31) is the "transmitter" of the socket. It accepts a single parameter, msg, which is the data to be sent. Line 26 sets up totalsent, which is the number of bytes actually transmitted. Sockets don't always send all the data at once. Each time you call self.sock.send, it will return how many bytes it actually sent. It is up to the program to keep calling self.sock.send until the entire message has been transmitted. Here I accomplish this with

<C>while totalsend < len ( msg )<C>

(line 27). If I try to send and zero bytes get transmitted, there's a problem with the socket connection. This is caught on lines 29-30. Finally

<C>totalsent = totalsent + sent<C>

updates the transmitted byte count.

In lines 33-45, receive is the counterpart to send. Line 36 asks the socket for a data chunk with self.sock.recv(64). (64 asks for no more than 64 bytes.) If the data returned is empty, then there's a problem with the socket connection. Lines 37-38 check for that. Then strip chunk removes any whitespace characters and adds the result to self.receivedData (line 39).

Line 40 checks for a semicolon in self.receivedData – the end of an incoming message. There's nothing special about the semicolon as end of message; its just the character I picked to signify it. If a semicolon is found, I create the string output, which is all of self.receivedData up to the location of the semicolon (line 41):

<C>self.receivedData.index ( ";" )<C>

nodeClass (lines 47-81) is the server representation of the hardware node I described earlier. The gameServer class (lines 83-252), which is initialized when the program starts and sets everything else in motion, is the main entry point in the code.

I initialize the curses library [4] to provide character-cell management of the terminal window and then set up variables that I use to manage instances of nodeClass and the screen itself. self.allNodes stores each instance of the class. self.nodeIndex is a count of the number of nodes that have connected. self.cursorIndex and self.oldCursor track the cursor movement in the user interface. Other variables track each team's score and the number of points to win the game.

The main loop appears in lines 116-178. Line 116 is an infinite while to continually process events from both the nodes and user input. Line 117 checks if self.q.empty() is False. If it is (the queue is not empty), then line 118 gets the next entry from the queue. If the first character of the received data is an "H" (a target has been hit), then I set up a loop to walk down self.allNodes. Once I find the matching IP address and port, I increase the hit counter, set the delay to one second from now, increment the appropriate team's score, and announce the hit.

The next instance of thread.start_new_thread.self.speak executes an external program, so it will block until the program finishes running. In this case, the external program is the speech program that makes an announcement. By launching the speech program in its own thread, the main program will continue to run even while speech is happening. Finally, I call self.checkScore, since scores have been updated. This will redraw the score on the interface. If a milestone has been hit, it will also announce a percentage of health remaining.

Lines 133 and 134 loop through all of the nodes again and call statusLine. The argument is the reference to self.screen, the curses screen buffer.

Line 137 uses self.screen.getch() for the most recent key pressed (if any). If no key has been pressed, then it will return -1. If a key has been pressed, then I move on to the blocks of if/elif to process each key. Lines 139-149 manage responses to a user pressing the arrow keys. The up and down arrows move the cursor up and down in the node list. The left and right arrows change the sensitivity for how hard the dart has to hit the target.

Lines 150-153 let you press r or b for self.allNodes [ self.cursorIndex ].team to refer to either the RED or BLUE team. This value is used for the overall game score to decide which team should be credited with a hit.

Pressing x on the keyboard calls the current node's clearHits method, which resets the node's hit counter to zero.

The checkScore function (lines 180-217) draws the score to the user interface and also plays speech if certain milestones are reached. Line 191 checks to see if self.redScore is greater than or equal to 50 percent of self.scoreGoal and that self.announceRed is less than 5. This signifies that the 50 percent announcement has not yet been spoken. If both conditions are true, then self.announceRed is set to 5 (line 194) and speak is called in a new thread. This process repeats for 70 percent, 90 percent, and 100 percent (game over). The entire block is then repeated for the blue team.

The speak function (lines 220-234) makes a system call to eSpeak [5]. Each type of message is customized within each if/elif. If this is a HIT then say "[team color] target hit!"

If this is a SCORE update and it's the red team, then pause for 1.5 seconds and say "red base " (calculate percent remaining by subtracting self.announceRed from 10 and then multiply by 10) and then say "percent." The message ends up being something like "red base 50 percent". The same thing happens for the blue team.

If the message is WINNER, wait 1.5 seconds and then say "[team color] base has fallen, game over!"

Starting Up

The startListening function (lines 236-242) invokes all of the socket commands to open a socket and listen for incoming connections from nodes.

socket.socket creates a socket; its parameters socket.AF_INET and socket.SOCK_STREAM specify IPv4 addresses and TCP sockets respectively. serversocket.bind tells the socket where to listen for connections. socket.gethostname asks the system what its name is on the network. You can also specify 127.0.0.1 to only accept local connections. The second argument is the port number, 9000 in this case. Then I call serversocket.listen for up to five connections. If connection number 6 arrives before any previous connections have been processed, the sixth connection attempt will fail. Once earlier connections have been established, further connections can proceed normally.

Line 240 enters an infinite loop to accept incoming connections and create a self.node thread for each connection. startListening itself runs in its own thread so the infinite loop waiting for connections won't hold up the main program.

The node function (lines 244-252) accepts three parameters: the client socket, the address its coming from, and q, which is the communications path back to the main thread. node runs in its own thread, so after creating its own socket handler and adding it to the list of allNodes, it sits in an infinite loop. When data is received, it is sent up the queue if it is not equal to None.

Line 254 instantiates an instance of the gameServer class. Without this step, everything I've just talked about is only definitions. This step sets everything in motion.

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

  • ReportLab and Panda3D

    A game of bingo illustrates how to use the ReportLab toolkit and Panda3D real-time 3D engine.

  • 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.

  • curses

    When you need some quick graphical output, the old school curses library can save you some time and effort.

  • Serial Communications

    We explore serial communications, from the electrical specs to protocols and libraries, with an example of serial communication with an Arduino.

  • 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.

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