Publishable client/server variant ready!

pull/2/head
kaqu 2 years ago
parent a314eaf2a7
commit 3949ab2f4f
  1. 61
      README.md
  2. 1
      cl1.sh
  3. 1
      cl2.sh
  4. BIN
      game_objects/__pycache__/pong_constants.cpython-38.pyc
  5. BIN
      game_objects/__pycache__/pong_game.cpython-38.pyc
  6. BIN
      game_objects/__pycache__/pong_globalvars.cpython-38.pyc
  7. BIN
      game_objects/__pycache__/pong_object.cpython-38.pyc
  8. BIN
      game_objects/__pycache__/pong_player.cpython-38.pyc
  9. 23
      game_objects/pong_constants.py
  10. 165
      game_objects/pong_game.py
  11. 11
      game_objects/pong_globalvars.py
  12. 44
      game_objects/pong_object.py
  13. 12
      game_objects/pong_player.py
  14. 2
      local.sh
  15. 94
      pandemic_pong.py
  16. 46
      pandemic_pong_client.py
  17. 79
      pandemic_pong_server.py
  18. 30
      pong_globalvars.py
  19. 1
      srv.sh

@ -23,22 +23,54 @@ With these installed we're prep'ed & ready!
## 2. Game usage & capabilities ##
Run the game fullscreen like:
### 2.1 Local gameplay ###
Run the game fullscreen locally like:
python3 pandemic_pong.py
or have a sizeable window with:
or have a sizeable window (locally) with:
python3 pandemic_pong.py --sizeable
or simply use
./local.sh
instead.
The game tries to identify the event inputs for two gamepads automatically (otherwise see troubleshooting section below!).
### 2.1 Scoring ###
### 2.2 Client/Server gameplay ###
Have all shell files (*.sh) executable!
Then to run a game server instance use
./srv.sh
#### On a gameserver: Make sure, inbound TCP & UDP traffic to port 5050 is not blocked by your firewall or router! ####
To connect player #1, use
./cl1.sh
To connect player #2, use
./cl1.sh
If you want to access a remote game server, edit the IP address within
the cl1.sh or cl2.sh file respectively.
Also, you may want to play a client in fullscreen.
To achieve this, substitute --sizeable by --fullscreen in both files.
### 2.3 Scoring ###
A game is won by the player who first reaches 10 points.
A match is won by the player who first wins 3 games.
### 2.2 Additional features ###
### 2.4 Additional features ###
The game features two additional capabilites beyond the original pong game (& may lack others ;).
@ -52,15 +84,32 @@ If a tainted ball approaches a player, he has to vaccinate himself w/ the same c
Yet, the vaccine diminishes over time as well ...
If the player chooses the wrong colour or no vaccine at all, the ball passes right through the player. The point is counted as a miss, the opponent gains a point.
### 2.3 Exiting ###
### 2.5 Exiting ###
The [Select] button to terminate the program is always available ...
Note, that a single player exiting with the [Select] button
terminates ALL game instances (server, clients) ...
For sizeable windows, use the GUI button ;)
## 3. Troubleshooting ##
### 3.1 Gamepad identification ###
In case you have other (differing) USB gamepads, you may need to identify button usage & other gamepad specifics.
For convenience, I copied the sample script (folder 'utilities').
Source adjustments will have to take place within 'pong_player.py'. Recommended approach: Map events here to their original correspondents.
Source adjustments will have to take place within 'pong_player.py'. Recommended approach: Map events here to their original correspondents.
### 3.2 Client/Server problems ###
You may experience initial delays due to fill up of streamed input queue.
After some 2-5s this effect will disappear. Currently, there is no clean initial synchronization ...
#### On a gameserver: Make sure, inbound TCP & UDP traffic to port 5050 is not blocked by your firewall or router! ####
## 4. Outlook ##
Streaming synchronization needs improvement.
Sounds sometimes get lost, need to work on this one ...
Maybe, HTTP streaming shall be moved to raw TCP streaming ...

@ -1 +1,2 @@
# Client, player #1
./pandemic_pong.py --sizeable --client 1 127.0.0.1

@ -1 +1,2 @@
# Client, player #2
./pandemic_pong.py --sizeable --client 2 127.0.0.1

