comparison libervia/backend/plugins/plugin_misc_quiz.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_misc_quiz.py@524856bd7b19
children
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3
4 # SAT plugin for managing Quiz game
5 # Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
21 from libervia.backend.core.constants import Const as C
22 from libervia.backend.core.log import getLogger
23
24 log = getLogger(__name__)
25 from twisted.words.xish import domish
26 from twisted.internet import reactor
27 from twisted.words.protocols.jabber import client as jabber_client, jid
28 from time import time
29
30
31 NS_QG = "http://www.goffi.org/protocol/quiz"
32 QG_TAG = "quiz"
33
34 PLUGIN_INFO = {
35 C.PI_NAME: "Quiz game plugin",
36 C.PI_IMPORT_NAME: "Quiz",
37 C.PI_TYPE: "Game",
38 C.PI_PROTOCOLS: [],
39 C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0249", "ROOM-GAME"],
40 C.PI_MAIN: "Quiz",
41 C.PI_HANDLER: "yes",
42 C.PI_DESCRIPTION: _("""Implementation of Quiz game"""),
43 }
44
45
46 class Quiz(object):
47 def inherit_from_room_game(self, host):
48 global RoomGame
49 RoomGame = host.plugins["ROOM-GAME"].__class__
50 self.__class__ = type(
51 self.__class__.__name__, (self.__class__, RoomGame, object), {}
52 )
53
54 def __init__(self, host):
55 log.info(_("Plugin Quiz initialization"))
56 self.inherit_from_room_game(host)
57 RoomGame._init_(
58 self,
59 host,
60 PLUGIN_INFO,
61 (NS_QG, QG_TAG),
62 game_init={"stage": None},
63 player_init={"score": 0},
64 )
65 host.bridge.add_method(
66 "quiz_game_launch",
67 ".plugin",
68 in_sign="asss",
69 out_sign="",
70 method=self._prepare_room,
71 ) # args: players, room_jid, profile
72 host.bridge.add_method(
73 "quiz_game_create",
74 ".plugin",
75 in_sign="sass",
76 out_sign="",
77 method=self._create_game,
78 ) # args: room_jid, players, profile
79 host.bridge.add_method(
80 "quiz_game_ready",
81 ".plugin",
82 in_sign="sss",
83 out_sign="",
84 method=self._player_ready,
85 ) # args: player, referee, profile
86 host.bridge.add_method(
87 "quiz_game_answer",
88 ".plugin",
89 in_sign="ssss",
90 out_sign="",
91 method=self.player_answer,
92 )
93 host.bridge.add_signal(
94 "quiz_game_started", ".plugin", signature="ssass"
95 ) # args: room_jid, referee, players, profile
96 host.bridge.add_signal(
97 "quiz_game_new",
98 ".plugin",
99 signature="sa{ss}s",
100 doc={
101 "summary": "Start a new game",
102 "param_0": "room_jid: jid of game's room",
103 "param_1": "game_data: data of the game",
104 "param_2": "%(doc_profile)s",
105 },
106 )
107 host.bridge.add_signal(
108 "quiz_game_question",
109 ".plugin",
110 signature="sssis",
111 doc={
112 "summary": "Send the current question",
113 "param_0": "room_jid: jid of game's room",
114 "param_1": "question_id: question id",
115 "param_2": "question: question to ask",
116 "param_3": "timer: timer",
117 "param_4": "%(doc_profile)s",
118 },
119 )
120 host.bridge.add_signal(
121 "quiz_game_player_buzzed",
122 ".plugin",
123 signature="ssbs",
124 doc={
125 "summary": "A player just pressed the buzzer",
126 "param_0": "room_jid: jid of game's room",
127 "param_1": "player: player who pushed the buzzer",
128 "param_2": "pause: should the game be paused ?",
129 "param_3": "%(doc_profile)s",
130 },
131 )
132 host.bridge.add_signal(
133 "quiz_game_player_says",
134 ".plugin",
135 signature="sssis",
136 doc={
137 "summary": "A player just pressed the buzzer",
138 "param_0": "room_jid: jid of game's room",
139 "param_1": "player: player who pushed the buzzer",
140 "param_2": "text: what the player say",
141 "param_3": "delay: how long, in seconds, the text must appear",
142 "param_4": "%(doc_profile)s",
143 },
144 )
145 host.bridge.add_signal(
146 "quiz_game_answer_result",
147 ".plugin",
148 signature="ssba{si}s",
149 doc={
150 "summary": "Result of the just given answer",
151 "param_0": "room_jid: jid of game's room",
152 "param_1": "player: player who gave the answer",
153 "param_2": "good_answer: True if the answer is right",
154 "param_3": "score: dict of score with player as key",
155 "param_4": "%(doc_profile)s",
156 },
157 )
158 host.bridge.add_signal(
159 "quiz_game_timer_expired",
160 ".plugin",
161 signature="ss",
162 doc={
163 "summary": "Nobody answered the question in time",
164 "param_0": "room_jid: jid of game's room",
165 "param_1": "%(doc_profile)s",
166 },
167 )
168 host.bridge.add_signal(
169 "quiz_game_timer_restarted",
170 ".plugin",
171 signature="sis",
172 doc={
173 "summary": "Nobody answered the question in time",
174 "param_0": "room_jid: jid of game's room",
175 "param_1": "time_left: time left before timer expiration",
176 "param_2": "%(doc_profile)s",
177 },
178 )
179
180 def __game_data_to_xml(self, game_data):
181 """Convert a game data dict to domish element"""
182 game_data_elt = domish.Element((None, "game_data"))
183 for data in game_data:
184 data_elt = domish.Element((None, data))
185 data_elt.addContent(game_data[data])
186 game_data_elt.addChild(data_elt)
187 return game_data_elt
188
189 def __xml_to_game_data(self, game_data_elt):
190 """Convert a domish element with game_data to a dict"""
191 game_data = {}
192 for data_elt in game_data_elt.elements():
193 game_data[data_elt.name] = str(data_elt)
194 return game_data
195
196 def __answer_result_to_signal_args(self, answer_result_elt):
197 """Parse answer result element and return a tuple of signal arguments
198 @param answer_result_elt: answer result element
199 @return: (player, good_answer, score)"""
200 score = {}
201 for score_elt in answer_result_elt.elements():
202 score[score_elt["player"]] = int(score_elt["score"])
203 return (
204 answer_result_elt["player"],
205 answer_result_elt["good_answer"] == str(True),
206 score,
207 )
208
209 def __answer_result(self, player_answering, good_answer, game_data):
210 """Convert a domish an answer_result element
211 @param player_answering: player who gave the answer
212 @param good_answer: True is the answer is right
213 @param game_data: data of the game"""
214 players_data = game_data["players_data"]
215 score = {}
216 for player in game_data["players"]:
217 score[player] = players_data[player]["score"]
218
219 answer_result_elt = domish.Element((None, "answer_result"))
220 answer_result_elt["player"] = player_answering
221 answer_result_elt["good_answer"] = str(good_answer)
222
223 for player in score:
224 score_elt = domish.Element((None, "score"))
225 score_elt["player"] = player
226 score_elt["score"] = str(score[player])
227 answer_result_elt.addChild(score_elt)
228
229 return answer_result_elt
230
231 def __ask_question(self, question_id, question, timer):
232 """Create a element for asking a question"""
233 question_elt = domish.Element((None, "question"))
234 question_elt["id"] = question_id
235 question_elt["timer"] = str(timer)
236 question_elt.addContent(question)
237 return question_elt
238
239 def __start_play(self, room_jid, game_data, profile):
240 """Start the game (tell to the first player after dealer to play"""
241 client = self.host.get_client(profile)
242 game_data["stage"] = "play"
243 next_player_idx = game_data["current_player"] = (
244 game_data["init_player"] + 1
245 ) % len(
246 game_data["players"]
247 ) # the player after the dealer start
248 game_data["first_player"] = next_player = game_data["players"][next_player_idx]
249 to_jid = jid.JID(room_jid.userhost() + "/" + next_player)
250 mess = self.createGameElt(to_jid)
251 mess.firstChildElement().addElement("your_turn")
252 client.send(mess)
253
254 def player_answer(self, player, referee, answer, profile_key=C.PROF_KEY_NONE):
255 """Called when a player give an answer"""
256 client = self.host.get_client(profile_key)
257 log.debug(
258 "new player answer (%(profile)s): %(answer)s"
259 % {"profile": client.profile, "answer": answer}
260 )
261 mess = self.createGameElt(jid.JID(referee))
262 answer_elt = mess.firstChildElement().addElement("player_answer")
263 answer_elt["player"] = player
264 answer_elt.addContent(answer)
265 client.send(mess)
266
267 def timer_expired(self, room_jid, profile):
268 """Called when nobody answered the question in time"""
269 client = self.host.get_client(profile)
270 game_data = self.games[room_jid]
271 game_data["stage"] = "expired"
272 mess = self.createGameElt(room_jid)
273 mess.firstChildElement().addElement("timer_expired")
274 client.send(mess)
275 reactor.callLater(4, self.ask_question, room_jid, client.profile)
276
277 def pause_timer(self, room_jid):
278 """Stop the timer and save the time left"""
279 game_data = self.games[room_jid]
280 left = max(0, game_data["timer"].getTime() - time())
281 game_data["timer"].cancel()
282 game_data["time_left"] = int(left)
283 game_data["previous_stage"] = game_data["stage"]
284 game_data["stage"] = "paused"
285
286 def restart_timer(self, room_jid, profile):
287 """Restart a timer with the saved time"""
288 client = self.host.get_client(profile)
289 game_data = self.games[room_jid]
290 assert game_data["time_left"] is not None
291 mess = self.createGameElt(room_jid)
292 mess.firstChildElement().addElement("timer_restarted")
293 jabber_client.restarted_elt["time_left"] = str(game_data["time_left"])
294 client.send(mess)
295 game_data["timer"] = reactor.callLater(
296 game_data["time_left"], self.timer_expired, room_jid, profile
297 )
298 game_data["time_left"] = None
299 game_data["stage"] = game_data["previous_stage"]
300 del game_data["previous_stage"]
301
302 def ask_question(self, room_jid, profile):
303 """Ask a new question"""
304 client = self.host.get_client(profile)
305 game_data = self.games[room_jid]
306 game_data["stage"] = "question"
307 game_data["question_id"] = "1"
308 timer = 30
309 mess = self.createGameElt(room_jid)
310 mess.firstChildElement().addChild(
311 self.__ask_question(
312 game_data["question_id"], "Quel est l'âge du capitaine ?", timer
313 )
314 )
315 client.send(mess)
316 game_data["timer"] = reactor.callLater(
317 timer, self.timer_expired, room_jid, profile
318 )
319 game_data["time_left"] = None
320
321 def check_answer(self, room_jid, player, answer, profile):
322 """Check if the answer given is right"""
323 client = self.host.get_client(profile)
324 game_data = self.games[room_jid]
325 players_data = game_data["players_data"]
326 good_answer = game_data["question_id"] == "1" and answer == "42"
327 players_data[player]["score"] += 1 if good_answer else -1
328 players_data[player]["score"] = min(9, max(0, players_data[player]["score"]))
329
330 mess = self.createGameElt(room_jid)
331 mess.firstChildElement().addChild(
332 self.__answer_result(player, good_answer, game_data)
333 )
334 client.send(mess)
335
336 if good_answer:
337 reactor.callLater(4, self.ask_question, room_jid, profile)
338 else:
339 reactor.callLater(4, self.restart_timer, room_jid, profile)
340
341 def new_game(self, room_jid, profile):
342 """Launch a new round"""
343 common_data = {"game_score": 0}
344 new_game_data = {
345 "instructions": _(
346 """Bienvenue dans cette partie rapide de quizz, le premier à atteindre le score de 9 remporte le jeu
347
348 Attention, tu es prêt ?"""
349 )
350 }
351 msg_elts = self.__game_data_to_xml(new_game_data)
352 RoomGame.new_round(self, room_jid, (common_data, msg_elts), profile)
353 reactor.callLater(10, self.ask_question, room_jid, profile)
354
355 def room_game_cmd(self, mess_elt, profile):
356 client = self.host.get_client(profile)
357 from_jid = jid.JID(mess_elt["from"])
358 room_jid = jid.JID(from_jid.userhost())
359 game_elt = mess_elt.firstChildElement()
360 game_data = self.games[room_jid]
361 #  if 'players_data' in game_data:
362 #   players_data = game_data['players_data']
363
364 for elt in game_elt.elements():
365
366 if elt.name == "started": # new game created
367 players = []
368 for player in elt.elements():
369 players.append(str(player))
370 self.host.bridge.quiz_game_started(
371 room_jid.userhost(), from_jid.full(), players, profile
372 )
373
374 elif elt.name == "player_ready": # ready to play
375 player = elt["player"]
376 status = self.games[room_jid]["status"]
377 nb_players = len(self.games[room_jid]["players"])
378 status[player] = "ready"
379 log.debug(
380 _("Player %(player)s is ready to start [status: %(status)s]")
381 % {"player": player, "status": status}
382 )
383 if (
384 list(status.values()).count("ready") == nb_players
385 ): # everybody is ready, we can start the game
386 self.new_game(room_jid, profile)
387
388 elif elt.name == "game_data":
389 self.host.bridge.quiz_game_new(
390 room_jid.userhost(), self.__xml_to_game_data(elt), profile
391 )
392
393 elif elt.name == "question": # A question is asked
394 self.host.bridge.quiz_game_question(
395 room_jid.userhost(),
396 elt["id"],
397 str(elt),
398 int(elt["timer"]),
399 profile,
400 )
401
402 elif elt.name == "player_answer":
403 player = elt["player"]
404 pause = (
405 game_data["stage"] == "question"
406 ) # we pause the game only if we are have a question at the moment
407 # we first send a buzzer message
408 mess = self.createGameElt(room_jid)
409 buzzer_elt = mess.firstChildElement().addElement("player_buzzed")
410 buzzer_elt["player"] = player
411 buzzer_elt["pause"] = str(pause)
412 client.send(mess)
413 if pause:
414 self.pause_timer(room_jid)
415 # and we send the player answer
416 mess = self.createGameElt(room_jid)
417 _answer = str(elt)
418 say_elt = mess.firstChildElement().addElement("player_says")
419 say_elt["player"] = player
420 say_elt.addContent(_answer)
421 say_elt["delay"] = "3"
422 reactor.callLater(2, client.send, mess)
423 reactor.callLater(
424 6, self.check_answer, room_jid, player, _answer, profile=profile
425 )
426
427 elif elt.name == "player_buzzed":
428 self.host.bridge.quiz_game_player_buzzed(
429 room_jid.userhost(), elt["player"], elt["pause"] == str(True), profile
430 )
431
432 elif elt.name == "player_says":
433 self.host.bridge.quiz_game_player_says(
434 room_jid.userhost(),
435 elt["player"],
436 str(elt),
437 int(elt["delay"]),
438 profile,
439 )
440
441 elif elt.name == "answer_result":
442 player, good_answer, score = self.__answer_result_to_signal_args(elt)
443 self.host.bridge.quiz_game_answer_result(
444 room_jid.userhost(), player, good_answer, score, profile
445 )
446
447 elif elt.name == "timer_expired":
448 self.host.bridge.quiz_game_timer_expired(room_jid.userhost(), profile)
449
450 elif elt.name == "timer_restarted":
451 self.host.bridge.quiz_game_timer_restarted(
452 room_jid.userhost(), int(elt["time_left"]), profile
453 )
454
455 else:
456 log.error(_("Unmanaged game element: %s") % elt.name)