Sound pulse caught, better docs

pull/2/head
kaqu 2020-12-02 12:45:27 +01:00
parent 92226732c8
commit d6b422be3c
7 changed files with 244 additions and 198 deletions

View File

@ -23,6 +23,10 @@ With these installed we're prep'ed & ready!
## 2. Game usage & capabilities ##
There are two options to run the game:
![Showtime](pictures/setups.png)
### 2.1 Local gameplay ###
Run the game fullscreen locally like:
@ -51,6 +55,9 @@ Then to run a game server instance use
#### On a gameserver: Make sure, inbound TCP & UDP traffic to port 5050 is not blocked by your firewall or router! ####
* To connect a player successfully, make sure the server is up & ready (past splash screen ...).
* Also, don't start players simultaneously (either one may fail to connect - try again ... ;).
To connect player #1, use
./cl1.sh
@ -108,8 +115,8 @@ After some 2-5s this effect will disappear. Currently, there is no clean initial
## 4. Outlook ##
Streaming synchronization needs improvement.
* Streaming synchronization needs improvement.
Sounds sometimes get lost, need to work on this one ...
* Maybe HTTP streaming shall be moved to raw TCP streaming ...
Maybe, HTTP streaming shall be moved to raw TCP streaming ...
* Game mechanics: Maybe introduce an incubation time? Or let infection die over time ...

View File