@ -56,12 +56,29 @@ GAMEWINSOUND = 6
MATCHWINSOUND = 7
"""IP network access"""
PANDEMIC_PONG_PORT = 5005
PANDEMIC_PONG_PORT = 5050 # PP (Pandemic Pong!)
PACKETTYPE_ALLDATA = 0xffa0 # Packet start indicator
PACKETTYPE_PLAYER = 1
UDP_TRANSFER_FORMAT = "iiiffffii"
TCP_TRANSFER_FORMAT = "i"+"iiiii"+"iiffffiii"+"iiffffiii"+"ffffffii"
TCP_TRANSFER_FORMAT = "i"+"iiiii"+"iiffffiii"+"iiffffiii"+"ffffffiii"
# 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
# Ball: x, y, w, h, delta_x, delta_y, color, delay, playsound
""" Sounds """
sounds = {
WALLCONTACTSOUND : wall_contact,
PLAYERCONTACTSOUND : player_contact,
PLAYERMISSSOUND : player_miss,
GAMESPLASHSOUND : game_splash,
GAMEEXITSOUND : game_exit,
GAMEWINSOUND : game_win,
MATCHWINSOUND : 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

@ -15,27 +15,31 @@ from http.server import BaseHTTPRequestHandler, HTTPServer # TODO: May be replac
from playsound import playsound
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
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)"""
@ -77,19 +81,21 @@ class ViewServer:
# 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)
# 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(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 ...
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)"""
@ -101,70 +107,70 @@ class Handler(BaseHTTPRequestHandler):
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.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>}
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?
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
#addr = ('', pgc.PANDEMIC_PONG_PORT)
self.game = game
threading.Thread.__init__(self)
self.i = i
self.daemon = True
@ -180,11 +186,12 @@ class Thread(threading.Thread):
print("Thread listening ...")
try:
httpd.serve_forever() # Never returns ...
except BrokenPipeError: # Not working?!
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 ...
class PongGame:
"""Actual game mechanics (mostly ;)"""
@ -201,8 +208,8 @@ class PongGame:
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.)...")
[Thread(i, self) for i in range(2)] # Spawn player listeners
print("Game server started & listening ...")
def game_fsm(self, player1, player2):
"""Game engine state machine"""
@ -221,8 +228,8 @@ class PongGame:
elif self.state == pgc.STATE_START: # Start new game
if (player1.state & pgc.BTN_BASE4) > 0 or (player2.state & pgc.BTN_BASE4) > 0: # [Start] pressed?
player1.reinit(10, pgv.GAMEAREA_MAX_Y/2-50, 0, 0)
player2.reinit(pgv.GAMEAREA_MAX_X-40, pgv.GAMEAREA_MAX_Y/2-50, 0, 0)
player1.reinit(10, pgc.GAMEAREA_MAX_Y/2-50, 0, 0)
player2.reinit(pgc.GAMEAREA_MAX_X-40, pgc.GAMEAREA_MAX_Y/2-50, 0, 0)
self.p1_game = 0
self.p2_game = 0
player1.score = 0
@ -251,8 +258,7 @@ class PongGame:
elif self.state == pgc.STATE_GAMERESULTS:
if self.delay == 90:
self.playsound = pgc.GAMEWINSOUND
#playsound(pgv.sounds[pgc.GAMEWINSOUND])
self.playsound = pgc.GAMEWINSOUND
self.delay = self.delay - 1
if self.delay < 1:
if (self.p1_game > 2) or (self.p2_game > 2): # Set won?
@ -261,14 +267,13 @@ class PongGame:
else:
player1.score = 0
player2.score = 0
player1.reinit(10, pgv.GAMEAREA_MAX_Y/2-50, 0, 0)
player2.reinit(pgv.GAMEAREA_MAX_X-40, pgv.GAMEAREA_MAX_Y/2-50, 0, 0)
player1.reinit(10, pgc.GAMEAREA_MAX_Y/2-50, 0, 0)
player2.reinit(pgc.GAMEAREA_MAX_X-40, pgc.GAMEAREA_MAX_Y/2-50, 0, 0)
self.state = pgc.STATE_PLAY # Nope, move on to next game
elif self.state == pgc.STATE_FINALRESULTS:
if self.delay == 90:
self.playsound = pgc.MATCHWINSOUND
#playsound(pgv.sounds[pgc.MATCHWINSOUND])
self.playsound = pgc.MATCHWINSOUND
self.delay = self.delay - 1
if self.delay < 1:
player1.state = 0
@ -277,12 +282,14 @@ class PongGame:
elif self.state == pgc.STATE_EXIT:
if self.delay == 40:
self.playsound = pgc.GAMEEXITSOUND
#playsound(pgv.sounds[pgc.GAMEEXITSOUND])
self.playsound = pgc.GAMEEXITSOUND
self.delay = self.delay - 1
if self.delay < 1:
return False
if (self.playsound != pgc.NOSOUND) and (pgv.bIsLocal == True): # Local play, sound output right away!
playsound(pgc.sounds[self.playsound])
return True
def crashvectors(self, ball, player1, player2):
@ -309,8 +316,7 @@ class PongGame:
ball.delta_y = ball.delta_y * 1.2 # Increase angle
else:
ball.delta_y = ball.delta_y * 0.8 # Decrease angle
self.playsound = pgc.PLAYERCONTACTSOUND
#playsound(pgv.sounds[pgc.PLAYERCONTACTSOUND])
self.playsound = 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?
@ -333,8 +339,7 @@ class PongGame:
ball.delta_y = ball.delta_y * 1.2 # Increase angle
else:
ball.delta_y = ball.delta_y * 0.8 # Decrease angle
self.playsound = pgc.PLAYERCONTACTSOUND
#playsound(pgv.sounds[pgc.PLAYERCONTACTSOUND])
self.playsound = pgc.PLAYERCONTACTSOUND
def pong_game(self, ball, player1, player2):
@ -352,8 +357,8 @@ class PongGame:
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)
bChanged = player1.eval_position(pgc.GAMEAREA_MIN_X, pgc.GAMEAREA_MAX_X/2-50)
bChanged = player2.eval_position(pgc.GAMEAREA_MAX_X/2+50, pgc.GAMEAREA_MAX_X)
# 3. Crash analyses
self.crashvectors(ball, player1, player2)
@ -364,13 +369,11 @@ class PongGame:
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])
self.playsound = 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])
self.playsound = pgc.PLAYERMISSSOUND
return True

