From 6187faf20384b0c5a4966343b2d4ca47f8b11e45 Mon Sep 17 00:00:00 2001 From: Philip Paquette Date: Wed, 26 Sep 2018 07:48:55 -0400 Subject: Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License --- diplomacy/client/network_game.py | 199 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 diplomacy/client/network_game.py (limited to 'diplomacy/client/network_game.py') diff --git a/diplomacy/client/network_game.py b/diplomacy/client/network_game.py new file mode 100644 index 0000000..e75687f --- /dev/null +++ b/diplomacy/client/network_game.py @@ -0,0 +1,199 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette, Steven Bocco +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see . +# ============================================================================== +""" Game object used on client side. """ +import logging + +from diplomacy.client.channel import Channel +from diplomacy.communication import notifications +from diplomacy.engine.game import Game +from diplomacy.utils.exceptions import DiplomacyException +from diplomacy.utils.game_phase_data import GamePhaseData + +LOGGER = logging.getLogger(__name__) + +def game_request_method(channel_method): + """Create a game request method that calls channel counterpart.""" + + def func(self, **kwargs): + """ Call channel-related method to send a game request with given kwargs. """ + # NB: Channel method returns a future. + if not self.channel: + raise DiplomacyException('Invalid client game.') + return channel_method(self.channel, game_object=self, **kwargs) + + return func + +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) + + return func + +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) + + 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 + when a corresponding game notification is received. + """ + __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 + """ + self.channel = channel + self.notification_callbacks = {} # {notification_class => [callback(game, notification)]} + self.data = None + # Initialize parent class with Jsonable attributes from received game. + # Received game should contain a valid `initial_state` attribute that will be used + # to set client game state. + super(NetworkGame, self).__init__(**{key: getattr(received_game, key) for key in received_game.get_model()}) + + # =========== + # Public API. + # =========== + + # 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) + + 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) + + def synchronize(self): + """ Send a 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()) + + # 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) + + # =============================== + # 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) + + 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. + """ + assert callable(notification_callback) + if notification_class not in self.notification_callbacks: + self.notification_callbacks[notification_class] = [notification_callback] + else: + self.notification_callbacks[notification_class].append(notification_callback) + + 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) + + def notify(self, notification): + """ Notify game with given notification (call associated callbacks if defined). """ + for callback in self.notification_callbacks.get(type(notification), ()): + callback(self, notification) + + def set_phase_data(self, phase_data, clear_history=True): + """ Overwrite base method to prevent call to channel methods. """ + if not phase_data: + return + if isinstance(phase_data, GamePhaseData): + phase_data = [phase_data] + elif not isinstance(phase_data, list): + phase_data = list(phase_data) + + if clear_history: + self._clear_history() + + for game_phase_data in phase_data[:-1]: # type: GamePhaseData + Game.extend_phase_history(self, game_phase_data) + + current_phase_data = phase_data[-1] # type: GamePhaseData + Game.set_state(self, current_phase_data.state, clear_history=clear_history) + for power_name, power_orders in current_phase_data.orders.items(): + Game.set_orders(self, power_name, power_orders) + self.messages = current_phase_data.messages.copy() + # We ignore 'results' for current phase data. -- cgit v1.2.3