@ -9,9 +9,6 @@ Pandemic Pong Game Engine
import time
from random import random
import struct # Packing/unpacking
import threading, socket, socketserver
from http.server import BaseHTTPRequestHandler, HTTPServer # TODO: May be replaced by raw TCP server?!
from playsound import playsound
@ -19,178 +16,7 @@ import game_objects.pong_constants as pgc # Pong global constants
import game_objects.pong_globalvars as pgv # Pong global variables
from game_objects.pong_player import PongPlayer
from game_objects.pong_object import PongObject
TCP_PACKET_SIZE = struct.calcsize(pgc.TCP_TRANSFER_FORMAT) # Size of receiver buffer
# Local structure ref. (TODO: Should become an object really ...)
sendgame = None
sendplayer1 = None
sendplayer2 = None
sendball = None
def init_send_structures(game, player1, player2, ball):
"""Make handler data accessible"""
global sendgame, sendplayer1, sendplayer2, sendball
# Only used as refs.
sendgame = game
sendplayer1 = player1
sendplayer2 = player2
sendball = ball
class ViewServer:
"""View server (one only instantiated)"""
def __init__(self):
self.tcpClientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcpClientSocket.connect((socket.gethostname(), pgc.PANDEMIC_PONG_PORT))
self.tcpClientSocket.send("GET / HTTP/1.1\r\n\r\n".encode()) # Initiate streaming on server
# Will return:
# HTTP/1.0 200 OK
# Server: BaseHTTP/0.6
# Python/3.8.5
# Date: Mon, 30 Nov 2020 09:59:52 GMT
# Content-type: application/octet-stream
# <CR/LF><CR/LF><CR/LF>
msg = self.tcpClientSocket.recv(130).decode() # Skip server header
print("Searching stream packet start ... ", end="")
self.tcpClientSocket.setblocking(False) # Do not block ...
iCount = 0
while True: # Hunting ffa0 (little endian: a0, ff!)
data = self.tcpClientSocket.recv(1)
if data == b'\xa0':
data = self.tcpClientSocket.recv(1)
if data == b'\xff':
break
iCount += 1 # Count retries
if iCount > TCP_PACKET_SIZE: # Somethin's wrong here ...
raise BlockingIOError # Well, some error ...
data = self.tcpClientSocket.recv(TCP_PACKET_SIZE-2) # Skip complete packet
print("detected! Receiving stream ...")
def receive_data(self):
try:
data = self.tcpClientSocket.recv(TCP_PACKET_SIZE)
if len(data) < 1:
print("Server side close detected.")
return -1 # Indicate abortion
# Datatype
# Game: state, delay, p1_game, p2_game, playsound
# Player1: player_index, state, x, y, delta_x, delta_y, color, delay
# Player2: player_index, state, x, y, delta_x, delta_y, color, delay
# Ball: x, y, w, h, delta_x, delta_y, color, delay, playsound
datatype, sendgame.state, sendgame.delay, sendgame.p1_game, sendgame.p2_game, sendgame.playsound, sendplayer1.player_index, sendplayer1.state, sendplayer1.x, sendplayer1.y, sendplayer1.delta_x, sendplayer1.delta_y, sendplayer1.color, sendplayer1.delay, sendplayer1.score, sendplayer2.player_index, sendplayer2.state, sendplayer2.x, sendplayer2.y, sendplayer2.delta_x, sendplayer2.delta_y, sendplayer2.color, sendplayer2.delay, sendplayer2.score, sendball.x, sendball.y, sendball.w, sendball.h, sendball.delta_x, sendball.delta_y, sendball.color, sendball.delay, sendball.playsound = struct.unpack(pgc.TCP_TRANSFER_FORMAT, data)
if sendgame.playsound != pgc.NOSOUND:
playsound(pgc.sounds[sendgame.playsound])
sendgame.playsound = pgc.NOSOUND
if sendball.playsound != pgc.NOSOUND:
playsound(pgc.sounds[sendball.playsound])
sendball.playsound = pgc.NOSOUND
return 1 # OK
except BlockingIOError: # Always on empty receives ...
return 0 # OK, empty queue
except:
return 0 #print("Receive error")
class Handler(BaseHTTPRequestHandler):
"""Simple streaming (even to browsers)"""
def do_GET(self):
"""HTTP-GET may be called from standard web browser to test ..."""
global sendgame, sendplayer1, sendplayer2, sendball
if self.path != '/': # Webserver: localhost:8000/ !
self.send_error(404, "Object not found")
return
self.send_response(200)
self.send_header('Content-type', 'application/octet-stream') # We will stream binary data
self.end_headers()
# Serve up an infinite stream
bError = False
while bError == False:
#msg = "{0:04x}".format(iCounter) # {<varindex>:<formatstr>}
try:
bData = struct.pack(pgc.TCP_TRANSFER_FORMAT,
pgc.PACKETTYPE_ALLDATA,
# Game: iiiii
sendgame.state,
sendgame.delay,
sendgame.p1_game,
sendgame.p2_game,
sendgame.playsound,
# Player1: iiffffiii
sendplayer1.player_index,
sendplayer1.state,
sendplayer1.x,
sendplayer1.y,
sendplayer1.delta_x,
sendplayer1.delta_y,
sendplayer1.color,
sendplayer1.delay,
sendplayer1.score,
# Player2: iiffffiii
sendplayer2.player_index,
sendplayer2.state,
sendplayer2.x,
sendplayer2.y,
sendplayer2.delta_x,
sendplayer2.delta_y,
sendplayer2.color,
sendplayer2.delay,
sendplayer2.score,
# Ball: ffffffiii
sendball.x,
sendball.y,
sendball.w,
sendball.h,
sendball.delta_x,
sendball.delta_y,
sendball.color,
sendball.delay,
sendball.playsound
)
except struct.error: # Eases testing ...
pass #print("sendgame.playsound={} sendball.playsound={}".format(sendgame.playsound, sendball.playsound))
try:
self.wfile.write(bData) # Stream!
time.sleep(0.05) # Minimum required on i7/3rd gen. machine
except BrokenPipeError:
print("Client terminated.")
bError = True
class Thread(threading.Thread):
"""TCP service streaming game data"""
def __init__(self, i, game):
print("Starting thread #{} ...".format(i))
self.game = game
threading.Thread.__init__(self)
self.i = i
self.daemon = True
self.start()
def run(self):
httpd = HTTPServer(self.game.addr, Handler, False)
# Prevent the HTTP server from re-binding every handler.
# https://stackoverflow.com/questions/46210672/
httpd.socket = self.game.tcpsocket
httpd.server_bind = self.server_close = lambda self: None
print("Thread listening ...")
try:
httpd.serve_forever() # Never returns ...
except BrokenPipeError: # TODO: Not working?! May be removed alltogether?
print("Client #{} terminated, respawning ...".format(self.i))
Thread(self.i) # Respawn right away, await another client
print("run(<thread>) exiting") # Never reached ...
import game_objects.pong_viewserver as vs # Pong TCP view server
class PongGame:
"""Actual game mechanics (mostly ;)"""
@ -201,19 +27,13 @@ class PongGame:
self.p1_game = 0 # Results (reset)
self.p2_game = 0
self.playsound = pgc.NOSOUND # Play the provided sound & reset locally
if bIsServer == True:
# Create ONE socket.
self.addr = ('', pgc.PANDEMIC_PONG_PORT)
self.tcpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.tcpsocket.bind(self.addr)
self.tcpsocket.listen(5)
[Thread(i, self) for i in range(2)] # Spawn player listeners
if bIsServer == True:
vs.init_server_socket() # Create the TCP server socket for streaming
[vs.Thread(i, self) for i in range(2)] # Spawn player listeners & streams
print("Game server started & listening ...")
def game_fsm(self, player1, player2):
"""Game engine state machine"""
sendgame.playsound = pgc.NOSOUND # Reset sound state
"""Game engine state machine"""
if (player1.state & pgc.BTN_BASE3) > 0 or (player2.state & pgc.BTN_BASE3) > 0:
player1.state = 0 # Don't play me again
@ -289,6 +109,7 @@ class PongGame:
if (self.playsound != pgc.NOSOUND) and (pgv.bIsLocal == True): # Local play, sound output right away!
playsound(pgc.sounds[self.playsound])
self.playsound = pgc.NOSOUND
return True