@ -0,0 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pong_globalvars.py
Pandemic Pong global variables
"""
bIsServer = False # Global server indication
bIsLocal = True # By default

@ -9,10 +9,14 @@ Pandemic Pong Objects logic (i.e. ball ;)
from random import random # For random start data ...
from playsound import playsound # Play some sounds
import pong_constants as pgc # Pong global constants
import pong_globalvars as pgv # Pong global variables
import game_objects.pong_constants as pgc # Pong global constants
import game_objects.pong_globalvars as pgv # Pong global variables
class PongObject:
"""Ball object"""
def __init__(self, x, y, w, h, delta_x, delta_y):
"""Initialize non-player object (i.e. 'ball' - but maybe more objects later!)"""
@ -23,17 +27,18 @@ class PongObject:
self.delta_x = delta_x # Current movement
self.delta_y = (random() - 0.5) * 10 # TODO: Move logic to caller ...
self.color = pgc.COL_WHITE
self.delay = 0
self.delay = 0
self.playsound = pgc.NOSOUND
def reinit(self, delta_x, delta_y):
"""Re-Initialize object for next game"""
self.x = pgv.GAMEAREA_MAX_X/2 - self.w/2
self.y = pgv.GAMEAREA_MAX_Y/2 - self.h/2
self.x = pgc.GAMEAREA_MAX_X/2 - self.w/2
self.y = pgc.GAMEAREA_MAX_Y/2 - self.h/2
self.delta_x = delta_x
self.delta_y = delta_y
self.color = pgc.COL_WHITE
self.delay = 0
self.delay = 0
def eval_object(self):
"""Object movement w/ sound support"""
@ -42,26 +47,27 @@ class PongObject:
sum_x = self.x + self.delta_x
sum_y = self.y + self.delta_y
if sum_x < pgv.GAMEAREA_MIN_X:
#sum_x = GAMEAREA_MIN_X
#self.delta_x = -self.delta_x
if sum_x < pgc.GAMEAREA_MIN_X:
bChanged = -1 # Miss left
elif sum_x + self.w > pgv.GAMEAREA_MAX_X:
#sum_x = GAMEAREA_MAX_X - self.w
#self.delta_x = -self.delta_x
elif sum_x + self.w > pgc.GAMEAREA_MAX_X:
bChanged = 1 # Miss right
if sum_y < pgv.GAMEAREA_MIN_Y:
sum_y = pgv.GAMEAREA_MIN_Y
self.playsound = pgc.NOSOUND
if sum_y < pgc.GAMEAREA_MIN_Y:
sum_y = pgc.GAMEAREA_MIN_Y
self.delta_y = -self.delta_y
playsound(pgc.wall_contact)
elif sum_y + self.h > pgv.GAMEAREA_MAX_Y:
sum_y = pgv.GAMEAREA_MAX_Y - self.h
self.delta_y = -self.delta_y
playsound(pgc.wall_contact)
self.playsound = pgc.WALLCONTACTSOUND
elif sum_y + self.h > pgc.GAMEAREA_MAX_Y:
sum_y = pgc.GAMEAREA_MAX_Y - self.h
self.delta_y = -self.delta_y
self.playsound = pgc.WALLCONTACTSOUND
self.x = sum_x
self.y = sum_y
if (self.playsound != pgc.NOSOUND) and (pgv.bIsLocal == True): # Local play, sound output right away!
playsound(pgc.sounds[self.playsound])
return bChanged

@ -12,8 +12,8 @@ import libevdev
import struct # Packing/unpacking
import socket # External communication
import pong_constants as pgc # Pong global constants
import pong_globalvars as pgv # Pong global variables
import game_objects.pong_constants as pgc # Pong global constants
import game_objects.pong_globalvars as pgv # Pong global variables
UDP_PACKET_SIZE = struct.calcsize(pgc.UDP_TRANSFER_FORMAT) # Size of receiver buffer
serverUDP = None
@ -249,10 +249,10 @@ class PongPlayer:
# Top/bottom positioning
sum_y = self.y + self.delta_y * 10
if sum_y < pgv.GAMEAREA_MIN_Y:
sum_y = pgv.GAMEAREA_MIN_Y
elif sum_y + self.h > pgv.GAMEAREA_MAX_Y:
sum_y = pgv.GAMEAREA_MAX_Y - self.h
if sum_y < pgc.GAMEAREA_MIN_Y:
sum_y = pgc.GAMEAREA_MIN_Y
elif sum_y + self.h > pgc.GAMEAREA_MAX_Y:
sum_y = pgc.GAMEAREA_MAX_Y - self.h
# Movement indication (TODO: may be removed later ...)
if (sum_x != self.x) or (sum_y != self.y):

@ -0,0 +1,2 @@
# simple local game
./pandemic_pong.py --sizeable

@ -24,15 +24,16 @@ from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from PyQt5.QtGui import QPainter, QBrush, QPen, QFont, QFontMetrics
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, init_send_structures, ViewServer
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
def draw_buttonstate(x, y, player, painter, scale_x, scale_y):
"""Indicate last button pressed (not strictly necessary ...)"""
"""View function: Indicate last button pressed (not strictly necessary ...)"""
if (player.state & pgc.BTN_TRIGGER) == pgc.BTN_TRIGGER:
painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine)) # Color/Linewidth/Pattern
painter.setBrush(QBrush(Qt.blue, Qt.SolidPattern)) # Fill
@ -51,6 +52,8 @@ def draw_buttonstate(x, y, player, painter, scale_x, scale_y):
painter.drawEllipse(int((x + 90) * scale_x), int(y * scale_y), int(20 * scale_x), int(20 * scale_y))
class pongWindow(QMainWindow):
"""Qt5 base class"""
def __init__(self):
super().__init__()
self.title = "Pandemic Pong"
@ -58,9 +61,10 @@ class pongWindow(QMainWindow):
self.left = 0
self.win_width = 1920
self.win_height = 960
self.InitWindow()
self.InitWindow() # s.b. !
def InitWindow(self):
"""Prepare window for later drawing"""
global debug_x, debug_y
self.setWindowTitle(self.title)
@ -83,12 +87,12 @@ class pongWindow(QMainWindow):
# Don't annoy developers (except for local play - to test game mechanics !) ...
if (DEVLOCAL == False) or (pgv.bIsServer == False):
# Enforce caching ...
playsound(pgv.sounds[pgc.PLAYERCONTACTSOUND])
playsound(pgv.sounds[pgc.PLAYERMISSSOUND])
playsound(pgv.sounds[pgc.WALLCONTACTSOUND])
playsound(pgv.sounds[pgc.GAMEEXITSOUND])
playsound(pgc.sounds[pgc.PLAYERCONTACTSOUND])
playsound(pgc.sounds[pgc.PLAYERMISSSOUND])
playsound(pgc.sounds[pgc.WALLCONTACTSOUND])
playsound(pgc.sounds[pgc.GAMEEXITSOUND])
time.sleep(0.5)
playsound(pgv.sounds[pgc.GAMESPLASHSOUND])
playsound(pgc.sounds[pgc.GAMESPLASHSOUND])
self.timer = QTimer() # Start processing 'loop'
self.timer.setInterval(25) # 25ms
@ -97,10 +101,11 @@ class pongWindow(QMainWindow):
self.timer.start()
def paintEvent(self, event):
"""Called whenever drawing is necessary ..."""
global game, ball, player1, player2
scale_x = self.width() / pgv.GAMEAREA_MAX_X
scale_y = self.height() / (pgv.GAMEAREA_MAX_Y + 20) # Compensate header
scale_x = self.width() / pgc.GAMEAREA_MAX_X
scale_y = self.height() / (pgc.GAMEAREA_MAX_Y + 20) # Compensate header
painter = QPainter(self) # Painter need's to be freshly picked ...
painter.setPen(QPen(Qt.white, 1, Qt.SolidLine)) # Color/Linewidth/Pattern
@ -120,17 +125,17 @@ class pongWindow(QMainWindow):
if game.state == pgc.STATE_WELCOME: # Welcome screen
painter.drawPixmap(self.rect(), QPixmap("pictures/pandemic_pong.png"))
painter.setFont(fntLarge)
painter.drawText(int(50 * scale_x), int((pgv.GAMEAREA_MAX_Y / 2 - 100) * scale_y), "PANDEMIC")
painter.drawText(int((pgv.GAMEAREA_MAX_X / 2 + 300) * scale_x), int((pgv.GAMEAREA_MAX_Y / 2 + 100) * scale_y), "PONG")
painter.drawText(int(50 * scale_x), int((pgc.GAMEAREA_MAX_Y / 2 - 100) * scale_y), "PANDEMIC")
painter.drawText(int((pgc.GAMEAREA_MAX_X / 2 + 300) * scale_x), int((pgc.GAMEAREA_MAX_Y / 2 + 100) * scale_y), "PONG")
elif game.state == pgc.STATE_START: # Wait for start button pressed
painter.setFont(fntMedium)
painter.drawText(int(150 * scale_x), int((pgv.GAMEAREA_MAX_Y / 2 - 100) * scale_y), "Press [Start] ...")
painter.drawText(int(50 * scale_x), int((pgv.GAMEAREA_MAX_Y / 2 + 100) * scale_y), "Press [Select] to abort")
painter.drawText(int((pgv.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgv.GAMEAREA_MAX_Y / 5 * scale_y), "Press [color] button")
painter.drawText(int((pgv.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgv.GAMEAREA_MAX_Y / 5 * 2 * scale_y), "to infect player,")
painter.drawText(int((pgv.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgv.GAMEAREA_MAX_Y / 5 * 3 * scale_y), "ball will pickup virus!")
painter.drawText(int((pgv.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgv.GAMEAREA_MAX_Y / 5 * 4 * scale_y), "Match color to deflect!")
painter.drawText(int(150 * scale_x), int((pgc.GAMEAREA_MAX_Y / 2 - 100) * scale_y), "Press [Start] ...")
painter.drawText(int(50 * scale_x), int((pgc.GAMEAREA_MAX_Y / 2 + 100) * scale_y), "Press [Select] to abort")
painter.drawText(int((pgc.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgc.GAMEAREA_MAX_Y / 5 * scale_y), "Press [color] button")
painter.drawText(int((pgc.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgc.GAMEAREA_MAX_Y / 5 * 2 * scale_y), "to infect player,")
painter.drawText(int((pgc.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgc.GAMEAREA_MAX_Y / 5 * 3 * scale_y), "ball will pickup virus!")
painter.drawText(int((pgc.GAMEAREA_MAX_X / 2 + 100) * scale_x), int(pgc.GAMEAREA_MAX_Y / 5 * 4 * scale_y), "Match color to deflect!")
elif game.state == pgc.STATE_PLAY: # Actual play 'til score reaches 10 points
# Ball gives NO indication ...
@ -180,38 +185,37 @@ class pongWindow(QMainWindow):
msg_width = fm.width(msg)
painter.drawText(int((800 - msg_width) * scale_x), int(154 * scale_y), "{}".format(msg))
# Score player #2
painter.drawText(int((pgv.GAMEAREA_MAX_X - 840) * scale_x), int(154 * scale_y), "{}".format(player2.score))
painter.drawText(int((pgc.GAMEAREA_MAX_X - 840) * scale_x), int(154 * scale_y), "{}".format(player2.score))
# Indicate Button states
draw_buttonstate(60, 50, player1, painter, scale_x, scale_y)
draw_buttonstate(pgv.GAMEAREA_MAX_X - 180, 50, player2, painter, scale_x, scale_y)
draw_buttonstate(pgc.GAMEAREA_MAX_X - 180, 50, player2, painter, scale_x, scale_y)
elif game.state == pgc.STATE_GAMERESULTS: # Display winner of this game
painter.setFont(fntMedium)
if player1.score > 9:
msg = "Game player #1 Total score {}:{}".format(game.p1_game, game.p2_game)
else:
msg = "Game player #2 Total score {}:{}".format(game.p1_game, game.p2_game)
painter.drawText(int(200 * scale_x), int(pgv.GAMEAREA_MAX_Y / 2 * scale_y), msg)
painter.drawText(int(200 * scale_x), int(pgc.GAMEAREA_MAX_Y / 2 * scale_y), msg)
elif game.state == pgc.STATE_FINALRESULTS: # Display set winner
painter.setFont(fntMedium)
if game.p1_game > 2:
msg = "Match player #1 Total score {}:{}".format(game.p1_game, game.p2_game)
else:
msg = "Match player #2 Total score {}:{}".format(game.p1_game, game.p2_game)
painter.drawText(int(200 * scale_x), int(pgv.GAMEAREA_MAX_Y / 2 * scale_y), msg)
painter.drawText(int(200 * scale_x), int(pgc.GAMEAREA_MAX_Y / 2 * scale_y), msg)
elif game.state == pgc.STATE_EXIT: # Indicate good bye ...
painter.setFont(fntMedium)
painter.drawText(int(50 * scale_x), int(pgv.GAMEAREA_MAX_Y / 2 * scale_x), "Bye!")
painter.drawText(int(50 * scale_x), int(pgc.GAMEAREA_MAX_Y / 2 * scale_x), "Bye!")
# Force font memory free (indication GC)
del fm
del fntMedium
del fntLarge
@pyqtSlot()
def on_timer(self):
'''Qt5 timer event'''
"""Qt5 timer event runs the game ..."""
global game, ball, player1, player2
self.timer.stop() # Block overrun
@ -231,8 +235,8 @@ class pongWindow(QMainWindow):
player1.exit()
sys.exit(0)
self.update() # Redraw
self.timer.start() # Re-enable
self.update() # Enforce redraw (explicitely)
self.timer.start() # Re-enable timer event
if __name__ == '__main__':
@ -252,16 +256,16 @@ if __name__ == '__main__':
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)
debug_x = int(pgc.GAMEAREA_MAX_X / 2.5) # Top centered window
debug_y = int(pgc.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))
debug_x = int(pgv.GAMEAREA_MAX_X / 3 * 4 * (player_index-1)) # Left(0) or right top window
debug_y = int(pgv.GAMEAREA_MAX_Y / 40)
debug_x = int(pgc.GAMEAREA_MAX_X / 3 * 4 * (player_index-1)) # Left(0) or right top window
debug_y = int(pgc.GAMEAREA_MAX_Y / 40)
else:
print(sMsg)
else:
@ -281,23 +285,23 @@ if __name__ == '__main__':
# Initialize
random.seed()
game = PongGame(pgv.bIsServer)
ball = PongObject(pgv.GAMEAREA_MAX_X/2, pgv.GAMEAREA_MAX_Y/2, 20, 20, 8.0, 2.5)
ball = PongObject(pgc.GAMEAREA_MAX_X/2, pgc.GAMEAREA_MAX_Y/2, 20, 20, 8.0, 2.5)
if player_index == 0: # Local or server?
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)
player1 = PongPlayer(None, True, 1, "", 10, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
player2 = PongPlayer(None, True, 2, "", pgc.GAMEAREA_MAX_X-40, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
else: # Local version (both gamepads assumed locally connected)
player1 = PongPlayer(sEventQueue1, False, 1, "", 10, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
player2 = PongPlayer(sEventQueue2, False, 2, "", pgv.GAMEAREA_MAX_X-40, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
player1 = PongPlayer(sEventQueue1, False, 1, "", 10, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
player2 = PongPlayer(sEventQueue2, False, 2, "", pgc.GAMEAREA_MAX_X-40, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
elif player_index == 1: # Client/server version, player #1
player1 = PongPlayer(sEventQueue1, False, player_index, player_server, 10, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
player2 = PongPlayer(None, False, 2, "", pgv.GAMEAREA_MAX_X-40, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
player1 = PongPlayer(sEventQueue1, False, player_index, player_server, 10, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
player2 = PongPlayer(None, False, 2, "", pgc.GAMEAREA_MAX_X-40, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
elif player_index == 2: # Client/server version, player #2
player1 = PongPlayer(None, False, 1, "", 10, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
player1 = PongPlayer(None, False, 1, "", 10, pgc.GAMEAREA_MAX_Y/2-50, 20, 160)
if DEVLOCAL == True: # 2 gamepads required FOR LOCAL DEVELOPMENT ONLY!
player2 = PongPlayer(sEventQueue2, False, player_index, player_server, pgv.GAMEAREA_MAX_X-40, pgv.GAMEAREA_MAX_Y/2-50, 20, 160)
player2 = PongPlayer(sEventQueue2, False, player_index, player_server, pgc.GAMEAREA_MAX_X-40, pgc.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)
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)

@ -1,46 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pandemic_pong_client.py
Pandemic Pong Client
"""
import socket
HEADERSIZE = 10
print("Client starting, ", end="")
sockClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sockClient.connect((socket.gethostname(), 5005))
#sockClient.setblocking(False) # Do not block ...
print("listening ...")
sockClient.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 = sockClient.recv(160).decode() # Skip server header
sockClient.setblocking(False) # Do not block ...
#print("Server:<<<{}>>>".format(msg))
while True:
try:
msg = sockClient.recv(4).decode()
if len(msg) < 1:
print("Server side close detected.")
break
print("{}: {}h".format(len(msg),msg))
except BlockingIOError: # Always on empty receives ...
pass
except KeyboardInterrupt:
print("Exiting")
break
except:
pass #print("Receive error")

