Skip to content

Commit 6610f01

Browse files
committed
Functional Game Ready
Game functional with 5 tasks. The game used in the event with a bugfix for player_id and input enforced to be alphanum characters. Project finished.
0 parents  commit 6610f01

12 files changed

+1327
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__init__.py
2+
__pycache__/*
3+
data/*
4+
logs/*
5+
server_code/__init__.py
6+
server_code/__pycache__/*
7+
.vscode/*

cleanup.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# clear all the logs
2+
rm -rf logs/server.log
3+
rm -rf logs/games/*.log
4+
rm -rf logs/players/*.log
5+
6+
# clear game database
7+
rm -rf data/game.db

main_server.py

+247
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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 += "\nEVENT 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 += "\nEVENT 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 += "\nEVENT 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+

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pyfiglet==0.8.post0

0 commit comments

Comments
 (0)