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