Mercurial > libervia-backend
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) |