import json import subprocess import random import time import sys from threading import Timer from flask import Flask, jsonify, request, render_template RUNNING_DIRECTORY = '/data/orbit-project/' #Absolute path to text files TIMEOUT_LENGTH = 1800.0 #Number of seconds of inactivity before session expires MINIMUM_TIME_PER_BARRIER = 0.83 MAXIMUM_TIME_PER_BARRIER = 1.33 server = Flask(__name__) sessions = dict() #Client sessions session_timeouts = dict() #Will contain timers that delete inactive sessions # The test function responds to '/test' fetches from the game clients. # If the server and game are both running properly, it will return a # message to the client to confirm that Flask is reacheable. @server.route('/test', methods=['GET', 'POST']) def test(): """Send message to confirm established connection to Flask API.""" if request.method == 'POST': print('Recieving:') print(request.get_json()) return 'OK', 200 else: message = {'message':'Page is communicating with Flask! \ (•◡•) /'} return jsonify(message) # Upon '/leaderboard' request, return the top 15 scores stored in leaderboard.txt @server.route('/leaderboard') def get_leaderboard(): """Responds to '/leaderboard' fetch requests, returns top 15 scores.""" with open(RUNNING_DIRECTORY + 'leaderboard.txt', 'r+') as leaderboard: scores = dict() lines = leaderboard.read().splitlines() for i in range(len(lines)): # Keys player names, scores in values try: split = lines[i].split('-:-;-:-') scores[i] = [split[0], int(split[1])] except: continue return jsonify(scores) # Upon, '/instructions' request, return text within instructions-message.txt @server.route('/instructions') def get_instructions(): """Responds to '/instructions' requests, returns game controls instructions.""" with open(RUNNING_DIRECTORY + 'instructions-message.txt', 'r+') as message: return message.read() # Sensor the user's name in preparation for adding it to the leaderboard. def censor(name): """Runs name against list of profane words, alters name to censor it. Keyword arguments: name -- username to be censored """ with open(RUNNING_DIRECTORY + 'bad-words.txt', 'r+') as bad_list: if len(name) == 0: return 'A Kangaroo' if '-:-;-:-' in name: name = name.replace('-:-;-:-', '') if len(name) > 15: name_copy = name name = '' for i in range(min(14, len(name_copy))): name = name + name_copy[i] for word in bad_list.read().splitlines(): if word.upper() in name.upper(): return 'A Squirrel' else: return name # Generates an 8 digit key def generate_id(): """Create an 8 digit random string of uppercase letters.""" key = '' for i in range(8): key = key + chr(random.randint(65, 90)) return key # Deletes a client session from the server after it expires or closes. def session_kill(session_id): """Delete a value from global dictionary named 'sessions'. Keyword arguments: session_id -- the key of the target value in global variable sessions """ if session_id in sessions.keys(): del sessions[session_id] print(sessions) # Create a timer to kill client session if inactive def start_timeout(session_id): """Begin Timer() to kill a session after inactivity. Keyword arguments: session_id -- id linking kill countdown to session """ session_timeouts[session_id] = Timer(TIMEOUT_LENGTH, session_kill, [session_id]) session_timeouts[session_id].start() # Reset timer to countdown from @server.route('/session_ping', methods=['GET', 'POST']) def extend_timeout(): """Upon '/session_ping' request, deletes and recreates timer for session.""" if request.method == 'POST': result = request.get_json() try: session_timeouts[result['id']].cancel() start_timeout(result['id']) return 'Session Active', 200 except(KeyError): return 'Session Expired', 200 # Append message to logs.txt, useful for debugging code when running in # WSGI and not printing to command line. def log_file(message): """Append message on new line of log file. Keyword arguments: message -- message to be appended to log file """ out = sys.stdout with open(RUNNING_DIRECTORY + 'logs.txt', 'a+') as log: sys.stdout = log print(message) sys.stdout = out # Upon '/create_session' request, creates a new session in sessions dictionary. @server.route('/create_session', methods=['GET', 'POST']) def create_session(): """Create new session in global dictionary with randomly generated key.""" session_id = generate_id() if session_id not in sessions.keys(): sessions[session_id] = { 'jumps':0, 'barriers':0, 'score':0, 'reported':False, 'time_start':time.time(), 'time_end':0} start_timeout(session_id) return session_id else: while session_id in sessions.keys(): session_id = generate_id() sessions[session_id] = { 'jumps':0, 'barriers':0, 'score':0, 'reported':False, 'time_start':time.time(), 'time_end':0} start_timeout(session_id) return session_id # Upon '/close_session' request, kill client's session. @server.route('/close_session', methods=['GET', 'POST']) def close_session(): """Calls session_kill passing request's session id parameter.""" if request.method == 'POST': session_kill(request.get_json()['id']) return 'OK', 200 # Upon '/end_session' request, recieve session results to verify score. @server.route('/end_session', methods=['GET', 'POST']) def get_score(): """Updates global sessions dictionary with session information.""" if request.method == 'POST': result = request.get_json() if result['id'] in sessions: sessions[result['id']]['jumps'] = result['jumps'] sessions[result['id']]['barriers'] = result['barriers'] sessions[result['id']]['score'] = result['score'] sessions[result['id']]['time_end'] = time.time() print(sessions) return 'OK', 200 else: return 'Invalid Session', 200 # Verfies that a score was attained within a possible time interval. def verify_time(start, end, score): """Verify that the amount of time passed matches a reported score. Keyword arguments: start -- epoch time at start end -- epoch time at end of session score -- client-reported score """ time = end - start min_time = score * MINIMUM_TIME_PER_BARRIER max_time = 10 + score * MAXIMUM_TIME_PER_BARRIER if min_time <= time <= max_time: return True else: return False # Checks various conditions to verify score def verify_score(session): """Check if score is appropriate for number of jumps and session lifetime.""" reported = session['reported'] jumps = session['jumps'] >= session['barriers'] * 0.95 score = session['barriers'] == session['score'] time = verify_time(session['time_start'], session['time_end'], session['score']) return ((not reported) and jumps and score and time) # Upon '/score' request, adds score to leaderboard after verification @server.route('/score', methods=['GET', 'POST']) def write_score(): """Recieve reported score from client, verify, and pass to append_score.""" if request.method == 'POST': result = request.get_json() if verify_score(sessions[result['id']]): append_score(result) return 'OK', 200 else: return 'Invalid Session', 200 # Appends a score to the leaderboard file separating name and score by -:-;-:- def append_score(result): """Open leaderboard file and append score to new last line. Keyword arguments: result -- client session object with game attributes """ with open(RUNNING_DIRECTORY + 'leaderboard.txt', 'a+') as leaderboard: sessions[result['id']]['reported'] = True name = censor(result['name']) score = str(sessions[result['id']]['score']) leaderboard.write('\n' + name + '-:-;-:-' + score) # Upon loading the game URL directory, html page in templates folder is served. @server.route('/') def serve_page(): """Serve html page from templates folder.""" return render_template('index.html')