comparison sat/plugins/plugin_misc_room_game.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_misc_room_game.py@0046283a285d
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT: a jabber client
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from twisted.words.protocols.jabber import jid
25 from twisted.words.xish import domish
26 from twisted.internet import defer
27 from time import time
28 from wokkel import disco, iwokkel
29 from zope.interface import implements
30 import copy
31 try:
32 from twisted.words.protocols.xmlstream import XMPPHandler
33 except ImportError:
34 from wokkel.subprotocols import XMPPHandler
35
36 # Don't forget to set it to False before you commit
37 _DEBUG = False
38
39 PLUGIN_INFO = {
40 C.PI_NAME: "Room game",
41 C.PI_IMPORT_NAME: "ROOM-GAME",
42 C.PI_TYPE: "MISC",
43 C.PI_PROTOCOLS: [],
44 C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0249"],
45 C.PI_MAIN: "RoomGame",
46 C.PI_HANDLER: "no", # handler MUST be "no" (dynamic inheritance)
47 C.PI_DESCRIPTION: _("""Base class for MUC games""")
48 }
49
50
51 # FIXME: this plugin is broken, need to be fixed
52
53 class RoomGame(object):
54 """This class is used to help launching a MUC game.
55
56 Bridge methods callbacks: _prepareRoom, _playerReady, _createGame
57 Triggered methods: userJoinedTrigger, userLeftTrigger
58 Also called from subclasses: newRound
59
60 For examples of messages sequences, please look in sub-classes.
61 """
62
63 # Values for self.invite_mode (who can invite after the game creation)
64 FROM_ALL, FROM_NONE, FROM_REFEREE, FROM_PLAYERS = xrange(0, 4)
65 # Values for self.wait_mode (for who we should wait before creating the game)
66 FOR_ALL, FOR_NONE = xrange(0, 2)
67 # Values for self.join_mode (who can join the game - NONE means solo game)
68 ALL, INVITED, NONE = xrange(0, 3)
69 # Values for ready_mode (how to turn a MUC user into a player)
70 ASK, FORCE = xrange(0, 2)
71
72 MESSAGE = '/message'
73 REQUEST = '%s/%s[@xmlns="%s"]'
74
75 def __init__(self, host):
76 """For other plugin to dynamically inherit this class, it is necessary to not use __init__ but _init_.
77 The subclass itself must be initialized this way:
78
79 class MyGame(object):
80
81 def inheritFromRoomGame(self, host):
82 global RoomGame
83 RoomGame = host.plugins["ROOM-GAME"].__class__
84 self.__class__ = type(self.__class__.__name__, (self.__class__, RoomGame, object), {})
85
86 def __init__(self, host):
87 self.inheritFromRoomGame(host)
88 RoomGame._init_(self, host, ...)
89
90 """
91 self.host = host
92
93 def _init_(self, host, plugin_info, ns_tag, game_init=None, player_init=None):
94 """
95 @param host
96 @param plugin_info: PLUGIN_INFO map of the game plugin
97 @param ns_tag: couple (nameservice, tag) to construct the messages
98 @param game_init: dictionary for general game initialization
99 @param player_init: dictionary for player initialization, applicable to each player
100 """
101 self.host = host
102 self.name = plugin_info["import_name"]
103 self.ns_tag = ns_tag
104 self.request = self.REQUEST % (self.MESSAGE, ns_tag[1], ns_tag[0])
105 if game_init is None:
106 game_init = {}
107 if player_init is None:
108 player_init = {}
109 self.game_init = game_init
110 self.player_init = player_init
111 self.games = {}
112 self.invitations = {} # values are a couple (x, y) with x the time and y a list of users
113
114 # These are the default settings, which can be overwritten by child class after initialization
115 self.invite_mode = self.FROM_PLAYERS if self.player_init == {} else self.FROM_NONE
116 self.wait_mode = self.FOR_NONE if self.player_init == {} else self.FOR_ALL
117 self.join_mode = self.INVITED
118 self.ready_mode = self.FORCE # TODO: asking for confirmation is not implemented
119
120 # this has been added for testing purpose. It is sometimes needed to remove a dependence
121 # while building the synchronization data, for example to replace a call to time.time()
122 # by an arbitrary value. If needed, this attribute would be set to True from the testcase.
123 self.testing = False
124
125 host.trigger.add("MUC user joined", self.userJoinedTrigger)
126 host.trigger.add("MUC user left", self.userLeftTrigger)
127
128 def _createOrInvite(self, room_jid, other_players, profile):
129 """
130 This is called only when someone explicitly wants to play.
131
132 The game will not be created if one already exists in the room,
133 also its creation could be postponed until all the expected players
134 join the room (in that case it will be created from userJoinedTrigger).
135 @param room (wokkel.muc.Room): the room
136 @param other_players (list[jid.JID]): list of the other players JID (bare)
137 """
138 # FIXME: broken !
139 raise NotImplementedError("To be fixed")
140 client = self.host.getClient(profile)
141 user_jid = self.host.getJidNStream(profile)[0]
142 nick = self.host.plugins["XEP-0045"].getRoomNick(client, room_jid)
143 nicks = [nick]
144 if self._gameExists(room_jid):
145 if not self._checkJoinAuth(room_jid, user_jid, nick):
146 return
147 nicks.extend(self._invitePlayers(room_jid, other_players, nick, profile))
148 self._updatePlayers(room_jid, nicks, True, profile)
149 else:
150 self._initGame(room_jid, nick)
151 (auth, waiting, missing) = self._checkWaitAuth(room_jid, other_players)
152 nicks.extend(waiting)
153 nicks.extend(self._invitePlayers(room_jid, missing, nick, profile))
154 if auth:
155 self.createGame(room_jid, nicks, profile)
156 else:
157 self._updatePlayers(room_jid, nicks, False, profile)
158
159 def _initGame(self, room_jid, referee_nick):
160 """
161
162 @param room_jid (jid.JID): JID of the room
163 @param referee_nick (unicode): nickname of the referee
164 """
165 # Important: do not add the referee to 'players' yet. For a
166 # <players /> message to be emitted whenever a new player is joining,
167 # it is necessary to not modify 'players' outside of _updatePlayers.
168 referee_jid = jid.JID(room_jid.userhost() + '/' + referee_nick)
169 self.games[room_jid] = {'referee': referee_jid, 'players': [], 'started': False, 'status': {}}
170 self.games[room_jid].update(copy.deepcopy(self.game_init))
171 self.invitations.setdefault(room_jid, [])
172
173 def _gameExists(self, room_jid, started=False):
174 """Return True if a game has been initialized/started.
175 @param started: if False, the game must be initialized to return True,
176 otherwise it must be initialized and started with createGame.
177 @return: True if a game is initialized/started in that room"""
178 return room_jid in self.games and (not started or self.games[room_jid]['started'])
179
180 def _checkJoinAuth(self, room_jid, user_jid=None, nick="", verbose=False):
181 """Checks if this profile is allowed to join the game.
182
183 The parameter nick is used to check if the user is already
184 a player in that game. When this method is called from
185 userJoinedTrigger, nick is also used to check the user
186 identity instead of user_jid_s (see TODO comment below).
187 @param room_jid (jid.JID): the JID of the room hosting the game
188 @param user_jid (jid.JID): JID of the user
189 @param nick (unicode): nick of the user
190 @return: True if this profile can join the game
191 """
192 auth = False
193 if not self._gameExists(room_jid):
194 auth = False
195 elif self.join_mode == self.ALL or self.isPlayer(room_jid, nick):
196 auth = True
197 elif self.join_mode == self.INVITED:
198 # considering all the batches of invitations
199 for invitations in self.invitations[room_jid]:
200 if user_jid is not None:
201 if user_jid.userhostJID() in invitations[1]:
202 auth = True
203 break
204 else:
205 # TODO: that's not secure enough but what to do if
206 # wokkel.muc.User's 'entity' attribute is not set?!
207 if nick in [invited.user for invited in invitations[1]]:
208 auth = True
209 break
210
211 if not auth and (verbose or _DEBUG):
212 log.debug(_(u"%(user)s not allowed to join the game %(game)s in %(room)s") % {'user': user_jid.userhost() or nick, 'game': self.name, 'room': room_jid.userhost()})
213 return auth
214
215 def _updatePlayers(self, room_jid, nicks, sync, profile):
216 """Update the list of players and signal to the room that some players joined the game.
217 If sync is True, the news players are synchronized with the game data they have missed.
218 Remark: self.games[room_jid]['players'] should not be modified outside this method.
219 @param room_jid (jid.JID): JID of the room
220 @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position)
221 @param sync (bool): set to True to send synchronization data to the new players
222 @param profile (unicode): %(doc_profile)s
223 """
224 if nicks == []:
225 return
226 # this is better than set(nicks).difference(...) as it keeps the order
227 new_nicks = [nick for nick in nicks if nick not in self.games[room_jid]['players']]
228 if len(new_nicks) == 0:
229 return
230
231 def setStatus(status):
232 for nick in new_nicks:
233 self.games[room_jid]['status'][nick] = status
234
235 sync = sync and self._gameExists(room_jid, True) and len(self.games[room_jid]['players']) > 0
236 setStatus('desync' if sync else 'init')
237 self.games[room_jid]['players'].extend(new_nicks)
238 self._synchronizeRoom(room_jid, [room_jid], profile)
239 if sync:
240 setStatus('init')
241
242 def _synchronizeRoom(self, room_jid, recipients, profile):
243 """Communicate the list of players to the whole room or only to some users,
244 also send the synchronization data to the players who recently joined the game.
245 @param room_jid (jid.JID): JID of the room
246 @recipients (list[jid.JID]): list of JIDs, the recipients of the message could be:
247 - room JID
248 - room JID + "/" + user nick
249 @param profile (unicode): %(doc_profile)s
250 """
251 if self._gameExists(room_jid, started=True):
252 element = self._createStartElement(self.games[room_jid]['players'])
253 else:
254 element = self._createStartElement(self.games[room_jid]['players'], name="players")
255 elements = [(element, None, None)]
256
257 sync_args = []
258 sync_data = self._getSyncData(room_jid)
259 for nick in sync_data:
260 user_jid = jid.JID(room_jid.userhost() + '/' + nick)
261 if user_jid in recipients:
262 user_elements = copy.deepcopy(elements)
263 for child in sync_data[nick]:
264 user_elements.append((child, None, None))
265 recipients.remove(user_jid)
266 else:
267 user_elements = [(child, None, None) for child in sync_data[nick]]
268 sync_args.append(([user_jid, user_elements], {'profile': profile}))
269
270 for recipient in recipients:
271 self._sendElements(recipient, elements, profile=profile)
272 for args, kwargs in sync_args:
273 self._sendElements(*args, **kwargs)
274
275 def _getSyncData(self, room_jid, force_nicks=None):
276 """The synchronization data are returned for each player who
277 has the state 'desync' or if he's been contained by force_nicks.
278 @param room_jid (jid.JID): JID of the room
279 @param force_nicks: force the synchronization for this list of the nicks
280 @return: a mapping between player nicks and a list of elements to
281 be sent by self._synchronizeRoom for the game to be synchronized.
282 """
283 if not self._gameExists(room_jid):
284 return {}
285 data = {}
286 status = self.games[room_jid]['status']
287 nicks = [nick for nick in status if status[nick] == 'desync']
288 if force_nicks is None:
289 force_nicks = []
290 for nick in force_nicks:
291 if nick not in nicks:
292 nicks.append(nick)
293 for nick in nicks:
294 elements = self.getSyncDataForPlayer(room_jid, nick)
295 if elements:
296 data[nick] = elements
297 return data
298
299 def getSyncDataForPlayer(self, room_jid, nick):
300 """This method may (and should probably) be overwritten by a child class.
301 @param room_jid (jid.JID): JID of the room
302 @param nick: the nick of the player to be synchronized
303 @return: a list of elements to synchronize this player with the game.
304 """
305 return []
306
307 def _invitePlayers(self, room_jid, other_players, nick, profile):
308 """Invite players to a room, associated game may exist or not.
309
310 @param other_players (list[jid.JID]): list of the players to invite
311 @param nick (unicode): nick of the user who send the invitation
312 @return: list[unicode] of room nicks for invited players who are already in the room
313 """
314 raise NotImplementedError("Need to be fixed !")
315 # FIXME: this is broken and unsecure !
316 if not self._checkInviteAuth(room_jid, nick):
317 return []
318 # TODO: remove invitation waiting for too long, using the time data
319 self.invitations[room_jid].append((time(), [player.userhostJID() for player in other_players]))
320 nicks = []
321 for player_jid in [player.userhostJID() for player in other_players]:
322 # TODO: find a way to make it secure
323 other_nick = self.host.plugins["XEP-0045"].getRoomEntityNick(room_jid, player_jid, secure=self.testing)
324 if other_nick is None:
325 self.host.plugins["XEP-0249"].invite(player_jid, room_jid, {"game": self.name}, profile)
326 else:
327 nicks.append(other_nick)
328 return nicks
329
330 def _checkInviteAuth(self, room_jid, nick, verbose=False):
331 """Checks if this user is allowed to invite players
332
333 @param room_jid (jid.JID): JID of the room
334 @param nick: user nick in the room
335 @param verbose: display debug message
336 @return: True if the user is allowed to invite other players
337 """
338 auth = False
339 if self.invite_mode == self.FROM_ALL or not self._gameExists(room_jid):
340 auth = True
341 elif self.invite_mode == self.FROM_NONE:
342 auth = not self._gameExists(room_jid, started=True) and self.isReferee(room_jid, nick)
343 elif self.invite_mode == self.FROM_REFEREE:
344 auth = self.isReferee(room_jid, nick)
345 elif self.invite_mode == self.FROM_PLAYERS:
346 auth = self.isPlayer(room_jid, nick)
347 if not auth and (verbose or _DEBUG):
348 log.debug(_(u"%(user)s not allowed to invite for the game %(game)s in %(room)s") % {'user': nick, 'game': self.name, 'room': room_jid.userhost()})
349 return auth
350
351 def isReferee(self, room_jid, nick):
352 """Checks if the player with this nick is the referee for the game in this room"
353 @param room_jid (jid.JID): room JID
354 @param nick: user nick in the room
355 @return: True if the user is the referee of the game in this room
356 """
357 if not self._gameExists(room_jid):
358 return False
359 return jid.JID(room_jid.userhost() + '/' + nick) == self.games[room_jid]['referee']
360
361 def isPlayer(self, room_jid, nick):
362 """Checks if the user with this nick is a player for the game in this room.
363 @param room_jid (jid.JID): JID of the room
364 @param nick: user nick in the room
365 @return: True if the user is a player of the game in this room
366 """
367 if not self._gameExists(room_jid):
368 return False
369 # Important: the referee is not in the 'players' list right after
370 # the game initialization, that's why we do also check with isReferee
371 return nick in self.games[room_jid]['players'] or self.isReferee(room_jid, nick)
372
373 def _checkWaitAuth(self, room, other_players, verbose=False):
374 """Check if we must wait for other players before starting the game.
375
376 @param room (wokkel.muc.Room): the room
377 @param other_players (list[jid.JID]): list of the players without the referee
378 @param verbose (bool): display debug message
379 @return: (x, y, z) with:
380 x: False if we must wait, True otherwise
381 y: the nicks of the players that have been checked and confirmed
382 z: the JID of the players that have not been checked or that are missing
383 """
384 if self.wait_mode == self.FOR_NONE or other_players == []:
385 result = (True, [], other_players)
386 elif len(room.roster) < len(other_players):
387 # do not check the players until we may actually have them all
388 result = (False, [], other_players)
389 else:
390 # TODO: find a way to make it secure
391 (nicks, missing) = self.host.plugins["XEP-0045"].getRoomNicksOfUsers(room, other_players, secure=False)
392 result = (len(nicks) == len(other_players), nicks, missing)
393 if not result[0] and (verbose or _DEBUG):
394 log.debug(_(u"Still waiting for %(users)s before starting the game %(game)s in %(room)s") % {'users': result[2], 'game': self.name, 'room': room.occupantJID.userhost()})
395 return result
396
397 def getUniqueName(self, muc_service=None, profile_key=C.PROF_KEY_NONE):
398 """Generate unique room name
399
400 @param muc_service (jid.JID): you can leave empty to autofind the muc service
401 @param profile_key (unicode): %(doc_profile_key)s
402 @return: jid.JID (unique name for a new room to be created)
403 """
404 client = self.host.getClient(profile_key)
405 # FIXME: jid.JID must be used instead of strings
406 room = self.host.plugins["XEP-0045"].getUniqueName(client, muc_service)
407 return jid.JID("sat_%s_%s" % (self.name.lower(), room.userhost()))
408
409 def _prepareRoom(self, other_players=None, room_jid_s='', profile_key=C.PROF_KEY_NONE):
410 room_jid = jid.JID(room_jid_s) if room_jid_s else None
411 other_players = [jid.JID(player).userhostJID() for player in other_players]
412 return self.prepareRoom(other_players, room_jid, profile_key)
413
414 def prepareRoom(self, other_players=None, room_jid=None, profile_key=C.PROF_KEY_NONE):
415 """Prepare the room for a game: create it if it doesn't exist and invite players.
416
417 @param other_players (list[JID]): list of other players JID (bare)
418 @param room_jid (jid.JID): JID of the room, or None to generate a unique name
419 @param profile_key (unicode): %(doc_profile_key)s
420 """
421 # FIXME: need to be refactored
422 client = self.host.getClient(profile_key)
423 log.debug(_(u'Preparing room for %s game') % self.name)
424 profile = self.host.memory.getProfileName(profile_key)
425 if not profile:
426 log.error(_("Unknown profile"))
427 return defer.succeed(None)
428 if other_players is None:
429 other_players = []
430
431 # Create/join the given room, or a unique generated one if no room is specified.
432 if room_jid is None:
433 room_jid = self.getUniqueName(profile_key=profile_key)
434 else:
435 self.host.plugins["XEP-0045"].checkRoomJoined(client, room_jid)
436 self._createOrInvite(client, room_jid, other_players)
437 return defer.succeed(None)
438
439 user_jid = self.host.getJidNStream(profile)[0]
440 d = self.host.plugins["XEP-0045"].join(room_jid, user_jid.user, {}, profile)
441 return d.addCallback(lambda dummy: self._createOrInvite(client, room_jid, other_players))
442
443 def userJoinedTrigger(self, room, user, profile):
444 """This trigger is used to check if the new user can take part of a game, create the game if we were waiting for him or just update the players list.
445
446 @room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User}
447 @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID
448 @return: True to not interrupt the main process.
449 """
450 room_jid = room.occupantJID.userhostJID()
451 profile_nick = room.occupantJID.resource
452 if not self.isReferee(room_jid, profile_nick):
453 return True # profile is not the referee
454 if not self._checkJoinAuth(room_jid, user.entity if user.entity else None, user.nick):
455 # user not allowed but let him know that we are playing :p
456 self._synchronizeRoom(room_jid, [jid.JID(room_jid.userhost() + '/' + user.nick)], profile)
457 return True
458 if self.wait_mode == self.FOR_ALL:
459 # considering the last batch of invitations
460 batch = len(self.invitations[room_jid]) - 1
461 if batch < 0:
462 log.error(u"Invitations from %s to play %s in %s have been lost!" % (profile_nick, self.name, room_jid.userhost()))
463 return True
464 other_players = self.invitations[room_jid][batch][1]
465 (auth, nicks, dummy) = self._checkWaitAuth(room, other_players)
466 if auth:
467 del self.invitations[room_jid][batch]
468 nicks.insert(0, profile_nick) # add the referee
469 self.createGame(room_jid, nicks, profile_key=profile)
470 return True
471 # let the room know that a new player joined
472 self._updatePlayers(room_jid, [user.nick], True, profile)
473 return True
474
475 def userLeftTrigger(self, room, user, profile):
476 """This trigger is used to update or stop the game when a user leaves.
477
478 @room: wokkel.muc.Room object. room.roster is a dict{wokkel.muc.User.nick: wokkel.muc.User}
479 @user: wokkel.muc.User object. user.nick is a unicode and user.entity a JID
480 @return: True to not interrupt the main process.
481 """
482 room_jid = room.occupantJID.userhostJID()
483 profile_nick = room.occupantJID.resource
484 if not self.isReferee(room_jid, profile_nick):
485 return True # profile is not the referee
486 if self.isPlayer(room_jid, user.nick):
487 try:
488 self.games[room_jid]['players'].remove(user.nick)
489 except ValueError:
490 pass
491 if len(self.games[room_jid]['players']) == 0:
492 return True
493 if self.wait_mode == self.FOR_ALL:
494 # allow this user to join the game again
495 user_jid = user.entity.userhostJID()
496 if len(self.invitations[room_jid]) == 0:
497 self.invitations[room_jid].append((time(), [user_jid]))
498 else:
499 batch = 0 # add to the first batch of invitations
500 if user_jid not in self.invitations[room_jid][batch][1]:
501 self.invitations[room_jid][batch][1].append(user_jid)
502 return True
503
504 def _checkCreateGameAndInit(self, room_jid, profile):
505 """Check if that profile can create the game. If the game can be created
506 but is not initialized yet, this method will also do the initialization.
507
508 @param room_jid (jid.JID): JID of the room
509 @param profile
510 @return: a couple (create, sync) with:
511 - create: set to True to allow the game creation
512 - sync: set to True to advice a game synchronization
513 """
514 user_nick = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile)
515 if not user_nick:
516 log.error(u'Internal error: profile %s has not joined the room %s' % (profile, room_jid.userhost()))
517 return False, False
518 if self._gameExists(room_jid):
519 is_referee = self.isReferee(room_jid, user_nick)
520 if self._gameExists(room_jid, started=True):
521 log.info(_(u"%(game)s game already created in room %(room)s") % {'game': self.name, 'room': room_jid.userhost()})
522 return False, is_referee
523 elif not is_referee:
524 log.info(_(u"%(game)s game in room %(room)s can only be created by %(user)s") % {'game': self.name, 'room': room_jid.userhost(), 'user': user_nick})
525 return False, False
526 else:
527 self._initGame(room_jid, user_nick)
528 return True, False
529
530 def _createGame(self, room_jid_s, nicks=None, profile_key=C.PROF_KEY_NONE):
531 self.createGame(jid.JID(room_jid_s), nicks, profile_key)
532
533 def createGame(self, room_jid, nicks=None, profile_key=C.PROF_KEY_NONE):
534 """Create a new game.
535
536 This can be called directly from a frontend and skips all the checks and invitation system,
537 but the game must not exist and all the players must be in the room already.
538 @param room_jid (jid.JID): JID of the room
539 @param nicks (list[unicode]): list of players nicks in the room (referee included, in first position)
540 @param profile_key (unicode): %(doc_profile_key)s
541 """
542 log.debug(_(u"Creating %(game)s game in room %(room)s") % {'game': self.name, 'room': room_jid})
543 profile = self.host.memory.getProfileName(profile_key)
544 if not profile:
545 log.error(_(u"profile %s is unknown") % profile_key)
546 return
547 (create, sync) = self._checkCreateGameAndInit(room_jid, profile)
548 if nicks is None:
549 nicks = []
550 if not create:
551 if sync:
552 self._updatePlayers(room_jid, nicks, True, profile)
553 return
554 self.games[room_jid]['started'] = True
555 self._updatePlayers(room_jid, nicks, False, profile)
556 if self.player_init:
557 # specific data to each player (score, private data)
558 self.games[room_jid].setdefault('players_data', {})
559 for nick in nicks:
560 # The dict must be COPIED otherwise it is shared between all users
561 self.games[room_jid]['players_data'][nick] = copy.deepcopy(self.player_init)
562
563 def _playerReady(self, player_nick, referee_jid_s, profile_key=C.PROF_KEY_NONE):
564 self.playerReady(player_nick, jid.JID(referee_jid_s), profile_key)
565
566 def playerReady(self, player_nick, referee_jid, profile_key=C.PROF_KEY_NONE):
567 """Must be called when player is ready to start a new game
568
569 @param player: the player nick in the room
570 @param referee_jid (jid.JID): JID of the referee
571 """
572 profile = self.host.memory.getProfileName(profile_key)
573 if not profile:
574 log.error(_(u"profile %s is unknown") % profile_key)
575 return
576 log.debug(u'new player ready: %s' % profile)
577 # TODO: we probably need to add the game and room names in the sent message
578 self.send(referee_jid, 'player_ready', {'player': player_nick}, profile=profile)
579
580 def newRound(self, room_jid, data, profile):
581 """Launch a new round (reinit the user data)
582
583 @param room_jid: room userhost
584 @param data: a couple (common_data, msg_elts) with:
585 - common_data: backend initialization data for the new round
586 - msg_elts: dict to map each user to his specific initialization message
587 @param profile
588 """
589 log.debug(_(u'new round for %s game') % self.name)
590 game_data = self.games[room_jid]
591 players = game_data['players']
592 players_data = game_data['players_data']
593 game_data['stage'] = "init"
594
595 common_data, msg_elts = copy.deepcopy(data) if data is not None else (None, None)
596
597 if isinstance(msg_elts, dict):
598 for player in players:
599 to_jid = jid.JID(room_jid.userhost() + "/" + player) # FIXME: gof:
600 elem = msg_elts[player] if isinstance(msg_elts[player], domish.Element) else None
601 self.send(to_jid, elem, profile=profile)
602 elif isinstance(msg_elts, domish.Element):
603 self.send(room_jid, msg_elts, profile=profile)
604 if common_data is not None:
605 for player in players:
606 players_data[player].update(copy.deepcopy(common_data))
607
608 def _createGameElt(self, to_jid):
609 """Create a generic domish Element for the game messages
610
611 @param to_jid: JID of the recipient
612 @return: the created element
613 """
614 type_ = "normal" if to_jid.resource else "groupchat"
615 elt = domish.Element((None, 'message'))
616 elt["to"] = to_jid.full()
617 elt["type"] = type_
618 elt.addElement(self.ns_tag)
619 return elt
620
621 def _createStartElement(self, players=None, name="started"):
622 """Create a domish Element listing the game users
623
624 @param players: list of the players
625 @param name: element name:
626 - "started" to signal the players that the game has been started
627 - "players" to signal the list of players when the game is not started yet
628 @return the create element
629 """
630 started_elt = domish.Element((None, name))
631 if players is None:
632 return started_elt
633 idx = 0
634 for player in players:
635 player_elt = domish.Element((None, 'player'))
636 player_elt.addContent(player)
637 player_elt['index'] = str(idx)
638 idx += 1
639 started_elt.addChild(player_elt)
640 return started_elt
641
642 def _sendElements(self, to_jid, data, profile=None):
643 """ TODO
644
645 @param to_jid: recipient JID
646 @param data: list of (elem, attr, content) with:
647 - elem: domish.Element, unicode or a couple:
648 - domish.Element to be directly added as a child to the message
649 - unicode name or couple (uri, name) to create a new domish.Element
650 and add it as a child to the message (see domish.Element.addElement)
651 - attrs: dictionary of attributes for the new child
652 - content: unicode that is appended to the child content
653 @param profile: the profile from which the message is sent
654 @return: a Deferred instance
655 """
656 client = self.host.getClient(profile)
657 msg = self._createGameElt(to_jid)
658 for elem, attrs, content in data:
659 if elem is not None:
660 if isinstance(elem, domish.Element):
661 msg.firstChildElement().addChild(elem)
662 else:
663 elem = msg.firstChildElement().addElement(elem)
664 if attrs is not None:
665 elem.attributes.update(attrs)
666 if content is not None:
667 elem.addContent(content)
668 client.send(msg)
669 return defer.succeed(None)
670
671 def send(self, to_jid, elem=None, attrs=None, content=None, profile=None):
672 """ TODO
673
674 @param to_jid: recipient JID
675 @param elem: domish.Element, unicode or a couple:
676 - domish.Element to be directly added as a child to the message
677 - unicode name or couple (uri, name) to create a new domish.Element
678 and add it as a child to the message (see domish.Element.addElement)
679 @param attrs: dictionary of attributes for the new child
680 @param content: unicode that is appended to the child content
681 @param profile: the profile from which the message is sent
682 @return: a Deferred instance
683 """
684 return self._sendElements(to_jid, [(elem, attrs, content)], profile)
685
686 def getHandler(self, client):
687 return RoomGameHandler(self)
688
689
690 class RoomGameHandler (XMPPHandler):
691 implements(iwokkel.IDisco)
692
693 def __init__(self, plugin_parent):
694 self.plugin_parent = plugin_parent
695 self.host = plugin_parent.host
696
697 def connectionInitialized(self):
698 self.xmlstream.addObserver(self.plugin_parent.request, self.plugin_parent.room_game_cmd, profile=self.parent.profile)
699
700 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
701 return [disco.DiscoFeature(self.plugin_parent.ns_tag[0])]
702
703 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
704 return []