1
+ # This will be the main game server
2
+ # Author: Noor
3
+ # Date: March 10, 2021
4
+ from server_code .my_time import show_time_since , convert_str_secs , show_time_to
5
+ from server_code .game import Game
6
+ from server_code .game_handler import GameHandler
7
+ from server_code .db_handler import DBHandler
8
+
9
+ import logging
10
+ import socket
11
+ from concurrent .futures import ThreadPoolExecutor
12
+ import threading
13
+ import time
14
+ import queue
15
+ from pyfiglet import Figlet
16
+
17
+ #TODO: add strong exception handling EVERYWHERE
18
+
19
+ # Setup game logging
20
+ logging .basicConfig (filename = "logs/server.log" ,
21
+ level = logging .DEBUG ,
22
+ format = "%(asctime)s: %(name)s: %(levelname)s: %(message)s" )
23
+ # The logger for this module
24
+ game_logger = logging .getLogger (__name__ )
25
+
26
+ # Event starting and ending times
27
+ # TODO: Move these into a configuration file for the project
28
+ event_start_str = "Mon Mar 15 09:00:00 2021"
29
+ event_end_str = "Mon Mar 22 17:00:00 2021"
30
+
31
+ def game_handler_thread (client_conn , client_addr , db_query_q , db_resp_q , db_query_lock , event_started , event_ended ):
32
+ """The thread which will run one client connection"""
33
+ client_handler = GameHandler (client_conn , client_addr , db_query_q , db_resp_q , db_query_lock , event_started , event_ended )
34
+ client_handler .run_game ()
35
+
36
+ def db_handler_thread (db_query_q , db_resp_q , db_query_lock ):
37
+ """This is the only thread that will manipulate the DB"""
38
+ db_handler = DBHandler ()
39
+ while True :
40
+ """
41
+ Each game thread will send 2 items on the queue, after acquiring the lock:
42
+ 1- the type of operation
43
+ 2- the data needed for the operation in CSV format
44
+
45
+ The handler will parse the operation type to determine how to make sense
46
+ of the data it has received as object 2.
47
+
48
+ Operation types can be:
49
+ create_player
50
+ player_exists
51
+ update_login
52
+ update_hints
53
+ update_task
54
+ and so on
55
+
56
+ Only need locks for player_exists type of operation
57
+ """
58
+ op_type = db_query_q .get () # first item is operation
59
+ op_data = db_query_q .get () # second item is the data
60
+ op_data = op_data .split ("," ) # CSV to list conversion
61
+
62
+ if op_type == "create_player" :
63
+ db_handler .create_new_player (op_data [0 ], op_data [1 ], op_data [2 ], op_data [3 ], op_data [4 ], op_data [5 ], op_data [6 ], op_data [7 ])
64
+ elif op_type == "player_exists" :
65
+ resp = db_handler .check_player_exists (op_data [0 ])
66
+ db_resp_q .put (resp )
67
+ elif op_type == "update_login_time" :
68
+ db_handler .update_login_time (op_data [0 ], op_data [1 ])
69
+ elif op_type == "update_hints" :
70
+ db_handler .update_hints_left (op_data [0 ], op_data [1 ])
71
+ elif op_type == "update_task" :
72
+ db_handler .update_current_task (op_data [0 ], op_data [1 ])
73
+ elif op_type == "update_points" :
74
+ db_handler .update_points (op_data [0 ], op_data [1 ])
75
+ elif op_type == "update_online_status" :
76
+ db_handler .update_online_status (op_data [0 ], op_data [1 ])
77
+ elif op_type == "update_hint_used" :
78
+ db_handler .update_hint_used (op_data [0 ], op_data [1 ])
79
+
80
+ # the operations to resume progress
81
+ elif op_type == "get_hints" :
82
+ resp = db_handler .get_hints (op_data [0 ])
83
+ db_resp_q .put (resp )
84
+ elif op_type == "get_task" :
85
+ resp = db_handler .get_task (op_data [0 ])
86
+ db_resp_q .put (resp )
87
+ elif op_type == "get_points" :
88
+ resp = db_handler .get_points (op_data [0 ])
89
+ db_resp_q .put (resp )
90
+ elif op_type == "get_hint_used" :
91
+ resp = db_handler .get_hint_used (op_data [0 ])
92
+ db_resp_q .put (resp )
93
+ elif op_type == "get_online_status" :
94
+ resp = db_handler .get_online_status (op_data [0 ])
95
+ db_resp_q .put (resp )
96
+
97
+ # data to populate the leaderboard
98
+ elif op_type == "get_leaderboard_data" :
99
+ resp = db_handler .get_leaderboard_data ()
100
+ db_resp_q .put (resp )
101
+
102
+ def get_player_data (db_query_q , db_resp_q , db_query_lock ):
103
+ """Perform a db operation to get total and online player count"""
104
+ player_data = []
105
+ n_players = 0
106
+ n_online = 0
107
+
108
+ with db_query_lock :
109
+ db_query_q .put ("get_leaderboard_data" )
110
+ db_query_q .put ("" ) # dummy data
111
+ player_data = db_resp_q .get ()
112
+
113
+ n_players = len (player_data )
114
+ n_online = 0
115
+ for each_player in player_data :
116
+ if each_player [1 ] == "ONLINE" :
117
+ n_online += 1
118
+ return (n_players , n_online )
119
+
120
+ def status_message_thread (db_query_q , db_resp_q , db_query_lock , server_up_timestamp , event_started , event_ended , event_start_str , event_end_str ):
121
+ """Prints various statistics about the game server every few seconds"""
122
+ # a figlet object is only created once in a thread
123
+ msg_figlet = Figlet ()
124
+ while not event_started .is_set () and not event_ended .is_set ():
125
+ status_msg = ""
126
+ status_msg += "\n EVENT STARTS IN"
127
+ status_msg += "\n {}\n " .format (msg_figlet .renderText (show_time_to (event_start_str )))
128
+ status_msg += "Server up since: {}({})\n " .format (server_up_timestamp , show_time_since (server_up_timestamp ))
129
+ status_msg += "=-" * 30
130
+ print (status_msg )
131
+ time .sleep (1 )
132
+
133
+ event_started .wait ()
134
+ while event_started .is_set () and not event_ended .is_set ():
135
+ n_players , n_online = get_player_data (db_query_q , db_resp_q , db_query_lock )
136
+ status_msg = ""
137
+ status_msg += "\n EVENT ENDS IN\n "
138
+ status_msg += "\n {}\n " .format (msg_figlet .renderText (show_time_to (event_end_str )))
139
+ status_msg += "Total players: {}, Online: {}\n " .format (n_players , n_online )
140
+ status_msg += "Server up since: {}({})\n " .format (server_up_timestamp , show_time_since (server_up_timestamp ))
141
+ status_msg += "=-" * 30
142
+ print (status_msg )
143
+ # to save processing during the event, delay the status a bit
144
+ time .sleep (3 )
145
+
146
+ event_ended .wait ()
147
+ while event_ended .is_set ():
148
+ n_players , n_online = get_player_data (db_query_q , db_resp_q , db_query_lock )
149
+ status_msg = ""
150
+ status_msg += "\n EVENT HAS ENDED\n "
151
+ status_msg += "Total players: {}, Online: {}\n " .format (n_players , n_online )
152
+ status_msg += "Server up since: {}({})\n " .format (server_up_timestamp , show_time_since (server_up_timestamp ))
153
+ status_msg += "=-" * 30
154
+ print (status_msg )
155
+ # restore per second status after the event
156
+ time .sleep (1 )
157
+
158
+ def event_notifier_thread (event_started , event_ended , event_start_str , event_end_str ):
159
+ """
160
+ This thread will use the given timestamps for the event and will independently
161
+ monitor if the given event has started/ended and set threading.Event flags appropriately"""
162
+ # Convert the event_start and end times to seconds
163
+ event_start = convert_str_secs (event_start_str )
164
+ event_end = convert_str_secs (event_end_str )
165
+
166
+ while True :
167
+ # time right now in seconds since epoch
168
+ time_now = time .time ()
169
+
170
+ # if the event hasn't started yet, or is not already started
171
+ if time_now >= event_start and not event_started .is_set ():
172
+ event_started .set ()
173
+ game_logger .info ("Event has started!" )
174
+
175
+ # if the event hasn't ended yet, or is not already ended
176
+ if time_now >= event_end and not event_ended .is_set ():
177
+ event_ended .set ()
178
+ game_logger .info ("Event has ended!" )
179
+
180
+ # otherwise, don't mess with the flags, just sleep and do nothing
181
+ time .sleep (1 )
182
+
183
+ def game_server_loop ():
184
+ """This will be the main server loop for the game"""
185
+
186
+ # get the timestamp to determine server uptime
187
+ server_up_timestamp = time .asctime ()
188
+
189
+ # The server socket
190
+ # server_ip = "172.30.235.100"
191
+ server_ip = "127.0.0.1"
192
+ server_addr = (server_ip , 3141 ) # will listen on any interface and port 3141
193
+ server_backlog = 5 # the connections to keep on hold before refusing
194
+
195
+ # Flags to determine event starting and stopping
196
+ event_started = threading .Event ()
197
+ event_ended = threading .Event ()
198
+
199
+ # a thread pool to handle game threads
200
+ server_thread_executor = ThreadPoolExecutor (max_workers = 50 )
201
+
202
+ # the query/resp queues which will be used by the threads to
203
+ # communicate with the database
204
+ db_query_q = queue .Queue (10 )
205
+ db_resp_q = queue .Queue (10 )
206
+ # a lock to ensure synchronization between threads
207
+ db_query_lock = threading .Lock ()
208
+
209
+ # a separate thread pool executor for management threads
210
+ management_thread_executor = ThreadPoolExecutor (max_workers = 10 )
211
+
212
+ # start the event notifier thread
213
+ management_thread_executor .submit (event_notifier_thread , event_started , event_ended , event_start_str , event_end_str )
214
+ # start the db thread
215
+ management_thread_executor .submit (db_handler_thread , db_query_q , db_resp_q , db_query_lock )
216
+ # start the server monitor thread
217
+ management_thread_executor .submit (status_message_thread , db_query_q , db_resp_q , db_query_lock , server_up_timestamp , event_started , event_ended , event_start_str , event_end_str )
218
+
219
+
220
+
221
+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
222
+ # to avoid a Address in Use error, make the binding reusable
223
+ s .setsockopt (socket .SOL_SOCKET , socket .SO_REUSEADDR , 1 )
224
+ s .bind (server_addr )
225
+
226
+ while True :
227
+ print ("Server listening..." )
228
+ # start listening with a backlog of 5 connections
229
+ s .listen (server_backlog )
230
+
231
+ # For debugging purposes
232
+ try :
233
+ client_conn , client_addr = s .accept ()
234
+ except KeyboardInterrupt :
235
+ print ("Exiting Server" )
236
+ # On a force closure, make sure to close the socket object properly
237
+ s .close ()
238
+ exit ()
239
+
240
+ print ("Accepted a new connection from {} on port {}" .format (client_addr [0 ], client_addr [1 ]))
241
+ server_thread_executor .submit (game_handler_thread , client_conn , client_addr ,db_query_q , db_resp_q , db_query_lock , event_started , event_ended )
242
+
243
+ if __name__ == '__main__' :
244
+ print ("Server Started." )
245
+ game_server_loop ()
246
+
247
+
0 commit comments