Home-built shooting game with Nerf targets and a Raspberry Pi
Ready, Aim, Fire
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.
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.")
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.
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
(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
-
Latest Cinnamon Desktop Releases with a Bold New Look
Just in time for the holidays, the developer of the Cinnamon desktop has shipped a new release to help spice up your eggnog with new features and a new look.
-
Armbian 24.11 Released with Expanded Hardware Support
If you've been waiting for Armbian to support OrangePi 5 Max and Radxa ROCK 5B+, the wait is over.
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.