C/S version mostly working ...

pull/2/head
kaqu 2 years ago
parent a6cf6829b6
commit a314eaf2a7
  1. 2
      .vscode/launch.json
  2. 46
      pandemic_pong.py
  3. 16
      pong_constants.py
  4. 254
      pong_game.py
  5. 16
      pong_globalvars.py
  6. 10
      pong_player.py

@ -10,7 +10,7 @@
"request": "launch",
"program": "${file}",
//"args": ["--sizeable", "--server"],
"args": ["--sizeable", "--client", "2", "127.0.0.1"],
"args": ["--sizeable", "--client", "1", "127.0.0.1"],
"console": "integratedTerminal"
}
]

@ -16,8 +16,6 @@ DEVLOCAL = True # TODO: Adjust for publication to False!!!
import sys, os, fcntl, time, random
from glob import glob
import os
import time
from playsound import playsound
@ -30,7 +28,7 @@ import pong_constants as pgc # Pong global constants
import pong_globalvars as pgv # Pong global variables
from pong_player import PongPlayer
from pong_object import PongObject
from pong_game import PongGame
from pong_game import PongGame, init_send_structures, ViewServer
def draw_buttonstate(x, y, player, painter, scale_x, scale_y):
@ -83,14 +81,14 @@ class pongWindow(QMainWindow):
self.show()
# Don't annoy developers (except for local play - to test game mechanics !) ...
if (DEVLOCAL == False) or (bServer == False):
if (DEVLOCAL == False) or (pgv.bIsServer == False):
# Enforce caching ...
playsound(pgc.player_contact)
playsound(pgc.player_miss)
playsound(pgc.wall_contact)
playsound(pgc.game_exit)
playsound(pgv.sounds[pgc.PLAYERCONTACTSOUND])
playsound(pgv.sounds[pgc.PLAYERMISSSOUND])
playsound(pgv.sounds[pgc.WALLCONTACTSOUND])
playsound(pgv.sounds[pgc.GAMEEXITSOUND])
time.sleep(0.5)
playsound(pgc.game_splash)
playsound(pgv.sounds[pgc.GAMESPLASHSOUND])
self.timer = QTimer() # Start processing 'loop'
self.timer.setInterval(25) # 25ms
@ -218,6 +216,15 @@ class pongWindow(QMainWindow):
self.timer.stop() # Block overrun
if (pgv.bIsServer == False) and (pgv.bIsLocal == False): # Player 1 or 2?
rc = viewserver.receive_data() # Retrieve server stati
if rc < 0: # Server down, abort game
player2.exit()
player1.exit()
sys.exit(0)
#if rc > 0: # May be useful later ...
# bChanged = True
if game.pong_game(ball, player1, player2) == False:
# Quit gracefully
player2.exit()
@ -230,7 +237,8 @@ class pongWindow(QMainWindow):
if __name__ == '__main__':
bSizeable = False
bServer = False
pgv.bIsServer = False
pgv.bIsLocal = True # by default
player_index = 0 # Illegal, local
player_server = ""
debug_x = 0 # Debug window positions
@ -241,12 +249,14 @@ if __name__ == '__main__':
bSizeable = True
if len(sys.argv) > 2:
if sys.argv[2] == "--server":
bServer = True
pgv.bIsServer = True
pgv.bIsLocal = False
print("*** PANDEMIC PONG (server) ***")
debug_x = int(pgv.GAMEAREA_MAX_X / 2.5) # Top centered window
debug_y = int(pgv.GAMEAREA_MAX_Y / 40)
elif len(sys.argv) > 4:
if sys.argv[2] == "--client":
pgv.bIsLocal = False
player_index = int(sys.argv[3])
player_server = sys.argv[4]
print("*** PANDEMIC PONG (player #{} talking to game server @{}) ***".format(player_index,player_server))
@ -269,11 +279,11 @@ if __name__ == '__main__':
try:
# Initialize
random.seed()
game = PongGame()
random.seed()
game = PongGame(pgv.bIsServer)
ball = PongObject(pgv.GAMEAREA_MAX_X/2, pgv.GAMEAREA_MAX_Y/2, 20, 20, 8.0, 2.5)
if player_index == 0: # Local or server?
if bServer == True: # Server version, no gamepads locally avail.
if pgv.bIsServer == True: # Server version, no gamepads locally avail.
player1 = PongPlayer(None, True, 1, "", 10, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
player2 = PongPlayer(None, True, 2, "", pgv.GAMEAREA_MAX_X-40, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
else: # Local version (both gamepads assumed locally connected)
@ -288,6 +298,14 @@ if __name__ == '__main__':
player2 = PongPlayer(sEventQueue2, False, player_index, player_server, pgv.GAMEAREA_MAX_X-40, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
else: # 2nd player assumed on 1st gamepad (only one pad required ... !)
player2 = PongPlayer(sEventQueue1, False, player_index, player_server, pgv.GAMEAREA_MAX_X-40, pgv.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)
# 3 View clients permissible
if player_index > 0: # i.e. NOT local only
viewserver = ViewServer() # Create a view server
except IOError as e:
import errno
if e.errno == errno.EACCES:

@ -46,8 +46,22 @@ game_exit = './sounds/bye.wav'
game_win = './sounds/gamewin.wav'
match_win = './sounds/matchwin.wav'
NOSOUND = 0
WALLCONTACTSOUND = 1
PLAYERCONTACTSOUND = 2
PLAYERMISSSOUND = 3
GAMESPLASHSOUND = 4
GAMEEXITSOUND = 5
GAMEWINSOUND = 6
MATCHWINSOUND = 7
"""IP network access"""
PANDEMIC_PONG_PORT = 5005
PACKETTYPE_ALLDATA = 0
PACKETTYPE_ALLDATA = 0xffa0 # Packet start indicator
PACKETTYPE_PLAYER = 1
UDP_TRANSFER_FORMAT = "iiiffffii"
TCP_TRANSFER_FORMAT = "i"+"iiiii"+"iiffffiii"+"iiffffiii"+"ffffffii"
# Game: state, delay, p1_game, p2_game, playsound
# Player1: player_index, state, x, y, delta_x, delta_y, color, delay, score
# Player2: player_index, state, x, y, delta_x, delta_y, color, delay, score
# Ball: x, y, w, h, delta_x, delta_y, color, delay

@ -7,7 +7,12 @@ 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
import pong_constants as pgc # Pong global constants
@ -15,16 +20,194 @@ import pong_globalvars as pgv # Pong global variables
from pong_player import PongPlayer
from pong_object import PongObject
TCP_PACKET_SIZE = struct.calcsize(pgc.TCP_TRANSFER_FORMAT) # Size of receiver buffer
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
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
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 = struct.unpack(pgc.TCP_TRANSFER_FORMAT, data)
if sendgame.playsound != pgc.NOSOUND:
playsound(pgv.sounds[sendgame.playsound])
sendgame.playsound = pgc.NOSOUND
#print("Ball: {}/{}".format(sendball.x, sendball.y)) # Testing ...
#print("Scores: {}/{}".format(sendgame.p1_game, sendgame.p2_game)) # Testing ...
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', 'text/html; charset=utf-8') # For web server requests
# Will prompt for download(!) on web servers!
self.send_header('Content-type', 'application/octet-stream') # But we transfer binary data
self.end_headers()
# Serve up an infinite stream
bError = False
while bError == False:
#msg = "{0:04x}".format(iCounter) # {<varindex>:<formatstr>}
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: ffffffii
sendball.x,
sendball.y,
sendball.w,
sendball.h,
sendball.delta_x,
sendball.delta_y,
sendball.color,
sendball.delay
)
try:
#print("Sending {}h".format(msg))
#self.wfile.write(msg.encode())
#print("Scores: {}/{}".format(sendgame.p1_game, sendgame.p2_game)) # Testing ...
self.wfile.write(bData)
time.sleep(0.05) # TODO: May be removed later or triggered?
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
#addr = ('', pgc.PANDEMIC_PONG_PORT)
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: # Not working?!
print("Client #{} terminated, respawning ...".format(self.i))
Thread(self.i) # Respawn right away, await another client
print("run(<thread>) exiting") # Never reached ...
class PongGame:
def __init__(self):
"""Actual game mechanics (mostly ;)"""
def __init__(self, bIsServer):
self.state = pgc.STATE_WELCOME # State of game
self.delay = 100 # Automatically leave splash screen ...
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(3)] # Spawn three listeners (player 1 & 2, perhaps server as well)
print("Game server started & listening (3 clients max.)...")
def game_fsm(self, player1, player2):
"""Game engine state machine"""
sendgame.playsound = pgc.NOSOUND # Reset sound state
if (player1.state & pgc.BTN_BASE3) > 0 or (player2.state & pgc.BTN_BASE3) > 0:
player1.state = 0 # Don't play me again
player2.state = 0
@ -68,7 +251,8 @@ class PongGame:
elif self.state == pgc.STATE_GAMERESULTS:
if self.delay == 90:
playsound(pgc.game_win)
self.playsound = pgc.GAMEWINSOUND
#playsound(pgv.sounds[pgc.GAMEWINSOUND])
self.delay = self.delay - 1
if self.delay < 1:
if (self.p1_game > 2) or (self.p2_game > 2): # Set won?
@ -83,7 +267,8 @@ class PongGame:
elif self.state == pgc.STATE_FINALRESULTS:
if self.delay == 90:
playsound(pgc.match_win)
self.playsound = pgc.MATCHWINSOUND
#playsound(pgv.sounds[pgc.MATCHWINSOUND])
self.delay = self.delay - 1
if self.delay < 1:
player1.state = 0
@ -92,7 +277,8 @@ class PongGame:
elif self.state == pgc.STATE_EXIT:
if self.delay == 40:
playsound(pgc.game_exit)
self.playsound = pgc.GAMEEXITSOUND
#playsound(pgv.sounds[pgc.GAMEEXITSOUND])
self.delay = self.delay - 1
if self.delay < 1:
return False
@ -123,8 +309,8 @@ class PongGame:
ball.delta_y = ball.delta_y * 1.2 # Increase angle
else:
ball.delta_y = ball.delta_y * 0.8 # Decrease angle
playsound(pgc.player_contact) # Enforce caching ...
self.playsound = pgc.PLAYERCONTACTSOUND
#playsound(pgv.sounds[pgc.PLAYERCONTACTSOUND])
else: # Ball moves left->right (---->)
if (ball.x + ball.w >= player2.x) and (ball.x < player2.x + player2.w): # Right player #2 in range?
@ -147,13 +333,14 @@ class PongGame:
ball.delta_y = ball.delta_y * 1.2 # Increase angle
else:
ball.delta_y = ball.delta_y * 0.8 # Decrease angle
playsound(pgc.player_contact) # Enforce caching ...
self.playsound = pgc.PLAYERCONTACTSOUND
#playsound(pgv.sounds[pgc.PLAYERCONTACTSOUND])
def pong_game(self, ball, player1, player2):
if self.game_fsm(player1, player2) == False: # Exit
return False
if (pgv.bIsServer == True) or (pgv.bIsLocal == True):
if self.game_fsm(player1, player2) == False: # Exit
return False
# 1. Retrieve user entries
if pgv.bIsServer == True:
@ -162,26 +349,29 @@ class PongGame:
bChanged = player1.eval_gamepad()
bChanged = player2.eval_gamepad()
if self.state == pgc.STATE_PLAY: # Actually playing?
# 2. Adjust player positions
bChanged = player1.eval_position(pgv.GAMEAREA_MIN_X, pgv.GAMEAREA_MAX_X/2-50)
bChanged = player2.eval_position(pgv.GAMEAREA_MAX_X/2+50, pgv.GAMEAREA_MAX_X)
# 3. Crash analyses
self.crashvectors(ball, player1, player2)
# 4. Adjust object positions (ball & potentially others ...)
rc = ball.eval_object()
if rc != 0: # Miss left(-1) or right(1)
if rc == -1: # Player #2: +1
player2.score = player2.score + 1
ball.reinit(8 + (random() - 0.5) * 4, (random() - 0.5) * 10)
playsound(pgc.player_miss)
else: # Player #1: +1
player1.score = player1.score + 1
ball.reinit(-(8 + (random() - 0.5) * 4), (random() - 0.5) * 10)
playsound(pgc.player_miss)
if (pgv.bIsServer == True) or (pgv.bIsLocal == True):
if self.state == pgc.STATE_PLAY: # Actually playing?
# 2. Adjust player positions
bChanged = player1.eval_position(pgv.GAMEAREA_MIN_X, pgv.GAMEAREA_MAX_X/2-50)
bChanged = player2.eval_position(pgv.GAMEAREA_MAX_X/2+50, pgv.GAMEAREA_MAX_X)
# 3. Crash analyses
self.crashvectors(ball, player1, player2)
# 4. Adjust object positions (ball & potentially others ...)
rc = ball.eval_object()
if rc != 0: # Miss left(-1) or right(1)
if rc == -1: # Player #2: +1
player2.score = player2.score + 1
ball.reinit(8 + (random() - 0.5) * 4, (random() - 0.5) * 10)
self.playsound = pgc.PLAYERMISSSOUND
#playsound(pgv.sounds[pgc.PLAYERMISSSOUND])
else: # Player #1: +1
player1.score = player1.score + 1
ball.reinit(-(8 + (random() - 0.5) * 4), (random() - 0.5) * 10)
self.playsound = pgc.PLAYERMISSSOUND
#playsound(pgv.sounds[pgc.PLAYERMISSSOUND])
return True

@ -7,10 +7,24 @@ Pandemic Pong global variables
"""
import pong_constants as pgc
""" Sounds """
sounds = {
pgc.WALLCONTACTSOUND : pgc.wall_contact,
pgc.PLAYERCONTACTSOUND : pgc.player_contact,
pgc.PLAYERMISSSOUND : pgc.player_miss,
pgc.GAMESPLASHSOUND : pgc.game_splash,
pgc.GAMEEXITSOUND : pgc.game_exit,
pgc.GAMEWINSOUND : pgc.game_win,
pgc.MATCHWINSOUND : pgc.match_win
}
""" Game area dimensions, shall be mutable to adjust for window resize (later)"""
GAMEAREA_MIN_X = 0 # Game area left border
GAMEAREA_MAX_X = 1920 # Assume FHD resolution by default
GAMEAREA_MIN_Y = 15 # Game area top border
GAMEAREA_MAX_Y = 940 # Game area bottom range
bIsServer = False # Global server indication
bIsServer = False # Global server indication
bIsLocal = True # By default

@ -52,13 +52,11 @@ class PongPlayer:
self.bUseServer = True
else:
self.bUseServer = False
if (pgv.bIsServer == False) and (bThisIsTheServer): # Is this THE server?
# But init. only once
if (player_index == 1) and (bThisIsTheServer): # Is this THE server? (But init. only once!)
UDP_PACKET_SIZE = struct.calcsize(pgc.UDP_TRANSFER_FORMAT) # Size of receiver buffer
serverUDP = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP Receiver
serverUDP.bind(("127.0.0.1", pgc.PANDEMIC_PONG_PORT)) # Localhost
serverUDP.setblocking(False) # Do not block ...
pgv.bIsServer = True # But call me only once ...
serverUDP.setblocking(False) # Do not block ...
self.state = pgc.BTN_STATE_NONE # Button mask (as listed above)
self.x = x # Current position
@ -200,6 +198,7 @@ class PongPlayer:
pass # Future extension reserve ...
else: # Player data
_, player_index, state, x, y, delta_x, delta_y, color, delay = struct.unpack(pgc.UDP_TRANSFER_FORMAT, data)
"""
print("({}) #{} S:{} X:{} Y:{} DX:{} DY:{} C:{} D:{}".format(
addr[0],
player_index,
@ -210,7 +209,8 @@ class PongPlayer:
delta_y,
color,
delay)
)
)
"""
# TODO: Needs to be sent to other player ...
if player_index == self.player_index:
self.state = state

Loading…
Cancel
Save