-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathclient.py
710 lines (500 loc) · 21.4 KB
/
client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
# ----------------------- Imports -----------------------
import sys
import pygame as pg
from threading import *
from socket import *
import time
import traceback
from platform import system
from player import Player
from wall import Wall
from common import *
from interpretor import *
from inlight import toVisible
# ----------------------- Variables -----------------------
DEBUG=False
SERVER_IP = ""
SERVER_PORT = 9998
CONNECTED = False
DISCONNECTION_WAITING_TIME = 5 # in seconds, time waited before disconnection without confirmation from the host
MAX_REQUESTS = 10 # number of requests without proper response before force disconnect
MESSAGES_LENGTH = 1024 * 3
FPS_GOAL = 60
FPS = None
SIZE = None
SCALE_FACTOR = None
SCREEN = None
WHITE = Color(255, 255, 255)
BLACK = Color(0, 0, 0)
RED = Color(255, 0, 0)
GREEN = Color(0, 255, 0)
BLUE = Color(0, 0, 255)
BACKGROUND_COLOR = Color(75, 75, 75)
FONT = "Arial" # Font used to display texts
FONT_SIZE_USERNAME = 25
FONT_SIZE_PING = 12
USERNAME = "John"
PLAYERS = []
WALLS = []
UNVISIBLE = []
SOCKET = None
WAITING_TIME = 0.100 # in seconds - period of connection requests when trying to connect to the host - must be < TIMEOUT
SOCKET_TIMEOUT = 0.500 # in seconds - 0 when set to non blocking mode - must be > waiting time
COMM_FREQUENCY = 0.05 # in seconds - time between two communications with the server
EXIT_TIMEOUT = 5 # in seconds - when trying to disconnect
PING = None # in milliseconds - ping with the server, None when disconnected
LAST_PING_TIME = None # in seconds - time when last ping was sent
PING_FREQUENCY = 10 # in loops
LOBBY = True
readyPlayers = []
DEFAULT_LOBBY_COLOR = WHITE
READY_LOBBY_COLOR = GREEN
# Teams display
TEAM_DISPLAY_HEIGHT = 100
TEAMS_NAMES = {0 : "Not assigned (press 'N' to join)",
1 : "Seekers (press 'R' to join)",
2 : "Hidders (press 'B' to join)"}
TEAMS = {0 : [], 1 : [], 2 : []}
TEAMS_COLOR = {0 : WHITE, 1 : RED, 2 : BLUE}
FONT_SIZE_TEAMS = 30
TEAMS_POSITIONS = {0 : 1, 1 : 0, 2 : 2}
TEAMS_FINAL_POSITIONS = {0 : None, 1 : None, 2 : None}
TEAMS_TEXTS = {0 : None, 1 : None, 2 : None}
WALL_VISIBLE = True
# In-game variables
GAME_TIME = None
# Transitions variables
IN_TRANSITION = False
TRANSITION_TEXT = ""
FONT_SIZE_TRANSITION = 40
# ----------------------- Threads -----------------------
def display():
"""Thread to display the current state of the game given by the server.
"""
global FPS
global SCREEN
global PLAYERS
global SCALE_FACTOR
global SIZE
global TEAMS
global TEAMS_FINAL_POSITIONS
pg.init()
PLATEFORM = system() # system name (Windows or Linux ... )
# sets screen size and scale factors
if PLATEFORM=="Linux":
info = pg.display.Info()
SCALE_FACTOR = info.current_w/SIZE[0],info.current_h/SIZE[1]
SCREEN = pg.display.set_mode((0,0),pg.FULLSCREEN)
SIZE=SCREEN.get_size()
elif PLATEFORM=="Windows":
info = pg.display.Info()
SCALE_FACTOR = info.current_w/SIZE[0],info.current_h/SIZE[1]
SIZE = info.current_w, info.current_h
SCREEN = pg.display.set_mode(SIZE)
else :
SCALE_FACTOR=1,1
# set fonts for ping, teams, usernames and in-game printings
pingFont = pg.font.SysFont(FONT, FONT_SIZE_PING)
usernameFont = pg.font.SysFont(FONT, FONT_SIZE_USERNAME)
teamFont = pg.font.SysFont(FONT, FONT_SIZE_TEAMS)
timeFont = teamFont
transitionFont = pg.font.SysFont(FONT, FONT_SIZE_TRANSITION)
# set teams display parameters
baseHeight = TEAM_DISPLAY_HEIGHT
for id in TEAMS_TEXTS:
teamSize = pg.font.Font.size(teamFont, TEAMS_NAMES[id])
baseHeight = TEAM_DISPLAY_HEIGHT + teamSize[1]
TEAMS_TEXTS[id] = pg.font.Font.render(teamFont, TEAMS_NAMES[id], False, TEAMS_COLOR[id].color)
TEAMS_FINAL_POSITIONS[id] = (SIZE[0] - teamSize[0]) // 2 * TEAMS_POSITIONS[id]
clock = pg.time.Clock()
last_fps_time = time.time()
while CONNECTED:
SCREEN.fill(BACKGROUND_COLOR.color) # May need to be custom
pg.event.pump() # Useless, just to make windows understand that the game has not crashed...
if not LOBBY and WALL_VISIBLE and len(UNVISIBLE) > 2: # draws shades under the walls
pg.draw.polygon(SCREEN, BLACK.color, [(x*SCALE_FACTOR[0],y*SCALE_FACTOR[1]) for (x,y) in UNVISIBLE])
# Walls
for wall in WALLS:
pg.draw.rect(SCREEN, wall.color.color, [wall.position.x*SCALE_FACTOR[0], wall.position.y*SCALE_FACTOR[1], wall.size.w*SCALE_FACTOR[0], wall.size.h*SCALE_FACTOR[1]])
#Unvisible
if not LOBBY and not(WALL_VISIBLE) and len(UNVISIBLE) > 2: #draw shades on top of the walls
pg.draw.polygon(SCREEN, BLACK.color, [(x*SCALE_FACTOR[0],y*SCALE_FACTOR[1]) for (x,y) in UNVISIBLE])
# Teams display
if LOBBY:
for id in TEAMS_NAMES:
SCREEN.blit(TEAMS_TEXTS[id], (TEAMS_FINAL_POSITIONS[id], TEAM_DISPLAY_HEIGHT))
TEAMS = {0 : [], 1 : [], 2 : []}
# Players
for player in PLAYERS:
pg.draw.rect(SCREEN, player.color.color, [player.position.x*SCALE_FACTOR[0], player.position.y*SCALE_FACTOR[1], player.size.w*SCALE_FACTOR[0], player.size.h*SCALE_FACTOR[1]])
usernameText = player.username
usernameSize = pg.font.Font.size(usernameFont, usernameText)
usernameSurface = pg.font.Font.render(usernameFont, usernameText, False, player.color.color)
SCREEN.blit(usernameSurface, (player.position.x*SCALE_FACTOR[0] + (player.size.w*SCALE_FACTOR[0] - usernameSize[0]) // 2, player.position.y*SCALE_FACTOR[1] - usernameSize[1]))
if(LOBBY):
h = baseHeight
if player.teamId in TEAMS:
TEAMS[player.teamId].append(player)
h = baseHeight + (len(TEAMS[player.teamId]) - 1) * usernameSize[1]
usernamePosition = (SIZE[0] - usernameSize[0]) // 2
if player.teamId in TEAMS_POSITIONS:
usernamePosition = (SIZE[0] - usernameSize[0]) // 2 * TEAMS_POSITIONS[player.teamId]
font_color = DEFAULT_LOBBY_COLOR
if(player.username in readyPlayers):
font_color = READY_LOBBY_COLOR
usernameSurface = pg.font.Font.render(usernameFont, usernameText, False, font_color.color)
SCREEN.blit(usernameSurface, (usernamePosition, h))
# Lights
if DEBUG: # Draw lights where they are meant to be in the server
pg.draw.rect(SCREEN, (255,255,0), [200*SCALE_FACTOR[0], 200*SCALE_FACTOR[1], 10, 10])
pg.draw.rect(SCREEN, (255,255,0), [500*SCALE_FACTOR[0], 800*SCALE_FACTOR[1], 10, 10])
pg.draw.rect(SCREEN, (255,255,0), [1500*SCALE_FACTOR[0], 500*SCALE_FACTOR[1], 10, 10])
# Teams display
if LOBBY:
for id in TEAMS_NAMES:
SCREEN.blit(TEAMS_TEXTS[id], (TEAMS_FINAL_POSITIONS[id], TEAM_DISPLAY_HEIGHT))
TEAMS = {0 : [], 1 : [], 2 : []}
# FPS
fpsText = "FPS : " + str(FPS)
fpsSize = pg.font.Font.size(pingFont, fpsText)
fpsSurface = pg.font.Font.render(pingFont, fpsText, False, WHITE.color)
# and Ping
pingText = "Ping : " + str(PING) + " ms"
pingSize = pg.font.Font.size(pingFont, pingText)
pingSurface = pg.font.Font.render(pingFont, pingText, False, WHITE.color)
offset = max(pingSize[0], fpsSize[0])
SCREEN.blit(fpsSurface, (SIZE[0] - offset, 0))
SCREEN.blit(pingSurface, (SIZE[0] - offset, fpsSize[1] + pingSize[1] // 2))
# Remaining game time
if not LOBBY:
timeText = "Remaining Seeking Time : " + str(GAME_TIME) + "s"
timeSize = pg.font.Font.size(timeFont, timeText)
timeSurface = pg.font.Font.render(timeFont, timeText, False, WHITE.color)
SCREEN.blit(timeSurface, (TEAMS_FINAL_POSITIONS[0], TEAM_DISPLAY_HEIGHT))
# In Transition state
if IN_TRANSITION:
transitionSize = pg.font.Font.size(transitionFont, TRANSITION_TEXT)
transitionSurface = pg.font.Font.render(transitionFont, TRANSITION_TEXT, False, WHITE.color)
SCREEN.blit(transitionSurface, ((SIZE[0] - transitionSize[0]) // 2, (SIZE[1] - transitionSize[1]) // 2))
# End
pg.display.update()
t = time.time()
FPS = int(1/(t - last_fps_time))
last_fps_time = t
clock.tick(FPS_GOAL)
def game():
"""Thread to send inputs to the server, receive the current state of the game from it, and update the client-side variables.
"""
global PLAYERS
global SERVER_IP
global SERVER_PORT
global SOCKET
global LOBBY
global LAST_PING_TIME
requestNumber=0
i = 0
while CONNECTED and requestNumber<MAX_REQUESTS:
inputs = getInputs()
if i%PING_FREQUENCY == 0:
t = time.time()
LAST_PING_TIME = t
update(sendPing(t))
state = send(inputs)
if (update(state)) : # request failed
requestNumber+=1
else :
requestNumber=0
if requestNumber>=MAX_REQUESTS:
exitError("Max number of request has been passed for inputs!")
i += 1
time.sleep(COMM_FREQUENCY)
if SOCKET != None:
SOCKET.close()
SOCKET = None
# ----------------------- Functions -----------------------
def connect():
"""Try to connect to the given SERVER_IP and SERVER_PORT. When successful, initialize the current state of the game.
Returns:
bool: is the connection successful ?
"""
global SERVER_PORT
global SIZE
message = send("CONNECT " + USERNAME + " END") # Should be "CONNECTED <New_Port> <Username> SIZE WALLS <WallsString> STATE <PlayersString> SHADES <ShadesString> END"
if message != None:
messages = message.split(" ")
else:
messages = None
if DEBUG:
print("messages: ", messages)
if (messages != None and len(messages) == 11 and messages[0] == "CONNECTED" and messages[2] == USERNAME and messages[4] == "WALLS" and messages[6] == "LOBBY" and messages[8] == "STATE" and messages[10] == "END"):
# get serveur default screen size
try:
portStr = "" + messages[1]
SERVER_PORT = int(portStr)
sizeStr = "" + messages[3]
sizeStr = sizeStr.replace("(", "")
sizeStr = sizeStr.replace(")", "")
sizeStr = sizeStr.split(",")
SIZE = (int(sizeStr[0]), int(sizeStr[1]))
except ValueError:
if DEBUG:
print("Size Error ! Size format was not correct !")
SIZE = (400, 300) # Some default size.
# set walls players and shades
update(messages[4] + " " + messages[5] + " " + messages[6] + " " + messages[7] + " " + messages[8] + " " + messages[9] + " END")
return True
# Manage failed connections
elif messages != None and "CONNECTED" not in messages:
askNewPseudo(message)
global SOCKET
SOCKET.close()
SOCKET = None
return False
def askNewPseudo(errorMessage:str):
"""Ask for another username when connection fails to try to connect again.
Args:
errorMessage (str): connection error message sent back by the server when the connection failed.
"""
global USERNAME
print("Server sent back : " + errorMessage)
print("Please try a new pseudo. (Your previous one was " + USERNAME + ")")
username = ""
while username == "":
username = input()
USERNAME = username
def sendPing(time):
"""Send a ping message to the server to identify the client ping
Args:
time (float): time when the ping message was sent
"""
return send("PING " + str(time) + " END")
def getInputs():
"""Get inputs from the keyboard and generate the corresponding request to send to the server.
Returns:
str: the normalized request to send to the server : "INPUT <Username> <Input> END"
"""
events = pg.event.get()
keys = pg.key.get_pressed()
for event in events:
if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
exit()
elif event.type == pg.KEYDOWN and event.key == pg.K_SPACE:
return "INPUT " + USERNAME + " READY END"
if keys[pg.K_LEFT] or keys[pg.K_q]:
return "INPUT " + USERNAME + " LEFT END"
elif keys[pg.K_RIGHT] or keys[pg.K_d]:
return "INPUT " + USERNAME + " RIGHT END"
elif keys[pg.K_UP] or keys[pg.K_z]:
return "INPUT " + USERNAME + " UP END"
elif keys[pg.K_DOWN] or keys[pg.K_s]:
return "INPUT " + USERNAME + " DOWN END"
elif keys[pg.K_r]:
return "INPUT " + USERNAME + " RED END"
elif keys[pg.K_b]:
return "INPUT " + USERNAME + " BLUE END"
elif keys[pg.K_n]:
return "INPUT " + USERNAME + " NEUTRAL END"
return "INPUT " + USERNAME + " . END"
def send(input="INPUT " + USERNAME + " . END"):
"""Send a normalized request to the server and listen for the normalized answer.
Args:
input (str): Normalized request to send to the server. Defaults to "INPUT <Username> . END".
Returns:
str: the normalized answer from the server.
"""
global PING
global SOCKET
# Initialization
if (SOCKET == None and input[0:7] == "CONNECT"):
SOCKET = socket(AF_INET, SOCK_DGRAM)
SOCKET.settimeout(SOCKET_TIMEOUT)
# try:
# SOCKET.connect((SERVER_IP, SERVER_PORT))
# except (BlockingIOError, TimeoutError, ConnectionError):
# if DEBUG:
# traceback.print_exc()
# exitError("Connection attempt failed, retrying...")
# SOCKET=None
# Usual behavior
if SOCKET != None:
# send data
try:
if DEBUG:
print("sending to: ", (SERVER_IP, SERVER_PORT))
print("input: ",input)
SOCKET.sendto(bytes(input, "utf-8"), (SERVER_IP, SERVER_PORT))
if DEBUG:
print("input sent!")
except (OSError):
if DEBUG:
traceback.print_exc()
exitError("Loss connection with the remote server while sending data.")
return
# receive answer
try:
if DEBUG:
print("listening for answer")
data, addr = SOCKET.recvfrom(MESSAGES_LENGTH)
answer = str(data.strip(), "utf-8")
if DEBUG:
print("receiving from: ", addr)
print("answer: ",answer)
return answer
except (BlockingIOError, TimeoutError):
pass
except (OSError):
if DEBUG:
traceback.print_exc()
exitError("Loss connection with the remote server while receiving data.")
return
def update(state="STATE [] END"):
"""Update the local variables representing the current state of the game from the given state.
Args:
state (str): The normalized state of the game. Defaults to "STATE [] END".
Returns:
bool: was there a problem in updating variables ?
"""
global PING
global WALLS
global PLAYERS
global UNVISIBLE
global readyPlayers
global LOBBY
global GAME_TIME
global IN_TRANSITION
global TRANSITION_TEXT
if state == None or type(state) != str or state == "":
return False
messages = state.split(" ")
### Simple commands : KEYWORD <content> END
if len(messages) == 3 and messages[0] == "PING" and messages[2] == "END":
try:
timeping = float(messages[1])
if timeping == LAST_PING_TIME:
PING = int((time.time() - LAST_PING_TIME) * 1000)
return False
except:
return True
if len(messages) == 3 and messages[0] == "STATE" and messages[2] == "END":
players = Player.toPlayers(messages[1],DEBUG)
if (players != None):
PLAYERS=players
return False
else: return True
elif len(messages) == 3 and messages[0] == "WALLS" and messages[2] == "END":
walls = Wall.toWalls(messages[1],DEBUG)
if (walls != None):
WALLS=walls
return False
else: return True
elif len(messages) == 3 and messages[0] == "SHADES" and messages[2] == "END":
unvisible = toVisible(messages[1],DEBUG)
if (unvisible != None):
UNVISIBLE=unvisible
return False
else: return True
elif len(messages) == 3 and messages[0] == "LOBBY" and messages[2] == "END":
readyPlayers = interp(messages[1], list=["", 0])["list"]
IN_TRANSITION = False
LOBBY = True
return False
elif len(messages) == 3 and messages[0] == "GAME" and messages[2] == "END":
GAME_TIME = interp(messages[1], time=0.0)["time"]
IN_TRANSITION = False
LOBBY = False
return False
elif(len(messages) == 3 and messages[0] == "TRANSITION_GAME_LOBBY" and messages[2] == "END"):
TRANSITION_TEXT = interp(messages[1], text="")["text"].replace("_", " ")
IN_TRANSITION = True
return False
elif(len(messages) == 3 and messages[0] == "TRANSITION_LOBBY_GAME" and messages[2] == "END"):
TRANSITION_TEXT = interp(messages[1], text="")["text"].replace("_", " ")
IN_TRANSITION = True
return False
### Concatenated commands
else:
# dictionary representing the known keywords and the number of parameters in <content> they take
keywords = {"PING" : 1, "STATE" : 1, "WALLS" : 1, "SHADES" : 1, "LOBBY" : 1, "GAME" : 1, "TRANSITION_GAME_LOBBY" : 1, "TRANSITION_LOBBY_GAME" : 1}
if len(messages) >= 3:
conc = messages
if conc[0] in keywords:
# generate partial command
command = conc[0] + " "
n = keywords[conc[0]]
for i in range(n):
command += conc[1 + i] + " "
command += "END"
conc[0:1 + n] = []
m = len(conc)
remains = ""
for i in range(m):
if i != m - 1:
remains += conc[i] + " "
else:
remains += conc[i]
return update(command) or update(remains)
else:
return True
elif messages == ["END"]:
return False
return True
def exit():
"""Send the normalized disconnection request and then exits the game.
"""
global CONNECTED
global SOCKET
SOCKET.settimeout(EXIT_TIMEOUT)
requestNumber=0
t = time.time()
while time.time() - t < DISCONNECTION_WAITING_TIME and requestNumber<MAX_REQUESTS:
if send("DISCONNECTION " + USERNAME + " END") == "DISCONNECTED " + USERNAME + " END":
CONNECTED = False
SOCKET.close()
SOCKET = None
break
requestNumber+=1
if requestNumber>=MAX_REQUESTS:
exitError("Max number of request has been passed for disconnection!")
def exitError(errorMessage="Sorry a problem occured..."):
"""Exit the game
Called if there has been a problem with the server
Args:
errorMessage (str): the string to print according to the error ("Sorry a problem occured..." by default)"""
global CONNECTED
global SOCKET
print(errorMessage)
CONNECTED=False
SOCKET.close()
SOCKET = None
def main():
"""Main function launching the parallel threads to play the game and communicate with the server.
"""
global CONNECTED
requestNumber=0
while not CONNECTED and requestNumber<MAX_REQUESTS:
CONNECTED = connect()
time.sleep(WAITING_TIME)
requestNumber+=1
if requestNumber>MAX_REQUESTS:
exitError("Max number of request has been passed for connections!")
if CONNECTED:
displayer = Thread(target=display)
gameUpdater = Thread(target=game)
displayer.daemon = True
gameUpdater.daemon = True
displayer.start()
gameUpdater.start()
while CONNECTED:
time.sleep(1)
# ----------------------- Client Side -----------------------
if __name__ == "__main__":
if len(sys.argv)==3:
SERVER_IP = "".join(sys.argv[1]) #"10.193.49.95" #"localhost"
SERVER_PORT = int("".join(sys.argv[2])) #9998
print("Server IP : ",SERVER_IP)
print("Server Port : ",SERVER_PORT)
print("Which username do you want to use ? (By default, it is " + USERNAME + ")")
username = input()
if username != "":
USERNAME = username
main()