@ -1,79 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pandemic_pong_server.py
Pandemic Pong Server
"""
import time, threading, socket
import socketserver
from http.server import BaseHTTPRequestHandler, HTTPServer
iCounter = 0
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
"""HTTP-GET may be called from standard web browser to test ..."""
global iCounter
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>}
try:
print("Sending {}h".format(msg))
self.wfile.write(msg.encode())
time.sleep(0.1)
iCounter += 1
except BrokenPipeError:
print("doGET: Client #{} terminated.".format(iCounter))
bError = True
# Launch 3 listener threads.
class Thread(threading.Thread):
def __init__(self, i):
print("Starting thread #{} ...".format(i))
threading.Thread.__init__(self)
self.i = i
self.daemon = True
self.start()
def run(self):
httpd = HTTPServer(addr, Handler, False)
# Prevent the HTTP server from re-binding every handler.
# https://stackoverflow.com/questions/46210672/
httpd.socket = sock
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 ...
# Create ONE socket.
addr = ('', 5005)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(addr)
sock.listen(5)
print("Server started & listening ...")
try:
[Thread(i) for i in range(3)]
time.sleep(9e9) # This shall be the main program service
except KeyboardInterrupt:
print("\rExiting server. ")

@ -1,30 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pong_globalvars.py
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
bIsLocal = True # By default

@ -1 +1,2 @@
# Run a server instance
./pandemic_pong.py --sizeable --server

Loading…
Cancel
Save