historically, Libervia (SàT before) was using camelCase as allowed by PEP8 when using a pre-PEP8 code, to use the same coding style as in Twisted. However, snake_case is more readable and it's better to follow PEP8 best practices, so it has been decided to move on full snake_case. Because Libervia has a huge codebase, this ended with a ugly mix of camelCase and snake_case. To fix that, this patch does a big refactoring by renaming every function and method (including bridge) that are not coming from Twisted or Wokkel, to use fully snake_case. This is a massive change, and may result in some bugs.
456 lines
17 KiB
Python
456 lines
17 KiB
Python
#!/usr/bin/env python3
|
||
|
||
|
||
# SAT plugin for managing Quiz game
|
||
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
|
||
|
||
# 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 <http://www.gnu.org/licenses/>.
|
||
|
||
from sat.core.i18n import _
|
||
from sat.core.constants import Const as C
|
||
from sat.core.log import getLogger
|
||
|
||
log = getLogger(__name__)
|
||
from twisted.words.xish import domish
|
||
from twisted.internet import reactor
|
||
from twisted.words.protocols.jabber import client as jabber_client, jid
|
||
from time import time
|
||
|
||
|
||
NS_QG = "http://www.goffi.org/protocol/quiz"
|
||
QG_TAG = "quiz"
|
||
|
||
PLUGIN_INFO = {
|
||
C.PI_NAME: "Quiz game plugin",
|
||
C.PI_IMPORT_NAME: "Quiz",
|
||
C.PI_TYPE: "Game",
|
||
C.PI_PROTOCOLS: [],
|
||
C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0249", "ROOM-GAME"],
|
||
C.PI_MAIN: "Quiz",
|
||
C.PI_HANDLER: "yes",
|
||
C.PI_DESCRIPTION: _("""Implementation of Quiz game"""),
|
||
}
|
||
|
||
|
||
class Quiz(object):
|
||
def inherit_from_room_game(self, host):
|
||
global RoomGame
|
||
RoomGame = host.plugins["ROOM-GAME"].__class__
|
||
self.__class__ = type(
|
||
self.__class__.__name__, (self.__class__, RoomGame, object), {}
|
||
)
|
||
|
||
def __init__(self, host):
|
||
log.info(_("Plugin Quiz initialization"))
|
||
self.inherit_from_room_game(host)
|
||
RoomGame._init_(
|
||
self,
|
||
host,
|
||
PLUGIN_INFO,
|
||
(NS_QG, QG_TAG),
|
||
game_init={"stage": None},
|
||
player_init={"score": 0},
|
||
)
|
||
host.bridge.add_method(
|
||
"quiz_game_launch",
|
||
".plugin",
|
||
in_sign="asss",
|
||
out_sign="",
|
||
method=self._prepare_room,
|
||
) # args: players, room_jid, profile
|
||
host.bridge.add_method(
|
||
"quiz_game_create",
|
||
".plugin",
|
||
in_sign="sass",
|
||
out_sign="",
|
||
method=self._create_game,
|
||
) # args: room_jid, players, profile
|
||
host.bridge.add_method(
|
||
"quiz_game_ready",
|
||
".plugin",
|
||
in_sign="sss",
|
||
out_sign="",
|
||
method=self._player_ready,
|
||
) # args: player, referee, profile
|
||
host.bridge.add_method(
|
||
"quiz_game_answer",
|
||
".plugin",
|
||
in_sign="ssss",
|
||
out_sign="",
|
||
method=self.player_answer,
|
||
)
|
||
host.bridge.add_signal(
|
||
"quiz_game_started", ".plugin", signature="ssass"
|
||
) # args: room_jid, referee, players, profile
|
||
host.bridge.add_signal(
|
||
"quiz_game_new",
|
||
".plugin",
|
||
signature="sa{ss}s",
|
||
doc={
|
||
"summary": "Start a new game",
|
||
"param_0": "room_jid: jid of game's room",
|
||
"param_1": "game_data: data of the game",
|
||
"param_2": "%(doc_profile)s",
|
||
},
|
||
)
|
||
host.bridge.add_signal(
|
||
"quiz_game_question",
|
||
".plugin",
|
||
signature="sssis",
|
||
doc={
|
||
"summary": "Send the current question",
|
||
"param_0": "room_jid: jid of game's room",
|
||
"param_1": "question_id: question id",
|
||
"param_2": "question: question to ask",
|
||
"param_3": "timer: timer",
|
||
"param_4": "%(doc_profile)s",
|
||
},
|
||
)
|
||
host.bridge.add_signal(
|
||
"quiz_game_player_buzzed",
|
||
".plugin",
|
||
signature="ssbs",
|
||
doc={
|
||
"summary": "A player just pressed the buzzer",
|
||
"param_0": "room_jid: jid of game's room",
|
||
"param_1": "player: player who pushed the buzzer",
|
||
"param_2": "pause: should the game be paused ?",
|
||
"param_3": "%(doc_profile)s",
|
||
},
|
||
)
|
||
host.bridge.add_signal(
|
||
"quiz_game_player_says",
|
||
".plugin",
|
||
signature="sssis",
|
||
doc={
|
||
"summary": "A player just pressed the buzzer",
|
||
"param_0": "room_jid: jid of game's room",
|
||
"param_1": "player: player who pushed the buzzer",
|
||
"param_2": "text: what the player say",
|
||
"param_3": "delay: how long, in seconds, the text must appear",
|
||
"param_4": "%(doc_profile)s",
|
||
},
|
||
)
|
||
host.bridge.add_signal(
|
||
"quiz_game_answer_result",
|
||
".plugin",
|
||
signature="ssba{si}s",
|
||
doc={
|
||
"summary": "Result of the just given answer",
|
||
"param_0": "room_jid: jid of game's room",
|
||
"param_1": "player: player who gave the answer",
|
||
"param_2": "good_answer: True if the answer is right",
|
||
"param_3": "score: dict of score with player as key",
|
||
"param_4": "%(doc_profile)s",
|
||
},
|
||
)
|
||
host.bridge.add_signal(
|
||
"quiz_game_timer_expired",
|
||
".plugin",
|
||
signature="ss",
|
||
doc={
|
||
"summary": "Nobody answered the question in time",
|
||
"param_0": "room_jid: jid of game's room",
|
||
"param_1": "%(doc_profile)s",
|
||
},
|
||
)
|
||
host.bridge.add_signal(
|
||
"quiz_game_timer_restarted",
|
||
".plugin",
|
||
signature="sis",
|
||
doc={
|
||
"summary": "Nobody answered the question in time",
|
||
"param_0": "room_jid: jid of game's room",
|
||
"param_1": "time_left: time left before timer expiration",
|
||
"param_2": "%(doc_profile)s",
|
||
},
|
||
)
|
||
|
||
def __game_data_to_xml(self, game_data):
|
||
"""Convert a game data dict to domish element"""
|
||
game_data_elt = domish.Element((None, "game_data"))
|
||
for data in game_data:
|
||
data_elt = domish.Element((None, data))
|
||
data_elt.addContent(game_data[data])
|
||
game_data_elt.addChild(data_elt)
|
||
return game_data_elt
|
||
|
||
def __xml_to_game_data(self, game_data_elt):
|
||
"""Convert a domish element with game_data to a dict"""
|
||
game_data = {}
|
||
for data_elt in game_data_elt.elements():
|
||
game_data[data_elt.name] = str(data_elt)
|
||
return game_data
|
||
|
||
def __answer_result_to_signal_args(self, answer_result_elt):
|
||
"""Parse answer result element and return a tuple of signal arguments
|
||
@param answer_result_elt: answer result element
|
||
@return: (player, good_answer, score)"""
|
||
score = {}
|
||
for score_elt in answer_result_elt.elements():
|
||
score[score_elt["player"]] = int(score_elt["score"])
|
||
return (
|
||
answer_result_elt["player"],
|
||
answer_result_elt["good_answer"] == str(True),
|
||
score,
|
||
)
|
||
|
||
def __answer_result(self, player_answering, good_answer, game_data):
|
||
"""Convert a domish an answer_result element
|
||
@param player_answering: player who gave the answer
|
||
@param good_answer: True is the answer is right
|
||
@param game_data: data of the game"""
|
||
players_data = game_data["players_data"]
|
||
score = {}
|
||
for player in game_data["players"]:
|
||
score[player] = players_data[player]["score"]
|
||
|
||
answer_result_elt = domish.Element((None, "answer_result"))
|
||
answer_result_elt["player"] = player_answering
|
||
answer_result_elt["good_answer"] = str(good_answer)
|
||
|
||
for player in score:
|
||
score_elt = domish.Element((None, "score"))
|
||
score_elt["player"] = player
|
||
score_elt["score"] = str(score[player])
|
||
answer_result_elt.addChild(score_elt)
|
||
|
||
return answer_result_elt
|
||
|
||
def __ask_question(self, question_id, question, timer):
|
||
"""Create a element for asking a question"""
|
||
question_elt = domish.Element((None, "question"))
|
||
question_elt["id"] = question_id
|
||
question_elt["timer"] = str(timer)
|
||
question_elt.addContent(question)
|
||
return question_elt
|
||
|
||
def __start_play(self, room_jid, game_data, profile):
|
||
"""Start the game (tell to the first player after dealer to play"""
|
||
client = self.host.get_client(profile)
|
||
game_data["stage"] = "play"
|
||
next_player_idx = game_data["current_player"] = (
|
||
game_data["init_player"] + 1
|
||
) % len(
|
||
game_data["players"]
|
||
) # the player after the dealer start
|
||
game_data["first_player"] = next_player = game_data["players"][next_player_idx]
|
||
to_jid = jid.JID(room_jid.userhost() + "/" + next_player)
|
||
mess = self.createGameElt(to_jid)
|
||
mess.firstChildElement().addElement("your_turn")
|
||
client.send(mess)
|
||
|
||
def player_answer(self, player, referee, answer, profile_key=C.PROF_KEY_NONE):
|
||
"""Called when a player give an answer"""
|
||
client = self.host.get_client(profile_key)
|
||
log.debug(
|
||
"new player answer (%(profile)s): %(answer)s"
|
||
% {"profile": client.profile, "answer": answer}
|
||
)
|
||
mess = self.createGameElt(jid.JID(referee))
|
||
answer_elt = mess.firstChildElement().addElement("player_answer")
|
||
answer_elt["player"] = player
|
||
answer_elt.addContent(answer)
|
||
client.send(mess)
|
||
|
||
def timer_expired(self, room_jid, profile):
|
||
"""Called when nobody answered the question in time"""
|
||
client = self.host.get_client(profile)
|
||
game_data = self.games[room_jid]
|
||
game_data["stage"] = "expired"
|
||
mess = self.createGameElt(room_jid)
|
||
mess.firstChildElement().addElement("timer_expired")
|
||
client.send(mess)
|
||
reactor.callLater(4, self.ask_question, room_jid, client.profile)
|
||
|
||
def pause_timer(self, room_jid):
|
||
"""Stop the timer and save the time left"""
|
||
game_data = self.games[room_jid]
|
||
left = max(0, game_data["timer"].getTime() - time())
|
||
game_data["timer"].cancel()
|
||
game_data["time_left"] = int(left)
|
||
game_data["previous_stage"] = game_data["stage"]
|
||
game_data["stage"] = "paused"
|
||
|
||
def restart_timer(self, room_jid, profile):
|
||
"""Restart a timer with the saved time"""
|
||
client = self.host.get_client(profile)
|
||
game_data = self.games[room_jid]
|
||
assert game_data["time_left"] is not None
|
||
mess = self.createGameElt(room_jid)
|
||
mess.firstChildElement().addElement("timer_restarted")
|
||
jabber_client.restarted_elt["time_left"] = str(game_data["time_left"])
|
||
client.send(mess)
|
||
game_data["timer"] = reactor.callLater(
|
||
game_data["time_left"], self.timer_expired, room_jid, profile
|
||
)
|
||
game_data["time_left"] = None
|
||
game_data["stage"] = game_data["previous_stage"]
|
||
del game_data["previous_stage"]
|
||
|
||
def ask_question(self, room_jid, profile):
|
||
"""Ask a new question"""
|
||
client = self.host.get_client(profile)
|
||
game_data = self.games[room_jid]
|
||
game_data["stage"] = "question"
|
||
game_data["question_id"] = "1"
|
||
timer = 30
|
||
mess = self.createGameElt(room_jid)
|
||
mess.firstChildElement().addChild(
|
||
self.__ask_question(
|
||
game_data["question_id"], "Quel est l'âge du capitaine ?", timer
|
||
)
|
||
)
|
||
client.send(mess)
|
||
game_data["timer"] = reactor.callLater(
|
||
timer, self.timer_expired, room_jid, profile
|
||
)
|
||
game_data["time_left"] = None
|
||
|
||
def check_answer(self, room_jid, player, answer, profile):
|
||
"""Check if the answer given is right"""
|
||
client = self.host.get_client(profile)
|
||
game_data = self.games[room_jid]
|
||
players_data = game_data["players_data"]
|
||
good_answer = game_data["question_id"] == "1" and answer == "42"
|
||
players_data[player]["score"] += 1 if good_answer else -1
|
||
players_data[player]["score"] = min(9, max(0, players_data[player]["score"]))
|
||
|
||
mess = self.createGameElt(room_jid)
|
||
mess.firstChildElement().addChild(
|
||
self.__answer_result(player, good_answer, game_data)
|
||
)
|
||
client.send(mess)
|
||
|
||
if good_answer:
|
||
reactor.callLater(4, self.ask_question, room_jid, profile)
|
||
else:
|
||
reactor.callLater(4, self.restart_timer, room_jid, profile)
|
||
|
||
def new_game(self, room_jid, profile):
|
||
"""Launch a new round"""
|
||
common_data = {"game_score": 0}
|
||
new_game_data = {
|
||
"instructions": _(
|
||
"""Bienvenue dans cette partie rapide de quizz, le premier à atteindre le score de 9 remporte le jeu
|
||
|
||
Attention, tu es prêt ?"""
|
||
)
|
||
}
|
||
msg_elts = self.__game_data_to_xml(new_game_data)
|
||
RoomGame.new_round(self, room_jid, (common_data, msg_elts), profile)
|
||
reactor.callLater(10, self.ask_question, room_jid, profile)
|
||
|
||
def room_game_cmd(self, mess_elt, profile):
|
||
client = self.host.get_client(profile)
|
||
from_jid = jid.JID(mess_elt["from"])
|
||
room_jid = jid.JID(from_jid.userhost())
|
||
game_elt = mess_elt.firstChildElement()
|
||
game_data = self.games[room_jid]
|
||
# if 'players_data' in game_data:
|
||
# players_data = game_data['players_data']
|
||
|
||
for elt in game_elt.elements():
|
||
|
||
if elt.name == "started": # new game created
|
||
players = []
|
||
for player in elt.elements():
|
||
players.append(str(player))
|
||
self.host.bridge.quiz_game_started(
|
||
room_jid.userhost(), from_jid.full(), players, profile
|
||
)
|
||
|
||
elif elt.name == "player_ready": # ready to play
|
||
player = elt["player"]
|
||
status = self.games[room_jid]["status"]
|
||
nb_players = len(self.games[room_jid]["players"])
|
||
status[player] = "ready"
|
||
log.debug(
|
||
_("Player %(player)s is ready to start [status: %(status)s]")
|
||
% {"player": player, "status": status}
|
||
)
|
||
if (
|
||
list(status.values()).count("ready") == nb_players
|
||
): # everybody is ready, we can start the game
|
||
self.new_game(room_jid, profile)
|
||
|
||
elif elt.name == "game_data":
|
||
self.host.bridge.quiz_game_new(
|
||
room_jid.userhost(), self.__xml_to_game_data(elt), profile
|
||
)
|
||
|
||
elif elt.name == "question": # A question is asked
|
||
self.host.bridge.quiz_game_question(
|
||
room_jid.userhost(),
|
||
elt["id"],
|
||
str(elt),
|
||
int(elt["timer"]),
|
||
profile,
|
||
)
|
||
|
||
elif elt.name == "player_answer":
|
||
player = elt["player"]
|
||
pause = (
|
||
game_data["stage"] == "question"
|
||
) # we pause the game only if we are have a question at the moment
|
||
# we first send a buzzer message
|
||
mess = self.createGameElt(room_jid)
|
||
buzzer_elt = mess.firstChildElement().addElement("player_buzzed")
|
||
buzzer_elt["player"] = player
|
||
buzzer_elt["pause"] = str(pause)
|
||
client.send(mess)
|
||
if pause:
|
||
self.pause_timer(room_jid)
|
||
# and we send the player answer
|
||
mess = self.createGameElt(room_jid)
|
||
_answer = str(elt)
|
||
say_elt = mess.firstChildElement().addElement("player_says")
|
||
say_elt["player"] = player
|
||
say_elt.addContent(_answer)
|
||
say_elt["delay"] = "3"
|
||
reactor.callLater(2, client.send, mess)
|
||
reactor.callLater(
|
||
6, self.check_answer, room_jid, player, _answer, profile=profile
|
||
)
|
||
|
||
elif elt.name == "player_buzzed":
|
||
self.host.bridge.quiz_game_player_buzzed(
|
||
room_jid.userhost(), elt["player"], elt["pause"] == str(True), profile
|
||
)
|
||
|
||
elif elt.name == "player_says":
|
||
self.host.bridge.quiz_game_player_says(
|
||
room_jid.userhost(),
|
||
elt["player"],
|
||
str(elt),
|
||
int(elt["delay"]),
|
||
profile,
|
||
)
|
||
|
||
elif elt.name == "answer_result":
|
||
player, good_answer, score = self.__answer_result_to_signal_args(elt)
|
||
self.host.bridge.quiz_game_answer_result(
|
||
room_jid.userhost(), player, good_answer, score, profile
|
||
)
|
||
|
||
elif elt.name == "timer_expired":
|
||
self.host.bridge.quiz_game_timer_expired(room_jid.userhost(), profile)
|
||
|
||
elif elt.name == "timer_restarted":
|
||
self.host.bridge.quiz_game_timer_restarted(
|
||
room_jid.userhost(), int(elt["time_left"]), profile
|
||
)
|
||
|
||
else:
|
||
log.error(_("Unmanaged game element: %s") % elt.name)
|