From abb42dcd4886705d6ba8af27f68ef605218ac67c Mon Sep 17 00:00:00 2001 From: Philip Paquette Date: Wed, 11 Sep 2019 12:58:45 -0400 Subject: Added ReadtheDocs documentation for the public API - Reformatted the docstring to be compatible - Added tests to make sure the documentation compiles properly - Added sphinx as a pip requirement Co-authored-by: Philip Paquette Co-authored-by: notoraptor --- diplomacy/client/channel.py | 134 +++-- diplomacy/client/connection.py | 668 ++++++++++++----------- diplomacy/client/game_instances_set.py | 3 + diplomacy/client/network_game.py | 156 +++--- diplomacy/client/notification_managers.py | 25 +- diplomacy/client/response_managers.py | 87 +-- diplomacy/communication/notifications.py | 93 +++- diplomacy/communication/requests.py | 506 +++++++++++++---- diplomacy/communication/responses.py | 145 +++-- diplomacy/daide/clauses.py | 38 +- diplomacy/daide/connection_handler.py | 11 +- diplomacy/daide/messages.py | 1 + diplomacy/daide/notification_managers.py | 8 +- diplomacy/daide/notifications.py | 91 ++- diplomacy/daide/request_managers.py | 22 + diplomacy/daide/requests.py | 102 +++- diplomacy/daide/responses.py | 206 +++++-- diplomacy/daide/server.py | 4 +- diplomacy/daide/tests/test_daide_game.py | 21 +- diplomacy/daide/tokens.py | 4 + diplomacy/daide/utils.py | 3 + diplomacy/engine/game.py | 616 +++++++++++++-------- diplomacy/engine/map.py | 229 ++++---- diplomacy/engine/message.py | 43 +- diplomacy/engine/power.py | 99 ++-- diplomacy/engine/renderer.py | 65 ++- diplomacy/integration/base_api.py | 33 +- diplomacy/integration/webdiplomacy_net/api.py | 25 +- diplomacy/integration/webdiplomacy_net/game.py | 32 +- diplomacy/integration/webdiplomacy_net/orders.py | 15 +- diplomacy/server/connection_handler.py | 13 +- diplomacy/server/notifier.py | 26 +- diplomacy/server/request_manager_utils.py | 60 +- diplomacy/server/request_managers.py | 155 +++--- diplomacy/server/run.py | 29 +- diplomacy/server/scheduler.py | 33 +- diplomacy/server/server.py | 145 +++-- diplomacy/server/server_game.py | 84 ++- diplomacy/server/users.py | 33 +- diplomacy/tests/network/run_real_game.py | 28 +- diplomacy/tests/network/test_real_game.py | 39 +- diplomacy/tests/test_datc.py | 2 +- diplomacy/utils/common.py | 100 ++-- diplomacy/utils/constants.py | 2 + diplomacy/utils/convoy_paths.py | 4 + diplomacy/utils/equilateral_triangle.py | 25 +- diplomacy/utils/errors.py | 3 + diplomacy/utils/export.py | 7 +- diplomacy/utils/jsonable.py | 44 +- diplomacy/utils/keywords.py | 1 + diplomacy/utils/network_data.py | 3 +- diplomacy/utils/order_results.py | 19 + diplomacy/utils/parsing.py | 34 +- diplomacy/utils/priority_dict.py | 11 +- diplomacy/utils/scheduler_event.py | 13 +- diplomacy/utils/sorted_dict.py | 38 +- diplomacy/utils/sorted_set.py | 24 +- diplomacy/utils/splitter.py | 3 + diplomacy/utils/strings.py | 6 +- diplomacy/utils/tests/test_jsonable_changes.py | 7 +- diplomacy/utils/tests/test_parsing.py | 2 +- diplomacy/utils/time.py | 3 + diplomacy/web/__init__.py | 19 + diplomacy/web/svg_to_react.py | 13 +- 64 files changed, 2999 insertions(+), 1514 deletions(-) create mode 100644 diplomacy/web/__init__.py (limited to 'diplomacy') diff --git a/diplomacy/client/channel.py b/diplomacy/client/channel.py index 0403e7f..146d5e3 100644 --- a/diplomacy/client/channel.py +++ b/diplomacy/client/channel.py @@ -15,6 +15,7 @@ # with this program. If not, see . # ============================================================================== """ Channel + - The channel object represents an authenticated connection over a socket. - It has a token that it sends with every request to authenticate itself. """ @@ -23,21 +24,26 @@ import logging from tornado import gen from diplomacy.communication import requests -from diplomacy.utils import strings +from diplomacy.utils import strings, common LOGGER = logging.getLogger(__name__) -def req_fn(request_class, local_req_fn=None, **request_args): +def _req_fn(request_class, local_req_fn=None, **request_args): """ Create channel request method that sends request with channel token. + :param request_class: class of request to send with channel request method. :param local_req_fn: (optional) Channel method to use locally to try retrieving a data instead of sending a request. If provided, local_req_fn is called with request args: + - if it returns anything else than None, then returned data is returned by channel request method. - else, request class is still sent and channel request method follows standard path (request sent, response received, response handler called and final handler result returned). + :param request_args: arguments to pass to request class to create the request object. :return: a Channel method. """ + str_params = (', '.join('%s=%s' % (key, common.to_string(value)) + for (key, value) in sorted(request_args.items()))) if request_args else '' @gen.coroutine def func(self, game_object=None, **kwargs): @@ -67,29 +73,51 @@ def req_fn(request_class, local_req_fn=None, **request_args): request = request_class(**kwargs) return (yield self.connection.send(request, game_object)) + func.__request_name__ = request_class.__name__ + func.__request_params__ = str_params + func.__doc__ = """ + Send request :class:`.%(request_name)s`%(with_params)s``kwargs``. + Return response data returned by server for this request. + See :class:`.%(request_name)s` about request parameters and response. + """ % {'request_name': request_class.__name__, + 'with_params': ' with forced parameters ``(%s)`` and additional request parameters ' + % str_params if request_args else ' with request parameters '} return func -class Channel(): +class Channel: """ Channel - Represents an authenticated connection over a physical socket """ + # pylint: disable=too-few-public-methods __slots__ = ['connection', 'token', 'game_id_to_instances', '__weakref__'] def __init__(self, connection, token): """ Initialize a channel. + + Properties: + + - **connection**: :class:`.Connection` object from which this channel originated. + - **token**: Channel token, used to identify channel on server. + - **game_id_to_instances**: Dictionary mapping a game ID to :class:`.NetworkGame` objects loaded for this + game. Each :class:`.NetworkGame` has a specific role, which is either an observer role, an omniscient + role, or a power (player) role. Network games for a specific game ID are managed within a + :class:`.GameInstancesSet`, which makes sure that there will be at most 1 :class:`.NetworkGame` instance + per possible role. + :param connection: a Connection object. :param token: Channel token. - :type connection: diplomacy.Connection + :type connection: diplomacy.client.connection.Connection + :type token: str """ self.connection = connection self.token = token self.game_id_to_instances = {} # {game id => GameInstances} - def local_join_game(self, **kwargs): + def _local_join_game(self, **kwargs): """ Look for a local game with given kwargs intended to be used to build a JoinGame request. Return None if no local game found, else local game found. - Game is identified with game ID and power name (optional). + Game is identified with game ID **(required)** and power name *(optional)*. If power name is None, we look for a "special" game (observer or omniscient game) - loaded locally. Note that there is at most 1 special game per channel + game ID: - either observer or omniscient, not both. + loaded locally. Note that there is at most 1 special game per (channel + game ID) + couple: either observer or omniscient, not both. """ game_id = kwargs[strings.GAME_ID] power_name = kwargs.get(strings.POWER_NAME, None) @@ -103,54 +131,54 @@ class Channel(): # Public channel API. # =================== - create_game = req_fn(requests.CreateGame) - get_available_maps = req_fn(requests.GetAvailableMaps) - get_playable_powers = req_fn(requests.GetPlayablePowers) - join_game = req_fn(requests.JoinGame, local_req_fn=local_join_game) - join_powers = req_fn(requests.JoinPowers) - list_games = req_fn(requests.ListGames) - get_games_info = req_fn(requests.GetGamesInfo) + create_game = _req_fn(requests.CreateGame) + get_available_maps = _req_fn(requests.GetAvailableMaps) + get_playable_powers = _req_fn(requests.GetPlayablePowers) + join_game = _req_fn(requests.JoinGame, local_req_fn=_local_join_game) + join_powers = _req_fn(requests.JoinPowers) + list_games = _req_fn(requests.ListGames) + get_games_info = _req_fn(requests.GetGamesInfo) + get_dummy_waiting_powers = _req_fn(requests.GetDummyWaitingPowers) # User Account API. - delete_account = req_fn(requests.DeleteAccount) - logout = req_fn(requests.Logout) + delete_account = _req_fn(requests.DeleteAccount) + logout = _req_fn(requests.Logout) # Admin / Moderator API. - make_omniscient = req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.PROMOTE) - remove_omniscient = req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.DEMOTE) - promote_administrator = req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.PROMOTE) - demote_administrator = req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.DEMOTE) - promote_moderator = req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.PROMOTE) - demote_moderator = req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.DEMOTE) - - # ================ - # Public game API. - # ================ - - get_dummy_waiting_powers = req_fn(requests.GetDummyWaitingPowers) - get_phase_history = req_fn(requests.GetPhaseHistory) - leave_game = req_fn(requests.LeaveGame) - send_game_message = req_fn(requests.SendGameMessage) - set_orders = req_fn(requests.SetOrders) - - clear_centers = req_fn(requests.ClearCenters) - clear_orders = req_fn(requests.ClearOrders) - clear_units = req_fn(requests.ClearUnits) - - wait = req_fn(requests.SetWaitFlag, wait=True) - no_wait = req_fn(requests.SetWaitFlag, wait=False) - vote = req_fn(requests.Vote) - save = req_fn(requests.SaveGame) - synchronize = req_fn(requests.Synchronize) + make_omniscient = _req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.PROMOTE) + remove_omniscient = _req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.DEMOTE) + promote_administrator = _req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.PROMOTE) + demote_administrator = _req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.DEMOTE) + promote_moderator = _req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.PROMOTE) + demote_moderator = _req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.DEMOTE) + + # ==================================================================== + # Game API. Intended to be called by NetworkGame object, not directly. + # ==================================================================== + + _get_phase_history = _req_fn(requests.GetPhaseHistory) + _leave_game = _req_fn(requests.LeaveGame) + _send_game_message = _req_fn(requests.SendGameMessage) + _set_orders = _req_fn(requests.SetOrders) + + _clear_centers = _req_fn(requests.ClearCenters) + _clear_orders = _req_fn(requests.ClearOrders) + _clear_units = _req_fn(requests.ClearUnits) + + _wait = _req_fn(requests.SetWaitFlag, wait=True) + _no_wait = _req_fn(requests.SetWaitFlag, wait=False) + _vote = _req_fn(requests.Vote) + _save = _req_fn(requests.SaveGame) + _synchronize = _req_fn(requests.Synchronize) # Admin / Moderator API. - delete_game = req_fn(requests.DeleteGame) - kick_powers = req_fn(requests.SetDummyPowers) - set_state = req_fn(requests.SetGameState) - process = req_fn(requests.ProcessGame) - query_schedule = req_fn(requests.QuerySchedule) - start = req_fn(requests.SetGameStatus, status=strings.ACTIVE) - pause = req_fn(requests.SetGameStatus, status=strings.PAUSED) - resume = req_fn(requests.SetGameStatus, status=strings.ACTIVE) - cancel = req_fn(requests.SetGameStatus, status=strings.CANCELED) - draw = req_fn(requests.SetGameStatus, status=strings.COMPLETED) + _delete_game = _req_fn(requests.DeleteGame) + _kick_powers = _req_fn(requests.SetDummyPowers) + _set_state = _req_fn(requests.SetGameState) + _process = _req_fn(requests.ProcessGame) + _query_schedule = _req_fn(requests.QuerySchedule) + _start = _req_fn(requests.SetGameStatus, status=strings.ACTIVE) + _pause = _req_fn(requests.SetGameStatus, status=strings.PAUSED) + _resume = _req_fn(requests.SetGameStatus, status=strings.ACTIVE) + _cancel = _req_fn(requests.SetGameStatus, status=strings.CANCELED) + _draw = _req_fn(requests.SetGameStatus, status=strings.COMPLETED) diff --git a/diplomacy/client/connection.py b/diplomacy/client/connection.py index d0d5902..f81d5b6 100644 --- a/diplomacy/client/connection.py +++ b/diplomacy/client/connection.py @@ -18,7 +18,7 @@ import logging import weakref from datetime import timedelta - +from typing import Dict from tornado import gen, ioloop from tornado.concurrent import Future from tornado.iostream import StreamClosedError @@ -34,245 +34,89 @@ from diplomacy.utils import exceptions, strings, constants LOGGER = logging.getLogger(__name__) -class MessageWrittenCallback(): - """ Helper class representing callback to call on a connection when a request is written in a websocket. """ - __slots__ = ['request_context'] - - def __init__(self, request_context): - """ Initialize the callback object. - :param request_context: a request context - :type request_context: RequestFutureContext - """ - self.request_context = request_context - - def callback(self, msg_future): - """ Called when request is effectively written on socket, and move the request - from `request to send` to `request assumed sent`. - """ - # Remove request context from `requests to send` in any case. - connection = self.request_context.connection # type: Connection - request_id = self.request_context.request_id - exception = msg_future.exception() - if exception is not None: - if isinstance(exception, (WebSocketClosedError, StreamClosedError)): - # Connection suddenly closed. - # Request context was stored in connection.requests_to_send - # and will be re-sent when reconnection succeeds. - # For more details, see method Connection.write_request(). - LOGGER.error('Connection was closed when sending a request. Silently waiting for a reconnection.') - else: - LOGGER.error('Fatal error occurred while writing a request.') - self.request_context.future.set_exception(exception) - else: - connection.requests_waiting_responses[request_id] = self.request_context - -class Reconnection(): - """ Class performing reconnection work for a given connection. - - Class properties: - ================= - - - connection: Connection object to reconnect. - - - games_phases: dictionary mapping each game address (game ID + game role) to server game info: - {game ID => {game role => responses.DataGamePhase}} - Server game info is a DataGamePhase response sent by server as response to a Synchronize request. - It contains 3 fields: game ID, current server game phase and current server game timestamp. - We currently use only game phase. - - - n_expected_games: number of games registered in games_phases. - - - n_synchronized_games: number of games already synchronized. - - Reconnection procedure: - ======================= - - - Mark all waiting responses as `re-sent` (may be useful on server-side) and - move them back to responses_to_send. - - - Remove all previous synchronization requests that are not yet sent. We will send new synchronization - requests with latest games timestamps. Future associated to removed requests will raise an exception. - - - Initialize games_phases associating None to each game object currently opened in connection. - - - Send synchronization request for each game object currently opened in connection. For each game: - - - server will send a response describing current server game phase (current phase and timestamp). This info - will be used to check local requests to send. Note that concrete synchronization is done via notifications. - Thus, when server responses is received, game synchronization may not be yet terminated, but at least - we will now current server game phase. - - - Server response is saved in games_phases (replacing None associated to game object). - - - n_synchronized_games is incremented. - - - When sync responses are received for all games registered in games_phases - (n_expected_games == n_synchronized_games), we can finalize reconnection: - - - Remove every phase-dependent game request not yet sent for which phase does not match - server game phase. Futures associated to removed request will raise an exception. - - - Finally send all remaining requests. - - These requests may be marked as re-sent. - For these requests, server is (currently) responsible for checking if they don't represent - a duplicated query. +@gen.coroutine +def connect(hostname, port): + """ Connect to given hostname and port. + :param hostname: a hostname + :param port: a port + :return: a Connection object connected. + :type hostname: str + :type port: int + :rtype: Connection """ + connection = Connection(hostname, port) + yield connection._connect('Trying to connect.') # pylint: disable=protected-access + return connection - __slots__ = ['connection', 'games_phases', 'n_expected_games', 'n_synchronized_games'] - - def __init__(self, connection): - """ Initialize reconnection data/ - :param connection: connection to reconnect. - :type connection: Connection - """ - self.connection = connection - self.games_phases = {} - self.n_expected_games = 0 - self.n_synchronized_games = 0 - - def reconnect(self): - """ Perform concrete reconnection work. """ +class Connection: + """ Connection class. + + The connection class should not be initiated directly, but through the connect method + + .. code-block:: python + + >>> from diplomacy.client.connection import connect + >>> connection = await connect(hostname, port) + + Properties: + + - **hostname**: :class:`str` hostname to connect (e.g. 'localhost') + - **port**: :class:`int` port to connect (e.g. 8888) + - **use_ssl**: :class:`bool` telling if connection should be securized (True) or not (False). + - **url**: (property) :class:`str` websocket url to connect (generated with hostname and port) + - **connection**: :class:`tornado.websocket.WebSocketClientConnection` a tornado websocket connection object + - **connection_count**: :class:`int` number of successful connections from this Connection object. + Used to check if message callbacks is already launched (if count > 0). + - **is_connecting**: :class:`tornado.locks.Event` a tornado Event used to keep connection status. + No request can be sent while is_connecting. + If connected, Synchronize requests can be sent immediately even if is_reconnecting. + Other requests must wait full reconnection. + - **is_reconnecting**: :class:`tornado.locks.Event` a tornado Event used to keep re-connection status. + Non-synchronize request cannot be sent while is_reconnecting. + If reconnected, all requests can be sent. + - **channels**: a :class:`weakref.WeakValueDictionary` mapping channel token to :class:`.Channel` object. + - **requests_to_send**: a :class:`Dict` mapping a request ID to the context of a request + **not sent**. If we are disconnected when trying to send a request, then request + context is added to this dictionary to be send later once reconnected. + - **requests_waiting_responses**: a :class:`Dict` mapping a request ID to the context of a + request **sent**. Contains requests that are waiting for a server response. + - **unknown_tokens**: :class:`set` a set of unknown tokens. We can safely ignore them, as the server has been + notified. + """ + __slots__ = ['hostname', 'port', 'use_ssl', 'connection', 'is_connecting', 'is_reconnecting', 'connection_count', + 'channels', 'requests_to_send', 'requests_waiting_responses', 'unknown_tokens'] - # Mark all waiting responses as `re-sent` and move them back to responses_to_send. - for waiting_context in self.connection.requests_waiting_responses.values(): # type: RequestFutureContext - waiting_context.request.re_sent = True - self.connection.requests_to_send.update(self.connection.requests_waiting_responses) - self.connection.requests_waiting_responses.clear() + def __init__(self, hostname, port, use_ssl=False): + """ Constructor - # Remove all previous synchronization requests. - requests_to_send_updated = {} - for context in self.connection.requests_to_send.values(): # type: RequestFutureContext - if isinstance(context.request, requests.Synchronize): - context.future.set_exception(exceptions.DiplomacyException( - 'Sync request invalidated for game ID %s.' % context.request.game_id)) - else: - requests_to_send_updated[context.request.request_id] = context - self.connection.requests_to_send = requests_to_send_updated + The connection class should not be initiated directly, but through the connect method - # Count games to synchronize. - for channel in self.connection.channels.values(): - for game_instance_set in channel.game_id_to_instances.values(): - for game in game_instance_set.get_games(): - self.games_phases.setdefault(game.game_id, {})[game.role] = None - self.n_expected_games += 1 + .. code-block:: python - if self.n_expected_games: - # Synchronize games. - for channel in self.connection.channels.values(): - for game_instance_set in channel.game_id_to_instances.values(): - for game in game_instance_set.get_games(): - game.synchronize().add_done_callback(self.generate_sync_callback(game)) - else: - # No game to sync, finish sync now. - self.sync_done() + >>> from diplomacy.client.connection import connect + >>> connection = await connect(hostname, port) - def generate_sync_callback(self, game): - """ Generate callback to call when response to sync request is received for given game. - :param game: game - :return: a callback. - :type game: diplomacy.client.network_game.NetworkGame + :param hostname: hostname to connect (e.g. 'localhost') + :param port: port to connect (e.g. 8888) + :param use_ssl: telling if connection should be securized (True) or not (False). + :type hostname: str + :type port: int + :type use_ssl: bool """ - - def on_sync(future): - """ Callback. If exception occurs, print it as logging error. Else, register server response, - and move forward to final reconnection work if all games received sync responses. - """ - exception = future.exception() - if exception is not None: - LOGGER.error(str(exception)) - else: - self.games_phases[game.game_id][game.role] = future.result() - self.n_synchronized_games += 1 - if self.n_synchronized_games == self.n_expected_games: - self.sync_done() - - return on_sync - - def sync_done(self): - """ Final reconnection work. Remove obsolete game requests and send remaining requests. """ - - # All sync requests sent have finished. - # Remove all obsolete game requests from connection. - # A game request is obsolete if it's phase-dependent and if its phase does not match current game phase. - - request_to_send_updated = {} - for context in self.connection.requests_to_send.values(): # type: RequestFutureContext - keep = True - if context.request.level == strings.GAME and context.request.phase_dependent: - request_phase = context.request.phase - server_phase = self.games_phases[context.request.game_id][context.request.game_role].phase - if request_phase != server_phase: - # Request is obsolete. - context.future.set_exception(exceptions.DiplomacyException( - 'Game %s: request %s: request phase %s does not match current server game phase %s.' - % (context.request.game_id, context.request.name, request_phase, server_phase))) - keep = False - if keep: - request_to_send_updated[context.request.request_id] = context - - LOGGER.debug('Keep %d/%d old requests to send.', - len(request_to_send_updated), len(self.connection.requests_to_send)) - - # All requests to send are stored in request_to_send_updated. - # Then we can empty connection.requests_to_send. - # If we fail to send a request, it will be re-added again. - self.connection.requests_to_send.clear() - - # Send requests. - for request_to_send in request_to_send_updated.values(): # type: RequestFutureContext - self.connection.write_request(request_to_send).add_done_callback( - MessageWrittenCallback(request_to_send).callback) - - # We are reconnected. - self.connection.is_reconnecting.set() - - LOGGER.info('Done reconnection work.') - -class Connection(): - """ Connection class. Properties: - - hostname: hostname to connect (e.g. 'localhost') - - port: port to connect (e.g. 8888) - - use_ssl: boolean telling if connection should be securized (True) or not (False). - - url (auto): websocket url to connect (generated with hostname and port) - - connection: a tornado websocket connection object - - connection_count: number of successful connections from this Connection object. - Used to check if message callbacks is already launched (if count > 0). - - connection_lock: a tornado lock used to access tornado websocket connection object - - is_connecting: a tornado Event used to keep connection status. - No request can be sent while is_connecting. - If connected, Synchronize requests can be sent immediately even if is_reconnecting. - Other requests must wait full reconnection. - - is_reconnecting: a tornado Event used to keep re-connection status. - Non-synchronize request cannot be sent while is_reconnecting. - If reconnected, all requests can be sent. - - channels: a WeakValueDictionary mapping channel token to Channel object. - - requests_to_send: a dictionary mapping a request ID to the context of a request - **not sent**. If we are disconnected when trying to send a request, then request - context is added to this dictionary to be send later once reconnected. - - requests_waiting_responses: a dictionary mapping a request ID to the context of a - request **sent**. Contains requests that are waiting for a server response. - - unknown_tokens: a set of unknown tokens. We can safely ignore them, as the server has been notified. - """ - __slots__ = ['hostname', 'port', 'use_ssl', 'connection', 'is_connecting', 'is_reconnecting', 'connection_count', - 'channels', 'requests_to_send', 'requests_waiting_responses', 'unknown_tokens'] - - def __init__(self, hostname, port, use_ssl=False): self.hostname = hostname self.port = port self.use_ssl = bool(use_ssl) self.connection = None + self.connection_count = 0 self.is_connecting = Event() self.is_reconnecting = Event() - self.connection_count = 0 - self.channels = weakref.WeakValueDictionary() # {token => Channel} - self.requests_to_send = {} # type: dict{str, RequestFutureContext} - self.requests_waiting_responses = {} # type: dict{str, RequestFutureContext} + self.requests_to_send = {} # type: Dict[str, RequestFutureContext] + self.requests_waiting_responses = {} # type: Dict[str, RequestFutureContext] self.unknown_tokens = set() # When connection is created, we are not yet connected, but reconnection does not matter @@ -281,12 +125,53 @@ class Connection(): url = property(lambda self: '%s://%s:%d' % ('wss' if self.use_ssl else 'ws', self.hostname, self.port)) + # =================== + # Public Methods. + # =================== + + @gen.coroutine + def authenticate(self, username, password, create_user=False): + """ Send a :class:`.SignIn` request. + + :param username: username + :param password: password + :param create_user: boolean indicating if you want to create a user or login to an existing user. + :return: a :class:`.Channel` object representing the authentication. + :type username: str + :type password: str + :type create_user: bool + :rtype: diplomacy.client.channel.Channel + """ + request = requests.SignIn(username=username, password=password, create_user=create_user) + return (yield self.send(request)) + + @gen.coroutine + def get_daide_port(self, game_id): + """ Send a :class:`.GetDaidePort` request. + + :param game_id: game id for which to retrieve the DAIDE port. + :return: the game DAIDE port + :type game_id: str + :rtype: int + """ + request = requests.GetDaidePort(game_id=game_id) + return (yield self.send(request)) + + # =================== + # Private Methods + # =================== + @gen.coroutine - def _connect(self): + def _connect(self, message=None): """ Create (force) a tornado websocket connection. Try NB_CONNECTION_ATTEMPTS attempts, waiting for ATTEMPT_DELAY_SECONDS seconds between 2 attempts. Raise an exception if it cannot connect. + + :param message: if provided, print this message as a logger info before starting to connect. + :type message: str, optional """ + if message: + LOGGER.info(message) # We are connecting. self.is_connecting.clear() @@ -319,71 +204,11 @@ class Connection(): @gen.coroutine def _reconnect(self): """ Reconnect. """ - LOGGER.info('Trying to reconnect.') # We are reconnecting. self.is_reconnecting.clear() - yield self._connect() + yield self._connect('Trying to reconnect.') # We will be reconnected when method Reconnection.sync_done() will finish. - Reconnection(self).reconnect() - - def _register_to_send(self, request_context): - """ Register given request context as a request to send as soon as possible. - :param request_context: context of request to send. - :type request_context: RequestFutureContext - """ - self.requests_to_send[request_context.request_id] = request_context - - def write_request(self, request_context): - """ Write a request into internal connection object. - :param request_context: context of request to send. - :type request_context: RequestFutureContext - """ - future = Future() - request = request_context.request - - def on_message_written(write_future): - """ 3) Writing returned, set future as done (with writing result) or with writing exception. """ - exception = write_future.exception() - if exception is not None: - future.set_exception(exception) - else: - future.set_result(write_future.result()) - - def on_connected(reconnected_future): - """ 2) Send request. """ - exception = reconnected_future.exception() - if exception is not None: - LOGGER.error('Fatal (re)connection error occurred while sending a request.') - future.set_exception(exception) - else: - try: - if self.connection is None: - raise WebSocketClosedError() - write_future = self.connection.write_message(request.json()) - except (WebSocketClosedError, StreamClosedError) as exc: - # We were disconnected. - # Save request context as a request to send. - # We will re-try to send it later once reconnected. - self._register_to_send(request_context) - # Transfer exception to returned future. - future.set_exception(exc) - else: - write_future.add_done_callback(on_message_written) - - # 1) Synchronize requests just wait for connection. - # Other requests wait for reconnection (which also implies connection). - if isinstance(request, requests.Synchronize): - self.is_connecting.wait().add_done_callback(on_connected) - else: - self.is_reconnecting.wait().add_done_callback(on_connected) - - return future - - @gen.coroutine - def connect(self): - """ Effectively connect this object. """ - LOGGER.info('Trying to connect.') - yield self._connect() + _Reconnection(self).reconnect() @gen.coroutine def _on_socket_message(self, socket_message): @@ -461,47 +286,264 @@ class Connection(): except (WebSocketClosedError, StreamClosedError): pass - # Public methods. - @gen.coroutine - def get_daide_port(self, game_id): - """ Send a GetDaidePort request. - :param game_id: game id - :return: int. the game DAIDE port - """ - request = requests.GetDaidePort(game_id=game_id) - return (yield self.send(request)) + def _register_to_send(self, request_context): + """ Register given request context as a request to send as soon as possible. - @gen.coroutine - def authenticate(self, username, password, create_user=False): - """ Send a SignIn request. - :param username: username - :param password: password - :param create_user: boolean indicating if you want to create a user or login to and existing user. - :return: a Channel object representing the authentication. + :param request_context: context of request to send. + :type request_context: RequestFutureContext """ - request = requests.SignIn(username=username, password=password, create_user=create_user) - return (yield self.send(request)) + self.requests_to_send[request_context.request_id] = request_context def send(self, request, for_game=None): """ Send a request. + :param request: request object. :param for_game: (optional) NetworkGame object (required for game requests). :return: a Future that returns the response handler result of this request. + :type request: diplomacy.communication.requests._AbstractRequest + :type for_game: diplomacy.client.network_game.NetworkGame, optional + :rtype: Future """ request_future = Future() request_context = RequestFutureContext(request=request, future=request_future, connection=self, game=for_game) - self.write_request(request_context).add_done_callback(MessageWrittenCallback(request_context).callback) + self.write_request(request_context).add_done_callback(_MessageWrittenCallback(request_context).callback) return gen.with_timeout(timedelta(seconds=constants.REQUEST_TIMEOUT_SECONDS), request_future) -@gen.coroutine -def connect(hostname, port): - """ Connect to given hostname and port. - :param hostname: a hostname - :param port: a port - :return: a Connection object connected. - :rtype: Connection + def write_request(self, request_context): + """ Write a request into internal connection object. + + :param request_context: context of request to send. + :type request_context: RequestFutureContext + """ + future = Future() + request = request_context.request + + def on_message_written(write_future): + """ 3) Writing returned, set future as done (with writing result) + or with writing exception. + """ + exception = write_future.exception() + if exception is not None: + future.set_exception(exception) + else: + future.set_result(write_future.result()) + + def on_connected(reconnected_future): + """ 2) Send request. """ + exception = reconnected_future.exception() + if exception is not None: + LOGGER.error('Fatal (re)connection error occurred while sending a request.') + future.set_exception(exception) + else: + try: + if self.connection is None: + raise WebSocketClosedError() + write_future = self.connection.write_message(request.json()) + except (WebSocketClosedError, StreamClosedError) as exc: + # We were disconnected. + # Save request context as a request to send. + # We will re-try to send it later once reconnected. + self._register_to_send(request_context) + # Transfer exception to returned future. + future.set_exception(exc) + else: + write_future.add_done_callback(on_message_written) + + # 1) Synchronize requests just wait for connection. + # Other requests wait for reconnection (which also implies connection). + if isinstance(request, requests.Synchronize): + self.is_connecting.wait().add_done_callback(on_connected) + else: + self.is_reconnecting.wait().add_done_callback(on_connected) + + return future + +class _Reconnection: + """ Class performing reconnection work for a given connection. + + Class properties: + + - connection: Connection object to reconnect. + - games_phases: dictionary mapping each game address (game ID + game role) to server game info: + {game ID => {game role => responses.DataGamePhase}}. + Server game info is a DataGamePhase response sent by server as response to a Synchronize request. + It contains 3 fields: game ID, current server game phase and current server game timestamp. + We currently use only game phase. + - n_expected_games: number of games registered in games_phases. + - n_synchronized_games: number of games already synchronized. + + Reconnection procedure: + + - Mark all waiting responses as `re-sent` (may be useful on server-side) and + move them back to responses_to_send. + - Remove all previous synchronization requests that are not yet sent. We will send new synchronization + requests with latest games timestamps. Future associated to removed requests will raise an exception. + - Initialize games_phases associating None to each game object currently opened in connection. + - Send synchronization request for each game object currently opened in connection. For each game: + + - server will send a response describing current server game phase (current phase and timestamp). This info + will be used to check local requests to send. Note that concrete synchronization is done via notifications. + Thus, when server responses is received, game synchronization may not be yet terminated, but at least + we will now current server game phase. + - Server response is saved in games_phases (replacing None associated to game object). + - n_synchronized_games is incremented. + + - When sync responses are received for all games registered in games_phases + (n_expected_games == n_synchronized_games), we can finalize reconnection: + + - Remove every phase-dependent game request not yet sent for which phase does not match + server game phase. Futures associated to removed request will raise an exception. + - Finally send all remaining requests. + + These requests may be marked as re-sent. + For these requests, server is (currently) responsible for checking if they don't represent + a duplicated query. """ - connection = Connection(hostname, port) - yield connection.connect() - return connection + + __slots__ = ['connection', 'games_phases', 'n_expected_games', 'n_synchronized_games'] + + def __init__(self, connection): + """ Initialize reconnection data/ + + :param connection: connection to reconnect. + :type connection: Connection + """ + self.connection = connection + self.games_phases = {} + self.n_expected_games = 0 + self.n_synchronized_games = 0 + + def reconnect(self): + """ Perform concrete reconnection work. """ + + # Mark all waiting responses as `re-sent` and move them back to responses_to_send. + for waiting_context in self.connection.requests_waiting_responses.values(): # type: RequestFutureContext + waiting_context.request.re_sent = True + self.connection.requests_to_send.update(self.connection.requests_waiting_responses) + self.connection.requests_waiting_responses.clear() + + # Remove all previous synchronization requests. + requests_to_send_updated = {} + for context in self.connection.requests_to_send.values(): # type: RequestFutureContext + if isinstance(context.request, requests.Synchronize): + context.future.set_exception(exceptions.DiplomacyException( + 'Sync request invalidated for game ID %s.' % context.request.game_id)) + else: + requests_to_send_updated[context.request.request_id] = context + self.connection.requests_to_send = requests_to_send_updated + + # Count games to synchronize. + for channel in self.connection.channels.values(): + for game_instance_set in channel.game_id_to_instances.values(): + for game in game_instance_set.get_games(): + self.games_phases.setdefault(game.game_id, {})[game.role] = None + self.n_expected_games += 1 + + if self.n_expected_games: + # Synchronize games. + for channel in self.connection.channels.values(): + for game_instance_set in channel.game_id_to_instances.values(): + for game in game_instance_set.get_games(): + game.synchronize().add_done_callback(self.generate_sync_callback(game)) + else: + # No game to sync, finish sync now. + self.sync_done() + + def generate_sync_callback(self, game): + """ Generate callback to call when response to sync request is received for given game. + + :param game: game + :return: a callback. + :type game: diplomacy.client.network_game.NetworkGame + """ + + def on_sync(future): + """ Callback. If exception occurs, print it as logging error. Else, register server response, + and move forward to final reconnection work if all games received sync responses. + """ + exception = future.exception() + if exception is not None: + LOGGER.error(str(exception)) + else: + self.games_phases[game.game_id][game.role] = future.result() + self.n_synchronized_games += 1 + if self.n_synchronized_games == self.n_expected_games: + self.sync_done() + + return on_sync + + def sync_done(self): + """ Final reconnection work. Remove obsolete game requests and send remaining requests. """ + + # All sync requests sent have finished. + # Remove all obsolete game requests from connection. + # A game request is obsolete if it's phase-dependent and if its phase does not match current game phase. + + request_to_send_updated = {} + for context in self.connection.requests_to_send.values(): # type: RequestFutureContext + keep = True + if context.request.level == strings.GAME and context.request.phase_dependent: + request_phase = context.request.phase + server_phase = self.games_phases[context.request.game_id][context.request.game_role].phase + if request_phase != server_phase: + # Request is obsolete. + context.future.set_exception(exceptions.DiplomacyException( + 'Game %s: request %s: request phase %s does not match current server game phase %s.' + % (context.request.game_id, context.request.name, request_phase, server_phase))) + keep = False + if keep: + request_to_send_updated[context.request.request_id] = context + + LOGGER.debug('Keep %d/%d old requests to send.', + len(request_to_send_updated), len(self.connection.requests_to_send)) + + # All requests to send are stored in request_to_send_updated. + # Then we can empty connection.requests_to_send. + # If we fail to send a request, it will be re-added again. + self.connection.requests_to_send.clear() + + # Send requests. + for request_to_send in request_to_send_updated.values(): # type: RequestFutureContext + self.connection.write_request(request_to_send).add_done_callback( + _MessageWrittenCallback(request_to_send).callback) + + # We are reconnected. + self.connection.is_reconnecting.set() + + LOGGER.info('Done reconnection work.') + +class _MessageWrittenCallback: + """ Helper class representing callback to call on a connection + when a request is written in a websocket. + """ + __slots__ = ['request_context'] + + def __init__(self, request_context): + """ Initialize the callback object. + + :param request_context: a request context + :type request_context: RequestFutureContext + """ + self.request_context = request_context + + def callback(self, msg_future): + """ Called when request is effectively written on socket, and move the request + from `request to send` to `request assumed sent`. + """ + # Remove request context from `requests to send` in any case. + connection = self.request_context.connection # type: Connection + request_id = self.request_context.request_id + exception = msg_future.exception() + if exception is not None: + if isinstance(exception, (WebSocketClosedError, StreamClosedError)): + # Connection suddenly closed. + # Request context was stored in connection.requests_to_send + # and will be re-sent when reconnection succeeds. + # For more details, see method Connection.write_request(). + LOGGER.error('Connection was closed when sending a request. Silently waiting for a reconnection.') + else: + LOGGER.error('Fatal error occurred while writing a request.') + self.request_context.future.set_exception(exception) + else: + connection.requests_waiting_responses[request_id] = self.request_context diff --git a/diplomacy/client/game_instances_set.py b/diplomacy/client/game_instances_set.py index f8c22b9..fd50507 100644 --- a/diplomacy/client/game_instances_set.py +++ b/diplomacy/client/game_instances_set.py @@ -32,7 +32,9 @@ class GameInstancesSet(): def __init__(self, game_id): """ Initialize a game instances set. + :param game_id: game ID of game instances to store. + :type game_id: str """ self.game_id = game_id self.games = weakref.WeakValueDictionary() # {power name => NetworkGame} @@ -60,6 +62,7 @@ class GameInstancesSet(): def add(self, game): """ Add given game. + :param game: a NetworkGame object. :type game: diplomacy.client.network_game.NetworkGame """ diff --git a/diplomacy/client/network_game.py b/diplomacy/client/network_game.py index 368d1d2..10b97d8 100644 --- a/diplomacy/client/network_game.py +++ b/diplomacy/client/network_game.py @@ -24,7 +24,7 @@ from diplomacy.utils.exceptions import DiplomacyException LOGGER = logging.getLogger(__name__) -def game_request_method(channel_method): +def _game_request_method(channel_method): """Create a game request method that calls channel counterpart.""" def func(self, **kwargs): @@ -34,40 +34,62 @@ def game_request_method(channel_method): raise DiplomacyException('Invalid client game.') return channel_method(self.channel, game_object=self, **kwargs) + func.__doc__ = """ + Send game request :class:`.%(request_name)s`%(with_params)s``kwargs``. + See :class:`.%(request_name)s` about request parameters and response. + """ % {'request_name': channel_method.__request_name__, + 'with_params': (' with forced parameters ``(%s)`` and additional request parameters ' + % (channel_method.__request_params__ + if channel_method.__request_params__ + else ' with request parameters '))} return func -def callback_setting_method(notification_class): +def _callback_setting_method(notification_class): """ Create a callback setting method for a given notification class. """ def func(self, notification_callback): """ Add given callback for this game notification class. """ self.add_notification_callback(notification_class, notification_callback) + func.__doc__ = """ + Add callback for notification :class:`.%(notification_name)s`. Callback signature: + ``callback(network_game, notification) -> None``. + """ % {'notification_name' : notification_class.__name__} + return func -def callback_clearing_method(notification_class): +def _callback_clearing_method(notification_class): """ Create a callback clearing method for a given notification class. """ def func(self): """ Clear user callbacks for this game notification class. """ self.clear_notification_callbacks(notification_class) + func.__doc__ = """ + Clear callbacks for notification :class:`.%(notification_name)s`.. + """ % {'notification_name': notification_class.__name__} + return func class NetworkGame(Game): - """ NetworkGame class. Properties: - - channel: associated Channel object. - - notification_callbacks: dict mapping a notification class name to a callback to be called + """ NetworkGame class. + + Properties: + + - **channel**: associated :class:`diplomacy.client.channel.Channel` object. + - **notification_callbacks**: :class:`Dict` mapping a notification class name to a callback to be called when a corresponding game notification is received. """ + # pylint: disable=protected-access __slots__ = ['channel', 'notification_callbacks', 'data', '__weakref__'] def __init__(self, channel, received_game): """ Initialize network game object with a channel and a game object sent by server. + :param channel: a Channel object. :param received_game: a Game object. :type channel: diplomacy.client.channel.Channel - :type received_game: diplomacy.Game + :type received_game: diplomacy.engine.game.Game """ self.channel = channel self.notification_callbacks = {} # {notification_class => [callback(game, notification)]} @@ -83,80 +105,83 @@ class NetworkGame(Game): # NB: Method get_all_possible_orders() is only local in Python code, # but is still a network call from web interface. - get_phase_history = game_request_method(Channel.get_phase_history) - leave = game_request_method(Channel.leave_game) - send_game_message = game_request_method(Channel.send_game_message) - set_orders = game_request_method(Channel.set_orders) + get_phase_history = _game_request_method(Channel._get_phase_history) + leave = _game_request_method(Channel._leave_game) + send_game_message = _game_request_method(Channel._send_game_message) + set_orders = _game_request_method(Channel._set_orders) - clear_centers = game_request_method(Channel.clear_centers) - clear_orders = game_request_method(Channel.clear_orders) - clear_units = game_request_method(Channel.clear_units) + clear_centers = _game_request_method(Channel._clear_centers) + clear_orders = _game_request_method(Channel._clear_orders) + clear_units = _game_request_method(Channel._clear_units) - wait = game_request_method(Channel.wait) - no_wait = game_request_method(Channel.no_wait) - vote = game_request_method(Channel.vote) - save = game_request_method(Channel.save) + wait = _game_request_method(Channel._wait) + no_wait = _game_request_method(Channel._no_wait) + vote = _game_request_method(Channel._vote) + save = _game_request_method(Channel._save) def synchronize(self): - """ Send a Synchronize request to synchronize this game with associated server game. """ + """ Send a :class:`.Synchronize` request to synchronize this game with associated server game. """ if not self.channel: raise DiplomacyException('Invalid client game.') - return self.channel.synchronize(game_object=self, timestamp=self.get_latest_timestamp()) + return self.channel._synchronize(game_object=self, timestamp=self.get_latest_timestamp()) # Admin / Moderator API. - delete = game_request_method(Channel.delete_game) - kick_powers = game_request_method(Channel.kick_powers) - set_state = game_request_method(Channel.set_state) - process = game_request_method(Channel.process) - query_schedule = game_request_method(Channel.query_schedule) - start = game_request_method(Channel.start) - pause = game_request_method(Channel.pause) - resume = game_request_method(Channel.resume) - cancel = game_request_method(Channel.cancel) - draw = game_request_method(Channel.draw) + delete = _game_request_method(Channel._delete_game) + kick_powers = _game_request_method(Channel._kick_powers) + set_state = _game_request_method(Channel._set_state) + process = _game_request_method(Channel._process) + query_schedule = _game_request_method(Channel._query_schedule) + start = _game_request_method(Channel._start) + pause = _game_request_method(Channel._pause) + resume = _game_request_method(Channel._resume) + cancel = _game_request_method(Channel._cancel) + draw = _game_request_method(Channel._draw) # =============================== # Notification callback settings. # =============================== - add_on_cleared_centers = callback_setting_method(notifications.ClearedCenters) - add_on_cleared_orders = callback_setting_method(notifications.ClearedOrders) - add_on_cleared_units = callback_setting_method(notifications.ClearedUnits) - add_on_game_deleted = callback_setting_method(notifications.GameDeleted) - add_on_game_message_received = callback_setting_method(notifications.GameMessageReceived) - add_on_game_processed = callback_setting_method(notifications.GameProcessed) - add_on_game_phase_update = callback_setting_method(notifications.GamePhaseUpdate) - add_on_game_status_update = callback_setting_method(notifications.GameStatusUpdate) - add_on_omniscient_updated = callback_setting_method(notifications.OmniscientUpdated) - add_on_power_orders_flag = callback_setting_method(notifications.PowerOrdersFlag) - add_on_power_orders_update = callback_setting_method(notifications.PowerOrdersUpdate) - add_on_power_vote_updated = callback_setting_method(notifications.PowerVoteUpdated) - add_on_power_wait_flag = callback_setting_method(notifications.PowerWaitFlag) - add_on_powers_controllers = callback_setting_method(notifications.PowersControllers) - add_on_vote_count_updated = callback_setting_method(notifications.VoteCountUpdated) - add_on_vote_updated = callback_setting_method(notifications.VoteUpdated) - - clear_on_cleared_centers = callback_clearing_method(notifications.ClearedCenters) - clear_on_cleared_orders = callback_clearing_method(notifications.ClearedOrders) - clear_on_cleared_units = callback_clearing_method(notifications.ClearedUnits) - clear_on_game_deleted = callback_clearing_method(notifications.GameDeleted) - clear_on_game_message_received = callback_clearing_method(notifications.GameMessageReceived) - clear_on_game_processed = callback_clearing_method(notifications.GameProcessed) - clear_on_game_phase_update = callback_clearing_method(notifications.GamePhaseUpdate) - clear_on_game_status_update = callback_clearing_method(notifications.GameStatusUpdate) - clear_on_omniscient_updated = callback_clearing_method(notifications.OmniscientUpdated) - clear_on_power_orders_flag = callback_clearing_method(notifications.PowerOrdersFlag) - clear_on_power_orders_update = callback_clearing_method(notifications.PowerOrdersUpdate) - clear_on_power_vote_updated = callback_clearing_method(notifications.PowerVoteUpdated) - clear_on_power_wait_flag = callback_clearing_method(notifications.PowerWaitFlag) - clear_on_powers_controllers = callback_clearing_method(notifications.PowersControllers) - clear_on_vote_count_updated = callback_clearing_method(notifications.VoteCountUpdated) - clear_on_vote_updated = callback_clearing_method(notifications.VoteUpdated) + add_on_cleared_centers = _callback_setting_method(notifications.ClearedCenters) + add_on_cleared_orders = _callback_setting_method(notifications.ClearedOrders) + add_on_cleared_units = _callback_setting_method(notifications.ClearedUnits) + add_on_game_deleted = _callback_setting_method(notifications.GameDeleted) + add_on_game_message_received = _callback_setting_method(notifications.GameMessageReceived) + add_on_game_processed = _callback_setting_method(notifications.GameProcessed) + add_on_game_phase_update = _callback_setting_method(notifications.GamePhaseUpdate) + add_on_game_status_update = _callback_setting_method(notifications.GameStatusUpdate) + add_on_omniscient_updated = _callback_setting_method(notifications.OmniscientUpdated) + add_on_power_orders_flag = _callback_setting_method(notifications.PowerOrdersFlag) + add_on_power_orders_update = _callback_setting_method(notifications.PowerOrdersUpdate) + add_on_power_vote_updated = _callback_setting_method(notifications.PowerVoteUpdated) + add_on_power_wait_flag = _callback_setting_method(notifications.PowerWaitFlag) + add_on_powers_controllers = _callback_setting_method(notifications.PowersControllers) + add_on_vote_count_updated = _callback_setting_method(notifications.VoteCountUpdated) + add_on_vote_updated = _callback_setting_method(notifications.VoteUpdated) + + clear_on_cleared_centers = _callback_clearing_method(notifications.ClearedCenters) + clear_on_cleared_orders = _callback_clearing_method(notifications.ClearedOrders) + clear_on_cleared_units = _callback_clearing_method(notifications.ClearedUnits) + clear_on_game_deleted = _callback_clearing_method(notifications.GameDeleted) + clear_on_game_message_received = _callback_clearing_method(notifications.GameMessageReceived) + clear_on_game_processed = _callback_clearing_method(notifications.GameProcessed) + clear_on_game_phase_update = _callback_clearing_method(notifications.GamePhaseUpdate) + clear_on_game_status_update = _callback_clearing_method(notifications.GameStatusUpdate) + clear_on_omniscient_updated = _callback_clearing_method(notifications.OmniscientUpdated) + clear_on_power_orders_flag = _callback_clearing_method(notifications.PowerOrdersFlag) + clear_on_power_orders_update = _callback_clearing_method(notifications.PowerOrdersUpdate) + clear_on_power_vote_updated = _callback_clearing_method(notifications.PowerVoteUpdated) + clear_on_power_wait_flag = _callback_clearing_method(notifications.PowerWaitFlag) + clear_on_powers_controllers = _callback_clearing_method(notifications.PowersControllers) + clear_on_vote_count_updated = _callback_clearing_method(notifications.VoteCountUpdated) + clear_on_vote_updated = _callback_clearing_method(notifications.VoteUpdated) def add_notification_callback(self, notification_class, notification_callback): """ Add a callback for a notification. - :param notification_class: a notification class - :param notification_callback: callback to add. + + :param notification_class: a notification class. + See :mod:`diplomacy.communication.notifications` about available notifications. + :param notification_callback: callback to add: + ``callback(network_game, notification) -> None``. """ assert callable(notification_callback) if notification_class not in self.notification_callbacks: @@ -166,6 +191,7 @@ class NetworkGame(Game): def clear_notification_callbacks(self, notification_class): """ Remove all user callbacks for a notification. + :param notification_class: a notification class """ self.notification_callbacks.pop(notification_class, None) diff --git a/diplomacy/client/notification_managers.py b/diplomacy/client/notification_managers.py index 317bd2e..05e6ab7 100644 --- a/diplomacy/client/notification_managers.py +++ b/diplomacy/client/notification_managers.py @@ -27,9 +27,10 @@ LOGGER = logging.getLogger(__name__) def _get_game_to_notify(connection, notification): """ Get notified game from connection using notification parameters. + :param connection: connection that receives the notification. :param notification: notification received. - :return: a NetWorkGame instance, or None if no game found. + :return: a NetworkGame instance, or None if no game found. :type connection: diplomacy.Connection :type notification: diplomacy.communication.notifications._GameNotification """ @@ -40,6 +41,7 @@ def _get_game_to_notify(connection, notification): def on_account_deleted(channel, notification): """ Manage notification AccountDeleted. + :param channel: channel associated to received notification. :param notification: received notification. :type channel: diplomacy.client.channel.Channel @@ -49,6 +51,7 @@ def on_account_deleted(channel, notification): def on_cleared_centers(game, notification): """ Manage notification ClearedCenters. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -58,6 +61,7 @@ def on_cleared_centers(game, notification): def on_cleared_orders(game, notification): """ Manage notification ClearedOrders. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -67,6 +71,7 @@ def on_cleared_orders(game, notification): def on_cleared_units(game, notification): """ Manage notification ClearedUnits. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -76,6 +81,7 @@ def on_cleared_units(game, notification): def on_powers_controllers(game, notification): """ Manage notification PowersControllers. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -90,6 +96,7 @@ def on_powers_controllers(game, notification): def on_game_deleted(game, notification): """ Manage notification GameDeleted. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -101,7 +108,8 @@ def on_game_deleted(game, notification): game.channel.game_id_to_instances[game.game_id].remove_special() def on_game_message_received(game, notification): - """ Manage notification GameMessageReceived.. + """ Manage notification GameMessageReceived. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -111,6 +119,7 @@ def on_game_message_received(game, notification): def on_game_processed(game, notification): """ Manage notification GamePhaseUpdate (for omniscient and observer games). + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -120,6 +129,7 @@ def on_game_processed(game, notification): def on_game_phase_update(game, notification): """ Manage notification GamePhaseUpdate. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -132,6 +142,7 @@ def on_game_phase_update(game, notification): def on_game_status_update(game, notification): """ Manage notification GameStatusUpdate. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -141,6 +152,7 @@ def on_game_status_update(game, notification): def on_omniscient_updated(game, notification): """ Manage notification OmniscientUpdated. + :param game: game associated to received notification. :param notification: received notification. :type game: diplomacy.client.network_game.NetworkGame @@ -153,10 +165,12 @@ def on_omniscient_updated(game, notification): else: assert notification.grade_update == strings.DEMOTE assert notification.game.is_observer_game() + # Save client game channel and invalidate client game. channel = game.channel game.channel = None channel.game_id_to_instances[notification.game_id].remove(game.role) + # Create a new client game with previous client game channel game sent by server. new_game = NetworkGame(channel, notification.game) new_game.notification_callbacks.update({key: value.copy() for key, value in game.notification_callbacks.items()}) @@ -165,6 +179,7 @@ def on_omniscient_updated(game, notification): def on_power_orders_update(game, notification): """ Manage notification PowerOrdersUpdate. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -174,6 +189,7 @@ def on_power_orders_update(game, notification): def on_power_orders_flag(game, notification): """ Manage notification PowerOrdersFlag. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -185,6 +201,7 @@ def on_power_orders_flag(game, notification): def on_power_vote_updated(game, notification): """ Manage notification PowerVoteUpdated (for power game). + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -195,6 +212,7 @@ def on_power_vote_updated(game, notification): def on_power_wait_flag(game, notification): """ Manage notification PowerWaitFlag. + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -204,6 +222,7 @@ def on_power_wait_flag(game, notification): def on_vote_count_updated(game, notification): """ Manage notification VoteCountUpdated (for observer game). + :param game: game associated to received notification. :param notification: received notification. :type game: diplomacy.client.network_game.NetworkGame @@ -212,6 +231,7 @@ def on_vote_count_updated(game, notification): def on_vote_updated(game, notification): """ Manage notification VoteUpdated (for omniscient game). + :param game: a Network game :param notification: notification received :type game: diplomacy.client.network_game.NetworkGame @@ -244,6 +264,7 @@ MAPPING = { def handle_notification(connection, notification): """ Call appropriate handler for given notification received by given connection. + :param connection: recipient connection. :param notification: received notification. :type connection: diplomacy.Connection diff --git a/diplomacy/client/response_managers.py b/diplomacy/client/response_managers.py index 991586f..0ecb453 100644 --- a/diplomacy/client/response_managers.py +++ b/diplomacy/client/response_managers.py @@ -28,7 +28,7 @@ from diplomacy.engine.game import Game from diplomacy.utils import exceptions from diplomacy.utils.game_phase_data import GamePhaseData -class RequestFutureContext(): +class RequestFutureContext: """ Helper class to store a context around a request (with future for response management, related connection and optional related game). """ @@ -36,6 +36,7 @@ class RequestFutureContext(): def __init__(self, request, future, connection, game=None): """ Initialize a request future context. + :param request: a request object (see diplomacy.communication.requests about possible classes). :param future: a tornado Future object. :param connection: a diplomacy.Connection object. @@ -63,6 +64,7 @@ class RequestFutureContext(): def new_game(self, received_game): """ Create, store (in associated connection) and return a new network game wrapping given game data. Returned game is already in appropriate type (observer game, omniscient game or power game). + :param received_game: game sent by server (Game object) :type received_game: Game """ @@ -90,6 +92,7 @@ def default_manager(context, response): Else, return response. Expect response to be either OK or a UniqueData (containing only 1 field intended to be returned by server for associated request). + :param context: request context :param response: response received :return: None, or data if response is a UniqueData. @@ -100,8 +103,42 @@ def default_manager(context, response): return None return response +def on_clear_centers(context, response): + """ Manage response for request ClearCenters. + + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.ClearCenters + Game.clear_centers(context.game, request.power_name) + +def on_clear_orders(context, response): + """ Manage response for request ClearOrders. + + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.ClearOrders + Game.clear_orders(context.game, request.power_name) + +def on_clear_units(context, response): + """ Manage response for request ClearUnits. + + :param context: request context + :param response: response received + :return: None + :type context: RequestFutureContext + """ + request = context.request # type: requests.ClearUnits + Game.clear_units(context.game, request.power_name) + def on_create_game(context, response): """ Manage response for request CreateGame. + :param context: request context :param response: response received :return: a new network game @@ -112,6 +149,7 @@ def on_create_game(context, response): def on_delete_account(context, response): """ Manage response for request DeleteAccount. + :param context: request context :param response: response received :return: None @@ -121,6 +159,7 @@ def on_delete_account(context, response): def on_delete_game(context, response): """ Manage response for request DeleteGame. + :param context: request context :param response: response received :return: None @@ -130,6 +169,7 @@ def on_delete_game(context, response): def on_get_phase_history(context, response): """ Manage response for request GetPhaseHistory. + :param context: request context :param response: response received :return: a list of game states @@ -143,6 +183,7 @@ def on_get_phase_history(context, response): def on_join_game(context, response): """ Manage response for request JoinGame. + :param context: request context :param response: response received :return: a new network game @@ -152,6 +193,7 @@ def on_join_game(context, response): def on_leave_game(context, response): """ Manage response for request LeaveGame. + :param context: request context :param response: response received :return: None @@ -161,6 +203,7 @@ def on_leave_game(context, response): def on_logout(context, response): """ Manage response for request Logout. + :param context: request context :param response: response received :return: None @@ -170,6 +213,7 @@ def on_logout(context, response): def on_send_game_message(context, response): """ Manage response for request SendGameMessage. + :param context: request context :param response: response received :return: None @@ -183,6 +227,7 @@ def on_send_game_message(context, response): def on_set_game_state(context, response): """ Manage response for request SetGameState. + :param context: request context :param response: response received :return: None @@ -197,6 +242,7 @@ def on_set_game_state(context, response): def on_set_game_status(context, response): """ Manage response for request SetGameStatus. + :param context: request context :param response: response received :return: None @@ -207,6 +253,7 @@ def on_set_game_status(context, response): def on_set_orders(context, response): """ Manage response for request SetOrders. + :param context: request context :param response: response received :return: None @@ -220,38 +267,9 @@ def on_set_orders(context, response): else: Game.set_orders(context.game, request.power_name, orders) -def on_clear_orders(context, response): - """ Manage response for request ClearOrders. - :param context: request context - :param response: response received - :return: None - :type context: RequestFutureContext - """ - request = context.request # type: requests.ClearOrders - Game.clear_orders(context.game, request.power_name) - -def on_clear_centers(context, response): - """ Manage response for request ClearCenters. - :param context: request context - :param response: response received - :return: None - :type context: RequestFutureContext - """ - request = context.request # type: requests.ClearCenters - Game.clear_centers(context.game, request.power_name) - -def on_clear_units(context, response): - """ Manage response for request ClearUnits. - :param context: request context - :param response: response received - :return: None - :type context: RequestFutureContext - """ - request = context.request # type: requests.ClearUnits - Game.clear_units(context.game, request.power_name) - def on_set_wait_flag(context, response): """ Manage response for request SetWaitFlag. + :param context: request context :param response: response received :return: None @@ -267,6 +285,7 @@ def on_set_wait_flag(context, response): def on_sign_in(context, response): """ Manage response for request SignIn. + :param context: request context :param response: response received :return: a new channel @@ -277,6 +296,7 @@ def on_sign_in(context, response): def on_vote(context, response): """ Manage response for request VoteAboutDraw. + :param context: request context :param response: response received :return: None @@ -300,13 +320,13 @@ MAPPING = { requests.GetAvailableMaps: default_manager, requests.GetDaidePort: default_manager, requests.GetDummyWaitingPowers: default_manager, - requests.GetPlayablePowers: default_manager, + requests.GetGamesInfo: default_manager, requests.GetPhaseHistory: on_get_phase_history, + requests.GetPlayablePowers: default_manager, requests.JoinGame: on_join_game, requests.JoinPowers: default_manager, requests.LeaveGame: on_leave_game, requests.ListGames: default_manager, - requests.GetGamesInfo: default_manager, requests.Logout: on_logout, requests.ProcessGame: default_manager, requests.QuerySchedule: default_manager, @@ -325,6 +345,7 @@ MAPPING = { def handle_response(context, response): """ Call appropriate handler for given response with given request context. + :param context: request context. :param response: response received. :return: value returned by handler. diff --git a/diplomacy/communication/notifications.py b/diplomacy/communication/notifications.py index c88d526..1751182 100644 --- a/diplomacy/communication/notifications.py +++ b/diplomacy/communication/notifications.py @@ -73,7 +73,13 @@ class AccountDeleted(_ChannelNotification): __slots__ = [] class OmniscientUpdated(_GameNotification): - """ Notification about a grade updated. Sent at channel level. """ + """ Notification about a grade updated. Sent at channel level. + + Properties: + + - **grade_update**: :class:`str` One of 'promote' or 'demote'. + - **game**: :class:`parsing.JsonableClassType(Game)` a :class:`diplomacy.engine.game.Game` object. + """ __slots__ = ['grade_update', 'game'] params = { strings.GRADE_UPDATE: parsing.EnumerationType(strings.ALL_GRADE_UPDATES), @@ -98,10 +104,12 @@ class ClearedUnits(_GameNotification): __slots__ = [] class VoteCountUpdated(_GameNotification): - """ Notification about new count of draw votes for a game (for observers) + """ Notification about new count of draw votes for a game (for observers). + Properties: - - count_voted: number of powers that have voted. - - count_expected: number of powers to be expected to vote. + + - **count_voted**: :class:`int` number of powers that have voted. + - **count_expected**: :class:`int` number of powers to be expected to vote. """ __slots__ = ['count_voted', 'count_expected'] params = { @@ -115,8 +123,12 @@ class VoteCountUpdated(_GameNotification): super(VoteCountUpdated, self).__init__(**kwargs) class VoteUpdated(_GameNotification): - """ Notification about votes updated for a game (for omniscient observers). Properties: - - vote: dictionary mapping a power name to a Vote object representing power vote. + """ Notification about votes updated for a game (for omniscient observers). + + Properties: + + - **vote**: :class:`Dict` mapping a power name to a Vote (:class:`str`) object representing power vote. + Possible votes are: yes, no, neutral. """ __slots__ = ['vote'] params = { @@ -128,8 +140,11 @@ class VoteUpdated(_GameNotification): super(VoteUpdated, self).__init__(**kwargs) class PowerVoteUpdated(VoteCountUpdated): - """ Notification about a new vote for a specific game power (for player games). Properties: - - vote: vote object representing associated power vote. + """ Notification about a new vote for a specific game power (for player games). + + Properties: + + - **vote**: :class:`str` vote object representing associated power vote. Can be yes, no, neutral. """ __slots__ = ['vote'] params = parsing.extend_model(VoteCountUpdated.params, { @@ -141,7 +156,13 @@ class PowerVoteUpdated(VoteCountUpdated): super(PowerVoteUpdated, self).__init__(**kwargs) class PowersControllers(_GameNotification): - """ Notification about current controller for each power in a game. """ + """ Notification about current controller for each power in a game. + + Properties: + + - **powers**: A :class:`Dict` that maps a power_name to a controller_name :class:`str`. + - **timestamps**: A :class:`Dict` that maps a power_name to timestamp where the controller took over. + """ __slots__ = ['powers', 'timestamps'] params = { # {power_name => controller_name} @@ -160,7 +181,13 @@ class GameDeleted(_GameNotification): __slots__ = [] class GameProcessed(_GameNotification): - """ Notification about a game phase update. Sent after game had processed a phase. """ + """ Notification about a game phase update. Sent after game has processed a phase. + + Properties: + + - **previous_phase_data**: :class:`diplomacy.utils.game_phase_data.GamePhaseData` of the previous phase + - **current_phase_data**: :class:`diplomacy.utils.game_phase_data.GamePhaseData` of the current phase + """ __slots__ = ['previous_phase_data', 'current_phase_data'] params = { strings.PREVIOUS_PHASE_DATA: parsing.JsonableClassType(GamePhaseData), @@ -173,7 +200,13 @@ class GameProcessed(_GameNotification): super(GameProcessed, self).__init__(**kwargs) class GamePhaseUpdate(_GameNotification): - """ Notification about a game phase update. """ + """ Notification about a game phase update. + + Properties: + + - **phase_data**: :class:`diplomacy.utils.game_phase_data.GamePhaseData` of the updated phase + - **phase_data_type**: :class:`str`. One of 'state_history', 'state', 'phase' + """ __slots__ = ['phase_data', 'phase_data_type'] params = { strings.PHASE_DATA: parsing.JsonableClassType(GamePhaseData), @@ -186,7 +219,12 @@ class GamePhaseUpdate(_GameNotification): super(GamePhaseUpdate, self).__init__(**kwargs) class GameStatusUpdate(_GameNotification): - """ Notification about a game status update. """ + """ Notification about a game status update. + + Properties: + + -**status**: :class:`str`. One of 'forming', 'active', 'paused', 'completed', 'canceled' + """ __slots__ = ['status'] params = { strings.STATUS: parsing.EnumerationType(strings.ALL_GAME_STATUSES), @@ -197,7 +235,12 @@ class GameStatusUpdate(_GameNotification): super(GameStatusUpdate, self).__init__(**kwargs) class GameMessageReceived(_GameNotification): - """ Notification about a game message received. """ + """ Notification about a game message received. + + Properties: + + - **message**: :class:`diplomacy.engine.message.Message` received. + """ __slots__ = ['message'] params = { strings.MESSAGE: parsing.JsonableClassType(Message), @@ -208,7 +251,12 @@ class GameMessageReceived(_GameNotification): super(GameMessageReceived, self).__init__(**kwargs) class PowerOrdersUpdate(_GameNotification): - """ Notification about a power order update. """ + """ Notification about a power order update. + + Properties: + + - **orders**: List of updated orders (i.e. :class:`str`) + """ __slots__ = ['orders'] params = { strings.ORDERS: parsing.OptionalValueType(parsing.SequenceType(str)), @@ -219,7 +267,12 @@ class PowerOrdersUpdate(_GameNotification): super(PowerOrdersUpdate, self).__init__(**kwargs) class PowerOrdersFlag(_GameNotification): - """ Notification about a power order flag update. """ + """ Notification about a power order flag update. + + Properties: + + - **order_is_set**: :class:`int`. O = ORDER_NOT_SET, 1 = ORDER_SET_EMPTY, 2 = ORDER_SET. + """ __slots__ = ['order_is_set'] params = { strings.ORDER_IS_SET: parsing.EnumerationType(OrderSettings.ALL_SETTINGS), @@ -230,7 +283,14 @@ class PowerOrdersFlag(_GameNotification): super(PowerOrdersFlag, self).__init__(**kwargs) class PowerWaitFlag(_GameNotification): - """ Notification about a power wait flag update. """ + """ Notification about a power wait flag update. + + Properties: + + - **wait**: :class:`bool` that indicates to wait until the deadline is reached before proceeding. Otherwise + if all powers are not waiting, the game is processed as soon as all non-eliminated powers have submitted + their orders. + """ __slots__ = ['wait'] params = { strings.WAIT: bool, @@ -242,6 +302,7 @@ class PowerWaitFlag(_GameNotification): def parse_dict(json_notification): """ Parse a JSON expected to represent a notification. Raise an exception if parsing failed. + :param json_notification: JSON dictionary. :return: a notification class instance. """ diff --git a/diplomacy/communication/requests.py b/diplomacy/communication/requests.py index 6f99f15..f16c33c 100644 --- a/diplomacy/communication/requests.py +++ b/diplomacy/communication/requests.py @@ -15,10 +15,70 @@ # with this program. If not, see . # ============================================================================== """ Client -> Server requests. - Notes: - If an error occurred on server-side while handling a request, client will receive - a ResponseException containing message about handling error. Request exceptions - are currently not more typed on client-side. + + This module contains the definition of request (as classes) + that a client can send to Diplomacy server implemented in this project. + + The client -> server communication follows this procedure: + + - Client sends a request to server. + All requests have parameters that must be filled by client before being sent. + - Server replies with a response, which is either an error response or a valid response. + - Client receives and handles server response. + + - If server response is an error, client converts it to a typed exception and raises it. + - If server response is a valid response, client return either the response data directly, + or make further treatments and return a derived data. + + Diplomacy package actually provides 2 clients: the Python client and the web front-end. + + Web front-end provides user-friendly forms to collect required request parameters, + makes all request calls internally, and then uses them to update graphical user interface. + So, when using front-end, you don't need to get familiar with underlying protocol, and documentation + in this module won't be really useful for you. + + Python client consists of three classes (:class:`.Connection`, :class:`.Channel` and + :class:`.NetworkGame`) which provide appropriate methods to automatically send requests, handle + server response, and either raise an exception (if server returns an error) or return a client-side + wrapped data (if server returns a valid response) where requests were called. Thus, these methods + still need to receive request parameters, and you need to know what kind of data they can return. + So, if you use Python client, you will need the documentation in this module, which describes, for + each request: + + - the request parameters (important) + - the server valid responses (less interesting) + - the Python client returned values (important) + + All requests classes inherit from :class:`._AbstractRequest` which require parameters + ``name`` (from parant class :class:`.NetworkData`), ``request_id`` and ``re_sent``. + These parameters are automatically filled by the client. + + From parent class :class:`._AbstractRequest`, we get 3 types of requests: + + - public requests, which directly inherit from :class:`._AbstractRequest`. + - channel requests, inherited from :class:`._AbstractChannelRequest`, which requires additional + parameter ``token``. Token is retrieved by client when he connected to server using + connection request :class:`.SignIn`, and is then used to create a :class:`.Channel` object. + Channel object will be responsible for sending all other channel requests, automatically + filling token field for these requests. + - game requests, intherited from :class:`._AbstractGameRequest`, which itself inherit from + :class:`._AbstractChannelRequest`, and requires additional parameters ``game_id``, ``game_role`` + and ``phase`` (game short phase name). Game ID, role and phase are retrieved for a specific game + by the client when he joined a game through one of featured :class:`.Channel` methods which return + a :class:`.NetworkGame` object. Network game will then be responsible for sending all other + game requests, automatically filling game ID, role and phase for these requests. + + Then, all other requests derived directly from either abstract request class, abstract channel + request class, or abstract game request class, may require additional parameters, and if so, these + parameters will need to be filled by the user, by passing them to related client methods. + + Check :class:`.Connection` for available public request methods (and associated requests). + + Check :class:`.Channel` for available channel request methods (and associated requests). + + Check :class:`.NetworkGame` for available game request methods (and associated requests). + + Then come here to get parameters and returned values for associated requests. """ import inspect import logging @@ -34,18 +94,17 @@ LOGGER = logging.getLogger(__name__) class _AbstractRequest(NetworkData): """ Abstract request class. - Field request_id is auto-filled if not defined. + Field **request_id** is auto-filled if not defined. - Field name is auto-filled with snake case version of request class name. + Field **name** is auto-filled with snake case version of request class name. - Field re_sent is False by default. It should be set to True if request is re-sent by client + Field **re_sent** is False by default. It should be set to True if request is re-sent by client (currently done by Connection object when reconnecting). - Timestamp field is auto-set with current local timestamp if not defined. + **Timestamp** field is auto-set with current local timestamp if not defined. For game request Synchronize, timestamp should be game latest timestamp instead (see method NetworkGame.synchronize()). """ - __slots__ = ['request_id', 're_sent'] header = { strings.REQUEST_ID: str, @@ -57,6 +116,7 @@ class _AbstractRequest(NetworkData): level = None def __init__(self, **kwargs): + """ Constructor. """ self.request_id = None # type: str self.re_sent = None # type: bool super(_AbstractRequest, self).__init__(**kwargs) @@ -70,7 +130,6 @@ class _AbstractChannelRequest(_AbstractRequest): """ Abstract class representing a channel request. Token field is automatically filled by a Channel object before sending request. """ - __slots__ = ['token'] header = parsing.update_model(_AbstractRequest.header, { strings.TOKEN: str @@ -85,7 +144,6 @@ class _AbstractGameRequest(_AbstractChannelRequest): """ Abstract class representing a game request. Game ID, game role and phase fields are automatically filled by a NetworkGame object before sending request. """ - __slots__ = ['game_id', 'game_role', 'phase'] header = parsing.extend_model(_AbstractChannelRequest.header, { @@ -115,7 +173,17 @@ class _AbstractGameRequest(_AbstractChannelRequest): # ==================== class GetDaidePort(_AbstractRequest): - """ Get game DAIDE port """ + """ Public request to get DAIDE port opened for a game. + + :param game_id: ID of game for which yu want to get DAIDE port + :type game_id: str + :return: + + - Server: :class:`.DataPort` + - Client: int - DAIDE port + + :raise diplomacy.utils.exceptions.DaidePortException: if there is no DAIDE port associated to given game ID. + """ __slots__ = ['game_id'] params = { strings.GAME_ID: str @@ -126,9 +194,20 @@ class GetDaidePort(_AbstractRequest): super(GetDaidePort, self).__init__(**kwargs) class SignIn(_AbstractRequest): - """ SignIn request. - Expected response: responses.DataToken - Expected response handler result: diplomacy.client.channel.Channel + """ Connection request. Log in or sign in to server. + + :param username: account username + :param password: account password + :param create_user: if True, server must create user. If False, server must login user. + :return: + + - Server: :class:`.DataToken` + - Client: a :class:`.Channel` object presenting user connected to the server. + If any sign in error occurs, raise an appropriate :class:`.ResponseException`. + + :type username: str + :type password: str + :type create_user: bool """ __slots__ = ['username', 'password', 'create_user'] params = { @@ -148,9 +227,47 @@ class SignIn(_AbstractRequest): # ================= class CreateGame(_AbstractChannelRequest): - """ CreateGame request. - Expected response: responses.DataGame - Expected response handler result: diplomacy.client.network_game.NetworkGame + """ Channel request to create a game. + + :param game_id: game ID. If not provided, a game ID will be generated. + :param n_controls: number of controlled powers required to start the game. + A power becomes controlled when a player joins the game to control this power. + Game won't start as long it does not have this number of controlled powers. + Game will stop (to ``forming`` state) if the number of controlled powers decrease under + this number (e.g. when powers are kicked, eliminated, or when a player controlling a power + leaves the game). If not provided, set with the number of powers on the map (e.g. ``7`` + on standard map). + :param deadline: (default ``300``) time (in seconds) for the game to wait before + processing a phase. ``0`` means no deadline, ie. game won't process a phase until either + all powers submit orders and turn off wait flag, or a game master forces game to process. + :param registration_password: password required to join the game. + If not provided, anyone can join the game. + :param power_name: power to control once game is created. + + - If provided, the user who send this request will be joined to the game as a player + controlling this power. + - If not provided, the user who send this request will be joined to the game as an + omniscient observer (ie. able to see everything in the game, including user messages). + Plus, as game creator, user will also be a game master, ie. able to send master requests, + e.g. to force game processing. + + :param state: game initial state (for expert users). + :param map_name: (default ``'standard'``) map to play on. + You can retrieve maps available on server by sending request :class:`GetAvailableMaps`. + :param rules: list of strings - game rules (for expert users). + :type game_id: str, optional + :type n_controls: int, optional + :type deadline: int, optional + :type registration_password: str, optional + :type power_name: str, optional + :type state: dict, optional + :type map_name: str, optional + :type rules: list, optional + :return: + + - Server: :class:`.DataGame` + - Client: a :class:`.NetworkGame` object representing a client version of the + game created and joined. Either a power game (if power name given) or an omniscient game. """ __slots__ = ['game_id', 'power_name', 'state', 'map_name', 'rules', 'n_controls', 'deadline', 'registration_password'] @@ -177,9 +294,15 @@ class CreateGame(_AbstractChannelRequest): super(CreateGame, self).__init__(**kwargs) class DeleteAccount(_AbstractChannelRequest): - """ DeleteAccount request. - Expected response: responses.Ok - Expected response handler result: None + """ Channel request to delete an account. + + :param username: name of user to delete account + + - if **not** given, then account to delete will be the one of user sending this request. + - if **provided**, then user submitting this request must have administrator privileges. + + :type username: str, optional + :return: None """ __slots__ = ['username'] params = { @@ -191,9 +314,25 @@ class DeleteAccount(_AbstractChannelRequest): super(DeleteAccount, self).__init__(**kwargs) class GetDummyWaitingPowers(_AbstractChannelRequest): - """ GetDummyWaitingPowers request. - Expected response: response.DataGamesToPowerNames - Expected response handler result: {dict mapping game IDs to lists of dummy powers names} + """ Channel request to get games with dummy waiting powers. + A dummy waiting power is a dummy (not controlled) power: + + - not yet eliminated, + - without orders submitted (for current game phase), + - but able to submit orders (for current game phase), + - and who is waiting for orders. + + It's a non-controlled orderable free power, which is then best suited to be controlled + by an automated player (e.g. a bot, or a learning algorithm). + + :param buffer_size: maximum number of powers to return. + :type buffer_size: int + :return: + + - Server: :class:`.DataGamesToPowerNames` + - Client: a dictionary mapping a game ID to a list of dummy waiting power names, + such that the total number of power names in the entire dictionary does not exceed + given buffer size. """ __slots__ = ['buffer_size'] params = { @@ -205,16 +344,26 @@ class GetDummyWaitingPowers(_AbstractChannelRequest): super(GetDummyWaitingPowers, self).__init__(**kwargs) class GetAvailableMaps(_AbstractChannelRequest): - """ GetAvailableMaps request. - Expected response: responses.DataMaps - Expected response handler result: {map name => [map power names]} + """ Channel request to get maps available on server. + + :return: + + - Server: :class:`.DataMaps` + - Client: a dictionary associating a map name to a dictionary of information related + to the map. You can especially check key ``'powers'`` to get the list of map power names. """ __slots__ = [] class GetPlayablePowers(_AbstractChannelRequest): - """ GetPlayablePowers request. - Expected response: responses.DataPowerNames - Expected response handler result: [power names] + """ Channel request to get the list of playable powers for a game. + A playable power is a dummy (uncontrolled) power not yet eliminated. + + :param game_id: ID of game to get playable powers + :type game_id: str + :return: + + - Server: :class:`.DataPowerNames` + - Client: set of playable power names for given game ID. """ __slots__ = ['game_id'] params = { @@ -226,9 +375,32 @@ class GetPlayablePowers(_AbstractChannelRequest): super(GetPlayablePowers, self).__init__(**kwargs) class JoinGame(_AbstractChannelRequest): - """ JoinGame request. - Expected response: responses.DataGame - Expected response handler result: diplomacy.client.network_game.NetworkGame + """ Channel request to join a game. + + :param game_id: ID of game to join + :param power_name: if provided, name of power to control. Otherwise, + user wants to observe game without playing. + :param registration_password: password to join game. If omitted while + game requires a password, server will return an error. + :type game_id: str + :type power_name: str, optional + :type registration_password: str, optional + :return: + + - Server: :class:`.DataGame` + - Client: a :class:`.NetworkGame` object representing the client game, which is either: + + - a power game (if power name was given), meaning that this network game allows user + to play a power + - an observer game, if power was not given and user does not have omniscient privileges + for this game. Observer role allows user to watch game phases changes, orders submitted + and orders results for each phase, but he can not see user messages and he can not + send any request that requires game master privileges. + - an omniscient game, if power was not given and user does have game master privileges. + Omniscient role allows user to see everything in the game, including user messages. + If user does only have omniscient privileges for this game, he can't do anything more, + If he does have up to game master privileges, then he can also send requests that + require game master privileges. """ __slots__ = ['game_id', 'power_name', 'registration_password'] params = { @@ -244,10 +416,22 @@ class JoinGame(_AbstractChannelRequest): super(JoinGame, self).__init__(**kwargs) class JoinPowers(_AbstractChannelRequest): - """ JoinPowers request to join many powers of a game with one query. - Useful to control many powers while still working only with 1 client game instance. - Expected response: responses.Ok - Expected response handler result: None + """ Channel request to join many powers of a game with one request. + + This request is mostly identical to :class:`.JoinGame`, except that list of power names + is mandatory. It's useful to allow the user to control many powers while still working + with 1 client game instance. + + :param game_id: ID of game to join + :param power_names: list of power names to join + :param registration_password: password to join the game + :type game_id: str + :type power_names: list, optional + :type registration_password: str, optionl + :return: None. If request succeeds, then the user is registered as player for all + given power names. The user can then simply join game to one of these powers (by sending + a :class:`.JoinGame` request), and he will be able to manage all the powers through + the client game returned by :class:`.JoinGame`. """ __slots__ = ['game_id', 'power_names', 'registration_password'] params = { @@ -263,9 +447,23 @@ class JoinPowers(_AbstractChannelRequest): super(JoinPowers, self).__init__(**kwargs) class ListGames(_AbstractChannelRequest): - """ ListGames request. - Expected response: responses.DataGames - Expected response handler result: responses.DataGames + """ Channel request to find games. + + :param game_id: if provided, look for games with game ID either containing or contained into this game ID. + :param status: if provided, look for games with this status. + :param map_name: if provided, look for games with this map name. + :param include_protected: (default True) tell if we must look into games protected by a password + :param for_omniscience: (default False) tell if we look for games where request user can be at least omniscient. + :type game_id: str, optional + :type status: str, optional + :type map_name: str, optional + :type include_protected: bool optional + :type for_omniscience: bool, optional + :return: + + - Server: :class:`.DataGames` + - Client: a list of :class:`.DataGameInfo` objects, each containing + a bunch of information about a game found. If no game found, list will be empty. """ __slots__ = ['game_id', 'status', 'map_name', 'include_protected', 'for_omniscience'] params = { @@ -285,36 +483,53 @@ class ListGames(_AbstractChannelRequest): super(ListGames, self).__init__(**kwargs) class GetGamesInfo(_AbstractChannelRequest): - """ Request used to get info for a given list of game IDs. - Expected response: responses.DataGames - Expected response handler result: responses.DataGames + """ Channel request to get information for a given list of game indices. + + :param games: list of game ID. + :type games: list + :return: + + - Server: :class:`.DataGames` + - Client: a list of :class:`.DataGameInfo` objects. """ __slots__ = ['games'] params = { strings.GAMES: parsing.SequenceType(str) } + def __init__(self, **kwargs): self.games = [] super(GetGamesInfo, self).__init__(**kwargs) class Logout(_AbstractChannelRequest): - """ Logout request. - Expected response: responses.Ok - Expected response handler result: None - """ + """ Channel request to logout. Returns nothing. """ __slots__ = [] class UnknownToken(_AbstractChannelRequest): - """ Request to tell server that a channel token is unknown. - Expected response: Nothing - Client does not even wait for a server response. - Expected response handler result: None + """ Channel request to tell server that a channel token is unknown. + + .. note:: + + Client does not even wait for a server response when sending this request, + which acts more like a "client notification" sent to server. """ __slots__ = [] class SetGrade(_AbstractChannelRequest): - """ SetGrade request. - Expected response: responses.Ok - Expected response handler result: None + """ Channel request to modify the grade of a user. + Require admin privileges to change admin grade, and at least game master privileges + to change omniscient or moderator grade. + + :param grade: grade to update (``'omniscient'``, ``'admin'`` or ``'moderator'``) + :param grade_update: how to make update (``'promote'`` or ``'demote'``) + :param username: user for which the grade must be modified + :param game_id: ID of game for which the grade must be modified. + Required only for ``'moderator'`` and ``'omniscient'`` grade. + :type grade: str + :type grade_update: str + :type username: str + :type game_id: str, optional + :return: None """ __slots__ = ['grade', 'grade_update', 'username', 'game_id'] params = { @@ -336,9 +551,11 @@ class SetGrade(_AbstractChannelRequest): # ============== class ClearCenters(_AbstractGameRequest): - """ ClearCenters request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to clear supply centers. See method :meth:`.Game.clear_centers`. + + :param power_name: if given, clear centers for this power. Otherwise, clear centers for all powers. + :type power_name: str, optional + :return: None """ __slots__ = ['power_name'] params = { @@ -350,9 +567,11 @@ class ClearCenters(_AbstractGameRequest): super(ClearCenters, self).__init__(**kwargs) class ClearOrders(_AbstractGameRequest): - """ ClearOrders request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to clear orders. + + :param power_name: if given, clear orders for this power. Otherwise, clear orders for all powers. + :type power_name: str, optional + :return: None """ __slots__ = ['power_name'] params = { @@ -364,9 +583,11 @@ class ClearOrders(_AbstractGameRequest): super(ClearOrders, self).__init__(**kwargs) class ClearUnits(_AbstractGameRequest): - """ ClearUnits request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to clear units. + + :param power_name: if given, clear units for this power. Otherwise, clear units for all powers. + :type power_name: str, optional + :return: None """ __slots__ = ['power_name'] params = { @@ -378,25 +599,31 @@ class ClearUnits(_AbstractGameRequest): super(ClearUnits, self).__init__(**kwargs) class DeleteGame(_AbstractGameRequest): - """ DeleteGame request. - Expected response: responses.Ok - Expected response handler result: None - """ + """ Game request to delete a game. Require game master privileges. Returns nothing. """ __slots__ = [] phase_dependent = False class GetAllPossibleOrders(_AbstractGameRequest): - """ GetAllPossibleOrders request. - Expected response: response.DataPossibleOrders - Expected response handler result: response.DataPossibleOrders + """ Game request to get all possible orders. + Return (server and client) a :class:`.DataPossibleOrders` object + containing possible orders and orderable locations. """ __slots__ = [] class GetPhaseHistory(_AbstractGameRequest): - """ Get a list of game phase data from game history for given phases interval. + """ Game request to get a list of game phase data from game history for given phases interval. A phase can be either None, a phase name (string) or a phase index (integer). - Expected response: responses.DataGamePhases - Expected response handler result: [GamePhaseData objects] + See :meth:`.Game.get_phase_history` about how phases are used to retrieve game phase data. + + :param from_phase: phase from which to look in game history + :param to_phase: phase up to which to look in game history + :type from_phase: str | int, optional + :type to_phase: str | int, optional + :return: + + - Server: DataGamePhases + - Client: a list of :class:`.GamePhaseData` objects corresponding to game phases + found between ``from_phase`` and ``to_phase`` in game history. """ __slots__ = ['from_phase', 'to_phase'] params = { @@ -411,37 +638,44 @@ class GetPhaseHistory(_AbstractGameRequest): super(GetPhaseHistory, self).__init__(**kwargs) class LeaveGame(_AbstractGameRequest): - """ LeaveGame request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to leave a game (logout from game). If request power name is set + (ie. request user was a player), then power will become uncontrolled. + Otherwise, user will be signed out from its observer (or omniscient) role. + Returns nothing. """ __slots__ = [] class ProcessGame(_AbstractGameRequest): - """ ProcessGame request. - Expected response: responses.Ok - Expected response handler result: None - """ + """ Game request to force a game processing. Require master privileges. Return nothing. """ __slots__ = [] class QuerySchedule(_AbstractGameRequest): - """ Query server for info about current scheduling for a game. - Expected response: response.DataGameSchedule - Expected response handler result: response.DataGameSchedule + """ Game request to get info about current scheduling for a game in server. + Returns (server and client) a :class:`.DataGameSchedule` object. """ __slots__ = [] class SaveGame(_AbstractGameRequest): - """ Get game saved format in JSON. - Expected response: response.DataSavedGame - Expected response handler result: response.DataSavedGame + """ Game request to get game exported in JSON format. + + :return: + + - Server: :class:`.DataSavedGame` + - Client: dict - the JSON dictionary. """ __slots__ = [] class SendGameMessage(_AbstractGameRequest): - """ SendGameMessage request. - Expected response: responses.DataTimeStamp - Expected response handler result: None + """ Game message to send a user request. + + :param message: message to send. See :class:`.Message` for more info. + message sender must be request user role (ie. power role, in such case). + Message time sent must not be defined, it will be allocated by server. + :type message: Message + :return: + + - Server: :class:`.DataTimeStamp` + - Client: nothing (returned timestamp is just used to update message locally) """ __slots__ = ['message'] params = { @@ -453,9 +687,15 @@ class SendGameMessage(_AbstractGameRequest): super(SendGameMessage, self).__init__(**kwargs) class SetDummyPowers(_AbstractGameRequest): - """ SetDummyPowers request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to set dummy powers. Require game master privileges. + If given powers are controlled, related players are kicked + and powers become dummy (uncontrolled). + + :param power_names: list of power names to set dummy. If not provided, will be all map power names. + :param username: if provided, only power names controlled by this user will be set dummy. + :type power_names: list, optional + :type user_name: str, optional + :return: None """ __slots__ = ['username', 'power_names'] params = { @@ -469,9 +709,17 @@ class SetDummyPowers(_AbstractGameRequest): super(SetDummyPowers, self).__init__(**kwargs) class SetGameState(_AbstractGameRequest): - """ Request to set a game state. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to set a game state (for exper users). Require game master privileges. + + :param state: game state + :param orders: dictionary mapping a power name to a list of orders strings + :param results: dictionary mapping a unit to a list of order result strings + :param messages: dictionary mapping a timestamp to a message + :type state: dict + :type orders: dict + :type results: dict + :type messages: dict + :return: None """ __slots__ = ['state', 'orders', 'results', 'messages'] params = { @@ -489,9 +737,18 @@ class SetGameState(_AbstractGameRequest): super(SetGameState, self).__init__(**kwargs) class SetGameStatus(_AbstractGameRequest): - """ SetGameStatus request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to force game status (only if new status differs from previous one). + Require game master privileges. + + :param status: game status to set. + Either ``'forming'``, ``'active'``, ``'paused'``, ``'completed'`` or ``'canceled'``. + + - If new status is ``'completed'``, game will be forced to draw. + - If new status is ``'active'``, game will be forced to start. + - If new status is ``'paused'``, game will be forced to pause. + - If new status is ``'canceled'``, game will be canceled and become invalid. + :type status: str + :return: None """ __slots__ = ['status'] params = { @@ -503,9 +760,16 @@ class SetGameStatus(_AbstractGameRequest): super(SetGameStatus, self).__init__(**kwargs) class SetOrders(_AbstractGameRequest): - """ SetOrders request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to set orders for a power. + + :param power_name: power name. If not given, request user must be a game player, + and power is inferred from request game role. + :param orders: list of power orders. + :param wait: if provided, wait flag to set for this power. + :type power_name: str, optional + :type orders: list + :type wait: bool, optional + :return: None """ __slots__ = ['power_name', 'orders', 'wait'] params = { @@ -521,9 +785,14 @@ class SetOrders(_AbstractGameRequest): super(SetOrders, self).__init__(**kwargs) class SetWaitFlag(_AbstractGameRequest): - """ SetWaitFlag request. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to set orders for a power. + + :param power_name: power name. If not given, request user must be a game player, + and power if inferred from request game role. + :param wait: wait flag to set. + :type power_name: str, optional + :type wait: bool + :return: None """ __slots__ = ['power_name', 'wait'] params = { @@ -537,9 +806,13 @@ class SetWaitFlag(_AbstractGameRequest): super(SetWaitFlag, self).__init__(**kwargs) class Synchronize(_AbstractGameRequest): - """ Synchronize request. - Expected response: responses.DataGameInfo - Expected response handler result: DataGameInfo + """ Game request to force synchronization of client game with server game. + If necessary, server will send appropriate notifications to client game so that it can + be up to date with server game state. + + :param timestamp: timestamp since which client game needs to synchronize. + :type timestamp: int + :return: (server and client) a :class:`.DataGameInfo` object. """ __slots__ = ['timestamp'] params = { @@ -552,11 +825,18 @@ class Synchronize(_AbstractGameRequest): super(Synchronize, self).__init__(**kwargs) class Vote(_AbstractGameRequest): - """ Vote request. - For powers only. - Allow a power to vote about game draw for current phase. - Expected response: responses.Ok - Expected response handler result: None + """ Game request to vote for draw decision. + If number of pro-draw votes > number of con-draw votes for current phase, + then server will automatically draw the game and send appropriate notifications. + Votes are reset after a game processing. + + :param power_name: power name who wants to vote. If not provided, request user must be a game player, + and power name will be inferred from request game role. + :param vote: vote to set. Either ``'yes'`` (power votes for draw), ``'no'`` (power votes against draw), + or ``'neutral'`` (power does not want to decide). + :type power_name: str, optional + :type vote: str + :return: None """ __slots__ = ['power_name', 'vote'] params = { @@ -570,7 +850,9 @@ class Vote(_AbstractGameRequest): super(Vote, self).__init__(**kwargs) def parse_dict(json_request): - """ Parse a JSON dictionary expected to represent a request. Raise an exception if parsing failed. + """ Parse a JSON dictionary expected to represent a request. + Raise an exception if parsing failed. + :param json_request: JSON dictionary. :return: a request class instance. :type json_request: dict diff --git a/diplomacy/communication/responses.py b/diplomacy/communication/responses.py index 6353150..579b576 100644 --- a/diplomacy/communication/responses.py +++ b/diplomacy/communication/responses.py @@ -14,15 +14,15 @@ # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . # ============================================================================== -""" Server -> Client responses sent by server when it received a request. """ +""" Server -> Client responses sent by server as replies to requests. """ import inspect from diplomacy.engine.game import Game from diplomacy.utils import common, parsing, strings -from diplomacy.utils.scheduler_event import SchedulerEvent -from diplomacy.utils.exceptions import ResponseException -from diplomacy.utils.network_data import NetworkData +from diplomacy.utils import exceptions from diplomacy.utils.game_phase_data import GamePhaseData +from diplomacy.utils.network_data import NetworkData +from diplomacy.utils.scheduler_event import SchedulerEvent class _AbstractResponse(NetworkData): """ Base response object """ @@ -38,22 +38,42 @@ class _AbstractResponse(NetworkData): super(_AbstractResponse, self).__init__(**kwargs) class Error(_AbstractResponse): - """ Error response sent when an error occurred on server-side while handling a request. """ - __slots__ = ['message'] + """ Error response sent when an error occurred on server-side while handling a request. + + Properties: + + - **error_type**: str - error type, containing the exception class name. + - **message**: str - error message + """ + __slots__ = ['message', 'error_type'] params = { - strings.MESSAGE: str + strings.MESSAGE: str, + strings.ERROR_TYPE: str } def __init__(self, **kwargs): self.message = None + self.error_type = None super(Error, self).__init__(**kwargs) + def throw(self): + """ Convert this error to an instance of a Diplomacy ResponseException class and raises it. """ + # If error type is the name of a ResponseException class, + # convert it to related class and raise it. + if hasattr(exceptions, self.error_type): + symbol = getattr(exceptions, self.error_type) + if inspect.isclass(symbol) and issubclass(symbol, exceptions.ResponseException): + raise symbol(self.message) + + # Otherwise, raise a generic ResponseException object. + raise exceptions.ResponseException('%s/%s' % (self.error_type, self.message)) + class Ok(_AbstractResponse): - """ Ok response sent by default after handling a request. """ + """ Ok response sent by default after handling a request. Contains nothing. """ __slots__ = [] class NoResponse(_AbstractResponse): - """ Indicates that no responses are required """ + """ Placeholder response to indicate that no responses are required """ __slots__ = [] def __bool__(self): @@ -61,7 +81,14 @@ class NoResponse(_AbstractResponse): return False class DataGameSchedule(_AbstractResponse): - """ Response with info about current scheduling for a game. """ + """ Response with info about current scheduling for a game. + + Properties: + + - **game_id**: str - game ID + - **phase**: str - game phase + - **schedule**: :class:`.SchedulerEvent` - scheduling information about the game + """ __slots__ = ['game_id', 'phase', 'schedule'] params = { 'game_id': str, @@ -76,18 +103,38 @@ class DataGameSchedule(_AbstractResponse): super(DataGameSchedule, self).__init__(**kwargs) class DataGameInfo(_AbstractResponse): - """ Response containing information about a game, to be used when no entire game object is required. """ - __slots__ = ['game_id', 'phase', 'timestamp', 'map_name', 'rules', 'status', 'n_players', 'n_controls', - 'deadline', 'registration_password', 'observer_level', 'controlled_powers', - 'timestamp_created'] + """ Response containing information about a game, to be used when no entire game object is required. + + Properties: + + - **game_id**: game ID + - **phase**: game phase + - **timestamp**: latest timestamp when data was saved into game on server + (ie. game state or message) + - **timestamp_created**: timestamp when game was created on server + - **map_name**: (optional) game map name + - **observer_level**: (optional) highest observer level allowed for the user who sends + the request. Either ``'observer_type'``, ``'omniscient_type'`` or ``'master_type'``. + - **controlled_powers**: (optional) list of power names controlled by the user who sends + the request. + - **rules**: (optional) game rules + - **status**: (optional) game status + - **n_players**: (optional) number of powers currently controlled in the game + - **n_controls**: (optional) number of controlled powers required by the game to be active + - **deadline**: (optional) game deadline - time to wait before processing a game phase + - **registration_password**: (optional) boolean - if True, a password is required to join the game + """ + __slots__ = ['game_id', 'phase', 'timestamp', 'map_name', 'rules', 'status', 'n_players', + 'n_controls', 'deadline', 'registration_password', 'observer_level', + 'controlled_powers', 'timestamp_created'] params = { strings.GAME_ID: str, strings.PHASE: str, strings.TIMESTAMP: int, strings.TIMESTAMP_CREATED: int, strings.MAP_NAME: parsing.OptionalValueType(str), - strings.OBSERVER_LEVEL: parsing.OptionalValueType( - parsing.EnumerationType((strings.MASTER_TYPE, strings.OMNISCIENT_TYPE, strings.OBSERVER_TYPE))), + strings.OBSERVER_LEVEL: parsing.OptionalValueType(parsing.EnumerationType( + (strings.MASTER_TYPE, strings.OMNISCIENT_TYPE, strings.OBSERVER_TYPE))), strings.CONTROLLED_POWERS: parsing.OptionalValueType(parsing.SequenceType(str)), strings.RULES: parsing.OptionalValueType(parsing.SequenceType(str)), strings.STATUS: parsing.OptionalValueType(parsing.EnumerationType(strings.ALL_GAME_STATUSES)), @@ -98,24 +145,28 @@ class DataGameInfo(_AbstractResponse): } def __init__(self, **kwargs): - self.game_id = None # type: str - self.phase = None # type: str - self.timestamp = None # type: int - self.timestamp_created = None # type: int - self.map_name = None # type: str - self.observer_level = None # type: str - self.controlled_powers = None # type: list - self.rules = None # type: list - self.status = None # type: str - self.n_players = None # type: int - self.n_controls = None # type: int - self.deadline = None # type: int - self.registration_password = None # type: bool + self.game_id = None # type: str + self.phase = None # type: str + self.timestamp = None # type: int + self.timestamp_created = None # type: int + self.map_name = None # type: str + self.observer_level = None # type: str + self.controlled_powers = None # type: list + self.rules = None # type: list + self.status = None # type: str + self.n_players = None # type: int + self.n_controls = None # type: int + self.deadline = None # type: int + self.registration_password = None # type: bool super(DataGameInfo, self).__init__(**kwargs) class DataPossibleOrders(_AbstractResponse): - """ Response containing a dict of all possibles orders per location and a dict of all orderable locations per power - for a game phase. + """ Response containing information about possible orders for a game at its current phase. + + Properties: + + - **possible_orders**: dictionary mapping a location short name to all possible orders here + - **orderable_locations**: dictionary mapping a power name to its orderable locations """ __slots__ = ['possible_orders', 'orderable_locations'] params = { @@ -124,15 +175,17 @@ class DataPossibleOrders(_AbstractResponse): # {power name => [locations]} strings.ORDERABLE_LOCATIONS: parsing.DictType(str, parsing.SequenceType(str)), } + def __init__(self, **kwargs): self.possible_orders = {} self.orderable_locations = {} super(DataPossibleOrders, self).__init__(**kwargs) class UniqueData(_AbstractResponse): - """ Response containing only 1 field named `data`. - `params` must have exactly one field named DATA. + """ Response containing only 1 field named ``data``. A derived class will contain + a specific typed value in this field. """ + # `params` must have exactly one field named DATA. __slots__ = ['data'] @classmethod @@ -151,7 +204,8 @@ class DataToken(UniqueData): } class DataMaps(UniqueData): - """ Unique data containing maps info. """ + """ Unique data containing maps info + (dictionary mapping a map name to a dictionary with map information). """ __slots__ = [] params = { # {map_id => {'powers': [power names], 'supply centers' => [supply centers], 'loc_type' => {loc => type}}} @@ -166,49 +220,50 @@ class DataPowerNames(UniqueData): } class DataGames(UniqueData): - """ Unique data containing a list of game info objects. """ + """ Unique data containing a list of :class:`.DataGameInfo` objects. """ __slots__ = [] params = { - strings.DATA: parsing.SequenceType(parsing.JsonableClassType(DataGameInfo)) # list of game info. + # list of game info. + strings.DATA: parsing.SequenceType(parsing.JsonableClassType(DataGameInfo)) } class DataPort(UniqueData): - """ Unique data containing a DAIDE port. """ + """ Unique data containing a DAIDE port (integer). """ __slots__ = [] params = { strings.DATA: int # DAIDE port } class DataTimeStamp(UniqueData): - """ Unique data containing a timestamp. """ + """ Unique data containing a timestamp (integer). """ __slots__ = [] params = { strings.DATA: int # microseconds } class DataGamePhases(UniqueData): - """ Unique data containing a list of GamePhaseData objects. """ + """ Unique data containing a list of :class:`.GamePhaseData` objects. """ __slots__ = [] params = { strings.DATA: parsing.SequenceType(parsing.JsonableClassType(GamePhaseData)) } class DataGame(UniqueData): - """ Unique data containing a Game object. """ + """ Unique data containing a :class:`.Game` object. """ __slots__ = [] params = { strings.DATA: parsing.JsonableClassType(Game) } class DataSavedGame(UniqueData): - """ Unique data containing a game saved in JSON format. """ + """ Unique data containing a game saved in JSON dictionary. """ __slots__ = [] params = { strings.DATA: dict } class DataGamesToPowerNames(UniqueData): - """ Unique data containing a dict of game IDs associated to sequences of power names. """ + """ Unique data containing a dictionary mapping a game ID to a list of power names. """ __slots__ = [] params = { strings.DATA: parsing.DictType(str, parsing.SequenceType(str)) @@ -217,20 +272,22 @@ class DataGamesToPowerNames(UniqueData): def parse_dict(json_response): """ Parse a JSON dictionary expected to represent a response. Raise an exception if either: + - parsing failed - response received is an Error response. In such case, a ResponseException is raised with the error message. + :param json_response: a JSON dict. :return: a Response class instance. """ assert isinstance(json_response, dict), 'Response parser expects a dict.' name = json_response.get(strings.NAME, None) if name is None: - raise ResponseException() + raise exceptions.ResponseException() expected_class_name = common.snake_case_to_upper_camel_case(name) response_class = globals()[expected_class_name] assert inspect.isclass(response_class) and issubclass(response_class, _AbstractResponse) response_object = response_class.from_dict(json_response) if isinstance(response_object, Error): - raise ResponseException('%s: %s' % (response_object.name, response_object.message)) + response_object.throw() return response_object diff --git a/diplomacy/daide/clauses.py b/diplomacy/daide/clauses.py index 9815527..bd3ab98 100644 --- a/diplomacy/daide/clauses.py +++ b/diplomacy/daide/clauses.py @@ -30,7 +30,7 @@ def break_next_group(daide_bytes): e.g. bytes for ENG AMY PAR would return -> '' + ENG AMY PAR since the byte array does not start with a "(" :return: A tuple consisting of the parenthesis group and the remaining bytes after the group - or an empty byte array and the entire byte array if the byte array does not start with a parenthesis + or an empty byte array and the entire byte array if the byte array does not start with a parenthesis """ if not daide_bytes: return b'', b'' @@ -69,6 +69,7 @@ def strip_parentheses(daide_bytes): def parse_bytes(clause_constructor, daide_bytes, on_error='raise'): """ Creates a clause object from a string of bytes + :param clause_constructor: The type of clause to build :param daide_bytes: The bytes to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') @@ -83,6 +84,7 @@ def parse_bytes(clause_constructor, daide_bytes, on_error='raise'): def parse_string(clause_constructor, string, on_error='raise'): """ Creates a clause object from a string + :param clause_constructor: The type of clause to build :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') @@ -114,6 +116,7 @@ class AbstractClause(metaclass=ABCMeta): @abstractmethod def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -123,6 +126,7 @@ class AbstractClause(metaclass=ABCMeta): @abstractmethod def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -130,6 +134,7 @@ class AbstractClause(metaclass=ABCMeta): def error(self, on_error, message=''): """ Performs the error action + :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :param message: The message to display """ @@ -141,9 +146,7 @@ class AbstractClause(metaclass=ABCMeta): self._is_valid = False class SingleToken(AbstractClause): - """ Extracts a single token - e.g. NME - """ + """ Extracts a single token (e.g. NME) """ def __init__(self): """ Constructor """ super(SingleToken, self).__init__() @@ -160,6 +163,7 @@ class SingleToken(AbstractClause): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -178,6 +182,7 @@ class SingleToken(AbstractClause): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -205,6 +210,7 @@ class Power(SingleToken): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -215,6 +221,7 @@ class Power(SingleToken): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -241,6 +248,7 @@ class String(AbstractClause): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -260,6 +268,7 @@ class String(AbstractClause): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -290,6 +299,7 @@ class Number(AbstractClause): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -311,6 +321,7 @@ class Number(AbstractClause): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -319,8 +330,10 @@ class Number(AbstractClause): class Province(AbstractClause): """ Each clause is an province token - Syntax: ADR - (STP ECS) + Syntax: + + - ADR + - (STP ECS) """ _alias_from_bytes = {'ECS': '/EC', 'NCS': '/NC', @@ -347,6 +360,7 @@ class Province(AbstractClause): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -381,6 +395,7 @@ class Province(AbstractClause): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -433,6 +448,7 @@ class Turn(AbstractClause): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -460,6 +476,7 @@ class Turn(AbstractClause): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -483,6 +500,7 @@ class UnitType(SingleToken): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -493,6 +511,7 @@ class UnitType(SingleToken): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -530,6 +549,7 @@ class Unit(AbstractClause): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -559,6 +579,7 @@ class Unit(AbstractClause): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -610,6 +631,7 @@ class OrderType(SingleToken): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -620,6 +642,7 @@ class OrderType(SingleToken): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build this clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ @@ -628,6 +651,7 @@ class OrderType(SingleToken): def parse_order_to_bytes(phase_type, order_split): """ Builds an order clause from a byte array + :param phase_type: The game phase :param order_split: An instance of diplomacy.utils.subject_split.OrderSplit :return: The order clause's bytes @@ -720,6 +744,7 @@ class Order(AbstractClause): def from_bytes(self, daide_bytes, on_error='raise'): """ Builds the clause from a byte array + :param daide_bytes: The bytes to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') :return: The remaining (unparsed) bytes @@ -796,6 +821,7 @@ class Order(AbstractClause): def from_string(self, string, on_error='raise'): """ Builds the clause from a string + :param string: The string to use to build the clause :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore') """ diff --git a/diplomacy/daide/connection_handler.py b/diplomacy/daide/connection_handler.py index 0b606bf..ab8d6cd 100644 --- a/diplomacy/daide/connection_handler.py +++ b/diplomacy/daide/connection_handler.py @@ -29,9 +29,12 @@ from diplomacy.utils import exceptions # Constants LOGGER = logging.getLogger(__name__) -class ConnectionHandler(): - """ ConnectionHandler class. Properties: - - server: server object representing running server. +class ConnectionHandler: + """ ConnectionHandler class. + + Properties: + + - **server**: server object representing running server. """ _NAME_VARIANT_PREFIX = 'DAIDE' _NAME_VARIANTS_POOL = [] @@ -54,6 +57,7 @@ class ConnectionHandler(): def initialize(self, stream, server, game_id): """ Initialize the connection handler. + :param server: a Server object. :type server: diplomacy.Server """ @@ -152,6 +156,7 @@ class ConnectionHandler(): def translate_notification(self, notification): """ Translate a notification to a DAIDE notification. + :param notification: a notification object to pass to handler function. See diplomacy.communication.notifications for possible notifications. :return: either None or an array of daide notifications. diff --git a/diplomacy/daide/messages.py b/diplomacy/daide/messages.py index d192d6e..30a037f 100644 --- a/diplomacy/daide/messages.py +++ b/diplomacy/daide/messages.py @@ -69,6 +69,7 @@ class DaideMessage(metaclass=ABCMeta): @gen.coroutine def from_stream(stream): """ Builds a message from the stream + :param stream: An opened Tornado stream. :type stream: tornado.iostream.BaseIOStream """ diff --git a/diplomacy/daide/notification_managers.py b/diplomacy/daide/notification_managers.py index f226a27..6f0957f 100644 --- a/diplomacy/daide/notification_managers.py +++ b/diplomacy/daide/notification_managers.py @@ -28,6 +28,7 @@ from diplomacy.utils import strings, splitter def _build_active_notifications(current_phase, powers, map_name, deadline): """ Build the list of notifications corresponding to an active game state + :param current_phase: the current phase :param powers: the list of game's powers :param map_name: the map name @@ -52,6 +53,7 @@ def _build_active_notifications(current_phase, powers, map_name, deadline): def _build_completed_notifications(server_users, has_draw_vote, powers, state_history): """ Build the list of notifications corresponding to a completed game state + :param server_users: the instance of `diplomacy.server.users` of the game's server :param has_draw_vote: true if the game has completed due to a draw vote :param powers: the list of game's powers @@ -88,6 +90,7 @@ def _build_completed_notifications(server_users, has_draw_vote, powers, state_hi def on_processed_notification(server, notification, connection_handler, game): """ Build the list of notifications for a game processed event + :param server: server which receives the request :param notification: internal notification :param connection_handler: connection handler from which the request was sent @@ -130,6 +133,7 @@ def on_processed_notification(server, notification, connection_handler, game): def on_status_update_notification(server, notification, connection_handler, game): """ Build the list of notificaitons for a status update event + :param server: server which receives the request :param notification: internal notification :param connection_handler: connection handler from which the request was sent @@ -159,11 +163,12 @@ def on_status_update_notification(server, notification, connection_handler, game def on_message_received_notification(server, notification, connection_handler, game): """ Build the list of notificaitons for a message received event + :param server: server which receives the request :param notification: internal notification :param connection_handler: connection handler from which the request was sent :param game: the game - :return: list of notificaitons + :return: list of notifications """ del server, connection_handler, game # Unused args notifs = [] @@ -173,6 +178,7 @@ def on_message_received_notification(server, notification, connection_handler, g def translate_notification(server, notification, connection_handler): """ Find notification handler function for associated notification, run it and return its result. + :param server: a Server object to pass to handler function. :param notification: a notification object to pass to handler function. See diplomacy.communication.notifications for possible notifications. diff --git a/diplomacy/daide/notifications.py b/diplomacy/daide/notifications.py index e9f6366..625c46a 100644 --- a/diplomacy/daide/notifications.py +++ b/diplomacy/daide/notifications.py @@ -22,7 +22,7 @@ from diplomacy.daide import tokens from diplomacy.daide.tokens import Token from diplomacy.daide.utils import bytes_to_str, str_to_bytes -class DaideNotification(): +class DaideNotification: """ Represents a DAIDE response. """ def __init__(self, **kwargs): """ Constructor """ @@ -48,7 +48,9 @@ class DaideNotification(): class MapNameNotification(DaideNotification): """ Represents a MAP DAIDE response. Sends the name of the current map to the client. - Syntax: + + Syntax: :: + MAP ('name') """ def __init__(self, map_name, **kwargs): @@ -60,19 +62,26 @@ class MapNameNotification(DaideNotification): + bytes(parse_string(String, map_name)) class HelloNotification(DaideNotification): - """ Represents a HLO DAIDE response. Sends the power to be played by the client with the passcode to rejoin the - game and the details of the game. - Syntax: + """ Represents a HLO DAIDE response. Sends the power to be played by the client with the + passcode to rejoin the game and the details of the game. + + Syntax: :: + HLO (power) (passcode) (variant) (variant) ... - Variant syntax: + + Variant syntax: :: + LVL n # Level of the syntax accepted MTL seconds # Movement time limit RTL seconds # Retreat time limit BTL seconds # Build time limit DSD # Disables the time limit when a client disconects AOA # Any orders accepted + LVL 10: - Variant syntax: + + Variant syntax: :: + PDA # Accept partial draws NPR # No press during retreat phases NPB # No press during build phases @@ -80,6 +89,7 @@ class HelloNotification(DaideNotification): """ def __init__(self, power_name, passcode, level, deadline, rules, **kwargs): """ Builds the response + :param power_name: The name of the power being played. :param passcode: Integer. A passcode to rejoin the game. :param level: Integer. The daide syntax level of the game @@ -109,11 +119,14 @@ class HelloNotification(DaideNotification): class SupplyCenterNotification(DaideNotification): """ Represents a SCO DAIDE notification. Sends the current supply centre ownership. - Syntax: + + Syntax: :: + SCO (power centre centre ...) (power centre centre ...) ... """ def __init__(self, powers_centers, map_name, **kwargs): """ Builds the notification + :param powers_centers: A dict of {power_name: centers} objects :param map_name: The name of the map """ @@ -150,14 +163,19 @@ class SupplyCenterNotification(DaideNotification): class CurrentPositionNotification(DaideNotification): """ Represents a NOW DAIDE notification. Sends the current turn, and the current unit positions. - Syntax: + + Syntax: :: + NOW (turn) (unit) (unit) ... - Unit syntax: + + Unit syntax: :: + power unit_type province power unit_type province MRT (province province ...) """ def __init__(self, phase_name, powers_units, powers_retreats, **kwargs): """ Builds the notification + :param phase_name: The name of the current phase (e.g. 'S1901M') :param powers: A list of `diplomacy.engine.power.Power` objects """ @@ -187,9 +205,11 @@ class CurrentPositionNotification(DaideNotification): self._bytes = bytes(tokens.NOW) + bytes(turn_clause) + b''.join(units_bytes_buffer) class MissingOrdersNotification(DaideNotification): - """ Represents a MIS DAIDE response. Sends the list of unit for which an order is missing or indication about - required disbands or builds. - Syntax: + """ Represents a MIS DAIDE response. Sends the list of unit for which an order is missing + or indication about required disbands or builds. + + Syntax: :: + MIS (unit) (unit) ... MIS (unit MRT (province province ...)) (unit MRT (province province ...)) ... MIS (number) @@ -268,10 +288,14 @@ class MissingOrdersNotification(DaideNotification): class OrderResultNotification(DaideNotification): """ Represents a ORD DAIDE response. Sends the result of an order after the turn has been processed. - Syntax: + + Syntax: :: + ORD (turn) (order) (result) ORD (turn) (order) (result RET) - Result syntax: + + Result syntax: :: + SUC # Order succeeded (can apply to any order). BNC # Move bounced (only for MTO, CTO or RTO orders). CUT # Support cut (only for SUP orders). @@ -281,6 +305,7 @@ class OrderResultNotification(DaideNotification): """ def __init__(self, phase_name, order_bytes, results, **kwargs): """ Builds the response + :param phase_name: The name of the current phase (e.g. 'S1901M') :param order_bytes: The bytes received for the order :param results: An array containing the error codes. @@ -299,7 +324,9 @@ class OrderResultNotification(DaideNotification): class TimeToDeadlineNotification(DaideNotification): """ Represents a TME DAIDE response. Sends the time to the next deadline. - Syntax: + + Syntax: :: + TME (seconds) """ def __init__(self, seconds, **kwargs): @@ -311,7 +338,9 @@ class TimeToDeadlineNotification(DaideNotification): class PowerInCivilDisorderNotification(DaideNotification): """ Represents a CCD DAIDE response. Sends the name of the power in civil disorder. - Syntax: + + Syntax: :: + CCD (power) """ def __init__(self, power_name, **kwargs): @@ -324,7 +353,9 @@ class PowerInCivilDisorderNotification(DaideNotification): class PowerIsEliminatedNotification(DaideNotification): """ Represents a OUT DAIDE response. Sends the name of the power eliminated. - Syntax: + + Syntax: :: + OUT (power) """ def __init__(self, power_name, **kwargs): @@ -337,7 +368,9 @@ class PowerIsEliminatedNotification(DaideNotification): class DrawNotification(DaideNotification): """ Represents a DRW DAIDE response. Indicates that the game has ended due to a draw - Syntax: + + Syntax: :: + DRW """ def __init__(self, **kwargs): @@ -348,7 +381,9 @@ class DrawNotification(DaideNotification): class MessageFromNotification(DaideNotification): """ Represents a FRM DAIDE response. Indicates that the game has ended due to a draw - Syntax: + + Syntax: :: + FRM (power) (power power ...) (press_message) FRM (power) (power power ...) (reply) """ @@ -367,7 +402,9 @@ class MessageFromNotification(DaideNotification): class SoloNotification(DaideNotification): """ Represents a SLO DAIDE response. Indicates that the game has ended due to a solo by the specified power - Syntax: + + Syntax: :: + SLO (power) """ def __init__(self, power_name, **kwargs): @@ -380,9 +417,13 @@ class SoloNotification(DaideNotification): class SummaryNotification(DaideNotification): """ Represents a SMR DAIDE response. Sends the summary for each power at the end of the game - Syntax: + + Syntax: :: + SMR (turn) (power_summary) ... - power_summary syntax: + + power_summary syntax: :: + power ('name') ('version') number_of_centres power ('name') ('version') number_of_centres year_of_elimination """ @@ -425,7 +466,9 @@ class SummaryNotification(DaideNotification): class TurnOffNotification(DaideNotification): """ Represents an OFF DAIDE response. Requests a client to exit - Syntax: + + Syntax: :: + OFF """ def __init__(self, **kwargs): diff --git a/diplomacy/daide/request_managers.py b/diplomacy/daide/request_managers.py index 085c5ef..8e3e5cb 100644 --- a/diplomacy/daide/request_managers.py +++ b/diplomacy/daide/request_managers.py @@ -35,6 +35,7 @@ from diplomacy.utils.order_results import OK @gen.coroutine def on_name_request(server, request, connection_handler, game): """ Manage NME request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -77,6 +78,7 @@ def on_name_request(server, request, connection_handler, game): def on_observer_request(server, request, connection_handler, game): """ Manage OBS request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -89,6 +91,7 @@ def on_observer_request(server, request, connection_handler, game): @gen.coroutine def on_i_am_request(server, request, connection_handler, game): """ Manage IAM request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -137,6 +140,7 @@ def on_i_am_request(server, request, connection_handler, game): def on_hello_request(server, request, connection_handler, game): """ Manage HLO request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -158,6 +162,7 @@ def on_hello_request(server, request, connection_handler, game): def on_map_request(server, request, connection_handler, game): """ Manage MAP request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -169,6 +174,7 @@ def on_map_request(server, request, connection_handler, game): def on_map_definition_request(server, request, connection_handler, game): """ Manage MDF request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -180,6 +186,7 @@ def on_map_definition_request(server, request, connection_handler, game): def on_supply_centre_ownership_request(server, request, connection_handler, game): """ Manage SCO request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -192,6 +199,7 @@ def on_supply_centre_ownership_request(server, request, connection_handler, game def on_current_position_request(server, request, connection_handler, game): """ Manage NOW request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -205,6 +213,7 @@ def on_current_position_request(server, request, connection_handler, game): def on_history_request(server, request, connection_handler, game): """ Manage HST request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -266,6 +275,7 @@ def on_history_request(server, request, connection_handler, game): @gen.coroutine def on_submit_orders_request(server, request, connection_handler, game): """ Manage SUB request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -328,6 +338,7 @@ def on_submit_orders_request(server, request, connection_handler, game): def on_missing_orders_request(server, request, connection_handler, game): """ Manage MIS request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -342,6 +353,7 @@ def on_missing_orders_request(server, request, connection_handler, game): @gen.coroutine def on_go_flag_request(server, request, connection_handler, game): """ Manage GOF request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -371,6 +383,7 @@ def on_go_flag_request(server, request, connection_handler, game): def on_time_to_deadline_request(server, request, connection_handler, game): """ Manage TME request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -383,6 +396,7 @@ def on_time_to_deadline_request(server, request, connection_handler, game): @gen.coroutine def on_draw_request(server, request, connection_handler, game): """ Manage DRW request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -404,6 +418,7 @@ def on_draw_request(server, request, connection_handler, game): @gen.coroutine def on_send_message_request(server, request, connection_handler, game): """ Manage SND request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -433,6 +448,7 @@ def on_send_message_request(server, request, connection_handler, game): @gen.coroutine def on_not_request(server, request, connection_handler, game): """ Manage NOT request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -489,6 +505,7 @@ def on_not_request(server, request, connection_handler, game): @gen.coroutine def on_accept_request(server, request, connection_handler, game): """ Manage YES request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -532,6 +549,7 @@ def on_accept_request(server, request, connection_handler, game): def on_reject_request(server, request, connection_handler, game): """ Manage REJ request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -550,6 +568,7 @@ def on_reject_request(server, request, connection_handler, game): def on_parenthesis_error_request(server, request, connection_handler, game): """ Manage PAR request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -560,6 +579,7 @@ def on_parenthesis_error_request(server, request, connection_handler, game): def on_syntax_error_request(server, request, connection_handler, game): """ Manage ERR request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -570,6 +590,7 @@ def on_syntax_error_request(server, request, connection_handler, game): def on_admin_message_request(server, request, connection_handler, game): """ Manage ADM request + :param server: server which receives the request :param request: request to manage :param connection_handler: connection handler from which the request was sent @@ -608,6 +629,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/daide/requests.py b/diplomacy/daide/requests.py index 0a47a52..b0e5a2b 100644 --- a/diplomacy/daide/requests.py +++ b/diplomacy/daide/requests.py @@ -22,11 +22,12 @@ from diplomacy.daide import tokens from diplomacy.daide.tokens import Token, is_ascii_token from diplomacy.utils import parsing, strings -class RequestBuilder(): +class RequestBuilder: """ Builds DaideRequest from bytes or tokens """ @staticmethod def from_bytes(daide_bytes, **kwargs): """ Builds a request from DAIDE bytes + :param daide_bytes: The bytes representation of a request :return: The DaideRequest built from the bytes """ @@ -82,7 +83,9 @@ class DaideRequest(_AbstractGameRequest): class NameRequest(DaideRequest): """ Represents a NME DAIDE request. Can be sent by the client as soon as it connects to the server. - Syntax: + + Syntax: :: + NME ('name') ('version') """ __slots__ = ['client_name', 'client_version'] @@ -114,7 +117,9 @@ class NameRequest(DaideRequest): class ObserverRequest(DaideRequest): """ Represents a NME DAIDE request. Can be sent by the client as soon as it connects to the server. - Syntax: + + Syntax: :: + OBS """ __slots__ = [] @@ -131,7 +136,9 @@ class ObserverRequest(DaideRequest): class IAmRequest(DaideRequest): """ Represents a IAM DAIDE request. Can be sent by the client at anytime to rejoin the game. - Syntax: + + Syntax: :: + IAM (power) (passcode) """ __slots__ = ['power_name', 'passcode'] @@ -173,7 +180,9 @@ class IAmRequest(DaideRequest): class HelloRequest(DaideRequest): """ Represents a HLO DAIDE request. Sent by the client to request a copy of the HLO message. - Syntax: + + Syntax: :: + HLO """ __slots__ = [] @@ -190,7 +199,9 @@ class HelloRequest(DaideRequest): class MapRequest(DaideRequest): """ Represents a MAP DAIDE request. Sent by the client to request a copy of the MAP message. - Syntax: + + Syntax: :: + MAP """ __slots__ = [] @@ -207,7 +218,9 @@ class MapRequest(DaideRequest): class MapDefinitionRequest(DaideRequest): """ Represents a MDF DAIDE request. Sent by the client to request the map definition of the game. - Syntax: + + Syntax: :: + MDF """ __slots__ = [] @@ -229,7 +242,9 @@ class MapDefinitionRequest(DaideRequest): class SupplyCentreOwnershipRequest(DaideRequest): """ Represents a SCO DAIDE request. Sent by the client to request a copy of the last SCO message. - Syntax: + + Syntax: :: + SCO """ __slots__ = [] @@ -246,7 +261,9 @@ class SupplyCentreOwnershipRequest(DaideRequest): class CurrentPositionRequest(DaideRequest): """ Represents a NOW DAIDE request. Sent by the client to request a copy of the last NOW message. - Syntax: + + Syntax: :: + NOW """ __slots__ = [] @@ -263,7 +280,9 @@ class CurrentPositionRequest(DaideRequest): class HistoryRequest(DaideRequest): """ Represents a HST DAIDE request. Sent by the client to request a copy of a previous ORD, SCO and NOW messages. - Syntax: + + Syntax: :: + HST (turn) """ def __init__(self, **kwargs): @@ -293,10 +312,14 @@ class HistoryRequest(DaideRequest): class SubmitOrdersRequest(DaideRequest): """ Represents a SUB DAIDE request. Sent by the client to submit orders. - Syntax: + + Syntax: :: + SUB (order) (order) ... SUB (turn) (order) (order) ... - order syntax: + + order syntax: :: + (unit) HLD # Hold (unit) MTO province # Move to (unit) SUP (unit) # Support @@ -343,7 +366,9 @@ class SubmitOrdersRequest(DaideRequest): class MissingOrdersRequest(DaideRequest): """ Represents a MIS DAIDE request. Sent by the client to request a copy of the current MIS message. - Syntax: + + Syntax: :: + MIS """ __slots__ = [] @@ -360,7 +385,9 @@ class MissingOrdersRequest(DaideRequest): class GoFlagRequest(DaideRequest): """ Represents a GOF DAIDE request. Sent by the client to notify that the client is ready to process the turn. - Syntax: + + Syntax: :: + GOF """ __slots__ = [] @@ -382,7 +409,9 @@ class GoFlagRequest(DaideRequest): class TimeToDeadlineRequest(DaideRequest): """ Represents a TME DAIDE request. Sent by the client to request a TME message or to request it at a later time. - Syntax: + + Syntax: :: + TME TME (seconds) """ @@ -421,9 +450,13 @@ class TimeToDeadlineRequest(DaideRequest): class DrawRequest(DaideRequest): """ Represents a DRW DAIDE request. Sent by the client to notify that the client would accept a draw. - Syntax: + + Syntax: :: + DRW - LVL 10: + + LVL 10: :: + DRW (power power ...) """ __slots__ = ['powers'] @@ -465,17 +498,23 @@ class DrawRequest(DaideRequest): class SendMessageRequest(DaideRequest): """ Represents a SND DAIDE request - Syntax: + + Syntax: :: + SND (power ...) (press_message) SND (power ...) (reply) SND (turn) (power ...) (press_message) SND (turn) (power ...) (reply) - Press message syntax: + + Press message syntax: :: + PRP (arrangement) CCL (press_message) FCT (arrangement) TRY (tokens) - Reply syntax: + + Reply syntax: :: + YES (press_message) REJ (press_message) BWX (press_message) @@ -531,7 +570,9 @@ class SendMessageRequest(DaideRequest): class NotRequest(DaideRequest): """ Represents a NOT DAIDE request. Sent by the client to cancel a previous request. - Syntax: + + Syntax: :: + NOT (SUB) # Cancel all submitted orders NOT (SUB (order)) # Cancel specific submitted order NOT (GOF) # Do not process orders until the deadline @@ -572,7 +613,9 @@ class NotRequest(DaideRequest): class AcceptRequest(DaideRequest): """ Represents a YES DAIDE request. - Syntax: + + Syntax: :: + YES (MAP ('name')) YES (SVE ('gamename')) """ @@ -602,7 +645,9 @@ class AcceptRequest(DaideRequest): class RejectRequest(DaideRequest): """ Represents a REJ DAIDE request. - Syntax: + + Syntax: :: + REJ (SVE ('gamename')) """ __slots__ = ['response_bytes'] @@ -635,7 +680,9 @@ class RejectRequest(DaideRequest): class ParenthesisErrorRequest(DaideRequest): """ Represents a PRN DAIDE request. Sent by the client to specify an error in the set of parenthesis. - Syntax: + + Syntax: :: + PRN (message) """ __slots__ = ['message_bytes'] @@ -668,7 +715,9 @@ class ParenthesisErrorRequest(DaideRequest): class SyntaxErrorRequest(DaideRequest): """ Represents a HUH DAIDE request. Sent by the client to specify an error in a message. - Syntax: + + Syntax: :: + HUH (message) """ __slots__ = ['message_bytes'] @@ -704,7 +753,8 @@ class AdminMessageRequest(DaideRequest): """ Represents a ADM DAIDE request. Can be sent by the client to send a message to all clients. Should not be used for negotiation. - Syntax: + Syntax: :: + ADM ('message') """ __slots__ = ['adm_message'] diff --git a/diplomacy/daide/responses.py b/diplomacy/daide/responses.py index cd7a5b4..fe77e1a 100644 --- a/diplomacy/daide/responses.py +++ b/diplomacy/daide/responses.py @@ -41,7 +41,9 @@ class DaideResponse(_AbstractResponse): class MapNameResponse(DaideResponse): """ Represents a MAP DAIDE response. Sends the name of the current map to the client. - Syntax: + + Syntax: :: + MAP ('name') """ def __init__(self, map_name, **kwargs): @@ -54,11 +56,17 @@ class MapNameResponse(DaideResponse): class MapDefinitionResponse(DaideResponse): """ Represents a MDF DAIDE response. Sends configuration of a map to a client - Syntax: + + Syntax: :: + MDF (powers) (provinces) (adjacencies) - powers syntax: + + powers syntax: :: + power power ... - power syntax: + + power syntax: :: + AUS # Austria ENG # England FRA # France @@ -66,11 +74,17 @@ class MapDefinitionResponse(DaideResponse): ITA # Italy RUS # Russia TUR # Turkey - provinces syntax: + + provinces syntax: :: + (supply_centres) (non_supply_centres) - supply_centres syntax: + + supply_centres syntax: :: + (power centre centre ...) (power centre centre ...) ... - supply_centres power syntax: + + supply_centres power syntax: :: + (power power ...) # This is currently not supported AUS # Austria ENG # England @@ -80,22 +94,33 @@ class MapDefinitionResponse(DaideResponse): RUS # Russia TUR # Turkey UNO # Unknown power - non_supply_centres syntax: + + non_supply_centres syntax: :: + province province ... # List of provinces - adjacencies syntax: + + adjacencies syntax: :: + (prov_adjacencies) (prov_adjacencies) ... - prov_adjacencies syntax: + + prov_adjacencies syntax: :: + province (unit_type adjacent_prov adjacent_prov ...) (unit_type adjacent_prov adjacent_prov ...) ... - unit_type syntax: + + unit_type syntax: :: + AMY # List of provinces an army can move to FLT # List of provinces a fleet can move to (FLT coast) # List of provinces a fleet can move to from the given coast - adjacent_prov syntax: + + adjacent_prov syntax: :: + province # A province which can be moved to (province coast) # A coast of a province that can be moved to """ def __init__(self, map_name, **kwargs): """ Builds the response + :param map_name: The name of the map """ super(MapDefinitionResponse, self).__init__(**kwargs) @@ -116,11 +141,17 @@ class MapDefinitionResponse(DaideResponse): @staticmethod def _build_powers_clause(game_map): """ Build the powers clause - Syntax: + + Syntax: :: + (powers) - powers syntax: + + powers syntax: :: + power power ... - power syntax: + + power syntax: :: + AUS # Austria ENG # England FRA # France @@ -140,13 +171,21 @@ class MapDefinitionResponse(DaideResponse): @staticmethod def _build_provinces_clause(game_map): """ Build the provinces clause - Syntax: + + Syntax: :: + (provinces) - provinces syntax: + + provinces syntax: :: + (supply_centres) (non_supply_centres) - supply_centres syntax: + + supply_centres syntax: :: + (power centre centre ...) (power centre centre ...) ... - supply_centres power syntax: + + supply_centres power syntax: :: + (power power ...) # This is currently not supported AUS # Austria ENG # England @@ -156,7 +195,9 @@ class MapDefinitionResponse(DaideResponse): RUS # Russia TUR # Turkey UNO # Unknown power - non_supply_centres syntax: + + non_supply_centres syntax: :: + province province ... # List of provinces """ unowned_scs = game_map.scs[:] @@ -215,17 +256,27 @@ class MapDefinitionResponse(DaideResponse): @staticmethod def _build_adjacencies_clause(game_map): """ Build the adjacencies clause - Syntax: + + Syntax: :: + (adjacencies) - adjacencies syntax: + + adjacencies syntax: :: + (prov_adjacencies) (prov_adjacencies) ... - prov_adjacencies syntax: + + prov_adjacencies syntax: :: + province (unit_type adjacent_prov adjacent_prov ...) (unit_type adjacent_prov adjacent_prov ...) ... - unit_type syntax: + + unit_type syntax: :: + AMY # List of provinces an army can move to FLT # List of provinces a fleet can move to (FLT coast) # List of provinces a fleet can move to from the given coast - adjacent_prov syntax: + + adjacent_prov syntax: :: + province # A province which can be moved to (province coast) # A coast of a province that can be moved to """ @@ -309,17 +360,24 @@ class MapDefinitionResponse(DaideResponse): class HelloResponse(DaideResponse): """ Represents a HLO DAIDE response. Sends the power to be played by the client with the passcode to rejoin the game and the details of the game. - Syntax: + + Syntax: :: + HLO (power) (passcode) (variant) (variant) ... - Variant syntax: + + Variant syntax: :: + LVL n # Level of the syntax accepted MTL seconds # Movement time limit RTL seconds # Retreat time limit BTL seconds # Build time limit DSD # Disables the time limit when a client disconects AOA # Any orders accepted + LVL 10: - Variant syntax: + + Variant syntax: :: + PDA # Accept partial draws NPR # No press during retreat phases NPB # No press during build phases @@ -327,6 +385,7 @@ class HelloResponse(DaideResponse): """ def __init__(self, power_name, passcode, level, deadline, rules, **kwargs): """ Builds the response + :param power_name: The name of the power being played. :param passcode: Integer. A passcode to rejoin the game. :param level: Integer. The daide syntax level of the game @@ -356,11 +415,14 @@ class HelloResponse(DaideResponse): class SupplyCenterResponse(DaideResponse): """ Represents a SCO DAIDE response. Sends the current supply centre ownership. - Syntax: + + Syntax: :: + SCO (power centre centre ...) (power centre centre ...) ... """ def __init__(self, powers_centers, map_name, **kwargs): """ Builds the response + :param powers_centers: A dict of {power_name: centers} objects :param map_name: The name of the map """ @@ -397,15 +459,20 @@ class SupplyCenterResponse(DaideResponse): class CurrentPositionResponse(DaideResponse): """ Represents a NOW DAIDE response. Sends the current turn, and the current unit positions. - Syntax: + + Syntax: :: + NOW (turn) (unit) (unit) ... - Unit syntax: + + Unit syntax: :: + power unit_type province power unit_type province MRT (province province ...) """ def __init__(self, phase_name, powers_units, powers_retreats, **kwargs): """ Builds the response + :param phase_name: The name of the current phase (e.g. 'S1901M') :param powers: A list of `diplomacy.engine.power.Power` objects """ @@ -436,9 +503,13 @@ class CurrentPositionResponse(DaideResponse): class ThanksResponse(DaideResponse): """ Represents a THX DAIDE response. Sends the result of an order after submission. - Syntax: + + Syntax: :: + THX (order) (note) - Note syntax: + + Note syntax: :: + MBV # Order is OK. FAR # Not adjacent. NSP # No such province @@ -461,6 +532,7 @@ class ThanksResponse(DaideResponse): """ def __init__(self, order_bytes, results, **kwargs): """ Builds the response + :param order_bytes: The bytes received for the order :param results: An array containing the error codes. """ @@ -476,13 +548,16 @@ class ThanksResponse(DaideResponse): class MissingOrdersResponse(DaideResponse): """ Represents a MIS DAIDE response. Sends the list of unit for which an order is missing or indication about required disbands or builds. - Syntax: + + Syntax: :: + MIS (unit) (unit) ... MIS (unit MRT (province province ...)) (unit MRT (province province ...)) ... MIS (number) """ def __init__(self, phase_name, power, **kwargs): """ Builds the response + :param phase_name: The name of the current phase (e.g. 'S1901M') :param power: The power to check for missing orders :type power: diplomacy.engine.power.Power @@ -558,10 +633,14 @@ class MissingOrdersResponse(DaideResponse): class OrderResultResponse(DaideResponse): """ Represents a ORD DAIDE response. Sends the result of an order after the turn has been processed. - Syntax: + + Syntax: :: + ORD (turn) (order) (result) ORD (turn) (order) (result RET) - Result syntax: + + Result syntax: :: + SUC # Order succeeded (can apply to any order). BNC # Move bounced (only for MTO, CTO or RTO orders). CUT # Support cut (only for SUP orders). @@ -571,6 +650,7 @@ class OrderResultResponse(DaideResponse): """ def __init__(self, phase_name, order_bytes, results, **kwargs): """ Builds the response + :param phase_name: The name of the current phase (e.g. 'S1901M') :param order_bytes: The bytes received for the order :param results: An array containing the error codes. @@ -587,11 +667,14 @@ class OrderResultResponse(DaideResponse): class TimeToDeadlineResponse(DaideResponse): """ Represents a TME DAIDE response. Sends the time to the next deadline. - Syntax: + + Syntax: :: + TME (seconds) """ def __init__(self, seconds, **kwargs): """ Builds the response + :param seconds: Integer. The number of seconds before deadline """ super(TimeToDeadlineResponse, self).__init__(**kwargs) @@ -599,7 +682,9 @@ class TimeToDeadlineResponse(DaideResponse): class AcceptResponse(DaideResponse): """ Represents a YES DAIDE request. - Syntax: + + Syntax: :: + YES (TME (seconds)) # Accepts to set the time when a TME message will be sent YES (NOT (TME)) # Accepts to cancel all requested time messages @@ -610,7 +695,9 @@ class AcceptResponse(DaideResponse): processing the orders for the turn YES (DRW) # Accepts to draw YES (NOT (DRW)) # Accepts to cancel a draw request - LVL 10: + + LVL 10: :: + YES (DRW (power power ...)) # Accepts a partial draw YES (NOT (DRW (power power ...))) # Accepts to cancel a partial draw request (? not mentinned in the DAIDE doc) @@ -619,6 +706,7 @@ class AcceptResponse(DaideResponse): """ def __init__(self, request_bytes, **kwargs): """ Builds the response + :param request_bytes: The bytes received for the request """ super(AcceptResponse, self).__init__(**kwargs) @@ -626,7 +714,9 @@ class AcceptResponse(DaideResponse): class RejectResponse(DaideResponse): """ Represents a REJ DAIDE request. - Syntax: + + Syntax: :: + REJ (NME ('name') ('version')) # Rejects a client in the game REJ (IAM (power) (passcode)) # Rejects a client to rejoin the game REJ (HLO) # Rejects to send the HLO message @@ -647,7 +737,9 @@ class RejectResponse(DaideResponse): REJ (ADM ('name') ('message') # Rejects the admin message REJ (DRW) # Rejects to draw REJ (NOT (DRW)) # Rejects to cancel a draw request - LVL 10: + + LVL 10: :: + REJ (DRW (power power ...)) # Rejects to partially draw REJ (NOT (DRW (power power ...))) # Rejects to cancel a partial draw request REJ (SND (power power ...) (press_message)) # Rejects a press message @@ -655,6 +747,7 @@ class RejectResponse(DaideResponse): """ def __init__(self, request_bytes, **kwargs): """ Builds the response + :param request_bytes: The bytes received for the request """ super(RejectResponse, self).__init__(**kwargs) @@ -662,7 +755,9 @@ class RejectResponse(DaideResponse): class NotResponse(DaideResponse): """ Represents a NOT DAIDE response. - Syntax: + + Syntax: :: + NOT (CCD (power)) """ def __init__(self, response_bytes, **kwargs): @@ -674,11 +769,14 @@ class NotResponse(DaideResponse): class PowerInCivilDisorderResponse(DaideResponse): """ Represents a CCD DAIDE response. Sends the name of the power in civil disorder. - Syntax: + + Syntax: :: + CCD (power) """ def __init__(self, power_name, **kwargs): """ Builds the response + :param power_name: The name of the power being played. """ super(PowerInCivilDisorderResponse, self).__init__(**kwargs) @@ -687,11 +785,14 @@ class PowerInCivilDisorderResponse(DaideResponse): class PowerIsEliminatedResponse(DaideResponse): """ Represents a OUT DAIDE response. Sends the name of the power eliminated. - Syntax: + + Syntax: :: + OUT (power) """ def __init__(self, power_name, **kwargs): """ Builds the response + :param power_name: The name of the power being played. """ super(PowerIsEliminatedResponse, self).__init__(**kwargs) @@ -700,11 +801,14 @@ class PowerIsEliminatedResponse(DaideResponse): class ParenthesisErrorResponse(DaideResponse): """ Represents a PRN DAIDE response. - Syntax: + + Syntax: :: + PRN (message) """ def __init__(self, request_bytes, **kwargs): """ Builds the response + :param request_bytes: The bytes received for the request """ super(ParenthesisErrorResponse, self).__init__(**kwargs) @@ -712,11 +816,14 @@ class ParenthesisErrorResponse(DaideResponse): class SyntaxErrorResponse(DaideResponse): """ Represents a HUH DAIDE response. - Syntax: + + Syntax: :: + HUH (message) """ def __init__(self, request_bytes, error_index, **kwargs): """ Builds the response + :param request_bytes: The bytes received for the request :param error_index: The index of the faulty token """ @@ -726,12 +833,13 @@ class SyntaxErrorResponse(DaideResponse): class TurnOffResponse(DaideResponse): """ Represents an OFF DAIDE response. Requests a client to exit - Syntax: + + Syntax: :: + OFF """ def __init__(self, **kwargs): - """ Builds the response - """ + """ Builds the response """ super(TurnOffResponse, self).__init__(**kwargs) self._bytes = bytes(tokens.OFF) diff --git a/diplomacy/daide/server.py b/diplomacy/daide/server.py index ceca122..70324ed 100644 --- a/diplomacy/daide/server.py +++ b/diplomacy/daide/server.py @@ -27,7 +27,8 @@ LOGGER = logging.getLogger(__name__) class Server(TCPServer): """ Represents a server to receive DAIDE communications """ def __init__(self, master_server, game_id): - """ Contructor + """ Constructor + :param master_server: the internal server :param game_id: the game id for which this server will receive communications """ @@ -55,6 +56,7 @@ class Server(TCPServer): @gen.coroutine def handle_stream(self, stream, address): """ Handle an open stream + :param stream: the stream to handle :param address: the address of the client """ diff --git a/diplomacy/daide/tests/test_daide_game.py b/diplomacy/daide/tests/test_daide_game.py index 1c7159a..3e970b2 100644 --- a/diplomacy/daide/tests/test_daide_game.py +++ b/diplomacy/daide/tests/test_daide_game.py @@ -62,10 +62,11 @@ def run_with_timeout(callable_fn, timeout): finally: signal.alarm(0) -class ClientCommsSimulator(): +class ClientCommsSimulator: """ Represents a client's comms """ def __init__(self, client_id): """ Constructor + :param client_id: the id """ self._id = client_id @@ -92,7 +93,8 @@ class ClientCommsSimulator(): """ Set the client's communications. The client's comms will be sorted to have the requests of a phase - preceeding the responses / notifications of the phase + preceding the responses / notifications of the phase + :param comms: the game's communications """ self._comms = [comm for comm in comms if comm.client_id == self._id] @@ -126,8 +128,9 @@ class ClientCommsSimulator(): def pop_next_request(self, comms): """ Pop the next request from a DAIDE communications list + :return: The next request along with the updated list of communications - or None and the updated list of communications + or None and the updated list of communications """ com = next(iter(comms), None) request = None @@ -148,8 +151,9 @@ class ClientCommsSimulator(): def pop_next_resp_notif(self, comms): """ Pop the next response or notifcation from a DAIDE communications list + :return: The next response or notifcation along with the updated list of communications - or None and the updated list of communications + or None and the updated list of communications """ com = next(iter(comms), None) resp_notif = None @@ -170,6 +174,7 @@ class ClientCommsSimulator(): @gen.coroutine def connect(self, game_port): """ Connect to the DAIDE server + :param game_port: the DAIDE game's port """ self._stream = yield TCPClient().connect('localhost', game_port) @@ -181,6 +186,7 @@ class ClientCommsSimulator(): @gen.coroutine def send_request(self, request): """ Sends a request + :param request: the request to send """ message = messages.DiplomacyMessage() @@ -190,6 +196,7 @@ class ClientCommsSimulator(): @gen.coroutine def validate_resp_notifs(self, expected_resp_notifs): """ Validate that expected response / notifications are received regardless of the order + :param expected_resp_notifs: the response / notifications to receive """ while expected_resp_notifs: @@ -211,6 +218,7 @@ class ClientCommsSimulator(): @gen.coroutine def execute_phase(self, game_id, channels): """ Execute a single communications phase + :param game_id: The game id of the current game :param channels: A dictionary of power name to its channel (BOT_KEYWORD for dummies) :return: True if there are communications left to execute in the game @@ -267,10 +275,11 @@ class ClientCommsSimulator(): return bool(self._comms) -class ClientsCommsSimulator(): +class ClientsCommsSimulator: """ Represents multi clients's communications """ def __init__(self, nb_clients, csv_file, game_id, channels): """ Constructor + :param nb_clients: the number of clients :param csv_file: the csv containing the communications in chronological order :param game_id: The game id on the server @@ -291,6 +300,7 @@ class ClientsCommsSimulator(): @gen.coroutine def retrieve_game_port(self, host, port): """ Retreive and store the game's port + :param host: the host :param port: the port :param game_id: the game id @@ -347,6 +357,7 @@ class ClientsCommsSimulator(): def run_game_data(nb_daide_clients, rules, csv_file): """ Start a server and a client to test DAIDE communications + :param port: The port of the DAIDE server :param csv_file: the csv file containing the list of DAIDE communications """ diff --git a/diplomacy/daide/tokens.py b/diplomacy/daide/tokens.py index 5009170..93500b0 100644 --- a/diplomacy/daide/tokens.py +++ b/diplomacy/daide/tokens.py @@ -27,6 +27,7 @@ class Token: def __init__(self, from_str=None, from_int=None, from_bytes=None): """ Initialize a token from its string representation, or from its bytes representation + :param from_str: The string representation of the token :param from_int: The integer representation of the token :param from_bytes: The byte representation of the token @@ -139,6 +140,7 @@ class Token: def is_ascii_token(token): """ Check if the token is an ascii token + :param token: An instance of Token :return: True if `token` is an acsii token. False otherwise """ @@ -147,6 +149,7 @@ def is_ascii_token(token): def is_integer_token(token): """ Check if the token is an integer token + :param token: An instance of Token :return: True if `token` is an integer token. False otherwise """ @@ -155,6 +158,7 @@ def is_integer_token(token): def register_token(str_repr, bytes_repr): """ Registers a token in the registry + :param str_repr: The DAIDE string representation of the token (e.g. 'ECS') :param bytes_repr: The bytes representation of the token (i.e. bytes of length 2) :return: The token that has been registered diff --git a/diplomacy/daide/utils.py b/diplomacy/daide/utils.py index e300071..8e6072f 100644 --- a/diplomacy/daide/utils.py +++ b/diplomacy/daide/utils.py @@ -22,6 +22,7 @@ ClientConnection = namedtuple('ClientConnection', ['username', 'daide_user', 'to def get_user_connection(server_users, game, connection_handler): """ Get the DAIDE user connection informations + :param server_users: The instance of `diplomacy.server.users` of the game's server :param game: The game the user has joined :param connection_handler: The connection_handler of the user @@ -38,6 +39,7 @@ def get_user_connection(server_users, game, connection_handler): def str_to_bytes(daide_str): """ Converts a str into its bytes representation + :param daide_str: A DAIDE string with tokens separated by spaces :return: The bytes representation of the string @@ -56,6 +58,7 @@ def str_to_bytes(daide_str): def bytes_to_str(daide_bytes): """ Converts a bytes into its str representation + :param daide_bytes: A DAIDE bytes with tokens separated by spaces :return: The bytes representation of the string diff --git a/diplomacy/engine/game.py b/diplomacy/engine/game.py index 5f0bb2e..fe4488e 100644 --- a/diplomacy/engine/game.py +++ b/diplomacy/engine/game.py @@ -16,6 +16,7 @@ # ============================================================================== # -*- coding: utf-8 -*- """ Game + - Contains the game engine """ # pylint: disable=too-many-lines @@ -37,7 +38,7 @@ from diplomacy.engine.renderer import Renderer from diplomacy.utils import PriorityDict, common, exceptions, parsing, strings from diplomacy.utils.jsonable import Jsonable from diplomacy.utils.sorted_dict import SortedDict -from diplomacy.utils.constants import OrderSettings +from diplomacy.utils.constants import OrderSettings, DEFAULT_GAME_RULES from diplomacy.utils.game_phase_data import GamePhaseData, MESSAGES_TYPE # Constants @@ -45,126 +46,176 @@ UNDETERMINED, POWER, UNIT, LOCATION, COAST, ORDER, MOVE_SEP, OTHER = 0, 1, 2, 3, LOGGER = logging.getLogger(__name__) class Game(Jsonable): - """ - - combat - Dictionary of dictionaries containing the strength of every attack on a location (including units - who don't count toward dislodgment) - - Format: {loc: attack_strength: [ ['src loc', [support loc] ]} - e.g. { 'MUN': { 1 : [ ['A MUN', [] ], ['A RUH', [] ] ], 2 : [ ['A SIL', ['A BOH'] ] ] } } - MUN is holding, being attack without support from RUH and being attacked with support from SIL - (S from BOH) - - command - Contains the list of finalized orders to be processed (same format as orders, but without .order) - e.g. {'A PAR': '- A MAR'} - - controlled_powers: for client games only. List of powers currently controlled by associated client user. - - convoy_paths - Contains the list of remaining convoys path for each convoyed unit to reach their destination - Note: This is used to see if there are still active convoy paths remaining. - Note: This also include the start and ending location - e.g. {'A PAR': [ ['PAR', 'ION','NAO', 'MAR], ['PAR', 'ION', 'MAR'] ], ... } - - convoy_paths_possible - Contains the list of possible convoy paths given the current fleet locations or None - e.g. [(START_LOC, {Fleets Req}, {possible dest}), ...] - - convoy_paths_dest - Contains a dictionary of possible paths to reach destination from start or None - e.g. {start_loc: {dest_loc_1: [{fleets}, {fleets}, {fleets}], dest_loc_2: [{fleets, fleets}]} - - daide_port: for client games only. Port when a DAIDE bot can connect, to play with this game. - - deadline: integer: game deadline in seconds. - - dislodged - Contains a dictionary of dislodged units (and the site that dislodged them') - e.g. { 'A PAR': 'MAR' } - - error - Contains a list of errors that the game generated - e.g. ['NO MASTER SPECIFIED'] - - fixed_state - used when game is a context of a with-block. - Store values that define the game state when entered in with-statement. - Compared to actual fixed state to detect any changes in methods where changes are not allowed. - Reset to None when exited from with-statement. - - game_id: String that contains the current game's ID - e.g. '123456' - - lost - Contains a dictionary of centers that have been lost during the term - e.g. {'PAR': 'FRANCE'} to indicate that PAR was lost by France (previous owner) - - map: Contains a reference to the current map (Map instance) - e.g. map = Map('standard') - - map_name: Contains a reference to the name of the map that was loaded (or a full path to a custom map file) - e.g. map_name = 'standard' or map_name = '/some/path/to/file.map' - - messages (only for non-observer games): history of messages exchanged inside this game. - Sorted dict mapping message timestamps to message objects (instances of diplomacy.Message). - Format: {message.time_sent => message} - - message_history: history of messages through all played phases. - Sorted dict mapping a short phase name to a message dict - (with same format as field `message` describe above). - Format: {short phase name => {message.time_sent => message}} - Wrapped in a sorted dict at runtime, see method __init__(). - - meta_rules - Contains the rules that have been processed as directives - e.g. ['NO_PRESS'] - - n_controls: integer: exact number of controlled powers allowed for this game. - If game start mode is not START_MASTER, then game starts as soon as this number of powers - are controlled. - - no_rules - Contains the list of rules that have been disabled (prefixed with '!') - e.g ['NO_PRESS'] - - note - A note to display on the rendering - e.g. 'Winner: FRANCE' - - observer_level: for client games only. Highest observation level allowed for associated client user. - Either "master_type", "omniscient_type" or "observer_type". - - orders - Contains the list of current orders (not yet processed) - e.g. {'A PAR': '- A MAR'} - - ordered_units - Contains a dictionary of the units ordered by each power in the last phase - e.g. {'FRANCE': ['A PAR', 'A MAR'], 'ENGLAND': ... } - - order_history - Contains the history of orders from each player from the beginning of the game. - Sorted dict mapping a short phase name to a dictionary of orders - (powers names as keys, powers orders as values). - Format: {short phase name => {power name => [orders]}} - Wrapped in a sorted dict at runtime, see method __init__(). - - outcome - Contains the game outcome - e.g. [lastPhase, victor1, victor2, victor3] - - phase: String that contains a long representation of the current phase - e.g. 'SPRING 1901 MOVEMENT' - - phase_type: Indicates the current phase type - (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjustment, '-' for non-playing phase) - e.g. 'M' - - popped - Contains a list of all retreaters who didn't make it - e.g. ['A PAR', 'A MAR'] - - powers - Contains a dictionary mapping power names to power instances in the game - e.g. {'FRANCE': FrancePower, 'ENGLAND': EnglishPower, ...} - - registration_password: ** hashed ** version of password to be sent by a player to join this game. - - renderer - Contains the object in charge of rendering the map - e.g. Renderer() - - result - Contains the result of the action for each unit. - In Movement Phase, result can be 'no convoy', 'bounce', 'void', 'cut', 'dislodged', 'disrupted' - e.g. { 'A PAR': ['cut', 'void'] } - In Retreats phase, result can be 'bounce', 'disband', 'void' - e.g. { 'A PAR': ['cut', 'void'] } - In Adjustments phase, result can be 'void' or '' - e.g. { 'A PAR': ['', 'void'] } # e.g. to indicate a successful build, and a void build. - - result_history - Contains the history of orders results for all played phases. - Sorted dict mapping a short phase name to a dictionary of order results for this phase. - Dictionary of order results maps a unit to a list of results. See field result for more details. - Format: {short phase name => {unit => [results]}} - Wrapped in a sorted dict at runtime, see method __init__(). - - role: game type (observer, omniscient, player or server game). - Either a power name (for player game) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES. - - rules: Contains a list of active rules - e.g. ['NO_PRESS', ...] - - state_history: history of previous game states (returned by method get_state()) for this game. - Sorted dict mapping a short phase name to a game state. - Each game state is associated to a timestamp generated when state is created by method get_state(). - State timestamp then represents the "end" time of the state, ie. time when this state was saved and - archived in state history. - Format: {short phase name => state} - Wrapped in a sorted dict at runtime, see method __init__(). - - status: game status (forming, active, paused, completed or canceled). - Possible values in diplomacy.utils.strings.ALL_GAME_STATUSES. - - supports - Contains a dictionary of support for each unit - - Format: { 'unit': [nb_of_support, [list of supporting units]] } - e.g. { 'A PAR': [2, ['A MAR']] } - 2 support, but the Marseille support does NOT count toward dislodgment - - timestamp_created: timestamp in microseconds when game object was created on server side. - - victory - Indicates the number of SUPPLY [default] centers one power must control to win the game - - Format: [reqFirstYear, reqSecondYear, ..., reqAllFurtherYears] - e.g. [10,10,18] for 10 the 1st year, 10 the 2nd year, 18 year 3+ - - win - Indicates the minimum number of centers required to win - e.g. 3 - - zobrist_hash - Contains the zobrist hash representing the current state of this game - e.g. 12545212418541325 - - ----- Caches ---- - - _unit_owner_cache - Contains a dictionary with (unit, coast_required) as key and owner as value - - Set to Note when the cache is not built - e.g. {('A PAR', True): , ('A PAR', False): ), ...} + """ Game class. + + Properties: + + - **combat**: + + - Dictionary of dictionaries containing the strength of every attack on a location (including units + who don't count toward dislodgment) + - Format: {loc: attack_strength: [ ['src loc', [support loc] ]} + - e.g. ``{ 'MUN': { 1 : [ ['A MUN', [] ], ['A RUH', [] ] ], 2 : [ ['A SIL', ['A BOH'] ] ] } }``. + MUN is holding, being attack without support from RUH and being attacked with support from SIL (S from BOH) + + - **command**: contains the list of finalized orders to be processed + (same format as orders, but without .order). e.g. {'A PAR': '- A MAR'} + - **controlled_powers**: *(for client games only)*. List of powers currently controlled + by associated client user. + - **convoy_paths**: + + - Contains the list of remaining convoys path for each convoyed unit to reach their destination + - Note: This is used to see if there are still active convoy paths remaining. + - Note: This also include the start and ending location + - e.g. {'A PAR': [ ['PAR', 'ION','NAO', 'MAR], ['PAR', 'ION', 'MAR'] ], ... } + + - **convoy_paths_possible**: + + - Contains the list of possible convoy paths given the current fleet locations or None + - e.g. [(START_LOC, {Fleets Req}, {possible dest}), ...] + + - **convoy_paths_dest**: + + - Contains a dictionary of possible paths to reach destination from start or None + - e.g. {start_loc: {dest_loc_1: [{fleets}, {fleets}, {fleets}], dest_loc_2: [{fleets, fleets}]} + + - **daide_port**: *(for client games only)*. Port when a DAIDE bot can connect, to play with this game. + - **deadline**: integer: game deadline in seconds. + - **dislodged**: contains a dictionary of dislodged units (and the site that dislodged them'). + e.g. { 'A PAR': 'MAR' } + - **error**: contains a list of errors that the game generated. e.g. ['NO MASTER SPECIFIED'] + - **fixed_state**: + + - used when game is a context of a with-block. + - Store values that define the game state when entered in with-statement. + - Compared to actual fixed state to detect any changes in methods where changes are not allowed. + - Reset to None when exited from with-statement. + + - **game_id**: String that contains the current game's ID. e.g. '123456' + - **lost**: + + - Contains a dictionary of centers that have been lost during the term + - e.g. {'PAR': 'FRANCE'} to indicate that PAR was lost by France (previous owner) + + - **map**: Contains a reference to the current map (Map instance). e.g. map = Map('standard') + - **map_name**: Contains a reference to the name of the map that was loaded (or a path to a custom map file) + e.g. map_name = 'standard' or map_name = '/some/path/to/file.map' + - **messages** *(for non-observer games only)*: + + - history of messages exchanged inside this game. + - Sorted dict mapping message timestamps to message objects (instances of diplomacy.Message). + - Format: {message.time_sent => message} + + - **message_history**: + + - history of messages through all played phases. + - Sorted dict mapping a short phase name to a message dict + (with same format as field `message` describe above). + - Format: {short phase name => {message.time_sent => message}} + - Wrapped in a sorted dict at runtime, see method __init__(). + + - **meta_rules**: contains the rules that have been processed as directives. e.g. ['NO_PRESS'] + - **n_controls**: integer: + + - exact number of controlled powers allowed for this game. + - If game start mode is not START_MASTER, then game starts as soon as + this number of powers are controlled. + + - **no_rules**: contains the list of rules that have been disabled (prefixed with '!'). + e.g ['NO_PRESS'] + - **note**: a note to display on the rendering. e.g. 'Winner: FRANCE' + - **observer_level** *(for client games only)*: + + - Highest observation level allowed for associated client user. + - Either "master_type", "omniscient_type" or "observer_type". + + - **orders**: contains the list of current orders (not yet processed). e.g. {'A PAR': '- A MAR'} + - **ordered_units**: + + - Contains a dictionary of the units ordered by each power in the last phase + - e.g. {'FRANCE': ['A PAR', 'A MAR'], 'ENGLAND': ... } + + - **order_history**: + + - Contains the history of orders from each player from the beginning of the game. + - Sorted dict mapping a short phase name to a dictionary of orders + (powers names as keys, powers orders as values). + - Format: {short phase name => {power name => [orders]}} + - Wrapped in a sorted dict at runtime, see method __init__(). + + - **outcome**: contains the game outcome. e.g. [lastPhase, victor1, victor2, victor3] + - **phase**: string that contains a long representation of the current phase. + e.g. 'SPRING 1901 MOVEMENT' + - **phase_type**: indicates the current phase type. + e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjustment, '-' for non-playing phase + - **popped**: contains a list of all retreaters who didn't make it. e.g. ['A PAR', 'A MAR'] + - **powers**: + + - Contains a dictionary mapping power names to power instances in the game + - e.g. {'FRANCE': FrancePower, 'ENGLAND': EnglishPower, ...} + + - **registration_password**: ** hashed ** version of password to be sent by a player to join this game. + - **renderer**: contains the object in charge of rendering the map. e.g. Renderer() + - **result**: + + - Contains the result of the action for each unit. + - In Movement Phase, result can be 'no convoy', 'bounce', 'void', 'cut', 'dislodged', 'disrupted'. + e.g. { 'A PAR': ['cut', 'void'] } + - In Retreats phase, result can be 'bounce', 'disband', 'void'. + e.g. { 'A PAR': ['cut', 'void'] } + - In Adjustments phase, result can be 'void' or ''. + e.g. { 'A PAR': ['', 'void'] } # e.g. to indicate a successful build, and a void build. + + - **result_history**: + + - Contains the history of orders results for all played phases. + - Sorted dict mapping a short phase name to a dictionary of order results for this phase. + - Dictionary of order results maps a unit to a list of results. See field result for more details. + - Format: {short phase name => {unit => [results]}} + - Wrapped in a sorted dict at runtime, see method __init__(). + + - **role**: Either a power name (for player game) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES. + - **rules**: Contains a list of active rules. e.g. ['NO_PRESS', ...]. Default is + :const:`diplomacy.utils.constants.DEFAULT_GAME_RULES`. + - **state_history**: + + - history of previous game states (returned by method get_state()) for this game. + - Sorted dict mapping a short phase name to a game state. + - Each game state is associated to a timestamp generated + when state is created by method get_state(). + - State timestamp then represents the "end" time of the state, + ie. time when this state was saved and archived in state history. + - Format: {short phase name => state} + - Wrapped in a sorted dict at runtime, see method __init__(). + + - **status**: game status (forming, active, paused, completed or canceled). + Possible values in diplomacy.utils.strings.ALL_GAME_STATUSES. + - **supports**: + + - Contains a dictionary of support for each unit + - Format: { 'unit': [nb_of_support, [list of supporting units]] } + - e.g. { 'A PAR': [2, ['A MAR']] }. 2 support, but the Marseille support + does NOT count toward dislodgment + + - **timestamp_created**: timestamp in microseconds when game object was created on server side. + - **victory**: + + - Indicates the number of SUPPLY [default] centers one power must control to win the game + - Format: [reqFirstYear, reqSecondYear, ..., reqAllFurtherYears] + - e.g. [10,10,18] for 10 the 1st year, 10 the 2nd year, 18 year 3+ + + - **win** - Indicates the minimum number of centers required to win. e.g. 3 + - **zobrist_hash** - Contains the zobrist hash representing the current state of this game. + e.g. 12545212418541325 + + Cache properties: + + - **unit_owner_cache**: + + - Contains a dictionary with (unit, coast_required) as key and owner as value + - Set to Note when the cache is not built + - e.g. {('A PAR', True): , ('A PAR', False): ), ...} + """ # pylint: disable=too-many-instance-attributes __slots__ = ['victory', 'no_rules', 'meta_rules', 'phase', 'note', 'map', 'powers', 'outcome', 'error', 'popped', @@ -262,15 +313,19 @@ class Game(Jsonable): raise exceptions.NaturalIntegerException('n_controls must be a natural integer.') if self.deadline < 0: raise exceptions.NaturalIntegerException('Deadline must be a natural integer.') + # Check rules. if rules is None: - rules = ['SOLITAIRE', 'NO_PRESS', 'IGNORE_ERRORS', 'POWER_CHOICE'] + rules = list(DEFAULT_GAME_RULES) + # Set game rules. for rule in rules: self.add_rule(rule) + # Check settings about rule NO_DEADLINE. if 'NO_DEADLINE' in self.rules: self.deadline = 0 + # Check settings about rule SOLITAIRE. if 'SOLITAIRE' in self.rules: self.n_controls = 0 @@ -375,8 +430,9 @@ class Game(Jsonable): @property def power(self): """ (only for player games) Return client power associated to this game. + :return: a Power object. - :rtype: Power + :rtype: diplomacy.engine.power.Power """ return self.powers[self.role] if self.is_player_game() else None @@ -408,18 +464,19 @@ class Game(Jsonable): def current_state(self): """ Returns the game object. To be used with the following syntax: - ``` - with game.current_state(): - orders = players.get_orders(game, power_name) - game.set_orders(power_name, orders) - ``` + + .. code-block:: python + + with game.current_state(): + orders = players.get_orders(game, power_name) + game.set_orders(power_name, orders) """ return self def __enter__(self): """ Enter into game context. Initialize fixed state. - Raise an exception if fixed state is already initialized to a different state, to prevent using the game - into multiple contexts at same time. + Raise an exception if fixed state is already initialized to a different state, + to prevent using the game into multiple contexts at same time. """ current_state = (self.get_current_phase(), self.get_hash()) if self.fixed_state and self.fixed_state != current_state: @@ -433,6 +490,7 @@ class Game(Jsonable): def is_fixed_state_unchanged(self, log_error=True): """ Check if actual state matches saved fixed state, if game is used as context of a with-block. + :param log_error: Boolean that indicates to log an error if state has changed :return: boolean that indicates if the state has changed. """ @@ -468,7 +526,12 @@ class Game(Jsonable): return common.is_valid_password(registration_password, self.registration_password) def is_controlled(self, power_name): - """ Return True if given power name is currently controlled. """ + """ Return True if given power name is currently controlled. + + :param power_name: power name + :type power_name: str + :rtype: bool + """ return self.get_power(power_name).is_controlled() def is_dummy(self, power_name): @@ -512,13 +575,10 @@ class Game(Jsonable): expected_count = len(self.powers) return expected_count - def get_map_power_names(self): - """ Return sequence of map power names. """ - return self.powers.keys() - def get_dummy_power_names(self): - """ Return sequence of dummy power names. """ - return set(power_name for power_name in self.get_map_power_names() if self.is_dummy(power_name)) + """ Return sequence of not eliminated dummy power names. """ + return set(power_name for power_name in self.get_map_power_names() + if self.is_dummy(power_name) and not self.get_power(power_name).is_eliminated()) def get_dummy_unordered_power_names(self): """ Return a sequence of playable dummy power names @@ -555,7 +615,9 @@ class Game(Jsonable): return playable_power_names[random.randint(0, len(playable_power_names) - 1)] def get_latest_timestamp(self): - """ Return timestamp of latest data saved into this game (either current state, archived state or message). + """ Return timestamp of latest data saved into this game + (either current state, archived state or message). + :return: a timestamp :rtype: int """ @@ -570,6 +632,7 @@ class Game(Jsonable): def filter_messages(cls, messages, game_role, timestamp_from=None, timestamp_to=None): """ Filter given messages based on given game role between given timestamps (bounds included). See method diplomacy.utils.SortedDict.sub() about bound rules. + :param messages: a sorted dictionary of messages to filter. :param game_role: game role requiring messages. Either a special power name (PowerName.OBSERVER or PowerName.OMNISCIENT), a power name, or a list of power names. @@ -603,16 +666,23 @@ class Game(Jsonable): def get_phase_history(self, from_phase=None, to_phase=None, game_role=None): """ Return a list of game phase data from game history between given phases (bounds included). Each GamePhaseData object contains game state, messages, orders and order results for a phase. + :param from_phase: either: + - a string: phase name - an integer: index of phase in game history - None (default): lowest phase stored in game history + :param to_phase: either: + - a string: phase name - an integer: index of phase in game history - None (default): latest phase stored in game history - :param game_role (optional): role of game for which phase history is retrieved. + + :param game_role: (optional) role of game for which phase history is retrieved. If none, messages in game history will not be filtered. + + :return: a list of GamePhaseHistory objects """ if isinstance(from_phase, int): from_phase = self.state_history.key_from_index(from_phase) @@ -653,6 +723,7 @@ class Game(Jsonable): def extend_phase_history(self, game_phase_data): """ Add data from a game phase to game history. + :param game_phase_data: a GamePhaseData object. :type game_phase_data: GamePhaseData """ @@ -673,10 +744,11 @@ class Game(Jsonable): def draw(self, winners=None): """ Force a draw for this game, set status as COMPLETED and finish the game. - :param winners: (optional) either None (all powers remaining to map are considered winners) or a sequence - of required power names to be considered as winners. - :return: a couple (previous state, current state) with game state before the draw and game state after - the draw. + + :param winners: (optional) either None (all powers remaining to map are considered winners) + or a sequence of required power names to be considered as winners. + :return: a couple (previous state, current state) + with game state before the draw and game state after the draw. """ if winners is None: # Draw with all powers which still have units in map. @@ -726,6 +798,7 @@ class Game(Jsonable): def update_dummy_powers(self, dummy_power_names): """ Force all power associated to given dummy power names to be uncontrolled. + :param dummy_power_names: Sequence of required dummy power names. """ for dummy_power_name in dummy_power_names: @@ -734,6 +807,7 @@ class Game(Jsonable): def update_powers_controllers(self, powers_controllers, timestamps): """ Update powers controllers. + :param powers_controllers: a dictionary mapping a power name to a controller name. :param timestamps: a dictionary mapping a power name to timestamp when related controller (in powers_controllers) was associated to power. @@ -746,6 +820,7 @@ class Game(Jsonable): """ Create a undated (without timestamp) power message to be sent from a power to another via server. Server will answer with timestamp, and message will be updated and added to local game messages. + :param recipient: recipient power name (string). :param body: message body (string). :return: a new GameMessage object. @@ -759,6 +834,7 @@ class Game(Jsonable): def new_global_message(self, body): """ Create an undated (without timestamp) global message to be sent from a power via server. Server will answer with timestamp, and message will be updated and added to local game messages. + :param body: message body (string). :return: a new GameMessage object. :rtype: Message @@ -770,6 +846,7 @@ class Game(Jsonable): """ Add message to current game data. Only a server game can add a message with no timestamp: game will auto-generate a timestamp for the message. + :param message: a GameMessage object to add. :return: message timestamp. :rtype: int @@ -820,13 +897,24 @@ class Game(Jsonable): # ============== # Basic methods. # ============== + + def get_map_power_names(self): + """ Return sequence of map power names. """ + return self.powers.keys() + + def get_current_phase(self): + """ Returns the current phase (format 'S1901M' or 'FORMING' or 'COMPLETED') """ + return self._phase_abbr() + def get_units(self, power_name=None): """ Retrieves the list of units for a power or for all powers - :param power_name: Optional. The name of the power (e.g. 'FRANCE') or None for all powers - :return: A list of units (e.g. ['A PAR', 'A MAR']) if a power name is provided - or a dictionary of powers with their units if None is provided (e.g. {'FRANCE': [...], ...} - Note: Dislodged units will appear with a leading asterisk (e.g. '*A PAR') + :param power_name: Optional. The name of the power (e.g. ``'FRANCE'``) or None for all powers + :return: A list of units (e.g. ``['A PAR', 'A MAR']``) if a power name is provided + or a dictionary of powers with their units if None is provided + (e.g. ``{'FRANCE': [...], ...}``) + + Note: Dislodged units will appear with a leading asterisk (e.g. ``'*A PAR'``) """ if power_name is not None: power_name = power_name.upper() @@ -842,10 +930,11 @@ class Game(Jsonable): def get_centers(self, power_name=None): """ Retrieves the list of owned supply centers for a power or for all powers + :param power_name: Optional. The name of the power (e.g. 'FRANCE') or None for all powers :return: A list of supply centers (e.g. ['PAR', 'MAR']) if a power name is provided - or a dictionary of powers with their supply centers if None is provided - (e.g. {'FRANCE': [...], ...} + or a dictionary of powers with their supply centers if None is provided + (e.g. {'FRANCE': [...], ...} """ if power_name is not None: power_name = power_name.upper() @@ -861,10 +950,11 @@ class Game(Jsonable): def get_orders(self, power_name=None): """ Retrieves the orders submitted by a specific power, or by all powers + :param power_name: Optional. The name of the power (e.g. 'FRANCE') or None for all powers :return: A list of orders (e.g. ['A PAR H', 'A MAR - BUR']) if a power name is provided - or a dictionary of powers with their orders if None is provided - (e.g. {'FRANCE': ['A PAR H', 'A MAR - BUR', ...], ...} + or a dictionary of powers with their orders if None is provided + (e.g. {'FRANCE': ['A PAR H', 'A MAR - BUR', ...], ...} """ if power_name is not None: power_name = power_name.upper() @@ -893,10 +983,11 @@ class Game(Jsonable): def get_orderable_locations(self, power_name=None): """ Find the location requiring an order for a power (or for all powers) + :param power_name: Optionally, the name of the power (e.g. 'FRANCE') or None for all powers :return: A list of orderable locations (e.g. ['PAR', 'MAR']) if a power name is provided - or a dictionary of powers with their orderable locations if None is not provided - (e.g. {'FRANCE': [...], ...} + or a dictionary of powers with their orderable locations if None is not provided + (e.g. {'FRANCE': [...], ...} """ if power_name is not None: power_name = power_name.upper() @@ -939,17 +1030,22 @@ class Game(Jsonable): def get_order_status(self, power_name=None, unit=None, loc=None): """ Returns a list or a dict representing the order status ('', 'no convoy', 'bounce', 'void', 'cut', 'dislodged', 'disrupted') for orders submitted in the last phase + :param power_name: Optional. If provided (e.g. 'FRANCE') will only return the order status of that - power's orders + power's orders :param unit: Optional. If provided (e.g. 'A PAR') will only return that specific unit order status. :param loc: Optional. If provided (e.g. 'PAR') will only return that specific loc order status. Mutually exclusive with unit :param phase_type: Optional. Returns the results of a specific phase type (e.g. 'M', 'R', or 'A') - :return: If unit is provided a list (e.g. [] or ['void', 'dislodged']) - If loc is provided, a couple of unit and list (e.g. ('A PAR', ['void', 'dislodged'])), - or loc, [] if unit not found. - If power is provided a dict (e.g. {'A PAR': ['void'], 'A MAR': []}) - Otherwise a 2-level dict (e.g. {'FRANCE: {'A PAR': ['void'], 'A MAR': []}, 'ENGLAND': {}, ... } + :return: + + - If unit is provided a list (e.g. [] or ['void', 'dislodged']) + - If loc is provided, a couple of unit and list (e.g. ('A PAR', ['void', 'dislodged'])), + or (loc, []) if unit not found. + - If power is provided a dict (e.g. {'A PAR': ['void'], 'A MAR': []}) + - Otherwise a 2-level dict + (e.g. {'FRANCE: {'A PAR': ['void'], 'A MAR': []}, 'ENGLAND': {}, ... } + """ # Specific location if unit or loc: @@ -986,15 +1082,17 @@ class Game(Jsonable): def get_power(self, power_name): """ Retrieves a power instance from given power name. + :param power_name: name of power instance to retrieve. Power name must be as given in map file. :return: the power instance, or None if power name is not found. - :rtype: Power + :rtype: diplomacy.engine.power.Power """ return self.powers.get(power_name, None) def set_units(self, power_name, units, reset=False): """ Sets units directly on the map + :param power_name: The name of the power who will own the units (e.g. 'FRANCE') :param units: An unit (e.g. 'A PAR') or a list of units (e.g. ['A PAR', 'A MAR']) to set Note units starting with a '*' will be set as dislodged @@ -1078,6 +1176,7 @@ class Game(Jsonable): def set_centers(self, power_name, centers, reset=False): """ Transfers supply centers ownership + :param power_name: The name of the power who will control the supply centers (e.g. 'FRANCE') :param centers: A loc (e.g. 'PAR') or a list of locations (e.g. ['PAR', 'MAR']) to transfer :param reset: Boolean. If, removes ownership of all power's SC before transferring ownership of the new SC @@ -1113,16 +1212,18 @@ class Game(Jsonable): def set_orders(self, power_name, orders, expand=True, replace=True): """ Sets the current orders for a power + :param power_name: The name of the power (e.g. 'FRANCE') :param orders: The list of orders (e.g. ['A MAR - PAR', 'A PAR - BER', ...]) :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.) - If false, expect orders in the following format. False gives a performance improvement. + If false, expect orders in the following format. False gives a performance improvement. :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. :return: Nothing - Expected format: - A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI - A IRO R MAO, A IRO D, A LON B, F LIV B + Expected format: :: + + A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, + F NWG C A NWY - EDI, A IRO R MAO, A IRO D, A LON B, F LIV B """ if not self.is_fixed_state_unchanged(log_error=bool(orders)): return @@ -1155,6 +1256,7 @@ class Game(Jsonable): def set_wait(self, power_name, wait): """ Set wait flag for a power. + :param power_name: name of power to set wait flag. :param wait: wait flag (boolean). """ @@ -1165,13 +1267,14 @@ class Game(Jsonable): if not self.has_power(power_name): return - power = self.get_power(power_name.upper()) # type: Power + power = self.get_power(power_name.upper()) # type: diplomacy.engine.power.Power power.wait = wait def clear_units(self, power_name=None): """ Clear the power's units + :param power_name: Optional. The name of the power whose units will be cleared (e.g. 'FRANCE'), - otherwise all units on the map will be cleared + otherwise all units on the map will be cleared :return: Nothing """ for power in self.powers.values(): @@ -1181,8 +1284,9 @@ class Game(Jsonable): def clear_centers(self, power_name=None): """ Removes ownership of supply centers + :param power_name: Optional. The name of the power whose centers will be cleared (e.g. 'FRANCE'), - otherwise all centers on the map will lose ownership. + otherwise all centers on the map will lose ownership. :return: Nothing """ for power in self.powers.values(): @@ -1192,8 +1296,9 @@ class Game(Jsonable): def clear_orders(self, power_name=None): """ Clears the power's orders + :param power_name: Optional. The name of the power to clear (e.g. 'FRANCE') or will clear orders for - all powers if None. + all powers if None. :return: Nothing """ if not self.is_fixed_state_unchanged(): @@ -1210,10 +1315,6 @@ class Game(Jsonable): self.convoy_paths_possible, self.convoy_paths_dest = None, None self._unit_owner_cache = None - def get_current_phase(self): - """ Returns the current phase (format 'S1901M' or 'FORMING' or 'COMPLETED' """ - return self._phase_abbr() - def set_current_phase(self, new_phase): """ Changes the phase to the specified new phase (e.g. 'S1901M') """ if new_phase in ('FORMING', 'COMPLETED'): @@ -1225,9 +1326,10 @@ class Game(Jsonable): def render(self, incl_orders=True, incl_abbrev=False, output_format='svg'): """ Renders the current game and returns its image representation + :param incl_orders: Optional. Flag to indicate we also want to render orders. :param incl_abbrev: Optional. Flag to indicate we also want to display the provinces abbreviations. - :param output_format: The desired output format. + :param output_format: The desired output format. Currently, only 'svg' is supported. :return: The rendered image in the specified format. """ if not self.renderer: @@ -1236,6 +1338,7 @@ class Game(Jsonable): def add_rule(self, rule): """ Adds a rule to the current rule list + :param rule: Name of rule to add (e.g. 'NO_PRESS') :return: Nothing """ @@ -1274,6 +1377,7 @@ class Game(Jsonable): def remove_rule(self, rule): """ Removes a rule from the current rule list + :param rule: Name of rule to remove (e.g. 'NO_PRESS') :return: Nothing """ @@ -1282,6 +1386,7 @@ class Game(Jsonable): def load_map(self, reinit_powers=True): """ Load a map and process directives + :param reinit_powers: Boolean. If true, empty powers dict. :return: Nothing, but stores the map in self.map """ @@ -1314,6 +1419,7 @@ class Game(Jsonable): def process(self): """ Processes the current phase of the game. + :return: game phase data with data before processing. """ previous_phase = self._phase_wrapper_type(self.current_short_phase) @@ -1361,6 +1467,7 @@ class Game(Jsonable): def rebuild_hash(self): """ Completely recalculate the Zobrist hash + :return: The updated hash value """ self.zobrist_hash = 0 @@ -1391,13 +1498,14 @@ class Game(Jsonable): def update_hash(self, power, unit_type='', loc='', is_dislodged=False, is_center=False, is_home=False): """ Updates the zobrist hash for the current game - :param power: The name of the power owning the unit, supply center or home - :param unit_type: Contains the unit type of the unit being added or remove from the board ('A' or 'F') - :param loc: Contains the location of the unit, supply center, of home being added or remove - :param is_dislodged: Indicates that the unit being added/removed is dislodged - :param is_center: Indicates that the location being added/removed is a supply center - :param is_home: Indicates that the location being added/removed is a home - :return: Nothing + + :param power: The name of the power owning the unit, supply center or home + :param unit_type: Contains the unit type of the unit being added or remove from the board ('A' or 'F') + :param loc: Contains the location of the unit, supply center, of home being added or remove + :param is_dislodged: Indicates that the unit being added/removed is dislodged + :param is_center: Indicates that the location being added/removed is a supply center + :param is_home: Indicates that the location being added/removed is a home + :return: Nothing """ if self.map is None: return @@ -1442,6 +1550,7 @@ class Game(Jsonable): def set_phase_data(self, phase_data, clear_history=True): """ Set game from phase data. + :param phase_data: either a GamePhaseData or a list of GamePhaseData. If phase_data is a GamePhaseData, it will be treated as a list of GamePhaseData with 1 element. Last phase data in given list will be used to set current game internal state. @@ -1485,8 +1594,8 @@ class Game(Jsonable): See field order_history to get orders from previous phases. To get a complete state of all data in this game object, consider using method Game.to_dict(). - :param make_copy: Boolean. If true, a deep copy of the game state is returned, otherwise the attributes are - returned directly. + :param make_copy: Boolean. If true, a deep copy of the game state is returned, + otherwise the attributes are returned directly. :return: The internal saved state (dict) of the game """ state = {} @@ -1527,6 +1636,7 @@ class Game(Jsonable): def set_state(self, state, clear_history=True): """ Sets the game from a saved internal state + :param state: The saved state (dict) :param clear_history: Boolean. If true, all game histories are cleared. :return: Nothing @@ -1576,6 +1686,7 @@ class Game(Jsonable): def get_all_possible_orders(self): """ Computes a list of all possible orders for all locations + :return: A dictionary with locations as keys, and their respective list of possible orders as values """ # pylint: disable=too-many-branches,too-many-nested-blocks @@ -1806,6 +1917,7 @@ class Game(Jsonable): def _is_convoyer(self, army, loc): """ Detects if there is a convoyer at thru location for army/fleet (e.g. can an army be convoyed through PAR) + :param army: Boolean to indicate if unit being convoyed is army (1) or fleet (0) :param loc: Location we are checking (e.g. 'STP/SC') :return: Boolean to indicate if unit can be convoyed through location @@ -1824,6 +1936,7 @@ class Game(Jsonable): def _is_moving_via_convoy(self, unit): """ Determines if a unit is moving via a convoy or through land + :param unit: The name of the unit (e.g. 'A PAR') :return: A boolean (True, False) to indicate if the unit is moving via convoy """ @@ -1838,6 +1951,7 @@ class Game(Jsonable): def _has_convoy_path(self, unit, start, end, convoying_loc=None): """ Determines if there is a convoy path for unit + :param unit: The unit BEING convoyed (e.g. 'A' or 'F') :param start: The start location of the unit (e.g. 'LON') :param end: The destination of the unit (e.g. 'MAR') @@ -1854,6 +1968,7 @@ class Game(Jsonable): def _get_convoying_units_for_path(self, unit, start, end): """ Returns a list of units who have submitted orders to convoy 'unit' from 'start' to 'end' + :param unit: The unit BEING convoyed (e.g. 'A' or 'F') :param start: The start location of the unit (e.g. 'LON') :param end: The destination of the unit (e.g. 'MAR') @@ -1869,10 +1984,11 @@ class Game(Jsonable): def _get_convoy_destinations(self, unit, start, unit_is_convoyer=False, exclude_convoy_locs=None): """ Returns a list of possible convoy destinations for a unit + :param unit: The unit BEING convoyed (e.g. 'A' or 'F') :param start: The start location of the unit (e.g. 'LON') :param unit_is_convoyer: Boolean flag. If true, list all the dests that an unit being convoyed by unit - could reach + could reach :param exclude_convoy_locs: Optional. A list of convoying location that needs to be excluded from all paths. :return: A list of convoying destinations (e.g. ['PAR', 'MAR']) that can be reached from start """ @@ -1908,11 +2024,12 @@ class Game(Jsonable): def _get_convoy_paths(self, unit_type, start, end, via, convoying_units): """ Return a list of all possible convoy paths (using convoying units) from start to end + :param unit_type: The unit type BEING convoyed (e.g. 'A' or 'F') :param start: The start location of the unit (e.g. 'LON') :param end: The destination of the unit (e.g. 'MAR') :param via: Boolean flag (0 or 1) to indicate if we want only paths with a local convoyer, or also paths - including only foreign convoyers + including only foreign convoyers :param convoying_units: The list of units who can convoy the unit :return: A list of paths from start to end using convoying_units """ @@ -1956,6 +2073,7 @@ class Game(Jsonable): def _get_distance_to_home(self, unit_type, start, homes): """ Calculate the distance from unit to one of its homes Armies can move over water (4.D.8 choice d) + :param unit_type: The unit type to calculate distance (e.g. 'A' or 'F') :param start: The start location of the unit (e.g. 'LON') :param homes: The list of homes (first one reached calculates the distance) @@ -2000,15 +2118,17 @@ class Game(Jsonable): # ==================================================================== def _valid_order(self, power, unit, order, report=1): """ Determines if an order is valid + :param power: The power submitting the order :param unit: The unit being affected by the order (e.g. 'A PAR') :param order: The actual order (e.g. 'H' or 'S A MAR') :param report: Boolean to report errors in self.errors :return: One of the following: - None - The order is NOT valid at all - -1 - It is NOT valid, BUT it does not get reported because it may be used to signal support - 0 - It is valid, BUT some unit mentioned does not exist - 1 - It is completed valid + + * None - The order is NOT valid at all + * -1 - It is NOT valid, BUT it does not get reported because it may be used to signal support + * 0 - It is valid, BUT some unit mentioned does not exist + * 1 - It is completed valid """ # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements # No order @@ -2261,8 +2381,9 @@ class Game(Jsonable): def _expand_order(self, word): """ Detects errors in order, convert to short version, and expand the default coast if necessary + :param word: The words (e.g. order.split()) for an order - (e.g. ['England:', 'Army', 'Rumania', 'SUPPORT', 'German', 'Army', 'Bulgaria']). + (e.g. ['England:', 'Army', 'Rumania', 'SUPPORT', 'German', 'Army', 'Bulgaria']). :return: The compacted and expanded order (e.g. ['A', 'RUM', 'S', 'A', 'BUL']) """ if not word: @@ -2350,6 +2471,7 @@ class Game(Jsonable): For Fleets: Adjust to correct coast if wrong coast is specified For Armies: Removes coast if coast is specified (e.g. if F is on SPA/SC but the order is F SPA/NC - LYO, the coast will be added or corrected) + :param word: A list of tokens (e.g. ['F', 'GRE', '-', 'BUL']) :return: The updated list of tokens (e.g. ['F', 'GRE', '-', 'BUL/SC']) """ @@ -2393,6 +2515,7 @@ class Game(Jsonable): def _add_unit_types(self, item): """ Adds any missing "A" and "F" designations and (current) coastal locations for fleets. + :param item: The words for expand_order() (e.g. ['A', 'RUM', 'S', 'BUL']) :return: The list of items with A/F and coasts added (e.g. ['A', 'RUM', 'S', 'A', 'BUL']) """ @@ -2431,8 +2554,9 @@ class Game(Jsonable): return word def _add_coasts(self): - """ This method adds the matching coast to orders supporting or (portage) convoying a fleet to - a multi-coast province. + """ This method adds the matching coast to orders supporting or (portage) + convoying a fleet to a multi-coast province. + :return: Nothing """ # converting to unique format @@ -2528,14 +2652,16 @@ class Game(Jsonable): # ==================================================================== def _load_rules(self): """ Loads the list of rules and their forced (+) and denied (!) corresponding rules - :return: A tuple of dictionaries: rules, forced, and denied - rules = {'NO_CHECK': - { 'group': '3 Movement Order', - 'variant': 'standard', - '!': ['RULE_1', 'RULE_2'], - '+': ['RULE_3'] } } - forced = {'payola': 'RULE_4'} - denied = {'payola': 'RULE_5'} + + :return: A tuple of dictionaries: rules, forced, and denied :: + + rules = {'NO_CHECK': + { 'group': '3 Movement Order', + 'variant': 'standard', + '!': ['RULE_1', 'RULE_2'], + '+': ['RULE_3'] } } + forced = {'payola': 'RULE_4'} + denied = {'payola': 'RULE_5'} """ if self.__class__.rule_cache: return self.__class__.rule_cache @@ -2612,6 +2738,7 @@ class Game(Jsonable): # ==================================================================== def _begin(self): """ Called to begin the game and move to the start phase + :return: Nothing """ self._move_to_start_phase() @@ -2655,6 +2782,7 @@ class Game(Jsonable): def _advance_phase(self): """ Advance the game to the next phase (skipping phases with no actions) + :return: A list of lines to put in the results """ @@ -2689,6 +2817,7 @@ class Game(Jsonable): def _move_to_start_phase(self): """ Moves to the map's start phase + :return: Nothing, but sets the self.phase and self.phase_type settings """ # Retrieve the beginning phase and phase type from the map @@ -2697,8 +2826,9 @@ class Game(Jsonable): def _find_next_phase(self, phase_type=None, skip=0): """ Returns the long name of the phase coming immediately after the current phase + :param phase_type: The type of phase we are looking for - (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.) + (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.) :param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after) :return: The long name of the next phase (e.g. FALL 1905 MOVEMENT) """ @@ -2706,8 +2836,9 @@ class Game(Jsonable): def _find_previous_phase(self, phase_type=None, skip=0): """ Returns the long name of the phase coming immediately before the current phase + :param phase_type: The type of phase we are looking for - (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.) + (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.) :param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after) :return: The long name of the previous phase (e.g. SPRING 1905 MOVEMENT) """ @@ -2723,6 +2854,7 @@ class Game(Jsonable): def _check_phase(self): """ Checks if we need to process a phase, or if we can skip it if there are no actions + :return: Boolean (0 or 1) - 0 if we need to process phase, 1 if we can skip it """ # pylint: disable=too-many-return-statements @@ -2786,6 +2918,7 @@ class Game(Jsonable): def _build_sites(self, power): """ Returns a list of sites where power can build units + :param power: The power instance to check :return: A list of build sites """ @@ -2805,8 +2938,12 @@ class Game(Jsonable): def _build_limit(self, power, sites=None): """ Determines the maximum number of builds a power can do in an adjustment phase - Note: This function assumes that one unit can be built per build sites + alternative sites - The actual maximum build limit would be less if units are built on alternative sites. + + Note: + + - This function assumes that one unit can be built per build sites + alternative sites. + - The actual maximum build limit would be less if units are built on alternative sites. + :param power: The power instance to check :param sites: The power's build sites (or None to compute them) :return: An integer representing the maximum number of simultaneous builds @@ -2820,6 +2957,7 @@ class Game(Jsonable): def _calculate_victory_score(self): """ Calculates the score to determine win for each power + :return: A dict containing the score for each power (e.g. {'FRANCE': 10, 'ENGLAND': 2}) """ score = {} @@ -2831,8 +2969,9 @@ class Game(Jsonable): def _determine_win(self, last_year): """ Determine if we have a win. + :param last_year: A dict containing the score for each power (e.g. {'FRANCE': 10, 'ENGLAND': 2}) - (from the previous year) + (from the previous year) :return: Nothing """ victors, this_year = [], self._calculate_victory_score() @@ -2863,6 +3002,7 @@ class Game(Jsonable): def _capture_centers(self): """ In Adjustment Phase, proceed with the capture of occupied supply centers + :return: Nothing """ victory_score_prev_year = self._calculate_victory_score() @@ -2913,6 +3053,7 @@ class Game(Jsonable): def _transfer_center(self, from_power, to_power, center): """ Transfers a supply center from a power to another + :param from_power: The power instance from whom the supply center is transfered :param to_power: The power instance to whom the supply center is transferred :param center: The supply center location (e.g. 'PAR') @@ -2927,6 +3068,7 @@ class Game(Jsonable): def _finish(self, victors): """ Indicates that a game is finished and has been won by 'victors' + :param victors: The list of victors (e.g. ['FRANCE', 'GERMANY']) :return: Nothing """ @@ -2942,6 +3084,7 @@ class Game(Jsonable): def _phase_abbr(self, phase=None): """ Constructs a 5 character representation (S1901M) from a phase (SPRING 1901 MOVEMENT) + :param phase: The full phase (e.g. SPRING 1901 MOVEMENT) :return: A 5 character representation of the phase """ @@ -2954,14 +3097,15 @@ class Game(Jsonable): """ Adds an order for a power :param power: The power instance issuing the order :param word: The order (e.g. ['A', 'PAR', '-', 'MAR']) - :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.) - If false, expect orders in the following format. False gives a performance improvement. + :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.). + If false, expect orders in the following format. False gives a performance improvement. :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. :return: Nothing, but adds error to self.error - Expected format: - A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI - A IRO R MAO, A IRO D, A LON B, F LIV B + Expected format: :: + + A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, + F NWG C A NWY - EDI, A IRO R MAO, A IRO D, A LON B, F LIV B """ if not word: return None @@ -3019,15 +3163,17 @@ class Game(Jsonable): def _update_orders(self, power, orders, expand=True, replace=True): """ Updates the orders of a power + :param power: The power instance (or None if updating multiple instances) :param orders: The updated list of orders - e.g. ['A MAR - PAR', 'A PAR - BER', ...] + e.g. ['A MAR - PAR', 'A PAR - BER', ...] :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.) - If false, expect orders in the following format. False gives a performance improvement. + If false, expect orders in the following format. False gives a performance improvement. :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. :return: Nothing - Expected format: + Expected format: :: + A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI A IRO R MAO, A IRO D, A LON B, F LIV B """ @@ -3081,15 +3227,17 @@ class Game(Jsonable): def _add_retreat_orders(self, power, orders, expand=True, replace=True): """ Adds a retreat order (Retreats Phase) + :param power: The power instance who is submitting orders (or None if power is in the orders) :param orders: The list of adjustment orders - (format can be [Country: order], [Country, order, order], or [order,order]) + (format can be [Country: order], [Country, order, order], or [order,order]) :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.) - If false, expect orders in the following format. False gives a performance improvement. + If false, expect orders in the following format. False gives a performance improvement. :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. :return: Nothing, but adds error to self.error - Expected format: + Expected format: :: + A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI A IRO R MAO, A IRO D, A LON B, F LIV B """ @@ -3178,14 +3326,16 @@ class Game(Jsonable): def _update_retreat_orders(self, power, orders, expand=True, replace=True): """ Updates order for Retreats phase + :param power: The power instance submitting the orders :param orders: The updated orders :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.) - If false, expect orders in the following format. False gives a performance improvement. + If false, expect orders in the following format. False gives a performance improvement. :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. :return: List of processing errors - Expected format: + Expected format: :: + A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI A IRO R MAO, A IRO D, A LON B, F LIV B """ @@ -3194,15 +3344,17 @@ class Game(Jsonable): def _add_adjust_orders(self, power, orders, expand=True, replace=True): """ Adds an adjustment order (Adjustment Phase) + :param power: The power instance who is submitting orders (or None if power is in the orders) :param orders: The list of adjustment orders (format can be [Country: order], - [Country, order, order], or [order,order]) + [Country, order, order], or [order,order]) :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.) - If false, expect orders in the following format. False gives a performance improvement. + If false, expect orders in the following format. False gives a performance improvement. :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. :return: Nothing, but adds error to self.error - Expected format: + Expected format: :: + A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI A IRO R MAO, A IRO D, A LON B, F LIV B """ @@ -3344,14 +3496,16 @@ class Game(Jsonable): def _update_adjust_orders(self, power, orders, expand=True, replace=True): """ Updates order for Adjustment phase + :param power: The power instance submitting the orders :param orders: The updated orders :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.) - If false, expect orders in the following format. False gives a performance improvement. + If false, expect orders in the following format. False gives a performance improvement. :param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders. :return: List of processing errors - Expected format: + Expected format: :: + A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI A IRO R MAO, A IRO D, A LON B, F LIV B """ @@ -3379,6 +3533,7 @@ class Game(Jsonable): def _default_orders(self, power): """ Issues default orders for a power (HOLD) + :param power: The power instance :return: Nothing """ @@ -3397,6 +3552,7 @@ class Game(Jsonable): # ==================================================================== def _abuts(self, unit_type, unit_loc, order_type, other_loc): """ Determines if a order for unit_type from unit_loc to other_loc is adjacent (Support and convoy only) + :param unit_type: The type of unit ('A' or 'F') :param unit_loc: The location of the unit ('BUR', 'BUL/EC') :param order_type: The type of order ('S' for Support, 'C' for Convoy', '-' for move) @@ -3422,9 +3578,10 @@ class Game(Jsonable): def _unit_owner(self, unit, coast_required=1): """ Finds the power who owns a unit + :param unit: The name of the unit to find (e.g. 'A PAR') :param coast_required: Indicates that the coast is in the unit - (if 0, you can search for 'F STP' for example, but if 1, you must specify 'F STP/SC') + (if 0, you can search for 'F STP' for example, but if 1, you must specify 'F STP/SC') :return: The power instance who owns the unit or None """ # If coast_required is 0 and unit does not contain a '/' @@ -3435,6 +3592,7 @@ class Game(Jsonable): def _occupant(self, site, any_coast=0): """ Finds the occupant of a site + :param site: The site name (e.g. "STP") :param any_coast: Boolean to indicate to return unit on any coast :return: The unit (e.g. "A STP", "F STP/NC") occupying the site, None otherwise @@ -3451,6 +3609,7 @@ class Game(Jsonable): """ This function sets self.combat to a dictionary of dictionaries, specifying each potential destination for every piece, with the strengths of each unit's attempt to get (or stay) there, and with the givers of supports that DON'T country dislodgement. (i.e. supports given by the power owning the occupying unit). + :return: Nothing, but sets self.combat """ # For example, the following orders, all by the same power: @@ -3483,6 +3642,7 @@ class Game(Jsonable): def _detect_paradox(self, starting_node, paradox_action, paradox_last_words): """ Paradox detection algorithm. Start at starting node and move chain to see if node if performing paradox action + :param starting_node: The location (e.g. PAR) where to start the paradox chain :param paradox_action: The action that would cause a paradox in the chain (e.g. 'S') :param paradox_last_words: The last words to detect in a order to cause a paradox (e.g. ['F', 'NTH']) @@ -3511,8 +3671,9 @@ class Game(Jsonable): def _check_disruptions(self, may_convoy, result, coresult=None): """ Determines convoy disruptions. + :param may_convoy: Contains the dictionary of all convoys that have a chance to succeed - (e.g. {'A PAR': ['BER', 'MUN']} + (e.g. {'A PAR': ['BER', 'MUN']} :param result: Result to set for the unit if the convoying fleet would be dislodged (e.g. 'maybe', 'no convoy') :param coresult: Result to set for the convoyer if the convoying fleet would be dislodged (e.g. 'dislodged') @@ -3583,6 +3744,7 @@ class Game(Jsonable): def _boing(self, unit): """ Mark a unit bounced, and update the combat table to show the unit as having strength one at its current location + :param unit: The unit to bounce (e.g. 'A PAR') :return: 1 """ @@ -3669,6 +3831,7 @@ class Game(Jsonable): def _cut_support(self, unit, direct=0): """ See if the order made by the unit cuts a support. If so, cut it. + :param unit: The unit who is attacking (and cutting support) :param direct: Boolean Flag - If set, the order must not only be a move, but also a non-convoyed move. :return: Nothing @@ -3709,6 +3872,7 @@ class Game(Jsonable): def _no_effect(self, unit, site): """ Removes a unit from the combat list of an attack site + :param unit: The unit attacking the site (e.g. ['A PAR', []]) :param site: The site being attacked (e.g. 'MAR') :return: Nothing @@ -3722,6 +3886,7 @@ class Game(Jsonable): def _unbounce(self, site): """ Unbounce any powerful-enough move that can now take the spot being vacated by the dislodger. + :param site: The site being attacked :return: Nothing """ @@ -4015,6 +4180,7 @@ class Game(Jsonable): def _move_results(self): """ Resolves moves (Movement phase) and returns a list of messages explaining what happened + :return: A list of lines for the results file explaining what happened during the phase """ # Resolving moves @@ -4097,6 +4263,7 @@ class Game(Jsonable): def _other_results(self): """ Resolves moves (Retreat and Adjustment phase) and returns a list of messages explaining what happened + :return: A list of lines for the results file explaining what happened during the phase """ # pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks @@ -4320,6 +4487,7 @@ class Game(Jsonable): def _resolve(self): """ Resolve the current phase + :return: A list of strings for the results file explaining how the phase was resolved. """ this_phase = self.phase_type diff --git a/diplomacy/engine/map.py b/diplomacy/engine/map.py index 65bcdfa..c23b502 100644 --- a/diplomacy/engine/map.py +++ b/diplomacy/engine/map.py @@ -30,84 +30,83 @@ UNDETERMINED, POWER, UNIT, LOCATION, COAST, ORDER, MOVE_SEP, OTHER = 0, 1, 2, 3, MAP_CACHE = {} -class Map(): - """ MAP Class - - Properties: - - abbrev: Contains the power abbreviation, otherwise defaults to first letter of PowerName - e.g. {'ENGLISH': 'E'} - - abuts_cache: Contains a cache of abuts for ['A,'F'] between all locations for orders ['S', 'C', '-'] - e.g. {(A, PAR, -, MAR): 1, ...} - - aliases: Contains a dict of all the aliases (e.g. full province name to 3 char) - e.g. {'EAST': 'EAS', 'STP ( /SC )': 'STP/SC', 'FRENCH': 'FRANCE', 'BUDAPEST': 'BUD', 'NOR': 'NWY', ... } - - centers: Contains a dict of currently owned supply centers for each player - e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... } - - convoy_paths: Contains a list of all possible convoys paths bucketed by number of fleets - format: {nb of fleets: [(START_LOC, {FLEET LOC}, {DEST LOCS})]} - - dest_with_coasts: Contains a dictionary of locs with all destinations (incl coasts) that can be reached - e.g. {'PAR': ['BRE', 'PIC', 'BUR', ...], ...} - - dummies: Indicates the list of powers that are dummies - e.g. ['FRANCE', 'ITALY'] - - error: Contains a list of errors that the map generated - e.g. [''DUPLICATE MAP ALIAS OR POWER: JAPAN'] - - files: Contains a list of files that were loaded (e.g. USES keyword) - e.g. ['standard.map', 'standard.politics', 'standard.geography', 'standard.military'] - - first_year: Indicates the year where the game is starting. - e.g. 1901 - - flow: List that contains the seasons with the phases - e.g. ['SPRING:MOVEMENT,RETREATS', 'FALL:MOVEMENT,RETREATS', 'WINTER:ADJUSTMENTS'] - - flow_sign: Indicate the direction of flow (1 is positive, -1 is negative) - e.g. 1 - - homes: Contains the list of supply centers where units can be built (i.e. assigned at the beginning) - e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... } - - inhabits: List that indicates which power have a INHABITS, HOME, or HOMES line - e.g. ['FRANCE'] - - keywords: Contains a dict of keywords to parse status files and orders - e.g. {'BUILDS': 'B', '>': '', 'SC': '/SC', 'REMOVING': 'D', 'WAIVED': 'V', 'ATTACK': '', ... } - - loc_abut: Contains a adjacency list for each province - e.g. {'LVP': ['CLY', 'edi', 'IRI', 'NAO', 'WAL', 'yor'], ...} - - loc_coasts: Contains a mapping of all coasts for every location - e.g. {'PAR': ['PAR'], 'BUL': ['BUL', 'BUL/EC', 'BUL/SC'], ... } - - loc_name: Dict that indicates the 3 letter name of each location - e.g. {'GULF OF LYON': 'LYO', 'BREST': 'BRE', 'BUDAPEST': 'BUD', 'RUHR': 'RUH', ... } - - loc_type: Dict that indicates if each location is 'WATER', 'COAST', 'LAND', or 'PORT' - e.g. {'MAO': 'WATER', 'SER': 'LAND', 'SYR': 'COAST', 'MOS': 'LAND', 'VEN': 'COAST', ... } - - locs: List of 3 letter locations (With coasts) - e.g. ['ADR', 'AEG', 'ALB', 'ANK', 'APU', 'ARM', 'BAL', 'BAR', 'BEL', 'BER', ... ] - - name: Name of the map (or full path to a custom map file) - e.g. 'standard' or '/some/path/to/file.map' - - own_word: Dict to indicate the word used to refer to people living in each power's country - e.g. {'RUSSIA': 'RUSSIAN', 'FRANCE': 'FRENCH', 'UNOWNED': 'UNOWNED', 'TURKEY': 'TURKISH', ... } - - owns: List that indicates which power have a OWNS or CENTERS line - e.g. ['FRANCE'] - - phase: String to indicate the beginning phase of the map - e.g. 'SPRING 1901 MOVEMENT' - - phase_abbrev: Dict to indicate the 1 letter abbreviation for each phase - e.g. {'A': 'ADJUSTMENTS', 'M': 'MOVEMENT', 'R': 'RETREATS'} - - pow_name: Dict to indicate the power's name - e.g. {'RUSSIA': 'RUSSIA', 'FRANCE': 'FRANCE', 'TURKEY': 'TURKEY', 'GERMANY': 'GERMANY', ... } - - powers: Contains the list of powers (players) in the game - e.g. ['AUSTRIA', 'ENGLAND', 'FRANCE', 'GERMANY', 'ITALY', 'RUSSIA', 'TURKEY'] - - root_map: Contains the name of the original map file loaded (before the USES keyword are applied) - A map that is called with MAP is the root_map - e.g. 'standard' - - rules: Contains a list of rules used by all variants (for display only) - e.g. ['RULE_1'] - - scs: Contains a list of all the supply centers in the game - e.g. ['MOS', 'SEV', 'STP', 'WAR', 'BRE', 'MAR', 'PAR', 'BEL', 'BUL', 'DEN', 'GRE', 'HOL', 'NWY', ... ] - - seq: [] Contains the sequence of seasons in format 'SEASON_NAME SEASON_TYPE' - e.g. ['NEWYEAR', 'SPRING MOVEMENT', 'SPRING RETREATS', 'FALL MOVEMENT', 'FALL RETREATS', - 'WINTER ADJUSTMENTS'] - - unclear: Contains the alias for ambiguous places - e.g. {'EAST': 'EAS'} - - unit_names: {} Contains a dict of the unit names - e.g. {'F': 'FLEET', 'A': 'ARMY'} - - units: Dict that contains the current position of each unit by power - e.g. {'FRANCE': ['F BRE', 'A MAR', 'A PAR'], 'RUSSIA': ['A WAR', 'A MOS', 'F SEV', 'F STP/SC'], ... } - - validated: Boolean to indicate if the map file has been validated - e.g. 1 - - victory: Indicates the number of supply centers to win the game (>50% required if None) - e.g. 18 +class Map: + """ Map Class + + Properties: + + - **abbrev**: Contains the power abbreviation, otherwise defaults to first letter of PowerName + e.g. {'ENGLISH': 'E'} + - **abuts_cache**: Contains a cache of abuts for ['A,'F'] between all locations for orders ['S', 'C', '-'] + e.g. {(A, PAR, -, MAR): 1, ...} + - **aliases**: Contains a dict of all the aliases (e.g. full province name to 3 char) + e.g. {'EAST': 'EAS', 'STP ( /SC )': 'STP/SC', 'FRENCH': 'FRANCE', 'BUDAPEST': 'BUD', 'NOR': 'NWY', ... } + - **centers**: Contains a dict of owned supply centers for each player at the beginning of the map + e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... } + - **convoy_paths**: Contains a list of all possible convoys paths bucketed by number of fleets + format: {nb of fleets: [(START_LOC, {FLEET LOC}, {DEST LOCS})]} + - **dest_with_coasts**: Contains a dictionary of locs with all destinations (incl coasts) that can be reached + e.g. {'PAR': ['BRE', 'PIC', 'BUR', ...], ...} + - **dummies**: Indicates the list of powers that are dummies + e.g. ['FRANCE', 'ITALY'] + - **error**: Contains a list of errors that the map generated + e.g. [''DUPLICATE MAP ALIAS OR POWER: JAPAN'] + - **files**: Contains a list of files that were loaded (e.g. USES keyword) + e.g. ['standard.map', 'standard.politics', 'standard.geography', 'standard.military'] + - **first_year**: Indicates the year where the game is starting. + e.g. 1901 + - **flow**: List that contains the seasons with the phases + e.g. ['SPRING:MOVEMENT,RETREATS', 'FALL:MOVEMENT,RETREATS', 'WINTER:ADJUSTMENTS'] + - **flow_sign**: Indicate the direction of flow (1 is positive, -1 is negative) + e.g. 1 + - **homes**: Contains the list of supply centers where units can be built (i.e. assigned at the beginning) + e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... } + - **inhabits**: List that indicates which power have a INHABITS, HOME, or HOMES line + e.g. ['FRANCE'] + - **keywords**: Contains a dict of keywords to parse status files and orders + e.g. {'BUILDS': 'B', '>': '', 'SC': '/SC', 'REMOVING': 'D', 'WAIVED': 'V', 'ATTACK': '', ... } + - **loc_abut**: Contains a adjacency list for each province + e.g. {'LVP': ['CLY', 'edi', 'IRI', 'NAO', 'WAL', 'yor'], ...} + - **loc_coasts**: Contains a mapping of all coasts for every location + e.g. {'PAR': ['PAR'], 'BUL': ['BUL', 'BUL/EC', 'BUL/SC'], ... } + - **loc_name**: Dict that indicates the 3 letter name of each location + e.g. {'GULF OF LYON': 'LYO', 'BREST': 'BRE', 'BUDAPEST': 'BUD', 'RUHR': 'RUH', ... } + - **loc_type**: Dict that indicates if each location is 'WATER', 'COAST', 'LAND', or 'PORT' + e.g. {'MAO': 'WATER', 'SER': 'LAND', 'SYR': 'COAST', 'MOS': 'LAND', 'VEN': 'COAST', ... } + - **locs**: List of 3 letter locations (With coasts) + e.g. ['ADR', 'AEG', 'ALB', 'ANK', 'APU', 'ARM', 'BAL', 'BAR', 'BEL', 'BER', ... ] + - **name**: Name of the map (or full path to a custom map file) + e.g. 'standard' or '/some/path/to/file.map' + - **own_word**: Dict to indicate the word used to refer to people living in each power's country + e.g. {'RUSSIA': 'RUSSIAN', 'FRANCE': 'FRENCH', 'UNOWNED': 'UNOWNED', 'TURKEY': 'TURKISH', ... } + - **owns**: List that indicates which power have a OWNS or CENTERS line + e.g. ['FRANCE'] + - **phase**: String to indicate the beginning phase of the map + e.g. 'SPRING 1901 MOVEMENT' + - **phase_abbrev**: Dict to indicate the 1 letter abbreviation for each phase + e.g. {'A': 'ADJUSTMENTS', 'M': 'MOVEMENT', 'R': 'RETREATS'} + - **pow_name**: Dict to indicate the power's name + e.g. {'RUSSIA': 'RUSSIA', 'FRANCE': 'FRANCE', 'TURKEY': 'TURKEY', 'GERMANY': 'GERMANY', ... } + - **powers**: Contains the list of powers (players) in the game + e.g. ['AUSTRIA', 'ENGLAND', 'FRANCE', 'GERMANY', 'ITALY', 'RUSSIA', 'TURKEY'] + - **root_map**: Contains the name of the original map file loaded (before the USES keyword are applied) + A map that is called with MAP is the root_map. e.g. 'standard' + - **rules**: Contains a list of rules used by all variants (for display only) + e.g. ['RULE_1'] + - **scs**: Contains a list of all the supply centers in the game + e.g. ['MOS', 'SEV', 'STP', 'WAR', 'BRE', 'MAR', 'PAR', 'BEL', 'BUL', 'DEN', 'GRE', 'HOL', 'NWY', ... ] + - **seq**: [] Contains the sequence of seasons in format 'SEASON_NAME SEASON_TYPE' + e.g. ['NEWYEAR', 'SPRING MOVEMENT', 'SPRING RETREATS', 'FALL MOVEMENT', 'FALL RETREATS', 'WINTER ADJUSTMENTS'] + - **unclear**: Contains the alias for ambiguous places + e.g. {'EAST': 'EAS'} + - **unit_names**: {} Contains a dict of the unit names + e.g. {'F': 'FLEET', 'A': 'ARMY'} + - **units**: Dict that contains the current position of each unit by power + e.g. {'FRANCE': ['F BRE', 'A MAR', 'A PAR'], 'RUSSIA': ['A WAR', 'A MOS', 'F SEV', 'F STP/SC'], ... } + - **validated**: Boolean to indicate if the map file has been validated + e.g. 1 + - **victory**: Indicates the number of supply centers to win the game (>50% required if None) + e.g. 18 """ # pylint: disable=too-many-instance-attributes @@ -119,6 +118,7 @@ class Map(): def __new__(cls, name='standard', use_cache=True): """ New function - Retrieving object from cache if possible + :param name: Name of the map to load :param use_cache: Boolean flag to indicate we want a blank object that doesn't use cache """ @@ -128,6 +128,7 @@ class Map(): def __init__(self, name='standard', use_cache=True): """ Constructor function + :param name: Name of the map to load (or full path to a custom map file) :param use_cache: Boolean flag to indicate we want a blank object that doesn't use cache """ @@ -168,8 +169,18 @@ class Map(): def __str__(self): return self.name + @property + def svg_path(self): + """ Return path to the SVG file of this map (or None if it does not exist) """ + for file_name in [self.name + '.svg', self.root_map + '.svg']: + svg_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', file_name) + if os.path.exists(svg_path): + return svg_path + return None + def validate(self, force=0): """ Validate that the configuration from a map file is correct + :param force: Indicate that we want to force a validation, even if the map is already validated :return: Nothing """ @@ -294,6 +305,7 @@ class Map(): def load(self, file_name=None): """ Loads a map file from disk + :param file_name: Optional. A string representing the file to open. Otherwise, defaults to the map name :return: Nothing """ @@ -716,8 +728,9 @@ class Map(): def add_homes(self, power, homes, reinit): """ Add new homes (and deletes previous homes if reinit) + :param power: Name of power (e.g. ITALY) - :param homes: List of homes e.g. ['BUR', '-POR', '*ITA', ... ] + :param homes: List of homes e.g. ``['BUR', '-POR', '*ITA', ... ]`` :param reinit: Indicates that we want to strip the list of homes before adding :return: Nothing """ @@ -753,6 +766,7 @@ class Map(): def drop(self, place): """ Drop a place + :param place: Name of place to remove :return: Nothing """ @@ -807,6 +821,7 @@ class Map(): def norm_power(self, power): """ Normalise the name of a power (removes spaces) + :param power: Name of power to normalise :return: Normalised power name """ @@ -814,6 +829,7 @@ class Map(): def norm(self, phrase): """ Normalise a sentence (add spaces before /, replace -+, with ' ', remove .: + :param phrase: Phrase to normalise :return: Normalised sentences """ @@ -828,8 +844,9 @@ class Map(): def compact(self, phrase): """ Compacts a full sentence into a list of short words + :param phrase: The full sentence to compact (e.g. 'England: Fleet Western Mediterranean -> Tyrrhenian - Sea. (*bounce*)') + Sea. (*bounce*)') :return: The compacted phrase in an array (e.g. ['ENGLAND', 'F', 'WES', 'TYS', '|']) """ if ':' in phrase: @@ -849,6 +866,7 @@ class Map(): def alias(self, word): """ This function is used to replace multi-words with their acronyms + :param word: The current list of words to try to shorten :return: alias, ix - alias is the shorten list of word, ix is the ix of the next non-processed word """ @@ -922,12 +940,20 @@ class Map(): def vet(self, word, strict=0): """ Determines the type of every word in a compacted order phrase - 0 - Undetermined, 1 - Power, 2 - Unit, 3 - Location, 4 - Coastal location - 5 - Order, 6 - Move Operator (-=_^), 7 - Non-move separator (|?~) or result (*!?~+) - :param word: The list of words to vet (e.g. ['A', 'POR', 'S', 'SPA/NC']) + + 0 - Undetermined, + 1 - Power, + 2 - Unit, + 3 - Location, + 4 - Coastal location + 5 - Order, + 6 - Move Operator ``(-=_^)``, + 7 - Non-move separator ``(|?~)`` or result ``(*!?~+)`` + + :param word: The list of words to vet (e.g. ``['A', 'POR', 'S', 'SPA/NC']``) :param strict: Boolean to indicate that we want to verify that the words actually exist. Numbers become negative if they don't exist - :return: A list of tuple (e.g. [('A', 2), ('POR', 3), ('S', 5), ('SPA/NC', 4)]) + :return: A list of tuple (e.g. ``[('A', 2), ('POR', 3), ('S', 5), ('SPA/NC', 4)]``) """ result = [] for thing in word: @@ -960,6 +986,7 @@ class Map(): def rearrange(self, word): """ This function is used to parse commands + :param word: The list of words to vet (e.g. ['ENGLAND', 'F', 'WES', 'TYS', '|']) :return: The list of words in the correct order to be processed (e.g. ['ENGLAND', 'F', 'WES', '-', 'TYS']) """ @@ -1070,6 +1097,7 @@ class Map(): def area_type(self, loc): """ Returns 'WATER', 'COAST', 'PORT', 'LAND', 'SHUT' + :param loc: The name of the location to query :return: Type of the location ('WATER', 'COAST', 'PORT', 'LAND', 'SHUT') """ @@ -1078,6 +1106,7 @@ class Map(): def default_coast(self, word): """ Returns the coast for a fleet move order that can only be to a single coast (e.g. F GRE-BUL returns F GRE-BUL/SC) + :param word: A list of tokens (e.g. ['F', 'GRE', '-', 'BUL']) :return: The updated list of tokens (e.g. ['F', 'GRE', '-', 'BUL/SC']) """ @@ -1096,14 +1125,16 @@ class Map(): def find_coasts(self, loc): """ Finds all coasts for a given location + :param loc: The name of a location (e.g. 'BUL') :return: Returns the list of all coasts, including the location (e.g. ['BUL', 'BUL/EC', 'BUL/SC'] """ return self.loc_coasts.get(loc.upper(), []) def abuts(self, unit_type, unit_loc, order_type, other_loc): - """ Determines if a order for unit_type from unit_loc to other_loc is adjacent - Note: This method uses the precomputed cache + """ Determines if a order for unit_type from unit_loc to other_loc is adjacent. + + **Note**: This method uses the precomputed cache :param unit_type: The type of unit ('A' or 'F') :param unit_loc: The location of the unit ('BUR', 'BUL/EC') @@ -1120,7 +1151,8 @@ class Map(): def _abuts(self, unit_type, unit_loc, order_type, other_loc): """ Determines if a order for unit_type from unit_loc to other_loc is adjacent - Note: This method is used to generate the abuts_cache + + **Note**: This method is used to generate the abuts_cache :param unit_type: The type of unit ('A' or 'F') :param unit_loc: The location of the unit ('BUR', 'BUL/EC') @@ -1186,6 +1218,7 @@ class Map(): def is_valid_unit(self, unit, no_coast_ok=0, shut_ok=0): """ Determines if a unit and location combination is valid (e.g. 'A BUR') is valid + :param unit: The name of the unit with its location (e.g. F SPA/SC) :param no_coast_ok: Indicates if a coastal location with no coast (e.g. SPA vs SPA/SC) is acceptable :param shut_ok: Indicates if a impassable country (e.g. Switzerland) is OK @@ -1209,12 +1242,17 @@ class Map(): def abut_list(self, site, incl_no_coast=False): """ Returns the adjacency list for the site + :param site: The province we want the adjacency list for :param incl_no_coast: Boolean flag that indicates to also include province without coast if it has coasts - e.g. will return ['BUL/SC', 'BUL/EC'] if False, and ['bul', 'BUL/SC', 'BUL/EC'] if True + e.g. will return ['BUL/SC', 'BUL/EC'] if False, and ['bul', 'BUL/SC', 'BUL/EC'] if True :return: A list of adjacent provinces - Note: abuts are returned in mixed cases (lowercase for A only, First capital letter for F only) + Note: abuts are returned in **mixed cases** + + - An adjacency that is lowercase (e.g. 'bur') can only be used by an army + - An adjacency that starts with a capital letter (e.g. 'Bal') can only be used by a fleet + - An adjacency that is uppercase can be used by both an army and a fleet """ if site in self.loc_abut: abut_list = self.loc_abut.get(site, []) @@ -1229,9 +1267,10 @@ class Map(): def find_next_phase(self, phase, phase_type=None, skip=0): """ Returns the long name of the phase coming immediately after the phase + :param phase: The long name of the current phase (e.g. SPRING 1905 RETREATS) - :param phase_type: The type of phase we are looking for (e.g. 'M' for Movement, 'R' for Retreats, - 'A' for Adjust.) + :param phase_type: The type of phase we are looking for + (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.) :param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after) :return: The long name of the next phase (e.g. FALL 1905 MOVEMENT) """ @@ -1277,9 +1316,10 @@ class Map(): def find_previous_phase(self, phase, phase_type=None, skip=0): """ Returns the long name of the phase coming immediately prior the phase + :param phase: The long name of the current phase (e.g. SPRING 1905 RETREATS) - :param phase_type: The type of phase we are looking for (e.g. 'M' for Movement, 'R' for Retreats, - 'A' for Adjust.) + :param phase_type: The type of phase we are looking for + (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.) :param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after) :return: The long name of the previous phase (e.g. SPRING 1905 MOVEMENT) """ @@ -1332,6 +1372,7 @@ class Map(): def compare_phases(self, phase1, phase2): """ Compare 2 phases (Strings) and return 1, -1, or 0 to indicate which phase is larger + :param phase1: The first phase (e.g. S1901M, FORMING, COMPLETED) :param phase2: The second phase (e.g. S1901M, FORMING, COMPLETED) :return: 1 if phase1 > phase2, -1 if phase2 > phase1 otherwise 0 if they are equal @@ -1378,6 +1419,7 @@ class Map(): @staticmethod def phase_abbr(phase, default='?????'): """ Constructs a 5 character representation (S1901M) from a phase (SPRING 1901 MOVEMENT) + :param phase: The full phase (e.g. SPRING 1901 MOVEMENT) :param default: The default value to return in case conversion fails :return: A 5 character representation of the phase @@ -1389,6 +1431,7 @@ class Map(): def phase_long(self, phase_abbr, default='?????'): """ Constructs a full sentence of a phase from a 5 character abbreviation + :param phase_abbr: 5 character abbrev. (e.g. S1901M) :param default: The default value to return in case conversion fails :return: A full phase description (e.g. SPRING 1901 MOVEMENT) diff --git a/diplomacy/engine/message.py b/diplomacy/engine/message.py index 150efd4..8094bfe 100644 --- a/diplomacy/engine/message.py +++ b/diplomacy/engine/message.py @@ -17,6 +17,7 @@ """ Game message. Represent a message exchanged inside a game. Possible messages exchanges: + - power 1 -> power 2 - power -> all game - system -> power @@ -25,10 +26,12 @@ - system -> omniscient observers Sender `system` is identified with constant SYSTEM defined below. - Recipients `all game`, `observers` and `omniscient observers` are identified respectively with constants - GLOBAL, OBSERVER and OMNISCIENT defined below. + + Recipients `all game`, `observers` and `omniscient observers` are identified respectively with + constants GLOBAL, OBSERVER and OMNISCIENT defined below. Consider using Game methods to generate appropriate messages instead of this class directly: + - Game.new_power_message() to send a message from a power to another. - Game.new_global_message() to send a message from a power to all game. - ServerGame.new_system_message() to send a server system message. @@ -44,14 +47,18 @@ OBSERVER = 'OBSERVER' # recipient (all observer tokens) OMNISCIENT = 'OMNISCIENT' # recipient (all omniscient tokens) class Message(Jsonable): - """ GameMessage class. Properties: - - sender: message sender name: either SYSTEM or a power name. - - recipient: message recipient name: either GLOBAL, OBSERVER, OMNISCIENT or a power name. - - time_sent: message timestamp in microseconds. - - phase: short name of game phase when message is sent. - - message: message body. - - Note about timestamp management: + """ Message class. + + Properties: + + - **sender**: message sender name: either SYSTEM or a power name. + - **recipient**: message recipient name: either GLOBAL, OBSERVER, OMNISCIENT or a power name. + - **time_sent**: message timestamp in microseconds. + - **phase**: short name of game phase when message is sent. + - **message**: message body. + + **Note about timestamp management**: + We assume a message has an unique timestamp inside one game. To respect this rule, the server is the only one responsible for generating message timestamps. This allow to generate timestamp or only 1 same machine (server) instead of managing timestamps from many user machines, to prevent timestamp inconsistency when messages @@ -60,19 +67,19 @@ class Message(Jsonable): """ __slots__ = ['sender', 'recipient', 'time_sent', 'phase', 'message'] model = { - strings.SENDER: str, # either SYSTEM or a power name. - strings.RECIPIENT: str, # either GLOBAL, OBSERVER, OMNISCIENT or a power name. + strings.SENDER: str, # either SYSTEM or a power name. + strings.RECIPIENT: str, # either GLOBAL, OBSERVER, OMNISCIENT or a power name. strings.TIME_SENT: parsing.OptionalValueType(int), # given by server. - strings.PHASE: str, # phase short name. + strings.PHASE: str, # phase short name (e.g. 'S1901M' or 'COMPLETED') strings.MESSAGE: str, } def __init__(self, **kwargs): - self.sender = None # type: str - self.recipient = None # type: str - self.time_sent = None # type: int - self.phase = None # type: str - self.message = None # type: str + self.sender = None # type: str + self.recipient = None # type: str + self.time_sent = None # type: int + self.phase = None # type: str + self.message = None # type: str super(Message, self).__init__(**kwargs) def __str__(self): diff --git a/diplomacy/engine/power.py b/diplomacy/engine/power.py index 1fe282b..c3826e6 100644 --- a/diplomacy/engine/power.py +++ b/diplomacy/engine/power.py @@ -15,6 +15,7 @@ # with this program. If not, see . # ============================================================================== """ Power + - Contains the power object representing a power in the game """ from copy import deepcopy @@ -28,33 +29,36 @@ from diplomacy.utils.constants import OrderSettings class Power(Jsonable): """ Power Class - Properties: - - abbrev - Contains the abbrev of the power (usually the first letter of the power name) (e.g. 'F' for FRANCE) - - adjust - List of pending adjustment orders - (e.g. ['A PAR B', 'A PAR R MAR', 'A MAR D', 'WAIVE']) - - centers - Contains the list of supply centers currently controlled by the power ['MOS', 'SEV', 'STP', 'WAR'] - - civil_disorder - Boolean flag to indicate that the power has been put in CIVIL_DISORDER (e.g. True or False) - - controller - Sorted dictionary mapping timestamp to controller (either dummy or a user ID) who takes - control of power at this timestamp. - - game - Contains a reference to the game object - - goner - Boolean to indicate that this power doesn't control any SCs any more (e.g. True or False) - - homes - Contains a list of homes supply centers (where you can build) - e.g. ['PAR', 'MAR', ... ] or None if empty - - influence - Contains a list of locations influenced by this power - Note: To influence a location, the power must have visited it last. - e.g ['PAR', 'MAR', ... ] - - name - Contains the name of the power - - orders - Contains a dictionary of units and their orders. - For NO_CHECK games, unit is 'ORDER 1', 'ORDER 2', ... - - e.g. {'A PAR': '- MAR' } or {'ORDER 1': 'A PAR - MAR', 'ORDER 2': '...', ... } - - Can also be {'REORDER 1': 'A PAR - MAR', 'INVALID 1': 'A PAR - MAR', ... } after validation - - retreats - Contains the list of units that need to retreat with their possible retreat locations - (e.g. {'A PAR': ['MAR', 'BER']}) - - role - Power type (observer, omniscient, player or server power). - Either the power name (for a player power) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES - - tokens - Only for server power: set of tokens of current power controlled (if not None). - - units - Contains the list of units (e.g. ['A PAR', 'A MAR', ...] - - vote - Only for omniscient, player and server power: power vote ('yes', 'no' or 'neutral'). + Properties: + + - **abbrev** - Contains the abbrev of the power (i.e. the first letter of the power name) (e.g. 'F' for FRANCE) + - **adjust** - List of pending adjustment orders + (e.g. ['A PAR B', 'A PAR R MAR', 'A MAR D', 'WAIVE']) + - **centers** - Contains the list of supply centers currently controlled by the power ['MOS', 'SEV', 'STP'] + - **civil_disorder** - Bool flag to indicate that the power has been put in CIVIL_DISORDER (e.g. True or False) + - **controller** - Sorted dictionary mapping timestamp to controller (either dummy or a user ID) who takes + control of power at this timestamp. + - **game** - Contains a reference to the game object + - **goner** - Boolean to indicate that this power doesn't control any SCs any more (e.g. True or False) + - **homes** - Contains a list of homes supply centers (where you can build) + e.g. ['PAR', 'MAR', ... ] or None if empty + - **influence** - Contains a list of locations influenced by this power + Note: To influence a location, the power must have visited it last. + e.g ['PAR', 'MAR', ... ] + - **name** - Contains the name of the power (e.g. 'FRANCE') + - **orders** - Contains a dictionary of units and their orders. + For NO_CHECK games, unit is 'ORDER 1', 'ORDER 2', ... + + - e.g. {'A PAR': '- MAR' } or {'ORDER 1': 'A PAR - MAR', 'ORDER 2': '...', ... } + - Can also be {'REORDER 1': 'A PAR - MAR', 'INVALID 1': 'A PAR - MAR', ... } after validation + + - **retreats** - Contains the list of units that need to retreat with their possible retreat locations + (e.g. {'A PAR': ['MAR', 'BER']}) + - **role** - Power type (observer, omniscient, player or server power). + Either the power name (for a player power) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES + - **tokens** - Only for server power: set of tokens of current power controlled (if not None). + - **units** - Contains the list of units (e.g. ['A PAR', 'A MAR', ...] + - **vote** - Only for omniscient, player and server power: power vote ('yes', 'no' or 'neutral'). """ __slots__ = ['game', 'name', 'abbrev', 'adjust', 'centers', 'units', 'influence', 'homes', 'retreats', 'goner', 'civil_disorder', 'orders', 'role', 'controller', 'vote', @@ -128,9 +132,7 @@ class Power(Jsonable): return text def __deepcopy__(self, memo): - """ Fast deep copy implementation - - (Not setting the game object) - """ + """ Fast deep copy implementation (**not setting the game object**) """ cls = self.__class__ result = cls.__new__(cls) @@ -145,6 +147,7 @@ class Power(Jsonable): def reinit(self, include_flags=6): """ Performs a reinitialization of some of the parameters + :param include_flags: Bit mask to indicate which params to reset (bit 1 = orders, 2 = persistent, 4 = transient) :return: None @@ -187,6 +190,7 @@ class Power(Jsonable): @staticmethod def compare(power_1, power_2): """ Comparator object - Compares two Power objects + :param power_1: The first Power object to compare :param power_2: The second Power object to compare :return: 1 if self is greater, -1 if other is greater, 0 if they are equal @@ -199,6 +203,7 @@ class Power(Jsonable): def initialize(self, game): """ Initializes a game and resets home, centers and units + :param game: The game to use for initialization :type game: diplomacy.Game """ @@ -233,6 +238,7 @@ class Power(Jsonable): def merge(self, other_power): """ Transfer all units, centers, and homes of the other_power to this power + :param other_power: The other power (will be empty after the merge) """ # Regular units @@ -291,12 +297,14 @@ class Power(Jsonable): def is_dummy(self): """ Indicates if the power is a dummy + :return: Boolean flag to indicate if the power is a dummy """ return self.controller.last_value() == strings.DUMMY def is_eliminated(self): """ Returns a flag to show if player is eliminated + :return: If the current power is eliminated """ # Not eliminated if has units left @@ -310,6 +318,7 @@ class Power(Jsonable): def moves_submitted(self): """ Returns a boolean to indicate if moves has been submitted + :return: 1 if not in Movement phase, or orders submitted, or no more units lefts """ if self.game.phase_type != 'M': @@ -321,35 +330,37 @@ class Power(Jsonable): # ============================================================== def is_observer_power(self): - """ Return True if this power is an observer power. """ + """ (Network Method) Return True if this power is an observer power. """ return self.role == strings.OBSERVER_TYPE def is_omniscient_power(self): - """ Return True if this power is an omniscient power. """ + """ (Network Method) Return True if this power is an omniscient power. """ return self.role == strings.OMNISCIENT_TYPE def is_player_power(self): - """ Return True if this power is a player power. """ + """ (Network Method) Return True if this power is a player power. """ return self.role == self.name def is_server_power(self): - """ Return True if this power is a server power. """ + """ (Network Method) Return True if this power is a server power. """ return self.role == strings.SERVER_TYPE def is_controlled(self): - """ Return True if this power is controlled. """ + """ (Network Method) Return True if this power is controlled. """ return self.controller.last_value() != strings.DUMMY def does_not_wait(self): - """ Return True if this power does not wait (ie. if we could already process orders of this power). """ + """ (Network Method) Return True if this power does not wait + (ie. if we could already process orders of this power). + """ return self.order_is_set and not self.wait def update_controller(self, username, timestamp): - """ Update controller with given username and timestamp. """ + """ (Network Method) Update controller with given username and timestamp. """ self.controller.put(timestamp, username) def set_controlled(self, username): - """ Control power with given username. Username may be None (meaning no controller). """ + """ (Network Method) Control power with given username. Username may be None (meaning no controller). """ if username is None or username == strings.DUMMY: if self.controller.last_value() != strings.DUMMY: self.controller.put(common.timestamp_microseconds(), strings.DUMMY) @@ -363,15 +374,15 @@ class Power(Jsonable): raise DiplomacyException('Power already controlled by someone else. Kick previous controller before.') def get_controller(self): - """ Return current power controller name ('dummy' if power is not controlled). """ + """ (Network Method) Return current power controller name ('dummy' if power is not controlled). """ return self.controller.last_value() def get_controller_timestamp(self): - """ Return timestamp when current controller took control of this power. """ + """ (Network Method) Return timestamp when current controller took control of this power. """ return self.controller.last_key() def is_controlled_by(self, username): - """ Return True if this power is controlled by given username. """ + """ (Network Method) Return True if this power is controlled by given username. """ if username == constants.PRIVATE_BOT_USERNAME: # Bot is connected if power is dummy and has some associated tokens. return self.is_dummy() and bool(self.tokens) @@ -380,16 +391,16 @@ class Power(Jsonable): # Server-only methods. def has_token(self, token): - """ Return True if this power has given token. """ + """ (Server Method) Return True if this power has given token. """ assert self.is_server_power() return token in self.tokens def add_token(self, token): - """ Add given token to this power. """ + """ (Server Method) Add given token to this power. """ assert self.is_server_power() self.tokens.add(token) def remove_tokens(self, tokens): - """ Remove sequence of tokens from this power. """ + """ (Server Method) Remove sequence of tokens from this power. """ assert self.is_server_power() self.tokens.difference_update(tokens) diff --git a/diplomacy/engine/renderer.py b/diplomacy/engine/renderer.py index 1c92886..e4c6ff3 100644 --- a/diplomacy/engine/renderer.py +++ b/diplomacy/engine/renderer.py @@ -16,6 +16,7 @@ # ============================================================================== # -*- coding: utf-8 -*- """ Renderer + - Contains the renderer object which is responsible for rendering a game state to svg """ import os @@ -35,14 +36,16 @@ def _attr(node_element, attr_name): """ Shorthand method to retrieve an XML attribute """ return node_element.attributes[attr_name].value -class Renderer(): +class Renderer: """ Renderer object responsible for rendering a game state to svg """ def __init__(self, game, svg_path=None): """ Constructor + :param game: The instantiated game object to render :param svg_path: Optional. Can be set to the full path of a custom SVG to use for rendering the map. :type game: diplomacy.Game + :type svg_path: str, optional """ self.game = game self.metadata = {} @@ -50,33 +53,25 @@ class Renderer(): # If no SVG path provided, we default to the one in the maps folder if not svg_path: - svg_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', self.game.map.name + '.svg') + for file_name in [self.game.map.name + '.svg', self.game.map.root_map + '.svg']: + svg_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', file_name) + if os.path.exists(svg_path): + break # Loading XML if os.path.exists(svg_path): self.xml_map = minidom.parse(svg_path).toxml() self._load_metadata() - def norm_order(self, order): - """ Normalizes the order format and split it into tokens - This is only used for **movement** orders (to make sure NO_CHECK games used the correct format) - Formats: - A PAR H - A PAR - BUR [VIA] - A PAR S BUR - A PAR S F BRE - PIC - F BRE C A PAR - LON - - :param order: The unformatted order (e.g. 'Paris - Burgundy') - :return: The tokens of the formatted order (e.g. ['A', 'PAR', '-', 'BUR']) - """ - return self.game._add_unit_types(self.game._expand_order(order.split())) # pylint: disable=protected-access - def render(self, incl_orders=True, incl_abbrev=False, output_format='svg'): """ Renders the current game and returns the XML representation + :param incl_orders: Optional. Flag to indicate we also want to render orders. :param incl_abbrev: Optional. Flag to indicate we also want to display the provinces abbreviations. - :param output_format: The desired output format. + :param output_format: The desired output format. Valid values are: 'svg' + :type incl_orders: bool, optional + :type incl_abbrev: bool, optional + :type output_format: str, optional :return: The rendered image in the specified format. """ # pylint: disable=too-many-branches @@ -127,7 +122,7 @@ class Renderer(): order = '{} {}'.format(order_key, power.orders[order_key]) # Normalizing and splitting in tokens - tokens = self.norm_order(order) + tokens = self._norm_order(order) unit_loc = tokens[1] # Parsing based on order type @@ -240,8 +235,26 @@ class Renderer(): svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:PROVINCE_DATA')[0]) self.xml_map = xml_map.toxml() + def _norm_order(self, order): + """ Normalizes the order format and split it into tokens + This is only used for **movement** orders (to make sure NO_CHECK games used the correct format) + + Formats: :: + + A PAR H + A PAR - BUR [VIA] + A PAR S BUR + A PAR S F BRE - PIC + F BRE C A PAR - LON + + :param order: The unformatted order (e.g. 'Paris - Burgundy') + :return: The tokens of the formatted order (e.g. ['A', 'PAR', '-', 'BUR']) + """ + return self.game._add_unit_types(self.game._expand_order(order.split())) # pylint: disable=protected-access + def _add_unit(self, xml_map, unit, power_name, is_dislodged): """ Adds a unit to the map + :param xml_map: The xml map being generated :param unit: The unit to add (e.g. 'A PAR') :param power_name: The name of the power owning the unit (e.g. 'FRANCE') @@ -271,6 +284,7 @@ class Renderer(): def _set_influence(self, xml_map, loc, power_name, has_supply_center=False): """ Sets the influence on the map + :param xml_map: The xml map being generated :param loc: The province being influenced (e.g. 'PAR') :param power_name: The name of the power influencing the province @@ -317,6 +331,7 @@ class Renderer(): @staticmethod def _set_current_phase(xml_map, current_phase): """ Sets the phase text at the bottom right of the the map + :param xml_map: The xml map being generated :param current_phase: The current phase (e.g. 'S1901M) :return: Nothing @@ -331,6 +346,7 @@ class Renderer(): @staticmethod def _set_note(xml_map, note_1, note_2): """ Sets a note at the top left of the map + :param xml_map: The xml map being generated :param note_1: The text to display on the first line :param note_2: The text to display on the second line @@ -347,6 +363,7 @@ class Renderer(): def _issue_hold_order(self, xml_map, loc, power_name): """ Adds a hold order to the map + :param xml_map: The xml map being generated :param loc: The province where the unit is holding (e.g. 'PAR') :param power_name: The name of the power owning the unit @@ -380,6 +397,7 @@ class Renderer(): def _issue_support_hold_order(self, xml_map, loc, dest_loc, power_name): """ Issues a support hold order + :param xml_map: The xml map being generated :param loc: The location of the unit sending support (e.g. 'BER') :param dest_loc: The location where the unit is holding from (e.g. 'PAR') @@ -442,6 +460,7 @@ class Renderer(): def _issue_move_order(self, xml_map, src_loc, dest_loc, power_name): """ Issues a move order + :param xml_map: The xml map being generated :param src_loc: The location where the unit is moving from (e.g. 'PAR') :param dest_loc: The location where the unit is moving to (e.g. 'MAR') @@ -501,6 +520,7 @@ class Renderer(): def _issue_support_move_order(self, xml_map, loc, src_loc, dest_loc, power_name): """ Issues a support move order + :param xml_map: The xml map being generated :param loc: The location of the unit sending support (e.g. 'BER') :param src_loc: The location where the unit is moving from (e.g. 'PAR') @@ -560,6 +580,7 @@ class Renderer(): def _issue_convoy_order(self, xml_map, loc, src_loc, dest_loc, power_name): """ Issues a convoy order + :param xml_map: The xml map being generated :param loc: The location of the unit convoying (e.g. 'BER') :param src_loc: The location where the unit being convoyed is moving from (e.g. 'PAR') @@ -666,6 +687,7 @@ class Renderer(): def _issue_build_order(self, xml_map, unit_type, loc, power_name): """ Adds a build army/fleet order to the map + :param xml_map: The xml map being generated :param unit_type: The unit type to build ('A' or 'F') :param loc: The province where the army is to be built (e.g. 'PAR') @@ -711,6 +733,7 @@ class Renderer(): def _issue_disband_order(self, xml_map, loc): """ Adds a disband order to the map + :param xml_map: The xml map being generated :param loc: The province where the unit is disbanded (e.g. 'PAR') :return: Nothing @@ -740,6 +763,7 @@ class Renderer(): def _center_symbol_around_unit(self, loc, is_dislodged, symbol): # type: (str, bool, str) -> Tuple[str, str] """ Compute top-left coordinates of a symbol to be centered around a unit. + :param loc: unit location (e.g. 'PAR') :param is_dislodged: boolean to tell if unit is dislodged :param symbol: symbol identifier (e.g. 'HoldUnit') @@ -756,6 +780,7 @@ class Renderer(): def _get_unit_center(self, loc, is_dislodged): # type: (str, bool) -> Tuple[float, float] """ Compute coordinates of unit center. + :param loc: unit location :param is_dislodged: boolean to tell if unit is dislodged :return: a couple of coordinates (x, y) as floating values @@ -769,12 +794,14 @@ class Renderer(): def _plain_stroke_width(self): # type: () -> float """ Return generic stroke width for plain lines. + :return: stroke width as floating value. """ return float(self.metadata['symbol_size']['Stroke'][0]) def _colored_stroke_width(self): # type: () -> float """ Return generic stroke width for colored or textured lines. + :return: stroke width as floating value. """ return float(self.metadata['symbol_size']['Stroke'][1]) diff --git a/diplomacy/integration/base_api.py b/diplomacy/integration/base_api.py index c0dca4e..584310b 100644 --- a/diplomacy/integration/base_api.py +++ b/diplomacy/integration/base_api.py @@ -28,9 +28,13 @@ class BaseAPI(metaclass=ABCMeta): def __init__(self, api_key, connect_timeout=30, request_timeout=60): """ Constructor + :param api_key: The API key to use for sending API requests :param connect_timeout: The maximum amount of time to wait for the connection to be established :param request_timeout: The maximum amount of time to wait for the request to be processed + :type api_key: str + :type connect_timeout: int, optional + :type request_timeout: int, optional """ self.api_key = api_key self.http_client = AsyncHTTPClient() @@ -39,30 +43,14 @@ class BaseAPI(metaclass=ABCMeta): @gen.coroutine @abstractmethod - def list_games_with_players_in_cd(self): - """ Lists the game on the standard map where a player is in CD and the bots needs to submit orders - :return: List of GameIdCountryId tuples [(game_id, country_id), (game_id, country_id)] - """ - raise NotImplementedError() - - @gen.coroutine - @abstractmethod - def list_games_with_missing_orders(self): - """ Lists of the game on the standard where the user has not submitted orders yet. - :return: List of GameIdCountryId tuples [(game_id, country_id), (game_id, country_id)] - """ - raise NotImplementedError() - - @gen.coroutine - @abstractmethod - def get_game_and_power(self, game_id, country_id, max_phases=None): + def get_game_and_power(self, *args, **kwargs): """ Returns the game and the power we are playing - :param game_id: The id of the game object (integer) - :param country_id: The id of the country for which we want the game state (integer) - :param max_phases: Optional. If set, improve speed by generating game only using the last 'x' phases. + *Arguments are specific to each implementation.* + :return: A tuple consisting of - 1) The diplomacy.Game object from the game state or None if an error occurred - 2) The power name (e.g. 'FRANCE') referred to by country_id + + #. The diplomacy.Game object or None if an error occurred + #. The power name (e.g. 'FRANCE') """ raise NotImplementedError() @@ -70,6 +58,7 @@ class BaseAPI(metaclass=ABCMeta): @abstractmethod def set_orders(self, game, power_name, orders, wait=None): """ Submits orders back to the server + :param game: A diplomacy.Game object representing the current state of the game :param power_name: The name of the power submitting the orders (e.g. 'FRANCE') :param orders: A list of strings representing the orders (e.g. ['A PAR H', 'F BRE - MAO']) diff --git a/diplomacy/integration/webdiplomacy_net/api.py b/diplomacy/integration/webdiplomacy_net/api.py index b4bde01..36b132c 100644 --- a/diplomacy/integration/webdiplomacy_net/api.py +++ b/diplomacy/integration/webdiplomacy_net/api.py @@ -39,8 +39,10 @@ class API(BaseAPI): @gen.coroutine def list_games_with_players_in_cd(self): - """ Lists the game on the standard map where a player is in CD and the bots needs to submit orders - :return: List of GameIdCountryId tuples [(game_id, country_id), (game_id, country_id)] + """ Lists the game on the standard map where a player is in CD (civil disorder) + and the bots needs to submit orders + + :return: List of :class:`.GameIdCountryId` tuples [(game_id, country_id), (game_id, country_id)] """ route = 'players/cd' url = '%s?%s' % (API_WEBDIPLOMACY_NET, urlencode({'route': route})) @@ -73,7 +75,8 @@ class API(BaseAPI): @gen.coroutine def list_games_with_missing_orders(self): """ Lists of the game on the standard where the user has not submitted orders yet. - :return: List of GameIdCountryId tuples [(game_id, country_id), (game_id, country_id)] + + :return: List of :class:`.GameIdCountryId` tuples [(game_id, country_id), (game_id, country_id)] """ route = 'players/missing_orders' url = '%s?%s' % (API_WEBDIPLOMACY_NET, urlencode({'route': route})) @@ -106,13 +109,19 @@ class API(BaseAPI): @gen.coroutine def get_game_and_power(self, game_id, country_id, max_phases=None): """ Returns the game and the power we are playing + :param game_id: The id of the game object (integer) :param country_id: The id of the country for which we want the game state (integer) :param max_phases: Optional. If set, improve speed by generating game only using the last 'x' phases. + :type game_id: int + :type country_id: int + :type max_phases: int | None, optional :return: A tuple consisting of - 1) The diplomacy.Game object from the game state or None if an error occurred - 2) The power name (e.g. 'FRANCE') referred to by country_id + + #. The diplomacy.Game object from the game state or None if an error occurred + #. The power name (e.g. 'FRANCE') referred to by country_id """ + # pylint: disable=arguments-differ route = 'game/status' url = '%s?%s' % (API_WEBDIPLOMACY_NET, urlencode({'route': route, 'gameID': game_id, 'countryID': country_id})) return_val = None, None @@ -144,12 +153,16 @@ class API(BaseAPI): @gen.coroutine def set_orders(self, game, power_name, orders, wait=None): """ Submits orders back to the server - :param game: A diplomacy.Game object representing the current state of the game + + :param game: A :class:`diplomacy.engine.game.Game` object representing the current state of the game :param power_name: The name of the power submitting the orders (e.g. 'FRANCE') :param orders: A list of strings representing the orders (e.g. ['A PAR H', 'F BRE - MAO']) :param wait: Optional. If True, sets ready=False, if False sets ready=True. :return: True for success, False for failure :type game: diplomacy.Game + :type power_name: str + :type orders: List[str] + :type wait: bool | None, optional """ # Logging orders LOGGER.info('[%s/%s/%s] - Submitting orders: %s', game.game_id, game.get_current_phase(), power_name, orders) diff --git a/diplomacy/integration/webdiplomacy_net/game.py b/diplomacy/integration/webdiplomacy_net/game.py index 0233ea9..a89933c 100644 --- a/diplomacy/integration/webdiplomacy_net/game.py +++ b/diplomacy/integration/webdiplomacy_net/game.py @@ -18,9 +18,7 @@ LOGGER = logging.getLogger(__name__) def turn_to_phase(turn, phase): - """ Converts a turn and phase to a short phase name - e.g. turn 1 - phase 'Retreats' to 'F1901R' - """ + """ Converts a turn and phase to a short phase name e.g. turn 1 - phase 'Retreats' to 'F1901R' """ year = 1901 + turn // 2 season = 'S' if turn % 2 == 0 else 'F' if phase == 'Builds': @@ -36,13 +34,14 @@ def turn_to_phase(turn, phase): # 'retreating': 'Yes'/'No'} def unit_dict_to_str(unit_dict, map_id=1): """ Converts a unit from the dictionary format to the string format - e.g. {'unitType': 'Army', 'terrID': 6, 'countryId': 1, 'retreating': 'No'} - to 'ENGLAND', 'A LON' + e.g. ``{'unitType': 'Army', 'terrID': 6, 'countryId': 1, 'retreating': 'No'}`` + to ``'ENGLAND', 'A LON'`` :param unit_dict: The unit in dictionary format from webdiplomacy.net :return: A tuple consisting of: - 1) The power owning the unit (e.g. 'FRANCE') - 2) The unit in string format (with a leading * when dislodged) (e.g. '*A PAR') + + #. The power owning the unit (e.g. 'FRANCE') + #. The unit in string format (with a leading ``*`` when dislodged) (e.g. ``'*A PAR'``) """ req_fields = ('unitType', 'terrID', 'countryID', 'retreating') if [1 for field in req_fields if field not in unit_dict]: @@ -84,12 +83,13 @@ def unit_dict_to_str(unit_dict, map_id=1): # 'countryId': integer} def center_dict_to_str(center_dict, map_id=1): """ Converts a supply center from the dictionary format to the string format - e.g. {'terrID': 6, 'countryId': 1} to 'ENGLAND', 'LON' + e.g. ``{'terrID': 6, 'countryId': 1}`` to ``'ENGLAND', 'LON'`` :param center_dict: The center in dictionary format from webdiplomacy.net :return: A tuple consisting of: - 1) The power owning the center (e.g. 'FRANCE') - 2) The location where the supply center is (e.g. 'PAR') + + #. The power owning the center (e.g. 'FRANCE') + #. The location where the supply center is (e.g. 'PAR') """ req_fields = ('terrID', 'countryID') if [1 for field in req_fields if field not in center_dict]: @@ -132,11 +132,13 @@ def center_dict_to_str(center_dict, map_id=1): # 'dislodged': 'Yes', 'No'} def order_dict_to_str(order_dict, phase, map_id=1): """ Converts an order from the dictionary format to the string format + :param order_dict: The order in dictionary format from webdiplomacy.net :param phase: The current phase ('Diplomacy', 'Retreats', 'Builds') :return: A tuple consisting of: - 1) The power who submitted the order (e.g. 'FRANCE') - 2) The order in string format (e.g. 'A PAR H') + + #. The power who submitted the order (e.g. 'FRANCE') + #. The order in string format (e.g. 'A PAR H') """ req_fields = ('countryID',) if [1 for field in req_fields if field not in order_dict]: @@ -223,12 +225,14 @@ def process_phase_dict(phase_dict, map_id=1): # 'standoffs': []} def state_dict_to_game_and_power(state_dict, country_id, max_phases=None): """ Converts a game state from the dictionary format to an actual diplomacy.Game object with the related power. + :param state_dict: The game state in dictionary format from webdiplomacy.net :param country_id: The country id we want to convert. :param max_phases: Optional. If set, improve speed by only keeping the last 'x' phases to regenerate the game. :return: A tuple of - 1) None, None - on error or if the conversion is not possible, or game is invalid / not-started / done - 2) game, power_name - on successful conversion + + #. None, None - on error or if the conversion is not possible, or game is invalid / not-started / done + #. game, power_name - on successful conversion """ if state_dict is None: return None, None diff --git a/diplomacy/integration/webdiplomacy_net/orders.py b/diplomacy/integration/webdiplomacy_net/orders.py index afacf03..8fa966c 100644 --- a/diplomacy/integration/webdiplomacy_net/orders.py +++ b/diplomacy/integration/webdiplomacy_net/orders.py @@ -18,9 +18,10 @@ LOGGER = logging.getLogger(__name__) def is_adjacent_for_convoy(loc_1, loc_2, map_object): """ Checks if two locations are adjacent (for convoy purposes) - - If loc_1 and loc_2 are water, loc_1 and loc_2 must be adjacent - - If loc_1 or loc_2 is land, then they are not adjacent - - If loc_1 or loc_2 are coast, then the other locations needs to be a water loc at most 1 loc away + + - If loc_1 and loc_2 are water, loc_1 and loc_2 must be adjacent + - If loc_1 or loc_2 is land, then they are not adjacent + - If loc_1 or loc_2 are coast, then the other locations needs to be a water loc at most 1 loc away :type map_object: diplomacy.Map """ @@ -48,6 +49,7 @@ def is_adjacent_for_convoy(loc_1, loc_2, map_object): def find_convoy_path(src, dest, map_object, game=None, including=None, excluding=None): """ Finds a convoy path from src to dest + :param src: The source location (e.g. 'BRE') :param dest: The destination location (e.g. 'LON') :param map_object: A diplomacy.Map object representation of the current map @@ -55,7 +57,7 @@ def find_convoy_path(src, dest, map_object, game=None, including=None, excluding :param including: Optional. A single province (e.g. 'NAO') or a list of provinces that must be in the path. :param excluding: Optional. A single province (e.g. 'NAO') or a list of provinces that must NOT be in the path. :return: Either an empty list if a convoy is not possible between src and dest - or a list of [src, fleet1, fleet2, ..., fleet_n, dest] to use to convoy A `src` - `dest`. + or a list of [src, fleet1, fleet2, ..., fleet_n, dest] to use to convoy A `src` - `dest`. :type map_object: diplomacy.Map :type game: diplomacy.Game """ @@ -122,11 +124,12 @@ def find_convoy_path(src, dest, map_object, game=None, including=None, excluding return [] -class Order(): +class Order: """ Class to convert order from string representation to dictionary (webdiplomacy.net) representation """ def __init__(self, order, map_id=None, map_name=None, phase_type=None, game=None): """ Constructor + :param order: An order (either as a string 'A PAR H' or as a dictionary) :param map_id: Optional. The map id of the map where orders are submitted (webdiplomacy format) :param map_name: Optional. The map name of the map where orders are submitted. @@ -162,6 +165,7 @@ class Order(): def _build_from_string(self, order, game=None): """ Builds this object from a string + :type order: str :type game: diplomacy.Game """ @@ -444,6 +448,7 @@ class Order(): def _build_from_dict(self, order): """ Builds this object from a dictionary + :type order: dict """ # pylint: disable=too-many-return-statements 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 . # ============================================================================== """ 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= # 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= + """ 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 . # ============================================================================== -""" 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 ``/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 `/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 """ diff --git a/diplomacy/tests/network/run_real_game.py b/diplomacy/tests/network/run_real_game.py index a9800b6..e243dec 100644 --- a/diplomacy/tests/network/run_real_game.py +++ b/diplomacy/tests/network/run_real_game.py @@ -18,17 +18,27 @@ """ Run tests from diplomacy.tests.network.test_real_game to test games in a real environment. Each test run a game and checks game messages and phases against an expected game data file. Current tested gama data files are JSON files located into folder diplomacy/tests/network: - 1.json - 2.json - 3.json - Need a local diplomacy server running. You must specify this server port using parameter --port=. - To run all tests: + + - 1.json + - 2.json + - 3.json + + Need a local diplomacy server running. You must specify + this server port using parameter ``--port=``. + + To run all tests: :: + python -m diplomacy.tests.network.run_real_game --port= - To run a specific test (e.g. 2.json, or 2.json and 1.json): - python -m diplomacy.tests.network.run_real_game --cases=20 --port= - python -m diplomacy.tests.network.run_real_game --cases=15,20 --port= - For help: + + To run a specific test (e.g. 2.json, or 2.json and 1.json): :: + + python -m diplomacy.tests.network.run_real_game --cases=2 --port= + python -m diplomacy.tests.network.run_real_game --cases=1,2 --port= + + For help: :: + python -m diplomacy.tests.network.run_real_game --help + """ import argparse from tornado import gen diff --git a/diplomacy/tests/network/test_real_game.py b/diplomacy/tests/network/test_real_game.py index a960b91..bd63da1 100644 --- a/diplomacy/tests/network/test_real_game.py +++ b/diplomacy/tests/network/test_real_game.py @@ -19,6 +19,7 @@ import logging import os import random +from typing import Dict from tornado import gen from tornado.concurrent import Future @@ -39,12 +40,13 @@ DEFAULT_HOSTNAME = 'localhost' DEFAULT_PORT = random.randint(9000, 10000) -class ExpectedPhase(): +class ExpectedPhase: """ Helper class to manage data from an expected phase. """ __slots__ = ['name', 'state', 'orders', 'messages'] def __init__(self, json_phase): """ Initialize expected phase. + :param json_phase: JSON dict representing a phase. Expected fields: name, state, orders, messages. """ self.name = json_phase['name'] @@ -63,12 +65,13 @@ class ExpectedPhase(): return [message for message in self.messages if message.sender == power_name or message.recipient in (power_name, GLOBAL)] -class ExpectedMessages(): +class ExpectedMessages: """ Expected list of messages sent and received by a power name. """ __slots__ = ['power_name', 'messages', 'next_messages_to_send'] def __init__(self, power_name, messages): """ Initialize the expected messages. + :param power_name: power name which exchanges these messages :param messages: messages exchanged """ @@ -100,13 +103,14 @@ class ExpectedMessages(): self.next_messages_to_send.extend(self.messages[:next_message_to_receive]) del self.messages[:next_message_to_receive] -class ExpectedData(): +class ExpectedData: """ Expected data for a power in a game. """ __slots__ = ['messages', 'phases', '__phase_index', 'playing'] def __init__(self, power_name, phases, phase_index): """ Initialize expected data for a game power. + :param power_name: name of power for which those data are expected. :param phases: list of expected phases. :param phase_index: index of current expected phase in given phases. @@ -130,12 +134,13 @@ class ExpectedData(): self.messages = ExpectedMessages( self.messages.power_name, self.phases[self.__phase_index].get_power_related_messages(self.power_name)) -class CaseData(): +class CaseData: """ Helper class to store test data. """ FILE_FOLDER_NAME = os.path.abspath(os.path.dirname(__file__)) def __init__(self, case_file_name, hostname=DEFAULT_HOSTNAME, port=DEFAULT_PORT): """ Initialize game test. + :param case_file_name: File name of JSON file containing expected game data. JSON file must be located in folder FILE_FOLDER_NAME. :param hostname: hostname to use to load server. @@ -157,7 +162,7 @@ class CaseData(): self.admin_channel = None self.admin_game = None self.user_games = {} - self.future_games_ended = {} # type: dict{str, Future} + self.future_games_ended = {} # type: Dict[str, Future] self.hostname = hostname self.port = port @@ -169,6 +174,7 @@ class CaseData(): @gen.coroutine def on_power_phase_update(self, game, notification=None): """ User game notification callback for game phase updated. + :param game: game :param notification: notification :type game: NetworkGame @@ -187,6 +193,7 @@ class CaseData(): @gen.coroutine def on_power_state_update(self, game, notification): """ User game notification callback for game state update. + :param game: game :param notification: notification :type game: NetworkGame @@ -198,6 +205,7 @@ class CaseData(): @gen.coroutine def send_messages_if_needed(game, expected_messages): """ Take messages to send in top of given messages list and send them. + :param game: a NetworkGame object. :param expected_messages: an instance of ExpectedMessages. :type game: NetworkGame @@ -222,6 +230,7 @@ def send_messages_if_needed(game, expected_messages): @gen.coroutine def send_current_orders(game): """ Send expected orders for current phase. + :param game: a Network game object. :type game: NetworkGame """ @@ -242,6 +251,7 @@ def send_current_orders(game): def on_message_received(game, notification): """ User game notification callback for messages received. + :param game: a NetworkGame object, :param notification: a notification received by this game. :type game: NetworkGame @@ -286,6 +296,7 @@ def on_message_received(game, notification): def on_admin_game_phase_update(admin_game, notification=None): """ Admin game notification callback for game phase update. + :param admin_game: admin game :param notification: notification :type admin_game: NetworkGame @@ -370,6 +381,7 @@ def on_admin_game_phase_update(admin_game, notification=None): def on_admin_game_state_update(admin_game, notification): """ Admin game notification callback for game state update. + :param admin_game: admin game :param notification: notification :type admin_game: NetworkGame @@ -380,6 +392,7 @@ def on_admin_game_state_update(admin_game, notification): def on_admin_powers_controllers(admin_game, notification): """ Admin game notification callback for powers controllers received (unexpected). + :param admin_game: game :param notification: notification :type admin_game: NetworkGame @@ -390,6 +403,7 @@ def on_admin_powers_controllers(admin_game, notification): def on_admin_game_status_update(admin_game, notification): """ Admin game notification callback for game status update. + :param admin_game: admin game :param notification: notification :type admin_game: NetworkGame @@ -399,9 +413,11 @@ def on_admin_game_status_update(admin_game, notification): @gen.coroutine def play_phase(game, expected_messages): """ Play a phase for a user game: - 1) Send messages - 2) wait for messages to receive - 3) send current orders. + + #. Send messages + #. wait for messages to receive + #. send current orders. + :param game: user game :param expected_messages: expected messages :type game: NetworkGame @@ -418,6 +434,7 @@ def play_phase(game, expected_messages): def on_game_status_update(game, notification): """ User game notification callback for game status update. Used to start the game locally when game started on server. + :param game: game :param notification: notification :type game: NetworkGame @@ -434,6 +451,7 @@ def on_game_status_update(game, notification): @gen.coroutine def verify_current_phase(game): """ Check and play current phase. + :param game: a NetWork game object. :type game: NetworkGame """ @@ -455,6 +473,7 @@ def verify_current_phase(game): def get_user_game_fn(case_data, power_name): """ Return a coroutine procedure that loads and play a user game for given power name. + :param case_data: case data :param power_name: str :return: a procedure. @@ -471,6 +490,7 @@ def get_user_game_fn(case_data, power_name): def get_future_game_done_fn(power_name): """ Return a callback to call when a power game is finished. Callback currently just prints a message to tell that power game is terminated. + :param power_name: power name of associated game. :return: a callable that receives the future done when game is finished. """ @@ -484,6 +504,7 @@ def get_future_game_done_fn(power_name): @gen.coroutine def load_power_game(case_data, power_name): """ Load and play a power game from admin game for given power name. + :type case_data: CaseData """ print('Loading game for power', power_name) @@ -519,6 +540,7 @@ def load_power_game(case_data, power_name): def main(case_data): """ Test real game environment with one game and all power controlled (no dummy powers). This method may be called form a non-test code to run a real game case. + :param case_data: test data :type case_data: CaseData """ @@ -567,6 +589,7 @@ def run(case_data, **server_kwargs): Load a server (with optional given server kwargs), call function main(case_data) as client code and wait for main function to terminate. + :type case_data: CaseData """ diff --git a/diplomacy/tests/test_datc.py b/diplomacy/tests/test_datc.py index 8c40363..093ed14 100644 --- a/diplomacy/tests/test_datc.py +++ b/diplomacy/tests/test_datc.py @@ -24,7 +24,7 @@ from diplomacy.utils.order_results import OK, NO_CONVOY, BOUNCE, VOID, CUT, DISL # ----------------- # DATC TEST CASES # ----------------- -class TestDATC(): +class TestDATC: """ DATC test cases""" # pylint: disable=too-many-public-methods diff --git a/diplomacy/utils/common.py b/diplomacy/utils/common.py index e8d49d6..dbe5f28 100644 --- a/diplomacy/utils/common.py +++ b/diplomacy/utils/common.py @@ -38,9 +38,12 @@ REGEX_UNDERSCORE_THEN_LETTER = re.compile('_([a-z])') REGEX_START_BY_LOWERCASE = re.compile('^[a-z]') def _sub_hash_password(password): - """ Hash long password to allow bcrypt to handle password longer than 72 characters. Module private method. + """ Hash long password to allow bcrypt to handle password longer than 72 characters. + Module private method. + :param password: password to hash. - :return: (String) The hashed password. + :return: The hashed password. + :rtype: str """ # Bcrypt only handles passwords up to 72 characters. We use this hashing method as a work around. # Suggested in bcrypt PyPI page (2018/02/08 12:36 EST): https://pypi.python.org/pypi/bcrypt/3.1.0 @@ -48,16 +51,20 @@ def _sub_hash_password(password): def is_valid_password(password, hashed): """ Check if password matches hashed. + :param password: password to check. :param hashed: a password hashed with method hash_password(). - :return: (Boolean). Indicates if the password matches the hash. + :return: Indicates if the password matches the hash. + :rtype: bool """ return bcrypt.checkpw(_sub_hash_password(password), hashed.encode('utf-8')) def hash_password(password): """ Hash password. Accepts password longer than 72 characters. Public method. + :param password: The password to hash - :return: (String). The hashed password. + :return: The hashed password. + :rtype: str """ return bcrypt.hashpw(_sub_hash_password(password), bcrypt.gensalt(14)).decode('utf-8') @@ -67,8 +74,10 @@ def generate_token(n_bytes=128): def is_dictionary(dict_to_check): """ Check if given variable is a dictionary-like object. + :param dict_to_check: Dictionary to check. - :return: (Boolean). Indicates if the object is a dictionary. + :return: Indicates if the object is a dictionary. + :rtype: bool """ return isinstance(dict_to_check, dict) or all( hasattr(dict_to_check, expected_attribute) @@ -87,8 +96,10 @@ def is_dictionary(dict_to_check): def is_sequence(seq_to_check): """ Check if given variable is a sequence-like object. Note that strings and dictionary-like objects will not be considered as sequences. + :param seq_to_check: Sequence-like object to check. - :return: (Boolean). Indicates if the object is sequence-like. + :return: Indicates if the object is sequence-like. + :rtype: bool """ # Strings and dicts are not valid sequences. if isinstance(seq_to_check, str) or is_dictionary(seq_to_check): @@ -97,8 +108,10 @@ def is_sequence(seq_to_check): def camel_case_to_snake_case(name): """ Convert a string (expected to be in camel case) to snake case. + :param name: string to convert. - :return: string: snake case version of given name. + :return: snake case version of given name. + :rtype: str """ if name == '': return name @@ -106,10 +119,12 @@ def camel_case_to_snake_case(name): return REGEX_LOWER_THEN_UPPER_CASES.sub(r'\1_\2', separated_consecutive_uppers).lower() def snake_case_to_upper_camel_case(name): - """ Convert a string (expected to be in snake case) to camel case and convert first letter to upper case - if it's in lowercase. + """ Convert a string (expected to be in snake case) to camel case and convert first letter + to upper case if it's in lowercase. + :param name: string to convert. :return: camel case version of given name. + :rtype: str """ if name == '': return name @@ -118,6 +133,7 @@ def snake_case_to_upper_camel_case(name): def assert_no_common_keys(dict1, dict2): """ Check that dictionaries does not share keys. + :param dict1: dict :param dict2: dict """ @@ -131,7 +147,8 @@ def assert_no_common_keys(dict1, dict2): def timestamp_microseconds(): """ Return current timestamp with microsecond resolution. - :return: int + + :rtype: int """ delta = datetime.now() - EPOCH return (delta.days * 24 * 60 * 60 + delta.seconds) * 1000000 + delta.microseconds @@ -140,27 +157,29 @@ def str_cmp_class(compare_function): """ Return a new class to be used as string comparator. Example: - ``` - def my_cmp_func(a, b): - # a and b are two strings to compare with a specific code. - # Return -1 if a < b, 0 if a == b, 1 otherwise. - - my_class = str_cmp_class(my_cmp_func) - wrapped_str_1 = my_class(str_to_compare_1) - wrapped_str_2 = my_class(str_to_compare_2) - my_list = [wrapped_str_1, wrapped_str_2] - - # my_list will be sorted according to my_cmp_func. - my_list.sort() - ``` - - :param compare_function: a callable that takes 2 strings a and b, and compares it according to custom rules. - This function should return: - -1 (or a negative value) if a < b - 0 if a == b - 1 (or a positive value) if a > b + .. code-block:: python + + def my_cmp_func(a, b): + # a and b are two strings to compare with a specific code. + # Return -1 if a < b, 0 if a == b, 1 otherwise. + + my_class = str_cmp_class(my_cmp_func) + wrapped_str_1 = my_class(str_to_compare_1) + wrapped_str_2 = my_class(str_to_compare_2) + my_list = [wrapped_str_1, wrapped_str_2] + + # my_list will be sorted according to my_cmp_func. + my_list.sort() + + :param compare_function: a callable that takes 2 strings a and b, and compares + it according to custom rules. This function should return: + + * -1 (or a negative value) if a < b + * 0 if a == b + * 1 (or a positive value) if a > b :return: a comparator class, instanciable with a string. + :type compare_function: callable """ class StringComparator: @@ -188,12 +207,28 @@ def str_cmp_class(compare_function): StringComparator.__name__ = 'StringComparator%s' % (id(compare_function)) return StringComparator -class StringableCode(): +def to_string(element): + """ Convert element to a string and make sure string is wrapped in either simple quotes + (if contains double quotes) or double quotes (if contains simple quotes). + + :param element: element to convert + :return: string version of element + :rtype: str + """ + element = str(element) + if '"' in element: + return "'%s'" % element + if "'" in element: + return '"%s"' % element + return element + +class StringableCode: """ Represents a stringable version of a code (with an optional message) """ def __init__(self, code, message=None): """ Build a StringableCode + :param code: int - code - :param message: Optional. human readable string message associated to the cide + :param message: Optional. human readable string message associated to the code """ if isinstance(code, str) or message is None: message = code @@ -246,13 +281,14 @@ class StringableCode(): """ Format the message of the result """ return StringableCode(self._code, self._message.format(*values)) -class Tornado(): +class Tornado: """ Utilities for Tornado. """ @staticmethod def stop_loop_on_callback_error(io_loop): """ Modify exception handler method of given IO loop so that IO loop stops and raises as soon as an exception is thrown from a callback. + :param io_loop: IO loop :type io_loop: tornado.ioloop.IOLoop """ diff --git a/diplomacy/utils/constants.py b/diplomacy/utils/constants.py index d929e33..47b1419 100644 --- a/diplomacy/utils/constants.py +++ b/diplomacy/utils/constants.py @@ -48,6 +48,8 @@ PRIVATE_BOT_PASSWORD = '#bot:password:28131821--mx1fh5g7hg5gg5g´[],s222222223dj # Time to wait to let a bot set orders for a dummy power. PRIVATE_BOT_TIMEOUT_SECONDS = 60 +# Default rules used to construct a Game object when no rules are provided. +DEFAULT_GAME_RULES = ('SOLITAIRE', 'NO_PRESS', 'IGNORE_ERRORS', 'POWER_CHOICE') class OrderSettings: """ Constants to define flags for attribute Power.order_is_set. """ diff --git a/diplomacy/utils/convoy_paths.py b/diplomacy/utils/convoy_paths.py index 27b6836..7c21863 100644 --- a/diplomacy/utils/convoy_paths.py +++ b/diplomacy/utils/convoy_paths.py @@ -47,6 +47,7 @@ DISK_CACHE_PATH = os.path.join(HOME_DIRECTORY, '.cache', 'diplomacy', CACHE_FILE def display_progress_bar(queue, max_loop_iters): """ Displays a progress bar + :param queue: Multiprocessing queue to display the progress bar :param max_loop_iters: The expected maximum number of iterations """ @@ -122,6 +123,7 @@ def get_convoy_paths(map_object, start_location, max_convoy_length, queue): def build_convoy_paths_cache(map_object, max_convoy_length): """ Builds the convoy paths cache for a map + :param map_object: The instantiated map object :param max_convoy_length: The maximum convoy length permitted :return: A dictionary where the key is the number of fleets in the path and @@ -158,6 +160,7 @@ def build_convoy_paths_cache(map_object, max_convoy_length): def get_file_md5(file_path): """ Calculates a file MD5 hash + :param file_path: The file path :return: The computed md5 hash """ @@ -169,6 +172,7 @@ def get_file_md5(file_path): def add_to_cache(map_name): """ Lazy generates convoys paths for a map and adds it to the disk cache + :param map_name: The name of the map :return: The convoy_paths for that map """ diff --git a/diplomacy/utils/equilateral_triangle.py b/diplomacy/utils/equilateral_triangle.py index d31fe79..6535665 100644 --- a/diplomacy/utils/equilateral_triangle.py +++ b/diplomacy/utils/equilateral_triangle.py @@ -16,15 +16,17 @@ # ============================================================================== # pylint: disable=anomalous-backslash-in-string """ Helper class to compute intersection of a line (OM) with a side of an equilateral triangle, - with O the barycenter of the equilateral triangle and M a point outside the triangle. - A - / | M - / O | - C /______| B - - A = top, B = right, C = left - O = center of triangle - M = point outside of triangle + with O the barycenter of the equilateral triangle and M a point outside the triangle.:: + + A + / | M + / O | + C /______| B + + A = top, B = right, C = left + O = center of triangle + M = point outside of triangle + """ class EquilateralTriangle: """ Helper class that represent an equilateral triangle. @@ -53,6 +55,7 @@ class EquilateralTriangle: def __line_om(self, x_m, y_m): """ Returns the slope and the intersect of the line between O and M + :return: a, b - respectively the slope and the intercept of the line OM """ # pylint:disable=invalid-name @@ -62,6 +65,7 @@ class EquilateralTriangle: def _intersection_with_ab(self, x_m, y_m): """ Return coordinates of intersection of line (OM) with line (AB). + :param x_m: x coordinate of M :param y_m: y coordinate of M :return: coordinates (x, y) of intersection, or (None, None) if either @@ -85,6 +89,7 @@ class EquilateralTriangle: def _intersection_with_ac(self, x_m, y_m): """ Return coordinates of intersection of line (OM) with line (AC). + :param x_m: x coordinate of M :param y_m: y coordinate of M :return: coordinates (x, y) of intersection, or (None, None) if either @@ -107,6 +112,7 @@ class EquilateralTriangle: def _intersection_with_bc(self, x_m, y_m): """ Return coordinates of intersection of line (OM) with line (BC). NB: (BC) is an horizontal line. + :param x_m: x coordinate of M :param y_m: y coordinate of M :return: coordinates (x, y) of intersection, or (None, None) if either @@ -129,6 +135,7 @@ class EquilateralTriangle: """ Return coordinates of the intersection of (OM) with equilateral triangle, with M the point with coordinates (x_m, y_m). Only the intersection with the side of triangle near M is considered. + :param x_m: x coordinate of M :param y_m: y coordinate of M :return: a couple (x, y) of floating values. diff --git a/diplomacy/utils/errors.py b/diplomacy/utils/errors.py index 64e760e..1a63bb1 100644 --- a/diplomacy/utils/errors.py +++ b/diplomacy/utils/errors.py @@ -31,6 +31,7 @@ class MapError(Error): """ Represents a map error """ def __init__(self, code, message): """ Build a MapError + :param code: int code of the error :param message: human readable string message associated to the error """ @@ -40,6 +41,7 @@ class GameError(Error): """ Represents a game error """ def __init__(self, code, message): """ Build a GameError + :param code: int code of the error :param message: human readable string message associated to the error """ @@ -49,6 +51,7 @@ class StdError(Error): """ Represents a standard error """ def __init__(self, code, message): """ Build a StdError + :param code: int code of the error :param message: human readable string message associated to the error """ diff --git a/diplomacy/utils/export.py b/diplomacy/utils/export.py index 6c54689..7ff68d2 100644 --- a/diplomacy/utils/export.py +++ b/diplomacy/utils/export.py @@ -26,9 +26,11 @@ RULES_TO_SKIP = ['SOLITAIRE', 'NO_DEADLINE', 'CD_DUMMIES', 'ALWAYS_WAIT', 'IGNOR def to_saved_game_format(game): """ Converts a game to a standardized JSON format + :param game: game to convert. - :return: A game in the standard JSON format used to saved game (returned object is a dictionary) - :type game: Game + :return: A game in the standard format used to saved game, that can be converted to JSON for serialization + :type game: diplomacy.engine.game.Game + :rtype: Dict """ # Get phase history. @@ -53,6 +55,7 @@ def to_saved_game_format(game): def is_valid_saved_game(saved_game): """ Checks if the saved game is valid. This is an expensive operation because it replays the game. + :param saved_game: The saved game (from to_saved_game_format) :return: A boolean that indicates if the game is valid """ diff --git a/diplomacy/utils/jsonable.py b/diplomacy/utils/jsonable.py index 0ce1a21..a103cbb 100644 --- a/diplomacy/utils/jsonable.py +++ b/diplomacy/utils/jsonable.py @@ -16,25 +16,30 @@ # ============================================================================== """ Abstract Jsonable class with automatic attributes checking and conversion to/from JSON dict. To write a Jsonable sub-class: + - Define a model with expected attribute names and types. Use module `parsing` to describe expected types. - - Override initializer __init__(**kwargs): - - **first**: initialize each attribute defined in model with value None. - - **then** : call parent __init__() method. Attributes will be checked and filled by - Jsonable's __init__() method. - - If needed, add further initialization code after call to parent __init__() method. At this point, - attributes were correctly set based on defined model, and you can now work with them. + - Override initializer ``__init__(**kwargs)``: + + - **first**: initialize each attribute defined in model with value None. + - **then** : call parent __init__() method. Attributes will be checked and filled by + Jsonable's __init__() method. + - If needed, add further initialization code after call to parent __init__() method. At this point, + attributes were correctly set based on defined model, and you can now work with them. Example: - ``` - class MyClass(Jsonable): - model = { - 'my_attribute': parsing.Sequence(int), - } - def __init__(**kwargs): - self.my_attribute = None - super(MyClass, self).__init__(**kwargs) - # my_attribute is now initialized based on model. You can then do any further initialization if needed. - ``` + + .. code-block:: python + + class MyClass(Jsonable): + model = { + 'my_attribute': parsing.Sequence(int), + } + def __init__(**kwargs): + self.my_attribute = None + super(MyClass, self).__init__(**kwargs) + # my_attribute is now initialized based on model. + # You can then do any further initialization if needed. + """ import logging import ujson as json @@ -43,7 +48,7 @@ from diplomacy.utils import exceptions, parsing LOGGER = logging.getLogger(__name__) -class Jsonable(): +class Jsonable: """ Abstract class to ease conversion from/to JSON dict. """ __slots__ = [] __cached__models__ = {} @@ -78,12 +83,14 @@ class Jsonable(): def json(self): """ Convert this object to a JSON string ready to be sent/saved. + :return: string """ return json.dumps(self.to_dict()) def to_dict(self): """ Convert this object to a python dictionary ready for any JSON work. + :return: dict """ model = self.get_model() @@ -95,6 +102,7 @@ class Jsonable(): JSON dictionary is passed by class method from_dict() (see below), and is guaranteed to contain at least all expected model keys. Some keys may be associated to None if initial JSON dictionary did not provide values for them. + :param json_dict: a JSON dictionary to be parsed. :type json_dict: dict """ @@ -102,6 +110,7 @@ class Jsonable(): @classmethod def from_dict(cls, json_dict): """ Convert a JSON dictionary to an instance of this class. + :param json_dict: a JSON dictionary to parse. Dictionary with basic types (int, bool, dict, str, None, etc.) :return: an instance from this class or from a derived one from which it's called. :rtype: cls @@ -133,6 +142,7 @@ class Jsonable(): def get_model(cls): """ Return model associated to current class, and cache it for future uses, to avoid multiple rendering of model for each class derived from Jsonable. Private method. + :return: dict: model associated to current class. """ if cls not in cls.__cached__models__: diff --git a/diplomacy/utils/keywords.py b/diplomacy/utils/keywords.py index cdd9362..ae55fcd 100644 --- a/diplomacy/utils/keywords.py +++ b/diplomacy/utils/keywords.py @@ -15,6 +15,7 @@ # with this program. If not, see . # ============================================================================== """ Aliases and keywords + - Contains aliases and keywords - Keywords are always single words - Aliases are only converted in a second pass, so if they contain a keyword, you should replace diff --git a/diplomacy/utils/network_data.py b/diplomacy/utils/network_data.py index 18e3869..d5fb5a3 100644 --- a/diplomacy/utils/network_data.py +++ b/diplomacy/utils/network_data.py @@ -15,14 +15,13 @@ # with this program. If not, see . # ============================================================================== """ Abstract Jsonable class to create data intended to be exchanged on network. + Used for requests, responses and notifications. To write a sub-class, you must first write a base class for data category (e.g. notifications): - Define header model for network data. - - Define ID field for data category (e.g. "notification_id"). This will be used to create unique identifier for every data exchanged on network. - - Then every sub-class from base class must define parameters (params) model. Params and header must not share any field. """ diff --git a/diplomacy/utils/order_results.py b/diplomacy/utils/order_results.py index fea2773..9720cfe 100644 --- a/diplomacy/utils/order_results.py +++ b/diplomacy/utils/order_results.py @@ -15,6 +15,7 @@ # with this program. If not, see . # ============================================================================== """ Results + - Contains the results labels and code used by the engine """ from diplomacy.utils.common import StringableCode @@ -26,17 +27,35 @@ class OrderResult(StringableCode): """ Represents an order result """ def __init__(self, code, message): """ Build a Order Result + :param code: int code of the order result :param message: human readable string message associated to the order result """ super(OrderResult, self).__init__(code, message) OK = OrderResult(0, '') +"""Order result OK, printed as ``''``""" + NO_CONVOY = OrderResult(ORDER_RESULT_OFFSET + 1, 'no convoy') +"""Order result NO_CONVOY, printed as ``'no convoy'``""" + BOUNCE = OrderResult(ORDER_RESULT_OFFSET + 2, 'bounce') +"""Order result BOUNCE, printed as ``'bounce'``""" + VOID = OrderResult(ORDER_RESULT_OFFSET + 3, 'void') +"""Order result VOID, printed as ``'void'``""" + CUT = OrderResult(ORDER_RESULT_OFFSET + 4, 'cut') +"""Order result CUT, printed as ``'cut'``""" + DISLODGED = OrderResult(ORDER_RESULT_OFFSET + 5, 'dislodged') +"""Order result DISLODGED, printed as ``'dislodged'``""" + DISRUPTED = OrderResult(ORDER_RESULT_OFFSET + 6, 'disrupted') +"""Order result DISRUPTED, printed as ``'disrupted'``""" + DISBAND = OrderResult(ORDER_RESULT_OFFSET + 7, 'disband') +"""Order result DISBAND, printed as ``'disband'``""" + MAYBE = OrderResult(ORDER_RESULT_OFFSET + 8, 'maybe') +"""Order result MAYBE, printed as ``'maybe'``""" diff --git a/diplomacy/utils/parsing.py b/diplomacy/utils/parsing.py index 5175a80..fb29f51 100644 --- a/diplomacy/utils/parsing.py +++ b/diplomacy/utils/parsing.py @@ -44,6 +44,7 @@ LOGGER = logging.getLogger(__name__) def update_model(model, additional_keys, allow_duplicate_keys=True): """ Return a copy of model updated with additional keys. + :param model: (Dictionary). Model to extend :param additional_keys: (Dictionary). Definition of the additional keys to use to update the model. :param allow_duplicate_keys: Boolean. If True, the model key will be updated if present in additional keys. @@ -60,6 +61,7 @@ def update_model(model, additional_keys, allow_duplicate_keys=True): def extend_model(model, additional_keys): """ Return a copy of model updated with additional model keys. Model and additional keys must no share any key. + :param model: (Dictionary). Model to update :param additional_keys: (Dictionary). Definition of the additional keys to add to model. :return: The updated model with the additional keys. @@ -68,6 +70,7 @@ def extend_model(model, additional_keys): def get_type(desired_type): """ Return a ParserType sub-class that matches given type. + :param desired_type: basic type or ParserType sub-class. :return: ParserType sub-class instance. """ @@ -88,8 +91,8 @@ def get_type(desired_type): return PrimitiveType(desired_type) def to_type(json_value, parser_type): - """ Convert a JSON value (python built-in type) to the type - described by parser_type. + """ Convert a JSON value (python built-in type) to the type described by parser_type. + :param json_value: JSON value to convert. :param parser_type: either an instance of a ParserType, or a type convertible to a ParserType (see function get_type() above). @@ -99,6 +102,7 @@ def to_type(json_value, parser_type): def to_json(raw_value, parser_type): """ Convert a value from the type described by parser_type to a JSON value. + :param raw_value: The raw value to convert to JSON. :param parser_type: Either an instance of a ParserType, or a type convertible to a ParserType. :return: The value converted to an equivalent JSON value. @@ -107,6 +111,7 @@ def to_json(raw_value, parser_type): def validate_data(data, model): """ Validates that the data complies with the model + :param data: (Dictionary). A dict of values to validate against the model. :param model: (Dictionary). The model to use for validation. """ @@ -125,6 +130,7 @@ def validate_data(data, model): def update_data(data, model): """ Modifies the data object to add default values if needed + :param data: (Dictionary). A dict of values to update. :param model: (Dictionary). The model to use. """ @@ -142,12 +148,14 @@ def update_data(data, model): class ParserType(metaclass=ABCMeta): """ Abstract base class to check a specific type. """ __slots__ = [] - # We include dict into primitive types to allow parser to accept raw untyped dict (e.g. engine game state). + # We include dict into primitive types to allow parser to accept raw untyped dict + # (e.g. engine game state). primitives = (bytes, int, float, bool, str, dict) @abstractmethod def validate(self, element): """ Makes sure the element is a valid element for this parser type + :param element: The element to validate. :return: None, but raises Error if needed. """ @@ -155,6 +163,7 @@ class ParserType(metaclass=ABCMeta): def update(self, element): """ Returns the correct value to use in the data object. + :param element: The element the model wants to store in the data object of this parser type. :return: The updated element to store in the data object. The updated element might be a different value (e.g. if a default value is present) @@ -164,6 +173,7 @@ class ParserType(metaclass=ABCMeta): def to_type(self, json_value): """ Converts a json_value to this parser type. + :param json_value: The JSON value to convert. :return: The converted JSON value. """ @@ -172,6 +182,7 @@ class ParserType(metaclass=ABCMeta): def to_json(self, raw_value): """ Converts a raw value (of this type) to JSON. + :param raw_value: The raw value (of this type) to convert. :return: The resulting JSON value. """ @@ -185,6 +196,7 @@ class ConverterType(ParserType): """ def __init__(self, element_type, converter_function, json_converter_function=None): """ Initialize a converter type. + :param element_type: expected type :param converter_function: function to be used to check and convert values to expected type. converter_function(value) -> value_compatible_with_expected_type @@ -217,6 +229,7 @@ class DefaultValueType(ParserType): def __init__(self, element_type, default_json_value): """ Initialize a default type checker with expected element type and a default value (if None is present). + :param element_type: The expected type for elements (except if None is provided). :param default_json_value: The default value to set if element=None. Must be a JSON value convertible to element_type, so that new default value is generated from this JSON value @@ -256,7 +269,8 @@ class OptionalValueType(DefaultValueType): def __init__(self, element_type): """ Initialized a optional type checker with expected element type. - :param element_type: The expected type for elements. + + :param element_type: The expected type for elements. """ super(OptionalValueType, self).__init__(element_type, None) @@ -266,6 +280,7 @@ class SequenceType(ParserType): def __init__(self, element_type, sequence_builder=None): """ Initialize a sequence type checker with value type and optional sequence builder. + :param element_type: Expected type for sequence elements. :param sequence_builder: (Optional). A callable used to build the sequence type. Expected args: Iterable @@ -301,6 +316,7 @@ class JsonableClassType(ParserType): def __init__(self, jsonable_element_type): """ Initialize a sub-class of Jsonable. + :param jsonable_element_type: Expected type (should be a subclass of Jsonable). """ # We import Jsonable here to prevent recursive import with module jsonable. @@ -331,12 +347,13 @@ class StringableType(ParserType): So, object may have any type as long as: str(obj) == str( object loaded from str(obj) ) - Expected type: a class with compatible str(cls(string_repr)) or str(cls.from_string(string_repr)). + Expected type: a class with compatible str(cls(str_repr)) or str(cls.from_string(str_repr)). """ __slots__ = ['element_type', 'use_from_string'] def __init__(self, element_type): """ Initialize a parser type with a type convertible from/to string. + :param element_type: Expected type. Needs to be convertible to/from String. """ if hasattr(element_type, 'from_string'): @@ -376,6 +393,7 @@ class DictType(ParserType): def __init__(self, key_type, val_type, dict_builder=None): """ Initialize a dict parser type with expected key type, val type, and optional dict builder. + :param key_type: The expected key type. Must be string or a stringable class. :param val_type: The expected value type. :param dict_builder: Callable to build attribute values. @@ -412,7 +430,8 @@ class IndexedSequenceType(ParserType): __slots__ = ['dict_type', 'sequence_type', 'key_name'] def __init__(self, dict_type, key_name): - """ Initializer: + """ Initializer. + :param dict_type: dictionary parser type to be used to manage object in memory. :param key_name: name of attribute to take in sequence elements to convert sequence to a dictionary. dct = {getattr(element, key_name): element for element in sequence} @@ -447,6 +466,7 @@ class EnumerationType(ParserType): def __init__(self, enum_values): """ Initialize sequence of values type with a sequence of allowed (primitive) values. + :param enum_values: Sequence of allowed values. """ enum_values = set(enum_values) @@ -472,6 +492,7 @@ class SequenceOfPrimitivesType(ParserType): def __init__(self, seq_of_primitives): """ Initialize sequence of primitives type with a sequence of allowed primitives. + :param seq_of_primitives: Sequence of primitives. """ assert seq_of_primitives and all(primitive in self.primitives for primitive in seq_of_primitives) @@ -491,6 +512,7 @@ class PrimitiveType(ParserType): def __init__(self, element_type): """ Initialize a primitive type. + :param element_type: Primitive type. """ assert element_type in self.primitives, 'Expected a primitive type, got %s.' % element_type diff --git a/diplomacy/utils/priority_dict.py b/diplomacy/utils/priority_dict.py index acef8f9..66662e7 100644 --- a/diplomacy/utils/priority_dict.py +++ b/diplomacy/utils/priority_dict.py @@ -18,13 +18,15 @@ import heapq # ------------------------------------------------ -# Adapted from (2018/03/14s): https://docs.python.org/3.6/library/heapq.html#priority-queue-implementation-notes +# Adapted from (2018/03/14s): +# https://docs.python.org/3.6/library/heapq.html#priority-queue-implementation-notes # Unlicensed class PriorityDict(dict): """ Priority Dictionary Implementation """ def __init__(self, **kwargs): """ Initialize the priority queue. + :param kwargs: (optional) initial values for priority queue. """ self.__heap = [] # Heap for entries. An entry is a triple (priority value, key, valid entry flag (boolean)). @@ -36,6 +38,7 @@ class PriorityDict(dict): def __setitem__(self, key, val): """ Sets a key with his associated priority + :param key: The key to set in the dictionary :param val: The priority to associate with the key :return: None @@ -48,7 +51,9 @@ class PriorityDict(dict): heapq.heappush(self.__heap, entry) def __delitem__(self, key): - """ Removes key from dict and marks associated heap entry as invalid (False). Raises KeyError if not found. """ + """ Removes key from dict and marks associated heap entry as invalid (False). + Raises KeyError if not found. + """ entry = self.pop(key) entry[-1] = False @@ -71,6 +76,7 @@ class PriorityDict(dict): def smallest(self): """ Finds the smallest item in the priority dict + :return: A tuple of (priority, key) for the item with the smallest priority, or None if dict is empty. """ while self.__heap and not self.__heap[0][-1]: @@ -85,6 +91,7 @@ class PriorityDict(dict): def copy(self): """ Return a copy of this priority dict. + :rtype: PriorityDict """ return PriorityDict(**{key: entry[0] for key, entry in dict.items(self)}) diff --git a/diplomacy/utils/scheduler_event.py b/diplomacy/utils/scheduler_event.py index 1d097d8..8eaf0ff 100644 --- a/diplomacy/utils/scheduler_event.py +++ b/diplomacy/utils/scheduler_event.py @@ -19,12 +19,15 @@ from diplomacy.utils.jsonable import Jsonable class SchedulerEvent(Jsonable): - """ Scheduler event class. Properties: - - time_unit: unit time (in seconds) used by scheduler (time between 2 tasks checkings). + """ Scheduler event class. + + Properties: + + - **time_unit**: unit time (in seconds) used by scheduler (time between 2 tasks checkings). Currently 1 second in server scheduler. - - time_added: scheduler time (nb. time units) when data was added to scheduler. - - delay: scheduler time (nb. time units) to wait before processing time. - - current_time: current scheduler time (nb. time units). + - **time_added**: scheduler time (nb. time units) when data was added to scheduler. + - **delay**: scheduler time (nb. time units) to wait before processing time. + - **current_time**: current scheduler time (nb. time units). """ __slots__ = ['time_unit', 'time_added', 'delay', 'current_time'] model = { diff --git a/diplomacy/utils/sorted_dict.py b/diplomacy/utils/sorted_dict.py index c1e5cd7..f5ac6d7 100644 --- a/diplomacy/utils/sorted_dict.py +++ b/diplomacy/utils/sorted_dict.py @@ -18,12 +18,13 @@ from diplomacy.utils.common import is_dictionary from diplomacy.utils.sorted_set import SortedSet -class SortedDict(): +class SortedDict: """ Dict with sorted keys. """ __slots__ = ['__val_type', '__keys', '__couples'] def __init__(self, key_type, val_type, kwargs=None): """ Initialize a typed SortedDict. + :param key_type: expected type for keys. :param val_type: expected type for values. :param kwargs: (optional) dictionary-like object: initial values for sorted dict. @@ -40,6 +41,9 @@ class SortedDict(): def builder(key_type, val_type): """ Return a function to build sorted dicts from a dictionary-like object. Returned function expects a dictionary parameter (an object with method items()). + + .. code-block:: python + builder_fn = SortedDict.builder(str, int) my_sorted_dict = builder_fn({'a': 1, 'b': 2}) @@ -157,17 +161,18 @@ class SortedDict(): return self.__keys[position_from:(position_to + 1)] def sub(self, key_from=None, key_to=None): - """ Return a list of values associated to keys between key_from and key_to (both bounds included). + """ Return a list of values associated to keys between key_from and key_to + (both bounds included). - If key_from is None, lowest key in dict is used. - If key_to is None, greatest key in dict is used. - If key_from is not in dict, lowest key in dict greater than key_from is used. - If key_to is not in dict, greatest key in dict less than key_to is used. + - If key_from is None, lowest key in dict is used. + - If key_to is None, greatest key in dict is used. + - If key_from is not in dict, lowest key in dict greater than key_from is used. + - If key_to is not in dict, greatest key in dict less than key_to is used. - If dict is empty, return empty list. - With keys (None, None) return a copy of all values. - With keys (None, key_to), return values from first to the one associated to key_to. - With keys (key_from, None), return values from the one associated to key_from to the last value. + - If dict is empty, return empty list. + - With keys (None, None) return a copy of all values. + - With keys (None, key_to), return values from first to the one associated to key_to. + - With keys (key_from, None), return values from the one associated to key_from to the last value. :param key_from: start key :param key_to: end key @@ -184,7 +189,7 @@ class SortedDict(): :param key_from: start key :param key_to: end key :return: nothing - """ + """ position_from, position_to = self._get_keys_interval(key_from, key_to) keys_to_remove = self.__keys[position_from:(position_to + 1)] for key in keys_to_remove: @@ -207,13 +212,12 @@ class SortedDict(): to easily retrieve values in closed interval [index of key_from; index of key_to] corresponding to Python slice [index of key_from : (index of key_to + 1)] - If dict is empty, return (0, -1), so that python slice [0 : -1 + 1] corresponds to empty interval. - If key_from is None, lowest key in dict is used. - If key_to is None, greatest key in dict is used. - If key_from is not in dict, lowest key in dict greater than key_from is used. - If key_to is not in dict, greatest key in dict less than key_to is used. + - If dict is empty, return (0, -1), so that python slice [0 : -1 + 1] corresponds to empty interval. + - If key_from is None, lowest key in dict is used. + - If key_to is None, greatest key in dict is used. + - If key_from is not in dict, lowest key in dict greater than key_from is used. + - If key_to is not in dict, greatest key in dict less than key_to is used. - Thus: - With keys (None, None), we get interval of all values. - With keys (key_from, None), we get interval for values from key_from to the last key. - With keys (None, key_to), we get interval for values from the first key to key_to. diff --git a/diplomacy/utils/sorted_set.py b/diplomacy/utils/sorted_set.py index 01cfb34..0bd327c 100644 --- a/diplomacy/utils/sorted_set.py +++ b/diplomacy/utils/sorted_set.py @@ -21,15 +21,16 @@ from copy import copy from diplomacy.utils import exceptions from diplomacy.utils.common import is_sequence -class SortedSet(): +class SortedSet: """ Sorted set (sorted values, each value appears once). """ __slots__ = ('__type', '__list') def __init__(self, element_type, content=()): """ Initialize a typed sorted set. + :param element_type: Expected type for values. :param content: (optional) Sequence of values to initialize sorted set with. - """ + """ if not is_sequence(content): raise exceptions.TypeException('sequence', type(content)) self.__type = element_type @@ -40,12 +41,15 @@ class SortedSet(): @staticmethod def builder(element_type): """ Return a function to build sorted sets from content (sequence of values). - :param element_type: expected type for sorted set values. - :return: callable - Returned function expects a content parameter like SortedSet initializer. + + .. code-block:: python + builder_fn = SortedSet.builder(str) my_sorted_set = builder_fn(['c', '3', 'p', '0']) + + :param element_type: expected type for sorted set values. + :return: callable """ return lambda iterable: SortedSet(element_type, iterable) @@ -104,9 +108,9 @@ class SortedSet(): return best_position def get_next_value(self, element): - """ Get lowest value in sorted set greater than given element, or None if such values does not exists - in the sorted set. Given element may not exists in the sorted set. - """ + """ Get lowest value in sorted set greater than given element, or None if such values + does not exists in the sorted set. Given element may not exists in the sorted set. + """ assert isinstance(element, self.__type) if self.__list: best_position = bisect.bisect_right(self.__list, element) @@ -118,8 +122,8 @@ class SortedSet(): return None def get_previous_value(self, element): - """ Get greatest value in sorted set less the given element, or None if such value does not exists - in the sorted set. Given element may not exists in the sorted set. + """ Get greatest value in sorted set less the given element, or None if such value + does not exists in the sorted set. Given element may not exists in the sorted set. """ assert isinstance(element, self.__type) if self.__list: diff --git a/diplomacy/utils/splitter.py b/diplomacy/utils/splitter.py index 5de502e..e1e4169 100644 --- a/diplomacy/utils/splitter.py +++ b/diplomacy/utils/splitter.py @@ -24,6 +24,7 @@ class AbstractStringSplitter(metaclass=ABCMeta): """ Breaks a string into its components - Generic class """ def __init__(self, string, length): """ Constructor + :param string: the string to split :param length: the maximum length of the split """ @@ -60,6 +61,7 @@ class OrderSplitter(AbstractStringSplitter): """ Splits an order into its components """ def __init__(self, string): """ Constructor + :param string: the string to split """ self._unit_index = None @@ -196,6 +198,7 @@ class PhaseSplitter(AbstractStringSplitter): """ Splits a phase into its components """ def __init__(self, string): """ Constructor + :param string: the string to split """ self._season_index = None diff --git a/diplomacy/utils/strings.py b/diplomacy/utils/strings.py index 65efb64..80b83a3 100644 --- a/diplomacy/utils/strings.py +++ b/diplomacy/utils/strings.py @@ -63,6 +63,7 @@ DUMMY = 'dummy' DUMMY_PLAYER = 'dummy_player' DUMMY_POWERS = 'dummy_powers' ERROR = 'error' +ERROR_TYPE = 'error_type' FOR_OMNISCIENCE = 'for_omniscience' FORCED = 'forced' FORCED_ORDERS = 'forced_orders' @@ -235,8 +236,9 @@ def role_is_special(role): def switch_special_role(role): """ Return opposite special role of given special role: - - observer role if omniscient role is given - - omniscient role if observer role is given + + - observer role if omniscient role is given + - omniscient role if observer role is given """ if role == OBSERVER_TYPE: return OMNISCIENT_TYPE diff --git a/diplomacy/utils/tests/test_jsonable_changes.py b/diplomacy/utils/tests/test_jsonable_changes.py index 992a8fb..2f37db3 100644 --- a/diplomacy/utils/tests/test_jsonable_changes.py +++ b/diplomacy/utils/tests/test_jsonable_changes.py @@ -103,9 +103,10 @@ class Version22(Jsonable): class Version3(Jsonable): """ Version 1 with a modified, b removed, e added. To parse a dict between Version3 and Version1: - - a must be convertible in both versions. - - b must be optional in Version1. - - e must be optional in Version3. + + - a must be convertible in both versions. + - b must be optional in Version1. + - e must be optional in Version3. """ model = { 'a': parsing.ConverterType(str, converter_function=str), diff --git a/diplomacy/utils/tests/test_parsing.py b/diplomacy/utils/tests/test_parsing.py index f64ad26..f96a981 100644 --- a/diplomacy/utils/tests/test_parsing.py +++ b/diplomacy/utils/tests/test_parsing.py @@ -21,7 +21,7 @@ from diplomacy.utils.sorted_set import SortedSet from diplomacy.utils.tests.test_common import assert_raises from diplomacy.utils.tests.test_jsonable import MyJsonable -class MyStringable(): +class MyStringable: """ Example of Stringable class. As instances of such class may be used as dict keys, class should define a proper __hash__(). """ diff --git a/diplomacy/utils/time.py b/diplomacy/utils/time.py index 6f250dd..9ac7edd 100644 --- a/diplomacy/utils/time.py +++ b/diplomacy/utils/time.py @@ -24,6 +24,7 @@ import pytz def str_to_seconds(offset_str): """ Converts a time in format 00W00D00H00M00S in number of seconds + :param offset_str: The string to convert (e.g. '20D') :return: Its equivalent in seconds = 1728000 """ @@ -47,6 +48,7 @@ def str_to_seconds(offset_str): def trunc_time(timestamp, trunc_interval, time_zone='GMT'): """ Truncates time at a specific interval (e.g. 20M) (i.e. Rounds to the next :20, :40, :60) + :param timestamp: The unix epoch to truncate (e.g. 1498746120) :param trunc_interval: The truncation interval (e.g. 60*60 or '1H') :param time_zone: The time to use for conversion (defaults to GMT otherwise) @@ -65,6 +67,7 @@ def trunc_time(timestamp, trunc_interval, time_zone='GMT'): def next_time_at(timestamp, time_at, time_zone='GMT'): """ Returns the next timestamp at a specific 'hh:mm' + :param timestamp: The unix timestamp to convert :param time_at: The next 'hh:mm' to have the time rounded to, or 0 to skip :param time_zone: The time to use for conversion (defaults to GMT otherwise) diff --git a/diplomacy/web/__init__.py b/diplomacy/web/__init__.py new file mode 100644 index 0000000..a2441fc --- /dev/null +++ b/diplomacy/web/__init__.py @@ -0,0 +1,19 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette, Steven Bocco +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# ============================================================================== +""" Web + - Contains the web interface +""" \ No newline at end of file diff --git a/diplomacy/web/svg_to_react.py b/diplomacy/web/svg_to_react.py index dafe4d3..6ee5049 100644 --- a/diplomacy/web/svg_to_react.py +++ b/diplomacy/web/svg_to_react.py @@ -15,7 +15,7 @@ # with this program. If not, see . # ============================================================================== """ Helper script to convert a SVG file into a React JS component file. - Type `python