diff options
Diffstat (limited to 'diplomacy/server')
-rw-r--r-- | diplomacy/server/connection_handler.py | 13 | ||||
-rw-r--r-- | diplomacy/server/notifier.py | 26 | ||||
-rw-r--r-- | diplomacy/server/request_manager_utils.py | 60 | ||||
-rw-r--r-- | diplomacy/server/request_managers.py | 155 | ||||
-rwxr-xr-x | diplomacy/server/run.py | 29 | ||||
-rw-r--r-- | diplomacy/server/scheduler.py | 33 | ||||
-rw-r--r-- | diplomacy/server/server.py | 145 | ||||
-rw-r--r-- | diplomacy/server/server_game.py | 84 | ||||
-rw-r--r-- | diplomacy/server/users.py | 33 |
9 files changed, 389 insertions, 189 deletions
diff --git a/diplomacy/server/connection_handler.py b/diplomacy/server/connection_handler.py index a70db7d..2b2ae4d 100644 --- a/diplomacy/server/connection_handler.py +++ b/diplomacy/server/connection_handler.py @@ -33,6 +33,7 @@ LOGGER = logging.getLogger(__name__) class ConnectionHandler(WebSocketHandler): """ ConnectionHandler class. Properties: + - server: server object representing running server. """ # pylint: disable=abstract-method @@ -43,6 +44,7 @@ class ConnectionHandler(WebSocketHandler): def initialize(self, server=None): """ Initialize the connection handler. + :param server: a Server object. :type server: diplomacy.Server """ @@ -69,6 +71,7 @@ class ConnectionHandler(WebSocketHandler): parsed_origin = urlparse(origin) origin = parsed_origin.netloc.split(':')[0] origin = origin.lower() + # Split host with ':' and keep only first piece to ignore eventual port. host = self.request.headers.get("Host").split(':')[0] return origin == host @@ -89,6 +92,7 @@ class ConnectionHandler(WebSocketHandler): @staticmethod def translate_notification(notification): """ Translate a notification to an array of notifications. + :param notification: a notification object to pass to handler function. See diplomacy.communication.notifications for possible notifications. :return: An array of notifications containing a single notification. @@ -103,8 +107,10 @@ class ConnectionHandler(WebSocketHandler): if not isinstance(json_request, dict): raise ValueError("Unable to convert a JSON string to a dictionary.") except ValueError as exc: - # Error occurred because either message is not a JSON string or parsed JSON object is not a dict. - response = responses.Error(message='%s/%s' % (type(exc).__name__, str(exc))) + # Error occurred because either message is not a JSON string + # or parsed JSON object is not a dict. + response = responses.Error(error_type=exceptions.ResponseException.__name__, + message=str(exc)) else: try: request = requests.parse_dict(json_request) @@ -118,7 +124,8 @@ class ConnectionHandler(WebSocketHandler): response = responses.Ok(request_id=request.request_id) except exceptions.ResponseException as exc: - response = responses.Error(message='%s/%s' % (type(exc).__name__, exc.message), + response = responses.Error(error_type=type(exc).__name__, + message=exc.message, request_id=json_request.get(strings.REQUEST_ID, None)) if response: diff --git a/diplomacy/server/notifier.py b/diplomacy/server/notifier.py index 81ca4b0..a658852 100644 --- a/diplomacy/server/notifier.py +++ b/diplomacy/server/notifier.py @@ -20,7 +20,7 @@ from tornado import gen from diplomacy.communication import notifications from diplomacy.utils import strings -class Notifier(): +class Notifier: """ Server notifier class. """ __slots__ = ['server', 'ignore_tokens', 'ignore_addresses'] @@ -28,6 +28,7 @@ class Notifier(): """ Initialize a server notifier. You can specify some tokens or addresses to ignore using ignore_tokens or ignore_addresses. Note that these parameters are mutually exclusive (you can use either none of them or only one of them). + :param server: a server object. :param ignore_tokens: (optional) sequence of tokens to ignore. :param ignore_addresses: (optional) sequence of couples (power name, token) to ignore. @@ -45,7 +46,8 @@ class Notifier(): self.ignore_tokens = set(ignore_tokens) # Expect a sequence of tuples (power name, token) to ignore. - # Convert it to a dict {power name => {token}} (each power name with all associated ignored tokens). + # Convert it to a dict {power name => {token}} + # (each power name with all associated ignored tokens). elif ignore_addresses: self.ignore_addresses = {} for power_name, token in ignore_addresses: @@ -55,6 +57,7 @@ class Notifier(): def ignores(self, notification): """ Return True if given notification must be ignored. + :param notification: :return: a boolean :type notification: notifications._AbstractNotification | notifications._GameNotification @@ -62,7 +65,8 @@ class Notifier(): if self.ignore_tokens: return notification.token in self.ignore_tokens if self.ignore_addresses and notification.level == strings.GAME: - # We can ignore addresses only for game requests (as other requests only have a token, not a full address). + # We can ignore addresses only for game requests + # (as other requests only have a token, not a full address). return (notification.game_role in self.ignore_addresses and notification.token in self.ignore_addresses[notification.game_role]) return False @@ -70,6 +74,7 @@ class Notifier(): @gen.coroutine def _notify(self, notification): """ Register a notification to send. + :param notification: a notification instance. :type notification: notifications._AbstractNotification | notifications._GameNotification """ @@ -84,6 +89,7 @@ class Notifier(): def _notify_game(self, server_game, notification_class, **kwargs): """ Send a game notification. Game token, game ID and game role will be automatically provided to notification object. + :param server_game: game to notify :param notification_class: class of notification to send :param kwargs: (optional) other notification parameters @@ -99,6 +105,7 @@ class Notifier(): def _notify_power(self, game_id, power, notification_class, **kwargs): """ Send a notification to all tokens of a power. Automatically add token, game ID and game role to notification parameters. + :param game_id: power game ID. :param power: power to send notification. :param notification_class: class of notification to send. @@ -114,6 +121,7 @@ class Notifier(): @gen.coroutine def notify_game_processed(self, server_game, previous_phase_data, current_phase_data): """ Notify all game tokens about a game phase update (game processing). + :param server_game: game to notify :param previous_phase_data: game phase data before phase update :param current_phase_data: game phase data after phase update @@ -157,6 +165,7 @@ class Notifier(): @gen.coroutine def notify_game_deleted(self, server_game): """ Notify all game tokens about game deleted. + :param server_game: game to notify :type server_game: diplomacy.server.server_game.ServerGame """ @@ -165,6 +174,7 @@ class Notifier(): @gen.coroutine def notify_game_powers_controllers(self, server_game): """ Notify all game tokens about current game powers controllers. + :param server_game: game to notify :type server_game: diplomacy.server.server_game.ServerGame """ @@ -175,6 +185,7 @@ class Notifier(): @gen.coroutine def notify_game_status(self, server_game): """ Notify all game tokens about current game status. + :param server_game: game to notify :type server_game: diplomacy.server.server_game.ServerGame """ @@ -183,6 +194,7 @@ class Notifier(): @gen.coroutine def notify_game_phase_data(self, server_game): """ Notify all game tokens about current game state. + :param server_game: game to notify :type server_game: diplomacy.server.server_game.ServerGame """ @@ -215,6 +227,7 @@ class Notifier(): def notify_game_vote_updated(self, server_game): """ Notify all game tokens about current game vote. Send relevant notifications to each type of tokens. + :param server_game: game to notify :type server_game: diplomacy.server.server_game.ServerGame """ @@ -242,6 +255,7 @@ class Notifier(): @gen.coroutine def notify_power_orders_update(self, server_game, power, orders): """ Notify all power tokens and all observers about new orders for given power. + :param server_game: game to notify :param power: power to notify :param orders: new power orders @@ -265,6 +279,7 @@ class Notifier(): @gen.coroutine def notify_power_wait_flag(self, server_game, power, wait_flag): """ Notify all power tokens about new wait flag for given power. + :param server_game: game to notify :param power: power to notify :param wait_flag: new wait flag @@ -275,6 +290,7 @@ class Notifier(): @gen.coroutine def notify_cleared_orders(self, server_game, power_name): """ Notify all game tokens about game orders cleared for a given power name. + :param server_game: game to notify :param power_name: name of power for which orders were cleared. None means all power orders were cleared. @@ -285,6 +301,7 @@ class Notifier(): @gen.coroutine def notify_cleared_units(self, server_game, power_name): """ Notify all game tokens about game units cleared for a given power name. + :param server_game: game to notify :param power_name: name of power for which units were cleared. None means all power units were cleared. @@ -295,6 +312,7 @@ class Notifier(): @gen.coroutine def notify_cleared_centers(self, server_game, power_name): """ Notify all game tokens about game centers cleared for a given power name. + :param server_game: game to notify :param power_name: name of power for which centers were cleared. None means all power centers were cleared. @@ -305,6 +323,7 @@ class Notifier(): @gen.coroutine def notify_game_message(self, server_game, game_message): """ Notify relevant users about a game message received. + :param server_game: Game data who handles this game message. :param game_message: the game message received. :return: None @@ -330,6 +349,7 @@ class Notifier(): """ Notify addresses of a game with a notification. Game ID is automatically provided to notification. Token and game role are automatically provided to notifications from given addresses. + :param game_id: related game ID :param addresses: addresses to notify. Sequence of couples (game role, token). :param notification_class: class of notification to send diff --git a/diplomacy/server/request_manager_utils.py b/diplomacy/server/request_manager_utils.py index 9ea8264..e8335fc 100644 --- a/diplomacy/server/request_manager_utils.py +++ b/diplomacy/server/request_manager_utils.py @@ -25,33 +25,45 @@ from diplomacy.server.notifier import Notifier from diplomacy.utils import strings, exceptions class SynchronizedData(namedtuple('SynchronizedData', ('timestamp', 'order', 'type', 'data'))): - """ Small class used to store and sort data to synchronize for a game. Properties: - - timestamp (int): timestamp of related data to synchronize. - - order (int): rank of data to synchronize. - - type (str): type name of data to synchronize. Possible values: + """ Small class used to store and sort data to synchronize for a game. + + Properties: + + - **timestamp** (int): timestamp of related data to synchronize. + - **order** (int): rank of data to synchronize. + - **type** (str): type name of data to synchronize. Possible values: + - 'message': data is a game message. Order is 0. - 'state_history': data is a game state for history. Order is 1. - 'state': data is current game state. Order is 2. - - data: proper data to synchronize. + + - **data**: proper data to synchronize. + Synchronized data are sorted using timestamp then order, meaning that: - - data are synchronized from former to later timestamps - - for a same timestamp, messages are synchronized first, then states for history, then current state. + + - data are synchronized from former to later timestamps + - for a same timestamp, messages are synchronized first, + then states for history, then current state. """ -class GameRequestLevel(): +class GameRequestLevel: """ Describe a game level retrieved from a game request. Used by some game requests managers - to determine user rights in a game. Possible game levels: power, observer, omniscient and master. + to determine user rights in a game. Possible game levels: + power, observer, omniscient and master. """ __slots__ = ['game', 'power_name', '__action_level'] def __init__(self, game, action_level, power_name): """ Initialize a game request level. + :param game: related game data :param action_level: action level, either: + - 'power' - 'observer' - 'omniscient' - 'master' + :param power_name: (optional) power name specified in game request. Required if level is 'power'. :type game: diplomacy.server.server_game.ServerGame :type action_level: str @@ -99,21 +111,29 @@ class GameRequestLevel(): return cls(game, 'master', power_name) def verify_request(server, request, connection_handler, - omniscient_role=True, observer_role=True, power_role=True, require_power=False, require_master=True): + omniscient_role=True, observer_role=True, power_role=True, + require_power=False, require_master=True): """ Verify request token, and game role and rights if request is a game request. Ignore connection requests (e.g. SignIn), as such requests don't have any token. Verifying token: + - check if server knows request token - check if request token is still valid. - Update token lifetime. See method Server.assert_token() for more details. + Verifying game role and rights: + - check if server knows request game ID. - check if request token is allowed to have request game role in associated game ID. + If request is a game request, return a GameRequestLevel containing: + - the server game object - the level of rights (power, observer or master) allowed for request sender. - the power name associated to request (if present), representing which power is queried by given request. + See class GameRequestLevel for more details. + :param server: server which receives the request :param request: request received by server :param connection_handler: connection handler which receives the request @@ -121,7 +141,7 @@ def verify_request(server, request, connection_handler, :param observer_role: (for game requests) Indicate if observer role is accepted for this request. :param power_role: (for game requests) Indicate if power role is accepted for this request. :param require_power: (for game requests) Indicate if a power name is required for this request. - If true, either game role must be power role, or request must have a non-null `power_name` request. + If true, either game role must be power role, or request must have a non-null `power_name` role. :param require_master: (for game requests) Indicate if an omniscient must be a master. If true and if request role is omniscient, then request token must be a master token for related game. :return: a GameRequestLevel object for game requests, else None. @@ -134,8 +154,8 @@ def verify_request(server, request, connection_handler, # A request may be a connection request, a channel request or a game request. # For connection request, field level is None. # For channel request, field level is CHANNEL. Channel request has a `token` field. - # For game request, field level is GAME. Game request is a channel request with supplementary fields - # `game_role` and `game_id`. + # For game request, field level is GAME. + # Game request is a channel request with supplementary fields `game_role` and `game_id`. # No permissions to check for connection requests (e.g. SignIn). if not request.level: @@ -222,14 +242,16 @@ def verify_request(server, request, connection_handler, return level def transfer_special_tokens(server_game, server, username, grade_update, from_observation=True): - """ Transfer tokens of given username from an observation role to the opposite in given server game, - and notify all user tokens about observation role update with given grade update. + """ Transfer tokens of given username from an observation role to the opposite in given + server game, and notify all user tokens about observation role update with given grade update. This method is used in request manager on_set_grade(). + :param server_game: server game in which tokens roles must be changed. :param server: server from which notifications will be sent. :param username: name of user whom tokens will be transferred. Only user tokens registered in server games as observer tokens or omniscient tokens will be updated. - :param grade_update: type of upgrading. Possibles values in strings.ALL_GRADE_UPDATES (PROMOTE or DEMOTE). + :param grade_update: type of upgrading. + Possibles values in strings.ALL_GRADE_UPDATES (PROMOTE or DEMOTE). :param from_observation: indicate transfer direction. If True, we expect to transfer role from observer to omniscient. If False, we expect to transfer role from omniscient to observer. @@ -245,7 +267,8 @@ def transfer_special_tokens(server_game, server, username, grade_update, from_ob new_role = strings.OBSERVER_TYPE token_filter = server_game.has_omniscient_token - connected_user_tokens = [user_token for user_token in server.users.get_tokens(username) if token_filter(user_token)] + connected_user_tokens = [user_token for user_token in server.users.get_tokens(username) + if token_filter(user_token)] if connected_user_tokens: @@ -256,10 +279,11 @@ def transfer_special_tokens(server_game, server, username, grade_update, from_ob addresses = [(old_role, user_token) for user_token in connected_user_tokens] Notifier(server).notify_game_addresses( server_game.game_id, addresses, notifications.OmniscientUpdated, - grade_update=grade_update, game=server_game.cast(new_role, username, server.users.has_admin(username))) + grade_update=grade_update, game=server_game.cast(new_role, username)) def assert_game_not_finished(server_game): """ Check if given game is not yet completed or canceled, otherwise raise a GameFinishedException. + :param server_game: server game to check :type server_game: diplomacy.server.server_game.ServerGame """ diff --git a/diplomacy/server/request_managers.py b/diplomacy/server/request_managers.py index 259147a..073a7ef 100644 --- a/diplomacy/server/request_managers.py +++ b/diplomacy/server/request_managers.py @@ -50,6 +50,7 @@ SERVER_GAME_RULES = ['NO_PRESS', 'IGNORE_ERRORS', 'POWER_CHOICE'] def on_clear_centers(server, request, connection_handler): """ Manage request ClearCenters. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -64,6 +65,7 @@ def on_clear_centers(server, request, connection_handler): def on_clear_orders(server, request, connection_handler): """ Manage request ClearOrders. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -81,6 +83,7 @@ def on_clear_orders(server, request, connection_handler): def on_clear_units(server, request, connection_handler): """ Manage request ClearUnits. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -95,6 +98,7 @@ def on_clear_units(server, request, connection_handler): def on_create_game(server, request, connection_handler): """ Manage request CreateGame. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -165,6 +169,7 @@ def on_create_game(server, request, connection_handler): def on_delete_account(server, request, connection_handler): """ Manage request DeleteAccount. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -212,6 +217,7 @@ def on_delete_account(server, request, connection_handler): def on_delete_game(server, request, connection_handler): """ Manage request DeleteGame. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -223,21 +229,9 @@ def on_delete_game(server, request, connection_handler): server.unschedule_game(level.game) Notifier(server, ignore_tokens=[request.token]).notify_game_deleted(level.game) -def on_get_dummy_waiting_powers(server, request, connection_handler): - """ Manage request GetAllDummyPowerNames. - :param server: server which receives the request. - :param request: request to manage. - :param connection_handler: connection handler from which the request was sent. - :return: an instance of responses.DataGamesToPowerNames - :type server: diplomacy.Server - :type request: diplomacy.communication.requests.GetDummyWaitingPowers - """ - verify_request(server, request, connection_handler) - return responses.DataGamesToPowerNames( - data=server.get_dummy_waiting_power_names(request.buffer_size, request.token), request_id=request.request_id) - def on_get_all_possible_orders(server, request, connection_handler): """ Manage request GetAllPossibleOrders + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -251,6 +245,7 @@ def on_get_all_possible_orders(server, request, connection_handler): def on_get_available_maps(server, request, connection_handler): """ Manage request GetAvailableMaps. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -263,6 +258,7 @@ def on_get_available_maps(server, request, connection_handler): def on_get_daide_port(server, request, connection_handler): """ Manage request GetDaidePort. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -277,21 +273,59 @@ def on_get_daide_port(server, request, connection_handler): "Invalid game id %s or game's DAIDE server is not started for that game" % request.game_id) return responses.DataPort(data=daide_port, request_id=request.request_id) -def on_get_playable_powers(server, request, connection_handler): - """ Manage request GetPlayablePowers. +def on_get_dummy_waiting_powers(server, request, connection_handler): + """ Manage request GetAllDummyPowerNames. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. - :return: None + :return: an instance of responses.DataGamesToPowerNames :type server: diplomacy.Server - :type request: diplomacy.communication.requests.GetPlayablePowers + :type request: diplomacy.communication.requests.GetDummyWaitingPowers """ verify_request(server, request, connection_handler) - return responses.DataPowerNames( - data=server.get_game(request.game_id).get_dummy_power_names(), request_id=request.request_id) + return responses.DataGamesToPowerNames( + data=server.get_dummy_waiting_power_names(request.buffer_size, request.token), request_id=request.request_id) + +def on_get_games_info(server, request, connection_handler): + """ Manage request GetGamesInfo. + + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: an instance of responses.DataGames + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetGamesInfo + """ + verify_request(server, request, connection_handler) + username = server.users.get_name(request.token) + games = [] + for game_id in request.games: + try: + server_game = server.load_game(game_id) + games.append(responses.DataGameInfo( + game_id=server_game.game_id, + phase=server_game.current_short_phase, + timestamp=server_game.get_latest_timestamp(), + timestamp_created=server_game.timestamp_created, + map_name=server_game.map_name, + observer_level=server_game.get_observer_level(username), + controlled_powers=server_game.get_controlled_power_names(username), + rules=server_game.rules, + status=server_game.status, + n_players=server_game.count_controlled_powers(), + n_controls=server_game.get_expected_controls_count(), + deadline=server_game.deadline, + registration_password=bool(server_game.registration_password) + )) + except exceptions.GameIdException: + # Invalid game ID, just pass. + pass + return responses.DataGames(data=games, request_id=request.request_id) def on_get_phase_history(server, request, connection_handler): """ Manage request GetPhaseHistory. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -304,8 +338,23 @@ def on_get_phase_history(server, request, connection_handler): game_phases = level.game.get_phase_history(request.from_phase, request.to_phase, request.game_role) return responses.DataGamePhases(data=game_phases, request_id=request.request_id) +def on_get_playable_powers(server, request, connection_handler): + """ Manage request GetPlayablePowers. + + :param server: server which receives the request. + :param request: request to manage. + :param connection_handler: connection handler from which the request was sent. + :return: None + :type server: diplomacy.Server + :type request: diplomacy.communication.requests.GetPlayablePowers + """ + verify_request(server, request, connection_handler) + return responses.DataPowerNames( + data=server.get_game(request.game_id).get_dummy_power_names(), request_id=request.request_id) + def on_join_game(server, request, connection_handler): """ Manage request JoinGame. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -479,6 +528,7 @@ def on_join_powers(server, request, connection_handler): """ Manage request JoinPowers. Current code does not care about rule POWER_CHOICE. It only checks if queried powers can be joined by request sender. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -588,6 +638,7 @@ def on_leave_game(server, request, connection_handler): """ Manage request LeaveGame. If user is an (omniscient) observer, stop observation. Else, stop to control given power name. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -606,6 +657,7 @@ def on_leave_game(server, request, connection_handler): def on_list_games(server, request, connection_handler): """ Manage request ListGames. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -618,7 +670,8 @@ def on_list_games(server, request, connection_handler): raise exceptions.MapIdException() selected_game_indices = [] for game_id in server.get_game_indices(): - if request.game_id and request.game_id not in game_id: + if request.game_id and not (game_id.lower() in request.game_id.lower() + or request.game_id.lower() in game_id.lower()): continue server_game = server.load_game(game_id) if request.for_omniscience and not server.token_is_omniscient(request.token, server_game): @@ -647,43 +700,9 @@ def on_list_games(server, request, connection_handler): )) return responses.DataGames(data=selected_game_indices, request_id=request.request_id) -def on_get_games_info(server, request, connection_handler): - """ Manage request GetGamesInfo. - :param server: server which receives the request. - :param request: request to manage. - :param connection_handler: connection handler from which the request was sent. - :return: an instance of responses.DataGames - :type server: diplomacy.Server - :type request: diplomacy.communication.requests.GetGamesInfo - """ - verify_request(server, request, connection_handler) - username = server.users.get_name(request.token) - games = [] - for game_id in request.games: - try: - server_game = server.load_game(game_id) - games.append(responses.DataGameInfo( - game_id=server_game.game_id, - phase=server_game.current_short_phase, - timestamp=server_game.get_latest_timestamp(), - timestamp_created=server_game.timestamp_created, - map_name=server_game.map_name, - observer_level=server_game.get_observer_level(username), - controlled_powers=server_game.get_controlled_power_names(username), - rules=server_game.rules, - status=server_game.status, - n_players=server_game.count_controlled_powers(), - n_controls=server_game.get_expected_controls_count(), - deadline=server_game.deadline, - registration_password=bool(server_game.registration_password) - )) - except exceptions.GameIdException: - # Invalid game ID, just pass. - pass - return responses.DataGames(data=games, request_id=request.request_id) - def on_logout(server, request, connection_handler): """ Manage request Logout. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -695,8 +714,8 @@ def on_logout(server, request, connection_handler): server.remove_token(request.token) def on_process_game(server, request, connection_handler): - """ Manage request ProcessGame. - Force a game to be processed the sooner. + """ Manage request ProcessGame. Force a game to be processed the sooner. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -719,7 +738,7 @@ def on_process_game(server, request, connection_handler): @gen.coroutine def on_query_schedule(server, request, connection_handler): """ Manage request QuerySchedule. - Force a game to be processed the sooner. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -740,6 +759,7 @@ def on_query_schedule(server, request, connection_handler): def on_save_game(server, request, connection_handler): """ Manage request SaveGame + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -752,6 +772,7 @@ def on_save_game(server, request, connection_handler): def on_send_game_message(server, request, connection_handler): """ Manage request SendGameMessage. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -806,6 +827,7 @@ def on_send_game_message(server, request, connection_handler): def on_set_dummy_powers(server, request, connection_handler): """ Manage request SetDummyPowers. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -834,6 +856,7 @@ def on_set_dummy_powers(server, request, connection_handler): def on_set_game_state(server, request, connection_handler): """ Manage request SetGameState. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -850,6 +873,7 @@ def on_set_game_state(server, request, connection_handler): def on_set_game_status(server, request, connection_handler): """ Manage request SetGameStatus. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -884,6 +908,7 @@ def on_set_game_status(server, request, connection_handler): def on_set_grade(server, request, connection_handler): """ Manage request SetGrade. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -979,11 +1004,11 @@ def on_set_grade(server, request, connection_handler): # Check if user omniscient rights was changed. user_is_omniscient_after = server.user_is_omniscient(username, server_game) if user_is_omniscient_before != user_is_omniscient_after: - transfer_special_tokens(server_game, server, username, grade_update, user_is_omniscient_after) def on_set_orders(server, request, connection_handler): """ Manage request SetOrders. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -1014,6 +1039,7 @@ def on_set_orders(server, request, connection_handler): def on_set_wait_flag(server, request, connection_handler): """ Manage request SetWaitFlag. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -1033,6 +1059,7 @@ def on_set_wait_flag(server, request, connection_handler): def on_sign_in(server, request, connection_handler): """ Manage request SignIn. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -1061,6 +1088,7 @@ def on_sign_in(server, request, connection_handler): def on_synchronize(server, request, connection_handler): """ Manage request Synchronize. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -1128,6 +1156,7 @@ def on_synchronize(server, request, connection_handler): def on_unknown_token(server, request, connection_handler): """ Manage notification request UnknownToken. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -1143,6 +1172,7 @@ def on_unknown_token(server, request, connection_handler): def on_vote(server, request, connection_handler): """ Manage request Vote. + :param server: server which receives the request. :param request: request to manage. :param connection_handler: connection handler from which the request was sent. @@ -1176,17 +1206,17 @@ MAPPING = { requests.CreateGame: on_create_game, requests.DeleteAccount: on_delete_account, requests.DeleteGame: on_delete_game, - requests.GetDummyWaitingPowers: on_get_dummy_waiting_powers, requests.GetAllPossibleOrders: on_get_all_possible_orders, requests.GetAvailableMaps: on_get_available_maps, requests.GetDaidePort: on_get_daide_port, - requests.GetPlayablePowers: on_get_playable_powers, + requests.GetDummyWaitingPowers: on_get_dummy_waiting_powers, + requests.GetGamesInfo: on_get_games_info, requests.GetPhaseHistory: on_get_phase_history, + requests.GetPlayablePowers: on_get_playable_powers, requests.JoinGame: on_join_game, requests.JoinPowers: on_join_powers, requests.LeaveGame: on_leave_game, requests.ListGames: on_list_games, - requests.GetGamesInfo: on_get_games_info, requests.Logout: on_logout, requests.ProcessGame: on_process_game, requests.QuerySchedule: on_query_schedule, @@ -1206,6 +1236,7 @@ MAPPING = { def handle_request(server, request, connection_handler): """ (coroutine) Find request handler function for associated request, run it and return its result. + :param server: a Server object to pass to handler function. :param request: a request object to pass to handler function. See diplomacy.communication.requests for possible requests. diff --git a/diplomacy/server/run.py b/diplomacy/server/run.py index f47ed4f..8f28e3d 100755 --- a/diplomacy/server/run.py +++ b/diplomacy/server/run.py @@ -16,20 +16,29 @@ # with this program. If not, see <https://www.gnu.org/licenses/>. # ============================================================================== """ Small module script to quickly start a server with pretty log-printing. + You can stop the server with keyboard interruption (Ctrl+C). Usage: - python -m diplomacy.server.run # run on port 8432. - python -m diplomacy.server.run --port=<given port> # run on given port. + + .. code-block:: bash + + # run on port 8432. + python -m diplomacy.server.run + + # run on given port. + python -m diplomacy.server.run --port=<given port> + """ import argparse from diplomacy import Server from diplomacy.utils import constants -PARSER = argparse.ArgumentParser(description='Run server.') -PARSER.add_argument('--port', '-p', type=int, default=constants.DEFAULT_PORT, - help='run on the given port (default: %s)' % constants.DEFAULT_PORT) -ARGS = PARSER.parse_args() +if __name__ == '__main__': + PARSER = argparse.ArgumentParser(description='Run server.') + PARSER.add_argument('--port', '-p', type=int, default=constants.DEFAULT_PORT, + help='run on the given port (default: %s)' % constants.DEFAULT_PORT) + ARGS = PARSER.parse_args() -try: - Server().start(port=ARGS.port) -except KeyboardInterrupt: - print('Keyboard interruption.') + try: + Server().start(port=ARGS.port) + except KeyboardInterrupt: + print('Keyboard interruption.') diff --git a/diplomacy/server/scheduler.py b/diplomacy/server/scheduler.py index 28bee74..ce34252 100644 --- a/diplomacy/server/scheduler.py +++ b/diplomacy/server/scheduler.py @@ -23,7 +23,7 @@ To set unit as a minute, create Scheduler with unit_in_seconds = 60. In such case, a task with deadline 2 means 2 minutes to wait to process this task. - TO set unit as a second, create Scheduler with unit_in_seconds = 1. + To set unit as a second, create Scheduler with unit_in_seconds = 1. In such case, a task with deadline 2 means 2 seconds to wait to process this task. """ from tornado import gen @@ -34,12 +34,13 @@ from diplomacy.utils.scheduler_event import SchedulerEvent from diplomacy.utils import exceptions from diplomacy.utils.priority_dict import PriorityDict -class _Deadline(): +class _Deadline: """ (internal) Deadline value, defined by a start time and a delay, such that deadline = start time + delay. """ __slots__ = ['start_time', 'delay'] def __init__(self, start_time, delay): """ Initialize a deadline with start time and delay, so that deadline = start time + delay. + :param start_time: (int) :param delay: (int) """ @@ -57,9 +58,10 @@ class _Deadline(): def __lt__(self, other): return self.deadline < other.deadline -class _Task(): - """ (internal) Task class used by scheduler to order scheduled data. It allows auto-rescheduling - of a task after it was processed, until either: +class _Task: + """ (internal) Task class used by scheduler to order scheduled data. + It allows auto-rescheduling of a task after it was processed, until either: + - task delay is 0. - task manager return a True boolean value (means "data fully processed"). - scheduler is explicitly required to remove associated data. @@ -68,6 +70,7 @@ class _Task(): def __init__(self, data, deadline): """ Initialize a task. + :param data: data to process. :param deadline: Deadline object. :type deadline: _Deadline @@ -94,6 +97,7 @@ class _ImmediateTask(_Task): def __init__(self, data, future_delay, processing_validator): """ Initialize an immediate task. + :param data: data to process. :param future_delay: delay to use to reschedule that task after first processing. :param processing_validator: either a Bool or a callable receiving the data and @@ -120,18 +124,19 @@ class _ImmediateTask(_Task): self.deadline.start_time = -new_delay self.deadline.delay = new_delay -class Scheduler(): +class Scheduler: """ (public) Scheduler class. """ __slots__ = ['unit', 'current_time', 'callback_process', 'data_in_queue', 'data_in_heap', 'tasks_queue', 'lock'] def __init__(self, unit_in_seconds, callback_process): """ Initialize a scheduler. + :param unit_in_seconds: number of seconds to wait for each step. :param callback_process: callback to call on every task. - Signature: - task_callback(task.data) -> bool - If callback return True, task is considered done and is removed from scheduler. - Otherwise, task is rescheduled for another delay. + + - Signature: ``task_callback(task.data) -> bool`` + - If callback return True, task is considered done and is removed from scheduler. + - Otherwise, task is rescheduled for another delay. """ assert isinstance(unit_in_seconds, int) and unit_in_seconds > 0 assert callable(callback_process) @@ -175,6 +180,7 @@ class Scheduler(): @gen.coroutine def add_data(self, data, nb_units_to_wait): """ Add data with a non-null deadline. For null deadlines, use no_wait(). + :param data: data to add :param nb_units_to_wait: time to wait (in number of units) """ @@ -189,6 +195,7 @@ class Scheduler(): @gen.coroutine def no_wait(self, data, nb_units_to_wait, processing_validator): """ Add a data to be processed the sooner. + :param data: data to add :param nb_units_to_wait: time to wait (in number of units) for data tasks after first task is executed. If null (0), data is processed once (first time) and then dropped. @@ -244,9 +251,11 @@ class Scheduler(): def process_tasks(self): """ Main task processing method (callback to register in ioloop). Consume and process tasks in queue and reschedule processed tasks when relevant. + A task is processed if associated data was not removed from scheduler. - A task is rescheduler if processing callback returns False (True meaning `task definitively done`) - AND if task deadline is not null. + + A task is rescheduled if processing callback returns False + (True means `task definitively done`) AND if task deadline is not null. """ while True: task = yield self.tasks_queue.get() # type: _Task diff --git a/diplomacy/server/server.py b/diplomacy/server/server.py index e0d0dee..c0c46e6 100644 --- a/diplomacy/server/server.py +++ b/diplomacy/server/server.py @@ -14,14 +14,20 @@ # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see <https://www.gnu.org/licenses/>. # ============================================================================== -""" Concret standalone server object. Manages and save server data and games on disk, send notifications, - receives requests and send responses. +""" Concrete standalone server object. Manages and save server data and games on disk, + send notifications, receives requests and send responses. Example: + + .. code-block:: python + >>> from diplomacy import Server >>> Server().start(port=1234) # If port is not given, a random port will be selected. You can interrupt server by sending a keyboard interrupt signal (Ctrl+C). + + .. code-block:: python + >>> from diplomacy import Server >>> try: >>> Server().start() @@ -29,21 +35,24 @@ >>> print('Server interrupted.') You can also configure some server attributes when instantiating it: + + .. code-block:: python + >>> from diplomacy import Server >>> server = Server(backup_delay_seconds=5) >>> server.start() These are public configurable server attributes. They are saved on disk at each server backup: - - allow_user_registrations: (bool) indicate if server accepts users registrations - (default True) - - backup_delay_seconds: (int) number of seconds to wait between two consecutive full server backup on disk - (default 10 minutes) - - ping_seconds: (int) ping period used by server to check is connected sockets are alive. - - max_games: (int) maximum number of games server accepts to create. If there are at least such number of games on - server, server will not accept further game creation requests. If 0, no limit. - (default 0) - - remove_canceled_games: (bool) indicate if games must be deleted from server database when they are canceled - (default False) + + - **allow_user_registrations**: (bool) indicate if server accepts users registrations (default True) + - **backup_delay_seconds**: (int) number of seconds to wait between two consecutive full server backup + on disk (default 10 minutes) + - **ping_seconds**: (int) ping period used by server to check is connected sockets are alive. + - **max_games**: (int) maximum number of games server accepts to create. + If there are at least such number of games on server, server will not accept + further game creation requests. If 0, no limit. (default 0) + - **remove_canceled_games**: (bool) indicate if games must be deleted from server database + when they are canceled (default False) """ import atexit @@ -80,6 +89,7 @@ LOGGER = logging.getLogger(__name__) def is_port_opened(port, hostname='127.0.0.1'): """ Checks if the specified port is opened + :param port: The port to check :param hostname: The hostname to check, defaults to '127.0.0.1' """ @@ -107,8 +117,12 @@ def save_json_on_disk(filename, json_dict): def load_json_from_disk(filename): """ Return a JSON dictionary loaded from given filename. - If JSON parsing fail for given filename, try to load JSON dictionary for a backup file (if present) - and rename backup file to given filename (backup file becomes current file versions). + If JSON parsing fail for given filename, try to load JSON dictionary for a backup file + (if present) and rename backup file to given filename + (backup file becomes current file versions). + + :param filename: file path to open + :return: JSON dictionary loaded from file :rtype: dict """ try: @@ -140,6 +154,7 @@ class InterruptionHandler(): def __init__(self, server): """ Initializer the handler. + :param server: server to save """ self.server = server # type: Server @@ -147,6 +162,7 @@ class InterruptionHandler(): def handler(self, signum, frame): """ Handler function. + :param signum: system signal received :param frame: frame received """ @@ -156,12 +172,15 @@ class InterruptionHandler(): if self.previous_handler: self.previous_handler(signum, frame) -class _ServerBackend(): - """ Class representing tornado objects used to run a server. Properties: - - port: (integer) port where server runs. - - application: tornado web Application object. - - http_server: tornado HTTP server object running server code. - - io_loop: tornado IO loop where server runs. +class _ServerBackend: + """ Class representing tornado objects used to run a server. + + Properties: + + - **port**: (integer) port where server runs. + - **application**: tornado web Application object. + - **http_server**: tornado HTTP server object running server code. + - **io_loop**: tornado IO loop where server runs. """ #pylint: disable=too-few-public-methods __slots__ = ['port', 'application', 'http_server', 'io_loop'] @@ -174,7 +193,7 @@ class _ServerBackend(): self.http_server = None self.io_loop = None -class Server(): +class Server: """ Server class. """ __slots__ = ['data_path', 'games_path', 'available_maps', 'maps_mtime', 'notifications', 'games_scheduler', 'allow_registrations', 'max_games', 'remove_canceled_games', 'users', 'games', @@ -195,11 +214,12 @@ class Server(): def __init__(self, server_dir=None, **kwargs): """ Initialize the server. + Server data is stored in folder ``<working directory>/data``. + :param server_dir: path of folder in (from) which server data will be saved (loaded). If None, working directory (where script is executed) will be used. :param kwargs: (optional) values for some public configurable server attributes. Given values will overwrite values saved on disk. - Server data is stored in folder `<working directory>/data`. """ # File paths and attributes related to database. @@ -336,7 +356,9 @@ class Server(): def _backup_server_data_now(self, force=False): """ Save latest backed-up version of server data on disk. This does not save games. - :param force: if True, force to save current server data even if it was not modified recently. + + :param force: if True, force to save current server data, + even if it was not modified recently. """ if force: self.save_data() @@ -347,6 +369,7 @@ class Server(): def _backup_games_now(self, force=False): """ Save latest backed-up versions of loaded games on disk. + :param force: if True, force to save all games currently loaded in memory even if they were not modified recently. """ @@ -362,7 +385,9 @@ class Server(): def backup_now(self, force=False): """ Save backup of server data and loaded games immediately. - :param force: if True, force to save server data and all loaded games even if there are no recent changes. + + :param force: if True, force to save server data and all loaded games + even if there are no recent changes. """ self._backup_server_data_now(force=force) self._backup_games_now(force=force) @@ -370,6 +395,7 @@ class Server(): @gen.coroutine def _process_game(self, server_game): """ Process given game and send relevant notifications. + :param server_game: server game to process :return: A boolean indicating if we must stop game. :type server_game: ServerGame @@ -437,7 +463,9 @@ class Server(): self.notifications.task_done() def set_tasks(self, io_loop: IOLoop): - """ Set server callbacks on given IO loop. Must be called once per server before starting IO loop. """ + """ Set server callbacks on given IO loop. + Must be called once per server before starting IO loop. + """ io_loop.add_callback(self._task_save_database) io_loop.add_callback(self._task_send_notifications) # These both coroutines are used to manage games. @@ -449,8 +477,9 @@ class Server(): def start(self, port=None, io_loop=None): """ Start server if not yet started. Raise an exception if server is already started. - :param port: (optional) port where server must run. If not provided, try to start on a random - selected port. Use property `port` to get current server port. + + :param port: (optional) port where server must run. If not provided, + try to start on a random selected port. Use property `port` to get current server port. :param io_loop: (optional) tornado IO lopp where server must run. If not provided, get default IO loop instance (tornado.ioloop.IOLoop.instance()). """ @@ -517,6 +546,7 @@ class Server(): def save_game(self, server_game): """ Update on-memory version of given server game. + :param server_game: server game :type server_game: ServerGame """ @@ -525,8 +555,8 @@ class Server(): self.register_dummy_power_names(server_game) def register_dummy_power_names(self, server_game): - """ Update internal registry of dummy power names waiting for orders - for given server games. + """ Update internal registry of dummy power names waiting for orders for given server games. + :param server_game: server game to check :type server_game: ServerGame """ @@ -551,6 +581,7 @@ class Server(): def get_dummy_waiting_power_names(self, buffer_size, bot_token): """ Return names of dummy powers waiting for orders for current loaded games. This query is allowed only for bot tokens. + :param buffer_size: maximum number of powers queried. :param bot_token: bot token :return: a dictionary mapping each game ID to a list of power names. @@ -592,11 +623,16 @@ class Server(): def load_game(self, game_id): """ Return a game matching given game ID from server database. Raise an exception if such game does not exists. + If such game is already stored in server object, return it. - Else, load it from disk but ** does not store it in server object **. + + Else, load it from disk but **does not store it in server object**. + To load and immediately store a game object in server object, please use method get_game(). + Method load_game() is convenient when you want to iterate over all games in server database without taking memory space. + :param game_id: ID of game to load. :return: a ServerGame object :rtype: ServerGame @@ -620,10 +656,10 @@ class Server(): # This should be an internal server error. raise exc - # def add_new_game(self, server_game): """ Add a new game data on server in memory and perform any addition processing. This does not save the game on disk. + :type server_game: ServerGame """ # Register game on memory. @@ -631,11 +667,12 @@ class Server(): # Start DAIDE server for this game. self.start_new_daide_server(server_game.game_id) - # def get_game(self, game_id): - """ Return game saved on server matching given game ID. Raise an exception if game ID not found. + """ Return game saved on server matching given game ID. + Raise an exception if game ID not found. Return game if already loaded on memory, else load it from disk, store it, perform any loading/addition processing and return it. + :param game_id: ID of game to load. :return: a ServerGame object. :rtype: ServerGame @@ -662,9 +699,10 @@ class Server(): self.schedule_game(server_game) return server_game - # def delete_game(self, server_game): - """ Delete given game from server (both from memory and disk) and perform any post-deletion processing. + """ Delete given game from server (both from memory and disk) + and perform any post-deletion processing. + :param server_game: game to delete :type server_game: ServerGame """ @@ -687,6 +725,7 @@ class Server(): def schedule_game(self, server_game): """ Add a game to scheduler only if game has a deadline and is not already scheduled. To add games without deadline, use force_game_processing(). + :param server_game: game :type server_game: ServerGame """ @@ -696,6 +735,7 @@ class Server(): @gen.coroutine def unschedule_game(self, server_game): """ Remove a game from scheduler. + :param server_game: game :type server_game: ServerGame """ @@ -706,6 +746,7 @@ class Server(): def force_game_processing(self, server_game): """ Add a game to scheduler to be processed as soon as possible. Use this method instead of schedule_game() to explicitly add games with null deadline. + :param server_game: game :type server_game: ServerGame """ @@ -713,6 +754,7 @@ class Server(): def start_game(self, server_game): """ Start given server game. + :param server_game: server game :type server_game: ServerGame """ @@ -721,7 +763,9 @@ class Server(): Notifier(self).notify_game_status(server_game) def stop_game_if_needed(self, server_game): - """ Stop game if it has not required number of controlled powers. Notify game if status changed. + """ Stop game if it has not required number of controlled powers. + Notify game if status changed. + :param server_game: game to check :param server_game: game :type server_game: ServerGame @@ -734,6 +778,7 @@ class Server(): def user_is_master(self, username, server_game): """ Return True if given username is a game master for given game data. + :param username: username :param server_game: game data :return: a boolean @@ -744,34 +789,40 @@ class Server(): def user_is_omniscient(self, username, server_game): """ Return True if given username is omniscient for given game data. + :param username: username :param server_game: game data :return: a boolean :type server_game: ServerGame :rtype: bool """ - return self.users.has_admin(username) or server_game.is_moderator(username) or server_game.is_omniscient( - username) + return (self.users.has_admin(username) + or server_game.is_moderator(username) + or server_game.is_omniscient(username)) def token_is_master(self, token, server_game): """ Return True if given token is a master token for given game data. + :param token: token :param server_game: game data :return: a boolean :type server_game: ServerGame :rtype: bool """ - return self.users.has_token(token) and self.user_is_master(self.users.get_name(token), server_game) + return (self.users.has_token(token) + and self.user_is_master(self.users.get_name(token), server_game)) def token_is_omniscient(self, token, server_game): """ Return True if given token is omniscient for given game data. + :param token: token :param server_game: game data :return: a boolean :type server_game: ServerGame :rtype: bool """ - return self.users.has_token(token) and self.user_is_omniscient(self.users.get_name(token), server_game) + return (self.users.has_token(token) + and self.user_is_omniscient(self.users.get_name(token), server_game)) def create_game_id(self): """ Create and return a game ID not already used by a game in server database. """ @@ -781,9 +832,8 @@ class Server(): return game_id def remove_token(self, token): - """ Disconnect given token from related user and loaded games. - Stop related games if needed, e.g. if a game does not have anymore - expected number of controlled powers. + """ Disconnect given token from related user and loaded games. Stop related games if needed, + e.g. if a game does not have anymore expected number of controlled powers. """ self.users.disconnect_token(token) for server_game in self.games.values(): # type: ServerGame @@ -793,8 +843,9 @@ class Server(): self.save_data() def assert_token(self, token, connection_handler): - """ Check if given token is associated to an user, check if token is still valid, and link token to given - connection handler. If any step failed, raise an exception. + """ Check if given token is associated to an user, check if token is still valid, + and link token to given connection handler. If any step failed, raise an exception. + :param token: token to check :param connection_handler: connection handler associated to this token """ @@ -817,6 +868,7 @@ class Server(): def assert_master_token(self, token, server_game): """ Check if given token is a master token for given game data. Raise an exception on error. + :param token: token :param server_game: game data :type server_game: ServerGame @@ -834,6 +886,7 @@ class Server(): def start_new_daide_server(self, game_id, port=None): """ Start a new DAIDE TCP server to handle DAIDE clients connections + :param game_id: game id to pass to the DAIDE server :param port: the port to use. If None, an available random port will be used """ @@ -856,6 +909,7 @@ class Server(): def stop_daide_server(self, game_id): """ Stop one or all DAIDE TCP server + :param game_id: game id of the DAIDE server. If None, all servers will be stopped :type game_id: str """ @@ -867,6 +921,7 @@ class Server(): def get_daide_port(self, game_id): """ Get the DAIDE port opened for a specific game_id + :param game_id: game id of the DAIDE server. """ for port, server in self.daide_servers.items(): diff --git a/diplomacy/server/server_game.py b/diplomacy/server/server_game.py index 4e90152..7075565 100644 --- a/diplomacy/server/server_game.py +++ b/diplomacy/server/server_game.py @@ -22,15 +22,18 @@ from diplomacy.utils import exceptions, parsing, strings from diplomacy.utils.game_phase_data import GamePhaseData class ServerGame(Game): - """ ServerGame class. Properties: - - server: (optional) server (Server object) that handles this game. - - omniscient_usernames (only for server games): + """ ServerGame class. + + Properties: + + - **server**: (optional) server (Server object) that handles this game. + - **omniscient_usernames** (only for server games): set of usernames allowed to be omniscient observers for this game. - - moderator_usernames (only for server games): + - **moderator_usernames** (only for server games): set of usernames allowed to be moderators for this game. - - observer (only for server games): + - **observer** (only for server games): special Power object (diplomacy.Power) used to manage observer tokens. - - omniscient (only for server games): + - **omniscient** (only for server games): special Power object (diplomacy.Power) used to manage omniscient tokens. """ __slots__ = ['server', 'omniscient_usernames', 'moderator_usernames', 'observer', 'omniscient'] @@ -43,11 +46,11 @@ class ServerGame(Game): def __init__(self, server=None, **kwargs): # Reference to a Server instance. - self.server = server # type: diplomacy.Server - self.omniscient_usernames = None # type: set - self.moderator_usernames = None # type: set - self.observer = None # type: Power - self.omniscient = None # type: Power + self.server = server # type: diplomacy.Server + self.omniscient_usernames = None # type: set + self.moderator_usernames = None # type: set + self.observer = None # type: Power + self.omniscient = None # type: Power super(ServerGame, self).__init__(**kwargs) assert self.is_server_game() @@ -72,6 +75,7 @@ class ServerGame(Game): def filter_phase_data(self, phase_data, role, is_current): """ Return a filtered version of given phase data for given gam role. + :param phase_data: GamePhaseData object to filter. :param role: game role to filter phase data for. :param is_current: Boolean. Indicate if given phase data is for a current phase (True), or for a pase phase. @@ -108,10 +112,13 @@ class ServerGame(Game): results=phase_data.results) def game_can_start(self): - """ Return True if server game can start. A game can start if all followings conditions are satisfied: + """ Return True if server game can start. + A game can start if all followings conditions are satisfied: + - Game has not yet started. - Game can start automatically (no rule START_MASTER). - Game has expected number of controlled powers. + :return: a boolean :rtype: bool """ @@ -135,18 +142,24 @@ class ServerGame(Game): def new_system_message(self, recipient, body): """ Create a system message (immediately dated) to be sent by server and add it to message history. To be used only by server game. + :param recipient: recipient description (string). Either: + - a power name. - 'GLOBAL' (all game tokens) - 'OBSERVER' (all special tokens [observers and omniscient observers]) - 'OMNISCIENT' (all omniscient tokens only) + :param body: message body (string). :return: a new GameMessage object. :rtype: Message """ assert (recipient in {GLOBAL, OBSERVER, OMNISCIENT} or self.has_power(recipient)) - message = Message(phase=self.current_short_phase, sender=SYSTEM, recipient=recipient, message=body) + message = Message(phase=self.current_short_phase, + sender=SYSTEM, + recipient=recipient, + message=body) # Message timestamp will be generated when adding message. self.add_message(message) return message @@ -205,7 +218,9 @@ class ServerGame(Game): return game def cast(self, role, for_username): - """ Return a copy of this game for given role (either observer role, omniscient role or a power role). """ + """ Return a copy of this game for given role + (either observer role, omniscient role or a power role). + """ assert strings.role_is_special(role) or self.has_power(role) if role == strings.OBSERVER_TYPE: return self.as_observer_game(for_username) @@ -219,6 +234,7 @@ class ServerGame(Game): def get_observer_level(self, username): """ Return the highest observation level allowed for given username. + :param username: name of user to get observation right :return: either 'master_type', 'omniscient_type', 'observer_type' or None. """ @@ -242,7 +258,8 @@ class ServerGame(Game): def get_special_addresses(self): """ Generate addresses (couples [power name, token]) of - omniscient observers and simple observers of this game. """ + omniscient observers and simple observers of this game. + """ for power in (self.omniscient, self.observer): for token in power.tokens: yield (power.name, token) @@ -253,7 +270,9 @@ class ServerGame(Game): yield (self.observer.name, token) def get_omniscient_addresses(self): - """ Generate addresses (couples [power name, token]) of omniscient observers of this game. """ + """ Generate addresses (couples [power name, token]) + of omniscient observers of this game. + """ for token in self.omniscient.tokens: yield (self.omniscient.name, token) @@ -266,7 +285,9 @@ class ServerGame(Game): raise exceptions.DiplomacyException('Unknown special token in game %s' % self.game_id) def get_power_addresses(self, power_name): - """ Generate addresses (couples [power name, token]) of user controlling given power name. """ + """ Generate addresses (couples [power name, token]) + of user controlling given power name. + """ for token in self.get_power(power_name).tokens: yield (power_name, token) @@ -293,6 +314,7 @@ class ServerGame(Game): def power_has_token(self, power_name, token): """ Return True if given power has given player token. + :param power_name: name of power to check. :param token: token to look for. :return: a boolean @@ -316,7 +338,9 @@ class ServerGame(Game): self.observer.add_token(token) def transfer_special_token(self, token): - """ Move given token from a special case to another (observer -> omniscient or omniscient -> observer). """ + """ Move given token from a special case to another + (observer -> omniscient or omniscient -> observer). + """ if self.has_observer_token(token): self.remove_observer_token(token) self.add_omniscient_token(token) @@ -345,7 +369,9 @@ class ServerGame(Game): self.omniscient.remove_tokens([token]) def remove_special_token(self, special_name, token): - """ Remove given token from given special power name (either __OBSERVER__ or __OMNISCIENT__). """ + """ Remove given token from given special power name + (either __OBSERVER__ or __OMNISCIENT__). + """ if special_name == self.observer.name: self.remove_observer_token(token) else: @@ -393,14 +419,19 @@ class ServerGame(Game): self.omniscient_usernames.remove(username) def filter_usernames(self, filter_function): - """ Remove each omniscient username, moderator username and player controller that does not match given - filter function (if filter_function(username) is False). + """ Remove each omniscient username, moderator username and player controller + that does not match given filter function (if filter_function(username) is False). + :param filter_function: a callable receiving a username and returning a boolean. :return: an integer, either: + * 0: nothing changed. * -1: something changed, but no player controllers removed. * 1: something changed, and some player controllers were removed. - So, if 1 is returned, there are new dummy powers in the game (some notifications may need to be sent). + + So, if 1 is returned, there are new dummy powers in the game + (some notifications may need to be sent). + """ n_kicked_players = 0 n_kicked_omniscients = len(self.omniscient_usernames) @@ -420,7 +451,9 @@ class ServerGame(Game): return 0 def filter_tokens(self, filter_function): - """ Remove from this game any token not matching given filter function (if filter_function(token) is False).""" + """ Remove from this game any token not matching given filter function + (if filter_function(token) is False). + """ self.observer.remove_tokens([token for token in self.observer.tokens if not filter_function(token)]) self.omniscient.remove_tokens([token for token in self.omniscient.tokens if not filter_function(token)]) for power in self.powers.values(): # type: Power @@ -428,13 +461,18 @@ class ServerGame(Game): def process(self): """ Process current game phase and move forward to next phase. + :return: a triple containing: + - previous game state (before the processing) - current game state (after processing and game updates) - A dictionary mapping kicked power names to tokens previously associated to these powers. Useful to notify kicked users as they will be not registered in game anymore. + If game was not active, triple is (None, None, None). + If game kicked powers, only kicked powers dict is returned: (None, None, kicked powers). + If game was correctly processed, only states are returned: (prev, curr, None). """ if not self.is_game_active: diff --git a/diplomacy/server/users.py b/diplomacy/server/users.py index d63df3e..3996e02 100644 --- a/diplomacy/server/users.py +++ b/diplomacy/server/users.py @@ -37,14 +37,17 @@ LOGGER = logging.getLogger(__name__) TOKEN_LIFETIME_SECONDS = 24 * 60 * 60 class Users(Jsonable): - """ Users class. Properties: - - users: dictionary mapping usernames to User object.s - - administrators: set of administrator usernames. - - token_timestamp: dictionary mapping each token to its creation/last confirmation timestamp. - - token_to_username: dictionary mapping each token to its username. - - username_to_tokens: dictionary mapping each username to a set of its tokens. - - token_to_connection_handler: (memory only) dictionary mapping each token to a connection handler - - connection_handler_to_tokens (memory only) dictionary mapping a connection handler to a set of its tokens + """ Users class. + + Properties: + + - **users**: dictionary mapping usernames to User object.s + - **administrators**: set of administrator usernames. + - **token_timestamp**: dictionary mapping each token to its creation/last confirmation timestamp. + - **token_to_username**: dictionary mapping each token to its username. + - **username_to_tokens**: dictionary mapping each username to a set of its tokens. + - **token_to_connection_handler**: (memory only) dictionary mapping each token to a connection handler + - **connection_handler_to_tokens**: (memory only) dictionary mapping a connection handler to a set of its tokens """ __slots__ = ['users', 'administrators', 'token_timestamp', 'token_to_username', 'username_to_tokens', 'token_to_connection_handler', 'connection_handler_to_tokens'] @@ -170,6 +173,7 @@ class Users(Jsonable): """ Remove given connection handler. Return tokens associated to this connection handler, or None if connection handler is unknown. + :param connection_handler: connection handler to remove. :param remove_tokens: if True, tokens related to connection handler are deleted. :return: either None or a set of tokens. @@ -188,8 +192,9 @@ class Users(Jsonable): return None def connect_user(self, username, connection_handler): - """ Connect given username to given connection handler with a new generated token, and return - token generated. + """ Connect given username to given connection handler with a new generated token, + and return token generated. + :param username: username to connect :param connection_handler: connection handler to link to user :return: a new token generated for connexion @@ -209,9 +214,11 @@ class Users(Jsonable): def attach_connection_handler(self, token, connection_handler): """ Associate given token with given connection handler if token is known. - If there is a previous connection handler associated to given token, it should be the same - as given connection handler, otherwise an error is raised (meaning previous connection handler - was not correctly disconnected from given token. It should be a programming error). + If there is a previous connection handler associated to given token, it should be + the same as given connection handler, otherwise an error is raised + (meaning previous connection handler was not correctly disconnected from given token. + It should be a programming error). + :param token: token :param connection_handler: connection handler """ |