View File

@ -42,7 +42,7 @@ class PongObject:
def eval_object(self):
"""Object movement w/ sound support"""
bChanged = 0
sum_x = self.x + self.delta_x
sum_y = self.y + self.delta_y
@ -51,8 +51,7 @@ class PongObject:
bChanged = -1 # Miss left
elif sum_x + self.w > pgc.GAMEAREA_MAX_X:
bChanged = 1 # Miss right
self.playsound = pgc.NOSOUND
if sum_y < pgc.GAMEAREA_MIN_Y:
sum_y = pgc.GAMEAREA_MIN_Y
self.delta_y = -self.delta_y
@ -67,7 +66,8 @@ class PongObject:
if (self.playsound != pgc.NOSOUND) and (pgv.bIsLocal == True): # Local play, sound output right away!
playsound(pgc.sounds[self.playsound])
self.playsound = pgc.NOSOUND
return bChanged

View File

@ -0,0 +1,216 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pong_viewserver.py
Pandemic Pong View Service (threads)
"""
import time
from random import random
import struct # Packing/unpacking
import threading, socket, socketserver
from http.server import BaseHTTPRequestHandler, HTTPServer # TODO: May be replaced by raw TCP server?!
from playsound import playsound
import game_objects.pong_constants as pgc # Pong global constants
import game_objects.pong_globalvars as pgv # Pong global variables
from game_objects.pong_player import PongPlayer
from game_objects.pong_object import PongObject
TCP_PACKET_SIZE = struct.calcsize(pgc.TCP_TRANSFER_FORMAT) # Size of receiver buffer
server_addr = None
server_tcpsocket = None
# Local structure ref. (TODO: Should become an object really ...)
sendgame = None
sendplayer1 = None
sendplayer2 = None
sendball = None
def init_send_structures(game, player1, player2, ball):
"""Make handler data accessible"""
global sendgame, sendplayer1, sendplayer2, sendball
# Only used as refs.
sendgame = game
sendplayer1 = player1
sendplayer2 = player2
sendball = ball
def init_server_socket():
"""Create ONE TCP server streaming socket"""
global server_addr, server_tcpsocket
server_addr = ('', pgc.PANDEMIC_PONG_PORT)
server_tcpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_tcpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_tcpsocket.bind(server_addr)
server_tcpsocket.listen(5)
class ViewServer:
"""View server (one only instantiated)"""
def __init__(self):
self.tcpClientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcpClientSocket.connect((socket.gethostname(), pgc.PANDEMIC_PONG_PORT))
self.tcpClientSocket.send("GET / HTTP/1.1\r\n\r\n".encode()) # Initiate streaming on server
# Will return:
# HTTP/1.0 200 OK
# Server: BaseHTTP/0.6
# Python/3.8.5
# Date: Mon, 30 Nov 2020 09:59:52 GMT
# Content-type: application/octet-stream
# <CR/LF><CR/LF><CR/LF>
msg = self.tcpClientSocket.recv(130).decode() # Skip server header
print("Searching stream packet start ... ", end="")
self.tcpClientSocket.setblocking(False) # Do not block ...
iCount = 0
while True: # Hunting ffa0 (little endian: a0, ff!)
data = self.tcpClientSocket.recv(1)
if data == b'\xa0':
data = self.tcpClientSocket.recv(1)
if data == b'\xff':
break
iCount += 1 # Count retries
if iCount > TCP_PACKET_SIZE: # Somethin's wrong here ...
raise BlockingIOError # Well, some error ...
data = self.tcpClientSocket.recv(TCP_PACKET_SIZE-2) # Skip complete packet
print("detected! Receiving stream ...")
def receive_data(self):
"""Client receiver function"""
try:
data = self.tcpClientSocket.recv(TCP_PACKET_SIZE)
if len(data) < 1:
print("Server side close detected.")
return -1 # Indicate abortion
# Datatype
# Game: state, delay, p1_game, p2_game, playsound
# Player1: player_index, state, x, y, delta_x, delta_y, color, delay
# Player2: player_index, state, x, y, delta_x, delta_y, color, delay
# Ball: x, y, w, h, delta_x, delta_y, color, delay, playsound
datatype, sendgame.state, sendgame.delay, sendgame.p1_game, sendgame.p2_game, sendgame.playsound, sendplayer1.player_index, sendplayer1.state, sendplayer1.x, sendplayer1.y, sendplayer1.delta_x, sendplayer1.delta_y, sendplayer1.color, sendplayer1.delay, sendplayer1.score, sendplayer2.player_index, sendplayer2.state, sendplayer2.x, sendplayer2.y, sendplayer2.delta_x, sendplayer2.delta_y, sendplayer2.color, sendplayer2.delay, sendplayer2.score, sendball.x, sendball.y, sendball.w, sendball.h, sendball.delta_x, sendball.delta_y, sendball.color, sendball.delay, sendball.playsound = struct.unpack(pgc.TCP_TRANSFER_FORMAT, data)
if sendgame.playsound != pgc.NOSOUND: # Client side sound pulse reset
playsound(pgc.sounds[sendgame.playsound])
sendgame.playsound = pgc.NOSOUND
if sendball.playsound != pgc.NOSOUND:
playsound(pgc.sounds[sendball.playsound])
sendball.playsound = pgc.NOSOUND
return 1 # OK
except BlockingIOError: # Always on empty receives ...
return 0 # OK, empty queue
except:
return 0 #print("Receive error")
class Handler(BaseHTTPRequestHandler):
"""Simple streaming server (even to browsers)"""
def do_GET(self):
"""HTTP-GET may be called from standard web browser to test ..."""
global sendgame, sendplayer1, sendplayer2, sendball
if self.path != '/': # Webserver: localhost:8000/ !
self.send_error(404, "Object not found")
return
self.send_response(200)
self.send_header('Content-type', 'application/octet-stream') # We will stream binary data
self.end_headers()
# Serve up an infinite stream
bError = False
while bError == False:
#msg = "{0:04x}".format(iCounter) # {<varindex>:<formatstr>}
try:
bData = struct.pack(pgc.TCP_TRANSFER_FORMAT,
pgc.PACKETTYPE_ALLDATA,
# Game: iiiii
sendgame.state,
sendgame.delay,
sendgame.p1_game,
sendgame.p2_game,
sendgame.playsound,
# Player1: iiffffiii
sendplayer1.player_index,
sendplayer1.state,
sendplayer1.x,
sendplayer1.y,
sendplayer1.delta_x,
sendplayer1.delta_y,
sendplayer1.color,
sendplayer1.delay,
sendplayer1.score,
# Player2: iiffffiii
sendplayer2.player_index,
sendplayer2.state,
sendplayer2.x,
sendplayer2.y,
sendplayer2.delta_x,
sendplayer2.delta_y,
sendplayer2.color,
sendplayer2.delay,
sendplayer2.score,
# Ball: ffffffiii
sendball.x,
sendball.y,
sendball.w,
sendball.h,
sendball.delta_x,
sendball.delta_y,
sendball.color,
sendball.delay,
sendball.playsound
)
except struct.error: # Eases testing ...
pass #print("sendgame.playsound={} sendball.playsound={}".format(sendgame.playsound, sendball.playsound))
try:
self.wfile.write(bData) # Stream!
if sendgame.playsound != pgc.NOSOUND: # Server side sound pulse reset
sendgame.playsound = pgc.NOSOUND
if sendball.playsound != pgc.NOSOUND:
sendball.playsound = pgc.NOSOUND
time.sleep(0.05) # Minimum required on i7/3rd gen. machine
except BrokenPipeError:
print("Client terminated.")
bError = True
class Thread(threading.Thread):
"""TCP service streaming game data"""
def __init__(self, i, game):
print("Starting thread #{} ...".format(i))
self.game = game
threading.Thread.__init__(self)
self.i = i
self.daemon = True
self.start()
def run(self):
"""Called from start() s.a.!"""
httpd = HTTPServer(server_addr, Handler, False)
# Prevent the HTTP server from re-binding every handler.
# https://stackoverflow.com/questions/46210672/
httpd.socket = server_tcpsocket
httpd.server_bind = self.server_close = lambda self: None
print("Thread listening ...")
try:
httpd.serve_forever() # Never returns ...
except BrokenPipeError: # TODO: Not working?! May be removed alltogether?
print("Client #{} terminated, respawning ...".format(self.i))
Thread(self.i) # Respawn right away, await another client
print("run(<thread>) exiting") # Never reached ...
if __name__ == "__main__":
print("pong_viewserver has no function, call 'pandemic_pong.py'")

View File

@ -10,6 +10,7 @@ History:
21.11.20/KQ Initial version
25.11.20/KQ Class based version w/ scaling ;)
27.11.20/KQ Client/server variant started
02.12.20/KQ Streamlined version published
"""
DEVLOCAL = True # TODO: Adjust for publication to False!!!
@ -28,8 +29,8 @@ import game_objects.pong_constants as pgc # Pong global constants
import game_objects.pong_globalvars as pgv # Pong global variables
from game_objects.pong_player import PongPlayer
from game_objects.pong_object import PongObject
from game_objects.pong_game import PongGame, init_send_structures, ViewServer
from game_objects.pong_game import PongGame
import game_objects.pong_viewserver as vs # Pong TCP view server (threads)
def draw_buttonstate(x, y, player, painter, scale_x, scale_y):
"""View function: Indicate last button pressed (not strictly necessary ...)"""
@ -114,11 +115,11 @@ class pongWindow(QMainWindow):
painter.drawPixmap(self.rect(), self.pic) # Game background
# Font metrics assume font is monospaced!
fntLarge = QFont("Monospace", int(120 * scale_y))
fntLarge = QFont("Monospace", int(120 * min(scale_x, scale_y)))
fntLarge.setKerning(False)
fntLarge.setFixedPitch(True)
fm = QFontMetrics(fntLarge)
fntMedium = QFont("Monospace", int(40 * scale_y))
fntMedium = QFont("Monospace", int(40 * min(scale_x, scale_y)))
fntMedium.setKerning(False)
fntMedium.setFixedPitch(True)
@ -281,6 +282,7 @@ if __name__ == '__main__':
sEventQueue1 = "/dev/input/event" + str(maxEvent-1)
sEventQueue2 = "/dev/input/event" + str(maxEvent)
print("I'm a slow starter, please be patient!") # Caching actually ...
try:
# Initialize
random.seed()
@ -304,11 +306,11 @@ if __name__ == '__main__':
player2 = PongPlayer(sEventQueue1, False, player_index, player_server, pgc.GAMEAREA_MAX_X-40, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
# TODO: Pretty ugly patch for handler data access ... (better: separate sender class)
init_send_structures(game, player1, player2, ball)
vs.init_send_structures(game, player1, player2, ball)
# 3 View clients permissible
if player_index > 0: # i.e. NOT local only
viewserver = ViewServer() # Create a view server
viewserver = vs.ViewServer() # Create a view server
except IOError as e:
import errno

BIN
pictures/setups.odp Normal file

Binary file not shown.

BIN
pictures/setups.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB