diff options
author | Philip Paquette <pcpaquette@gmail.com> | 2019-09-11 12:58:45 -0400 |
---|---|---|
committer | Philip Paquette <pcpaquette@gmail.com> | 2019-09-14 18:18:53 -0400 |
commit | abb42dcd4886705d6ba8af27f68ef605218ac67c (patch) | |
tree | 9ae16f7a09fff539fa72e65198e284bca6ac3376 /diplomacy/communication | |
parent | a954a00d263750c279dbb2c0a9ae85707022bcd7 (diff) |
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 <pcpaquette@gmail.com>
Co-authored-by: notoraptor <stevenbocco@gmail.com>
Diffstat (limited to 'diplomacy/communication')
-rw-r--r-- | diplomacy/communication/notifications.py | 93 | ||||
-rw-r--r-- | diplomacy/communication/requests.py | 506 | ||||
-rw-r--r-- | diplomacy/communication/responses.py | 145 |
3 files changed, 572 insertions, 172 deletions
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 <https://www.gnu.org/licenses/>. # ============================================================================== """ 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 <https://www.gnu.org/licenses/>. # ============================================================================== -""" 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 |