Coverage for /home/kale/kxgames/libraries/kxg/kxg/forums.py: 100%

# Coverage for /home/kale/kxgames/libraries/kxg/kxg/forums.py : 100%

## 145 statements 145 run 0 missing 0 excluded

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | #!/usr/bin/env python3 from .errors import * class Forum: def __init__(self): self.world = None self.actors = None def execute_message(self, message): info("executing message: {message}") # Relay the messages to clients running on other machines, if this is a # multiplayer game. Since the tokens referenced in the message might # be changed once the message is executed, the message has to be # relayed before then. for actor in self.actors: actor._relay_message(message) # Normally, tokens can only call methods that have been decorated with # @read_only. This is a precaution to help keep the worlds in sync on # all the clients. This restriction is lifted when the tokens are # handling messages and enforced again once the actors are handling # messages. with self.world._unlock_temporarily(): # First, let the message update the state of the game world. message._execute(self.world) # Second, let the world react to the message. The main effect of # the message should have already been carried out above. These # callbacks should take care of more peripheral effects. self.world._react_to_message(message) # Third, let the actors and the extensions react to the message. This # step is carried out last so that the actors can be sure that the # world has a consistent state by the time their handlers are called. for actor in self.actors: actor._react_to_message(message) def connect_everyone(self, world, actors): # Save references to the world and the actors in the forum. self.world = world self.actors = actors # Save references to the actors in the world. The world doesn't need # to know about the forum because it can't send messages. It needs to # know about the actors so it can create token extensions. self.world._set_actors(actors) # Save references to the forum and the world in the actors. Also # assign each actor a factory it can use to generate unique token ids. # # In multiplayer games, each client needs the ability to create tokens, # so that messages can be instantly handled. Tokens still need unique # ids though, so this method provides each actor with an IdFactory that # generates ids using an offset and a spacing to ensure uniqueness. # # Actors take their own id numbers (used for figuring out who messages # were sent by) from the offset parameter of the id factory. Since the # Referee must have an id of 0 if it's present, care is taken to make # that happen. id_factories = self._assign_id_factories() for actor in self.actors: actor._set_world(world) actor._set_forum(self, id_factories[actor]) def on_start_game(self): pass def on_update_game(self): # Forum doesn't do anything on a timer; it does everything in response # to a message being sent. But the ClientForum uses this method to # react to message that have arrived from the server. pass def on_finish_game(self): pass def _assign_id_factories(self): id_factories = {} actors = sorted(self.actors, key=lambda x: not x.is_referee()) first_id = self.world.get_last_id() + 1 spacing = len(self.actors) for offset, actor in enumerate(actors, first_id): id_factories[actor] = IdFactory(offset, spacing) return id_factories class ForumObserver: from collections import namedtuple CallbackInfo = namedtuple('CallbackInfo', 'message_cls, callback') def __init__(self): super().__init__() # Create a data structure to hold all the callbacks registered with # this observer. Using a dictionary to distinguish between the regular # message handlers, the soft sync error handlers, and the hard sync # error handlers (instead of just having three different lists) makes # it easy to write protected helpers to do most of the work. self._callbacks = { 'message': [], 'sync_response': [], 'undo_response': [], } # Create a member variable indicating whether or not the ability to # subscribe to or unsubscribe from messages should be enabled. Token # disable this functionality until they've been added to the world and # RemoteActors disable it permanently. self._is_enabled = True # Decorators can be used to automatically label methods that should be # callbacks. Here, we look for methods that have been labeled in this # way and register them appropriately. from inspect import getmembers, ismethod for _, method in getmembers(self, ismethod): for message_cls in getattr(method, '_kxg_subscribe_to_message', []): self.subscribe_to_message(message_cls, method) for message_cls in getattr(method, '_kxg_subscribe_to_sync_response', []): self.subscribe_to_sync_response(message_cls, method) for message_cls in getattr(method, '_kxg_subscribe_to_undo_response', []): self.subscribe_to_undo_response(message_cls, method) def __getstate__(self): state = self.__dict__.copy() del state['_callbacks'] del state['_is_enabled'] return state def __setstate__(self, state): ForumObserver.__init__(self) self.__dict__.update(state) def subscribe_to_message(self, message_cls, callback): self._add_callback('message', message_cls, callback) def subscribe_to_sync_response(self, message_cls, callback): self._add_callback('sync_response', message_cls, callback) def subscribe_to_undo_response(self, message_cls, callback): self._add_callback('undo_response', message_cls, callback) def unsubscribe_from_message(self, message_cls, callback=None): self._drop_callback('message', message_cls, callback) def unsubscribe_from_sync_response(self, message_cls, callback=None): self._drop_callback('sync_response', message_cls, callback) def unsubscribe_from_undo_response(self, message_cls, callback=None): self._drop_callback('undo_response', message_cls, callback) def _react_to_message(self, message): self._call_callbacks('message', message) def _react_to_sync_response(self, message): self._call_callbacks('sync_response', message) def _react_to_undo_response(self, message): self._call_callbacks('undo_response', message) def _enable_forum_observation(self): self._is_enabled = True def _disable_forum_observation(self): self._is_enabled = False def _check_if_forum_observation_enabled(self): if not self._is_enabled: raise ApiUsageError("""\ {self} has disabled forum observation.""") def _add_callback(self, event, message_cls, callback): from .messages import require_message_cls require_message_cls(message_cls) self._check_if_forum_observation_enabled() callback_info = ForumObserver.CallbackInfo(message_cls, callback) self._callbacks[event].append(callback_info) def _drop_callback(self, event, message_cls, callback): from .messages import require_message_cls require_message_cls(message_cls) self._check_if_forum_observation_enabled() self._callbacks[event] = [ x for x in self._callbacks[event] if not ((x.message_cls is message_cls) and (x.callback is callback or callback is None) ) ] def _call_callbacks(self, event, message): self._check_if_forum_observation_enabled() # Call the callbacks stored in this observer. for callback_info in self._callbacks[event]: if isinstance(message, callback_info.message_cls): callback_info.callback(message) # Call the callbacks stored in nested observers. for observer in self._get_nested_observers(): observer._call_callbacks(event, message) def _get_nested_observers(self): return [] class IdFactory: def __init__(self, offset, spacing): self.offset = offset self.spacing = spacing self.num_ids_assigned = 0 def __repr__(self): return 'IdFactory(offset={}, spacing={})'.format( self.offset, self.spacing) def __contains__(self, id): return id % self.spacing == self.offset % self.spacing def get(self): return self.offset def next(self): next_id = self.num_ids_assigned * self.spacing + self.offset self.num_ids_assigned += 1 return next_id @debug_only def require_forum(object): require_instance(Forum(), object) def subscribe_to_message(message_cls): def decorator(function): try: function._kxg_subscribe_to_message.append(message_cls) except AttributeError: function._kxg_subscribe_to_message = [message_cls] return function return decorator def subscribe_to_sync_response(message_cls): def decorator(function): try: function._kxg_subscribe_to_sync_response.append(message_cls) except AttributeError: function._kxg_subscribe_to_sync_response = [message_cls] return function return decorator def subscribe_to_undo_response(message_cls): def decorator(function): try: function._kxg_subscribe_to_undo_response.append(message_cls) except AttributeError: function._kxg_subscribe_to_undo_response = [message_cls] return function return decorator