aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/client
diff options
context:
space:
mode:
Diffstat (limited to 'diplomacy/client')
-rw-r--r--diplomacy/client/channel.py134
-rw-r--r--diplomacy/client/connection.py668
-rw-r--r--diplomacy/client/game_instances_set.py3
-rw-r--r--diplomacy/client/network_game.py156
-rw-r--r--diplomacy/client/notification_managers.py25
-rw-r--r--diplomacy/client/response_managers.py87
6 files changed, 607 insertions, 466 deletions
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 <https://www.gnu.org/licenses/>.
# ==============================================================================
""" 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.