# ============================================================================== # 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.