"""lg-rez / features / Gestion des votes et actions
Ouverture / fermeture / rappels des votes et actions (+ refill)
"""
import datetime
from discord.ext import commands
from lgrez import config
from lgrez.blocs import tools
from lgrez.features import gestion_actions
from lgrez.bdd import (Joueur, Action, BaseAction, Tache, CandidHaro,
Utilisation, CandidHaroType, ActionTrigger,
UtilEtat, Vote)
[docs]async def recup_joueurs(quoi, qui, heure=None):
"""Renvoie les joueurs concernés par la tâche !quoi <qui> [heure].
Args:
quoi (str): évènement, ``"open" / "close" / "remind"``.
qui (:class:`.bdd.Vote` | :class:`str`):
=========== ===========
``Vote`` pour le vote correspondant
``action`` pour les actions commençant à ``heure``
``{id}`` pour une action précise (:attr:`bdd.Action.id`)
=========== ===========
heure (str): si ``qui == "action"``, heure associée
(au format ``HHhMM``).
Returns:
:class:`list`\[:class:`.bdd.Joueur`\]
Examples:
``!open cond`` -> joueurs avec droit de vote
``!close action 17h`` -> joueurs dont l'action se termine à 17h
"""
if quoi not in ["open", "close", "remind"]:
raise ValueError(f"recup_joueurs: bad value for `quoi`: `{quoi}`")
if isinstance(qui, Vote):
# Critère principale : présence/absence d'une action actuellement
# ouverte (et non traitée pour remind)
criteres = {
"open": ~Joueur.actions.any(Action.is_open, vote=qui),
"close": Joueur.actions.any(Action.is_open, vote=qui),
"remind": Joueur.actions.any(Action.is_waiting, vote=qui),
}
critere = criteres[quoi]
if quoi == "open":
# Open : le joueur doit en plus avoir votant_village/loups True
if qui == Vote.loups:
critere &= Joueur.votant_loups.is_(True)
else:
critere &= Joueur.votant_village.is_(True)
return Joueur.query.filter(critere).all()
# Liste des joueurs répondant aux critères
elif qui == "action":
if heure and isinstance(heure, str):
# Si l'heure est précisée, on convertit "HHhMM" -> datetime.time
tps = tools.heure_to_time(heure)
else:
raise commands.BadArgument(
"[heure] doit être spécifiée lorque <qui> == \"action\""
)
actions = gestion_actions.get_actions(
quoi, ActionTrigger.temporel, tps
)
dic = {}
for action in actions:
joueur = action.joueur
if joueur not in dic:
dic[joueur] = []
dic[joueur].append(action)
return dic
# Formerly :
# {joueur.player_id:[action for action in actions if
# action.player_id == joueur.player_id] for joueur in
# [Joueur.query.get(action.player_id) for action in actions]}
elif qui.isdigit():
action = Action.query.get(int(qui))
if not action:
raise commands.BadArgument(f"Pas d'action d'ID = {qui}")
if not action.active:
raise commands.BadArgument(f"Action d'ID = {qui} inactive")
# Appel direct action par son numéro (perma : rappel seulement)
if ((quoi == "open" and (
not action.is_open
or action.base.trigger_debut == ActionTrigger.perma
))
or (quoi == "close" and action.is_open)
or (quoi == "remind" and action.is_waiting)):
# Action lançable
return {action.joueur: [action]}
else:
return {}
else:
raise commands.BadArgument(f"""Argument <qui> == \"{qui}" invalide""")
async def _do_refill(motif, action):
# Détermination nouveau nombre de charges
if motif in config.refills_full:
# Refill -> nombre de charges initial de l'action
new_charges = action.base.base_charges
else:
# Refill -> + 1 charge
new_charges = action.charges + 1
# Refill proprement dit
if new_charges <= action.charges:
# Pas de rechargement à faire (déjà base_charges)
return
if (not action.charges
and action.base.trigger_debut == ActionTrigger.perma):
# Action permanente qui était épuisée : on ré-ouvre !
if tools.en_pause():
ts = tools.fin_pause()
else:
ts = datetime.datetime.now() + datetime.timedelta(seconds=10)
# + 10 secondes pour ouvrir après le message de refill
Tache(timestamp=ts,
commande=f"!open {action.id}",
action=action).add()
action.charges = new_charges
config.session.commit()
await action.joueur.private_chan.send(
f"Ton action {action.base.slug} vient d'être rechargée, "
f"tu as maintenant {new_charges} charge(s) disponible(s) !"
)
[docs]class OpenClose(commands.Cog):
"""Commandes de gestion des votes et actions"""
@commands.command()
@tools.mjs_only
async def open(self, ctx, qui, heure=None, heure_chain=None):
"""Lance un vote / des actions de rôle (COMMANDE BOT / MJ)
Args:
qui:
=========== ===========
``cond`` pour le vote du condamné
``maire`` pour le vote du maire
``loups`` pour le vote des loups
``action`` pour les actions commençant à ``heure``
``{id}`` pour une action spécifique
=========== ===========
heure:
- si ``qui == "cond"``, ``"maire"`` ou ``"loup"``,
programme en plus la fermeture à ``heure``
(et un rappel 30 minutes avant) ;
- si ``qui == "action"``, il est obligatoire : heure des
actions à lancer (cf plus haut). Pour les actions, la
fermeture est de toute façon programmée le cas échéant
(``trigger_fin`` ``temporel`` ou ``delta``).
Dans tous les cas, format ``HHh`` ou ``HHhMM``.
heure_chain:
permet de chaîner des votes : lance le vote immédiatement
et programme sa fermeture à ``heure``, en appellant ``!close``
de sorte à programmer une nouvelle ouverture le lendemain à
``heure_chain``, et ainsi de suite.
Format ``HHh`` ou ``HHhMM``.
Une sécurité empêche de lancer un vote ou une action déjà en cours.
Cette commande a pour vocation première d'être exécutée
automatiquement par des tâches planifiées.
Elle peut être utilisée à la main, mais attention à ne pas
faire n'importe quoi (penser à envoyer / planifier la fermeture
des votes, par exemple).
Examples:
- ``!open maire`` : lance un vote maire maintenant
- ``!open cond 19h`` : lance un vote condamné maintenant
et programme sa fermeture à 19h00 (ex. Juge Bègue)
- ``!open cond 18h 10h`` : lance un vote condamné maintenant,
programme sa fermeture à 18h00, et une prochaine ouverture
à 10h qui se fermera à 18h, et ainsi de suite
- ``!open action 19h`` : lance toutes les actions
commençant à 19h00
- ``!open 122`` : lance l'action d'ID 122
"""
try:
qui = Vote[qui.lower()] # cond / maire / loups
except KeyError:
pass
joueurs = await recup_joueurs("open", qui, heure)
# Liste de joueurs (votes) ou dictionnaire joueur : action
str_joueurs = "\n - ".join([joueur.nom for joueur in joueurs])
await tools.send_code_blocs(
ctx,
f"Utilisateur(s) répondant aux critères ({len(joueurs)}) : \n"
+ str_joueurs
)
# Création utilisations & envoi messages
for joueur in joueurs:
chan = joueur.private_chan
if isinstance(qui, Vote):
action = joueur.action_vote(qui)
if action.is_open: # Sécurité : action ouverte depuis
continue
util = Utilisation(action=action)
util.add()
util.open()
if qui == Vote.cond:
message = await chan.send(
f"{tools.montre()} Le vote pour le condamné du "
f"jour est ouvert ! {config.Emoji.bucher} \n"
+ (f"Tu as jusqu'à {heure} pour voter. \n"
if heure else "")
+ tools.ital(f"Tape {tools.code('!vote (nom du joueur)')}"
" ou utilise la réaction pour voter.")
)
await message.add_reaction(config.Emoji.bucher)
elif qui == Vote.maire:
message = await chan.send(
f"{tools.montre()} Le vote pour l'élection du "
f"maire est ouvert ! {config.Emoji.maire} \n"
+ (f"Tu as jusqu'à {heure} pour voter. \n"
if heure else "")
+ tools.ital(
f"Tape {tools.code('!votemaire (nom du joueur)')} "
"ou utilise la réaction pour voter."
)
)
await message.add_reaction(config.Emoji.maire)
elif qui == Vote.loups:
message = await chan.send(
f"{tools.montre()} Le vote pour la victime de "
f"cette nuit est ouvert ! {config.Emoji.lune} \n"
+ (f"Tu as jusqu'à {heure} pour voter. \n"
if heure else "")
+ tools.ital(
f"Tape {tools.code('!voteloups (nom du joueur)')} "
"ou utilise la réaction pour voter."
)
)
await message.add_reaction(config.Emoji.lune)
else: # Action
for action in joueurs[joueur]:
await gestion_actions.open_action(action)
config.session.commit()
# Actions déclenchées par ouverture
if isinstance(qui, Vote):
for action in Action.query.filter(Action.base.has(
BaseAction.trigger_debut == ActionTrigger.open(qui))):
await gestion_actions.open_action(action)
for action in Action.query.filter(Action.base.has(
BaseAction.trigger_fin == ActionTrigger.open(qui))):
await gestion_actions.close_action(action)
# Réinitialise haros/candids
items = []
if qui == Vote.cond:
items = CandidHaro.query.filter_by(
type=CandidHaroType.haro).all()
elif qui == Vote.maire:
items = CandidHaro.query.filter_by(
type=CandidHaroType.candidature).all()
if items:
CandidHaro.delete(*items)
await tools.log(f"!open {qui.name} : haros/candids wiped")
await config.Channel.haros.send(
f"{config.Emoji.void}\n" * 30
+ "Nouveau vote, nouveaux haros !\n"
+ tools.ital(
"Les posts ci-dessus sont invalides pour le vote actuel. "
f"Utilisez {tools.code('!haro')} pour en relancer."
)
)
# Programme fermeture
if isinstance(qui, Vote) and heure:
ts = tools.next_occurence(tools.heure_to_time(heure))
Tache(timestamp=ts - datetime.timedelta(minutes=30),
commande=f"!remind {qui.name}").add()
if heure_chain:
Tache(timestamp=ts,
commande=f"!close {qui.name} {heure_chain} {heure}"
).add()
# Programmera prochaine ouverture
else:
Tache(timestamp=ts, commande=f"!close {qui.name}").add()
@commands.command()
@tools.mjs_only
async def close(self, ctx, qui, heure=None, heure_chain=None):
"""Ferme un vote / des actions de rôle (COMMANDE BOT / MJ)
Args:
qui:
=========== ===========
``cond`` pour le vote du condamné
``maire`` pour le vote du maire
``loups`` pour le vote des loups
``action`` pour les actions se terminant à ``heure``
``{id}`` pour une action spécifique
=========== ===========
heure:
- si ``qui == "cond"``, ``"maire"`` ou ``"loup"``,
programme en plus une prochaine ouverture à ``heure``
(et un rappel 30 minutes avant) ;
- si ``qui == "action"``, il est obligatoire : heure des
actions à lancer (cf plus haut). Pour les actions, la
prochaine est de toute façon programmée le cas échéant
(cooldown à 0 et reste des charges).
Dans tous les cas, format ``HHh`` ou ``HHhMM``.
heure_chain:
permet de chaîner des votes : ferme le vote immédiatement
et programme une prochaine ouverture à ``heure``, en
appellant ``!close`` de sorte à programmer une nouvelle
fermeture le lendemain à ``heure_chain``, et ainsi de suite.
Format ``HHh`` ou ``HHhMM``.
Une sécurité empêche de fermer un vote ou une action
qui n'est pas en cours.
Cette commande a pour vocation première d'être exécutée
automatiquement par des tâches planifiées.
Elle peut être utilisée à la main, mais attention à ne pas
faire n'importe quoi (penser à envoyer / planifier la fermeture
des votes, par exemple).
Examples:
- ``!close maire`` : ferme le vote condamné maintenant
- ``!close cond 10h`` : ferme le vote condamné maintenant
et programme une prochaine ouverture à 10h00
- ``!close cond 10h 18h`` : ferme le vote condamné maintenant,
programme une prochaine ouverture à 10h00, qui sera fermé à
18h, puis une nouvelle ouverture à 10h, etc
- ``!close action 22h`` : ferme toutes les actions
se terminant à 22h00
- ``!close 122`` : ferme l'action d'ID 122
"""
try:
qui = Vote[qui.lower()] # cond / maire / loups
except KeyError:
pass
joueurs = await recup_joueurs("close", qui, heure)
str_joueurs = "\n - ".join([joueur.nom for joueur in joueurs])
await tools.send_code_blocs(
ctx,
f"Utilisateur(s) répondant aux critères ({len(joueurs)}) : \n"
+ str_joueurs
)
# Fermeture utilisations et envoi messages
for joueur in joueurs:
chan = joueur.private_chan
if isinstance(qui, Vote):
action = joueur.action_vote(qui)
if not action.is_open: # Sécurité : action fermée depuis
continue
util = joueur.action_vote(qui).utilisation_ouverte
nom_cible = util.cible.nom if util.cible else "*non défini*"
util.close() # update direct pour empêcher de voter
if qui == Vote.cond:
await chan.send(
f"{tools.montre()} Fin du vote pour le condamné du jour !"
f"\nVote définitif : {nom_cible}\n"
f"Les résultats arrivent dans l'heure !\n"
)
elif qui == Vote.maire:
await chan.send(
f"{tools.montre()} Fin du vote pour le maire ! \n"
f"Vote définitif : {nom_cible}"
)
elif qui == Vote.loups:
await chan.send(
f"{tools.montre()} Fin du vote pour la victime du soir !"
f"\nVote définitif : {nom_cible}"
)
else: # Action
for action in joueurs[joueur]:
await chan.send(
f"{tools.montre()} Fin de la possiblité d'utiliser "
f"ton action {tools.code(action.base.slug)} ! \n"
f"Action définitive : {action.decision}"
)
await gestion_actions.close_action(action)
config.session.commit()
# Actions déclenchées par fermeture
if isinstance(qui, Vote):
for action in Action.query.filter(Action.base.has(
BaseAction.trigger_debut == ActionTrigger.close(qui))):
await gestion_actions.open_action(action)
for action in Action.query.filter(Action.base.has(
BaseAction.trigger_fin == ActionTrigger.close(qui))):
await gestion_actions.close_action(action)
# Programme prochaine ouverture
if isinstance(qui, Vote) and heure:
ts = tools.next_occurence(tools.heure_to_time(heure))
if heure_chain:
Tache(timestamp=ts,
commande=f"!open {qui.name} {heure_chain} {heure}").add()
# Programmera prochaine fermeture
else:
Tache(timestamp=ts, commande=f"!open {qui.name}").add()
@commands.command()
@tools.mjs_only
async def remind(self, ctx, qui, heure=None):
"""Envoi un rappel de vote / actions de rôle (COMMANDE BOT / MJ)
Args:
qui:
=========== ===========
``cond`` pour le vote du condamné
``maire`` pour le vote du maire
``loups`` pour le vote des loups
``action`` pour les actions se terminant à ``heure``
``{id}`` pour une action spécifique
=========== ===========
heure: ne sert que dans le cas où <qui> == "action"
(il est alors obligatoire), contrairement à !open et
!close.
Format HHh ou HHhMM.
Le bot n'envoie un message qu'aux joueurs n'ayant pas encore
voté / agi, si le vote ou l'action est bien en cours.
Cette commande a pour vocation première d'être exécutée
automatiquement par des tâches planifiées.
Elle peut être utilisée à la main, mais attention à ne pas
faire n'importe quoi !.
Examples:
- ``!remind maire`` : rappelle le vote maire maintenant
- ``!remind action 22h`` : rappelle toutes les actions
se terminant à 22h00
- ``!remind 122`` : rappelle l'action d'ID 122
"""
try:
qui = Vote[qui.lower()] # cond / maire / loups
except KeyError:
pass
joueurs = await recup_joueurs("remind", qui, heure)
str_joueurs = "\n - ".join([joueur.nom for joueur in joueurs])
await ctx.send(tools.code_bloc(
f"Utilisateur(s) répondant aux critères ({len(joueurs)}) : \n"
+ str_joueurs
))
for joueur in joueurs:
chan = joueur.private_chan
member = joueur.member
if qui == Vote.cond:
message = await chan.send(
f"⏰ {member.mention} Plus que 30 minutes pour voter "
"pour le condamné du jour ! 😱 \n"
)
await message.add_reaction(config.Emoji.bucher)
elif qui == Vote.maire:
message = await chan.send(
f"⏰ {member.mention} Plus que 30 minutes pour élire "
"le nouveau maire ! 😱 \n"
)
await message.add_reaction(config.Emoji.maire)
elif qui == Vote.loups:
message = await chan.send(
f"⏰ {member.mention} Plus que 30 minutes pour voter "
"pour la victime du soir ! 😱 \n"
)
await message.add_reaction(config.Emoji.lune)
else: # Action
for action in joueurs[joueur]:
message = await chan.send(
f"⏰ {member.mention} Plus que 30 minutes pour "
f"utiliser ton action {tools.code(action.base.slug)}"
" ! 😱 \n"
)
await message.add_reaction(config.Emoji.action)
@commands.command()
@tools.mjs_only
async def refill(self, ctx, motif, *, cible=None):
"""Recharger un/des pouvoirs rechargeables (COMMANDE BOT / MJ)
Args:
motif: ``"weekends"``, ``"forgeron"``, ``"rebouteux"``
ou ``"divin"`` (forcer le refill car les MJs
tout-puissants l'ont décidé)
cible: ``"all"`` ou le nom d'un joueur
"""
motif = motif.lower()
if motif not in [*config.refills_full, *config.refills_one]:
await ctx.send(f"{motif} n'est pas un motif valide")
return
if motif in config.refills_divins:
if cible != "all":
target = await tools.boucle_query_joueur(
ctx, cible=cible, message="Qui veux-tu recharger ?"
)
refillable = Action.query.filter(
Action.charges.isnot(None)).filter_by(joueur=target).all()
else:
m = await ctx.send(
"Tu as choisi de recharger le pouvoir de "
"TOUS les joueurs actifs, en es-tu sûr ?"
)
if await tools.yes_no(m):
refillable = Action.query.filter(
Action.charges.isnot(None)).all()
else:
await ctx.send("Mission aborted.")
return
else: # refill WE, forgeron ou rebouteux
if cible != "all":
target = await tools.boucle_query_joueur(
ctx, cible=cible, message="Qui veux-tu recharger ?"
)
refillable = Action.query.filter(Action.base.has(
BaseAction.refill.contains(motif))).filter_by(
joueur=target).all()
else:
refillable = Action.query.filter(Action.base.has(
BaseAction.refill.contains(motif))).all()
# do refill
await tools.log(refillable, code=True,
prefixe=f"Refill {motif} {cible} :")
*_, msg = await tools.send_code_blocs(
ctx,
"\n".join(f"{tools.emoji_chiffre(i + 1)} {action.base.slug}, "
f"id = {action.id} \n"
for i, action in enumerate(refillable)),
prefixe="Action(s) répondant aux critères :\n"
)
n = 1
if len(refillable) > 1:
n = await tools.choice(msg, len(refillable))
await _do_refill(motif, refillable[n - 1])
@commands.command()
@tools.mjs_only
async def cparti(self, ctx):
"""Lance le jeu (COMMANDE MJ)
- Programme les votes condamnés quotidiens (avec chaînage) 10h-18h
- Programme un vote maire 10h-18h
- Programme les actions au lancement du jeu (choix de mentor...)
et permanentes (forgeron)... à 19h
- Crée les "actions de vote", sans quoi !open plante
À utiliser le jour du lancement après 10h (lance les premières
actions le soir et les votes le lendemain)
"""
message = await ctx.send(
"C'est parti ?\n"
"Les rôles ont bien été attribués et synchronisés ?"
" (si non, le faire AVANT de valider)\n\n"
"On est bien après 10h le jour du lancement ?\n\n"
"Tu es conscient que tous les joueurs reçevront à 18h55 un message"
" en mode « happy Hunger Games » ? (codé en dur parce que flemme)"
)
if not await tools.yes_no(message):
await ctx.send("Mission aborted.")
return
message = await ctx.send(
"Les actions des joueurs ont été attribuées à la synchronisation "
"des rôles, mais les !open n'ont aucun impact tant que tout le "
"monde est en `role_actif == False` sur le Tableau de bord.\n"
"Il faut donc **passer tout le monde à `True` maintenant**"
"(puis `!sync silent`) avant de continuer."
)
if not await tools.yes_no(message):
await ctx.send("Mission aborted.")
return
message = await ctx.send(
"Dernière chose à faire : activer le backup automatique du "
"Tableau de bord tous les jours. Pour ce faire, l'ouvrir et "
"aller dans `Extensions > Apps Script` puis dans le pannel "
"`Déclencheurs` à gauche (l'horloge) et cliquer sur "
"`Ajouter un déclencheur` en bas à droite.\n\n"
"Remplir les paramètres : `Backupfeuille`, `Head`, "
"`Déchencheur horaire`, `Quotidien`, `Entre 1h et 2h` (pas "
"plus tard car les votes du jour changent à 3h)."
)
if not await tools.yes_no(message):
await ctx.send("Mission aborted.")
return
taches = []
r = "C'est parti !\n"
n10 = tools.next_occurence(datetime.time(hour=10))
n19 = tools.next_occurence(datetime.time(hour=19))
# Programmation votes condamnés chainés 10h-18h
r += "\nProgrammation des votes :\n"
taches.append(Tache(timestamp=n10, commande="!open cond 18h 10h"))
r += " - À 10h : !open cond 18h 10h\n"
# Programmation votes loups chainés 19h-23h
taches.append(Tache(timestamp=n19, commande="!open loups 23h 19h"))
r += " - À 19h : !open loups 23h 19h\n"
# Programmation premier vote maire 10h-17h
taches.append(Tache(timestamp=n10, commande="!open maire 17h"))
r += " - À 10h : !open maire 17h\n"
# Programmation actions au lancement et actions permanentes
r += "\nProgrammation des actions start / perma :\n"
start_perma = Action.query.filter(
Action.base.has(BaseAction.trigger_debut.in_(
[ActionTrigger.start, ActionTrigger.perma]
))).all()
for action in start_perma:
r += (f" - À 19h : !open {action.id} "
f"(trigger_debut == {action.base.trigger_debut})\n")
taches.append(Tache(timestamp=n19,
commande=f"!open {action.id}",
action=action))
# Programmation refill weekends
# r += "\nProgrammation des refills weekends :\n"
# ts = tools.fin_pause() - datetime.timedelta(minutes=5)
# taches.append(Tache(timestamp=ts,
# commande=f"!refill weekends all"))
# r += " - Dimanche à 18h55 : !refill weekends all\n"
# Programmation envoi d'un message aux connards
r += ("\nEt, à 18h50 : !send all [message de hype oue oue "
"c'est génial]\n")
taches.append(Tache(
timestamp=(n19 - datetime.timedelta(minutes=10)),
commande=(
"!send all Ah {member.mention}... J'espère que tu "
"es prêt(e), parce que la partie commence DANS 10 "
" MINUTES !!! https://tenor.com/view/thehungergames-"
"hungergames-thggifs-effie-gif-5114734"
)
))
await tools.log(r, code=True)
# Drop (éventuel) et (re-)création actions de vote
Action.query.filter_by(base=None).delete()
actions = []
for joueur in Joueur.query.all():
for vote in Vote:
actions.append(Action(joueur=joueur, vote=vote))
Tache.add(*taches) # On enregistre et programme le tout !
Action.add(*actions)
await ctx.send(
f"C'est tout bon ! (détails dans {config.Channel.logs.mention})"
)
@commands.command()
@tools.mjs_only
async def cfini(self, ctx):
"""✨ Clôture le jeu (COMMANDE MJ)
Supprime toutes les tâches planifiées, ce qui stoppe de fait le jeu.
"""
message = await ctx.send(
"C'est fini ?\nATTENTION : Confirmer supprimera TOUTES LES "
"TÂCHES EN ATTENTE, ce qui est compliqué à annuler !"
)
if not await tools.yes_no(message):
await ctx.send("Mission aborted.")
return
await ctx.send("Suppression des tâches...")
async with ctx.typing():
taches = Tache.query.all()
Tache.delete(*taches) # On supprime et déprogramme le tout !
await ctx.send(
"C'est tout bon !\n"
"Dernière chose : penser à désactiver le backup automatique du "
"Tableau de bord !. Pour ce faire, l'ouvrir et "
"aller dans `Extensions > Apps Script` puis dans le pannel "
"`Déclencheurs` à gauche (l'horloge) et cliquer sur "
"les trois points à droite du déclencheur > Supprimer."
)