import argparse import requests import multiprocessing import time import getpass import traceback from datetime import datetime import reconchess from reconchess import load_player, play_remote_game import sys import signal class RBCServer: def __init__(self, server_url, auth): self.server_url = server_url self.invitations_url = '{}/api/invitations'.format(server_url) self.user_url = '{}/api/users'.format(server_url) self.me_url = '{}/api/users/me'.format(server_url) self.game_url = '{}/api/games'.format(server_url) self.session = requests.Session() self.session.auth = auth def _get(self, endpoint): response = self.session.get(endpoint) while response.status_code >= 500: time.sleep(0.5) response = self.session.get(endpoint) if response.status_code == 401: print('Authentication Error!') print(response.text) quit() return response.json() def _post(self, endpoint, json=None): response = self.session.post(endpoint, json=json) while response.status_code >= 500: time.sleep(0.5) response = self.session.post(endpoint, json=json) if response.status_code == 401: print('Authentication Error!') print(response.text) quit() return response.json() def get_reconchess_version(self): return self._get('{}/api/version'.format(self.server_url))['version'] def set_max_games(self, max_games): self._post('{}/max_games'.format(self.me_url), json={'max_games': max_games}) def get_active_users(self): return self._get('{}/'.format(self.user_url))['usernames'] def send_invitation(self, opponent, color): return self._post('{}/'.format(self.invitations_url), json={ 'opponent': opponent, 'color': color, })['game_id'] def get_invitations(self): return self._get('{}/'.format(self.invitations_url))['invitations'] def accept_invitation(self, invitation_id): return self._post('{}/{}'.format(self.invitations_url, invitation_id))['game_id'] def finish_invitation(self, invitation_id): self._post('{}/{}/finish'.format(self.invitations_url, invitation_id)) def error_resign(self, game_id): return self._post('{}/{}/error_resign'.format(self.game_url, game_id)) def set_ranked(self, ranked): self._post('{}/ranked'.format(self.me_url), json={'ranked': ranked}) def get_bot_version(self): return self._get('{}/version'.format(self.me_url))['version'] def increment_version(self): self._post('{}/version'.format(self.me_url)) def accept_invitation_and_play(server_url, auth, invitation_id, bot_cls, finished): # make sure this process doesn't react to interrupt signals signal.signal(signal.SIGTERM, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN) print('[{}] Accepting invitation {}.'.format(datetime.now(), invitation_id)) server = RBCServer(server_url, auth) game_id = server.accept_invitation(invitation_id) print('[{}] Invitation {} accepted. Playing game {}.'.format(datetime.now(), invitation_id, game_id)) try: play_remote_game(server_url, game_id, auth, bot_cls()) print('[{}] Finished game {}'.format(datetime.now(), game_id)) except: print('[{}] Fatal error in game {}:'.format(datetime.now(), game_id)) traceback.print_exc() server.error_resign(game_id) finally: server.finish_invitation(invitation_id) finished.value = True def check_package_version(server): server_version = server.get_reconchess_version() if server_version != reconchess.__version__: print('Your reconchess package is out of date!'.format(reconchess.__version__)) print('Current:', 'reconchess v{}'.format(reconchess.__version__)) print('Latest :', 'reconchess v{}'.format(server_version)) print('Please run the command `pip install --upgrade reconchess` to upgrade to the latest.') quit() def ranked_mode(server, keep_version): current_version = server.get_bot_version() # ask if they want to update their version number if current_version == 0: should_increment_version = True elif keep_version: should_increment_version = False else: should_increment_version = confirm('Is this a new version of your bot?') # compute the new version depending on their answer next_version = current_version + (1 if should_increment_version else 0) # make sure they want to participate with this info if not keep_version and not confirm( 'Are you sure you want to participate in ranked matches as v{} (currently v{})?'.format( next_version, current_version)): quit() # update information on server if should_increment_version: server.increment_version() server.set_ranked(True) def unranked_mode(server): server.set_ranked(False) def listen_for_invitations(server, bot_cls, max_concurrent_games): connected = False process_by_invitation = {} finished_by_invitation = {} while True: try: # get unaccepted invitations invitations = server.get_invitations() # set max games on server if this is the first successful connection after being disconnected if not connected: print('[{}] Connected successfully to server!'.format(datetime.now())) connected = True server.set_max_games(max_concurrent_games) # filter out finished processes finished_invitations = [] for invitation in process_by_invitation.keys(): if not process_by_invitation[invitation].is_alive() or finished_by_invitation[invitation].value: finished_invitations.append(invitation) for invitation in finished_invitations: print('[{}] Terminating process for invitation {}'.format(datetime.now(), invitation)) process_by_invitation[invitation].terminate() del process_by_invitation[invitation] del finished_by_invitation[invitation] # accept invitations until we have #max_concurrent_games processes alive for invitation in invitations: # only accept the invitation if we have room and the invite doesn't have a process already if invitation not in process_by_invitation: print('[{}] Received invitation {}.'.format(datetime.now(), invitation)) if len(process_by_invitation) < max_concurrent_games: # start the process for playing a game finished = multiprocessing.Value('b', False) process = multiprocessing.Process( target=accept_invitation_and_play, args=(server.server_url, server.session.auth, invitation, bot_cls, finished)) process.start() # store the process so we can check when it finishes process_by_invitation[invitation] = process finished_by_invitation[invitation] = finished else: print('[{}] Not enough game slots to play invitation {}.'.format(datetime.now(), invitation)) except requests.RequestException as e: connected = False print('[{}] Failed to connect to server'.format(datetime.now())) print(e) except Exception: print("Error in invitation processing: ") traceback.print_exc() time.sleep(5) def ask_for_username(): return input('Username: ') def ask_for_password(): return getpass.getpass() def ask_for_auth(): return ask_for_username(), ask_for_password() def confirm(prompt): while True: response = input(prompt + ' [y/n]') if len(response) == 0 or response.lower()[0] == 'y': return True elif len(response) > 0 and response.lower()[0] == 'n': return False else: print('Invalid input... please enter one of {"y", "n", "yes", "no", <Enter>, ...}') def main(): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('bot_path', help='Path to bot source or bot module name.') parser.add_argument('--username', default=None, help='Username for login. Enter with prompt if not specified.') parser.add_argument('--password', default=None, help='Password for login. Enter with prompt if not specified.') parser.add_argument('--server-url', default='https://rbc.jhuapl.edu', help='URL of the server.') parser.add_argument('--ranked', action='store_true', default=False, help='Whether you want to play ranked matches.') parser.add_argument('--keep-version', action='store_true', default=False, help='Force your ranked version to stay the same with no prompts.') parser.add_argument('--max-concurrent-games', type=int, default=4, help='The maximum number of games to play at the same time.') args = parser.parse_args() bot_name, bot_cls = load_player(args.bot_path) username = ask_for_username() if args.username is None else args.username password = ask_for_password() if args.password is None else args.password auth = username, password server = RBCServer(args.server_url, auth) # verify we have the correct version of reconchess package check_package_version(server) def handle_term(signum, frame): print('[{}] Received terminate signal, waiting for games to finish and then exiting...'.format(datetime.now())) unranked_mode(server) sys.exit(0) signal.signal(signal.SIGINT, handle_term) signal.signal(signal.SIGTERM, handle_term) # tell the server whether we want to do ranked matches or not if args.ranked: ranked_mode(server, args.keep_version) else: unranked_mode(server) listen_for_invitations(server, bot_cls, args.max_concurrent_games) if __name__ == '__main__': main()