Compare commits

..

19 Commits

Author SHA1 Message Date
ea14a2612c Corrections mineures de défis 2025-03-15 10:38:12 +01:00
fa8a5da027 Typo 2025-03-15 10:31:28 +01:00
61f8215a4a Nouveaux défis 2025-03-15 10:29:34 +01:00
0f82a8aa7a Correction emoji 2025-03-14 00:05:22 +01:00
b4c9607ce6 Pas de bonus pour les défis de vol 2025-03-14 00:04:08 +01:00
8e67805eac Pas de bonus pour les défis de vol 2025-03-14 00:01:56 +01:00
830c5d7786 competition => vol 2025-03-14 00:00:33 +01:00
e9c2ab95c5 Réordonnement des défis 2025-03-13 23:58:58 +01:00
d25a233590 Implémentation des défis 2025-03-13 23:56:25 +01:00
67650bcd60 Ajout support des powerups 2025-03-13 19:48:58 +01:00
e74184a4ce Correction messages envoyés après défi terminé 2025-03-12 00:36:44 +01:00
f6ea33d51c Correction messages envoyés après défi terminé 2025-03-12 00:20:03 +01:00
dfac15796e Correction boutons par utilisateur⋅rice 2025-03-12 00:08:16 +01:00
f66e5b5198 Ajout de boutons pour terminer les défis en un clic 2025-03-11 23:45:38 +01:00
9eb13560f2 Ajout d'un chronomètre 2025-03-11 22:42:29 +01:00
a3c71800af Défis de compétition et remisage 2025-03-11 22:04:03 +01:00
3cd8bf82dc Ajout mélange de main 2025-03-11 21:18:15 +01:00
3f0ba14166 Gestion de la fin des défis et des échanges 2025-03-11 20:37:56 +01:00
273dc88daa Ajout d'une commande de debug pour afficher l'état de la mémoire 2025-03-11 18:13:30 +01:00
2 changed files with 560 additions and 89 deletions

332
bot.py
View File

@ -2,6 +2,7 @@
from collections import namedtuple from collections import namedtuple
import copy import copy
from datetime import datetime, timedelta
from functools import partial from functools import partial
import json import json
from pathlib import Path from pathlib import Path
@ -36,7 +37,7 @@ CANTONS = {
"SH": "Schaffhouse", "SH": "Schaffhouse",
"SO": "Soleure", "SO": "Soleure",
"SZ": "Schwytz", "SZ": "Schwytz",
"TH": "Thurgovie", "TG": "Thurgovie",
"TI": "Tessin", "TI": "Tessin",
"UR": "Uri", "UR": "Uri",
"VD": "Vaud", "VD": "Vaud",
@ -45,15 +46,18 @@ CANTONS = {
"ZH": "Zurich", "ZH": "Zurich",
} }
EQUIPES = ["rouge", "vert"]
CodeCanton = Literal["AG", "AI", "AR", "BE", "BL", "BS", "FR", "GE", "GL", "GR", "JU", "LU", "NE", CodeCanton = Literal["AG", "AI", "AR", "BE", "BL", "BS", "FR", "GE", "GL", "GR", "JU", "LU", "NE",
"NW", "OW", "SG", "SH", "SO", "SZ", "TH", "TI", "UR", "VD", "VS", "ZG", "ZH"] "NW", "OW", "SG", "SH", "SO", "SZ", "TG", "TI", "UR", "VD", "VS", "ZG", "ZH"]
Couleur = Literal["rouge", "vert"] Couleur = Literal["rouge", "vert"]
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
bot = commands.Bot(command_prefix='$', intents=intents) PREFIX = '!'
bot = commands.Bot(command_prefix=PREFIX, intents=intents)
DATA_FILE = Path(__file__).parent / "data.json" DATA_FILE = Path(__file__).parent / "data.json"
if DATA_FILE.exists(): if DATA_FILE.exists():
@ -61,12 +65,13 @@ if DATA_FILE.exists():
data = json.load(data_file) data = json.load(data_file)
else: else:
data = { data = {
'equipes': {'rouge': [], 'vert': []}, 'equipes': {equipe: [] for equipe in EQUIPES},
'cantons': {code_canton: {'capture': None, 'verrouille': False} for code_canton in CANTONS.keys()}, 'cantons': {code_canton: {'capture': None, 'verrouille': False} for code_canton in CANTONS.keys()},
'defis': { 'defis': {
'mains': {'rouge': [], 'vert': []}, 'mains': {equipe: [] for equipe in EQUIPES},
'bonus': {equipe: 0 for equipe in EQUIPES},
'tires_capture': [], 'tires_capture': [],
'tires_competition': [], 'tires_vol': [],
} }
} }
with DATA_FILE.open('w') as data_file: with DATA_FILE.open('w') as data_file:
@ -92,7 +97,7 @@ def generer_carte():
cairosvg.svg2png(url='map.svg', write_to='map.png') cairosvg.svg2png(url='map.svg', write_to='map.png')
@bot.command() @bot.command(brief="Affche la carte des cantons capturés.")
async def carte(ctx: commands.Context): async def carte(ctx: commands.Context):
rouges = list(canton_code for canton_code, canton in data['cantons'].items() rouges = list(canton_code for canton_code, canton in data['cantons'].items()
if canton['capture'] == "rouge") if canton['capture'] == "rouge")
@ -114,7 +119,7 @@ async def carte(ctx: commands.Context):
await ctx.send(message, file=discord.File(f, filename="battle4suisse.png")) await ctx.send(message, file=discord.File(f, filename="battle4suisse.png"))
@bot.command() @bot.command(brief=f"Capture un canton pour son équipe : {PREFIX}capturer CODE_CANTON [EQUIPE]")
async def capturer(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None): async def capturer(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None):
if couleur is None: if couleur is None:
author_id = ctx.author.id author_id = ctx.author.id
@ -122,7 +127,7 @@ async def capturer(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleu
if author_id in membres_equipe: if author_id in membres_equipe:
break break
else: else:
raise commands.BadArgument("Vous n'appartez à aucune équipe. Merci de faire `$equipe [rouge|vert]`.") raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
data['cantons'][canton]['capture'] = couleur data['cantons'][canton]['capture'] = couleur
with DATA_FILE.open('w') as data_file: with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2) json.dump(data, data_file, indent=2)
@ -138,7 +143,7 @@ async def capture_error(ctx, error):
await ctx.send(str(error)) await ctx.send(str(error))
@bot.command() @bot.command(brief=f"Verrouille un canton sur la carte pour son équipe : {PREFIX}verrouiller CODE_CANTON [EQUIPE]")
async def verrouiller(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None): async def verrouiller(ctx: commands.Context, canton: CodeCanton, *, couleur: Couleur | None = None):
if couleur is None: if couleur is None:
author_id = ctx.author.id author_id = ctx.author.id
@ -146,7 +151,7 @@ async def verrouiller(ctx: commands.Context, canton: CodeCanton, *, couleur: Cou
if author_id in membres_equipe: if author_id in membres_equipe:
break break
else: else:
raise commands.BadArgument("Vous n'appartez à aucune équipe. Merci de faire `$equipe [rouge|vert]`.") raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
data['cantons'][canton]['capture'] = couleur data['cantons'][canton]['capture'] = couleur
data['cantons'][canton]['verrouille'] = True data['cantons'][canton]['verrouille'] = True
with DATA_FILE.open('w') as data_file: with DATA_FILE.open('w') as data_file:
@ -156,7 +161,7 @@ async def verrouiller(ctx: commands.Context, canton: CodeCanton, *, couleur: Cou
return await carte(ctx) return await carte(ctx)
@bot.command() @bot.command(brief=f"Réinitialise l'état de capture d'un canton : {PREFIX}reset CODE_CANTON")
async def reset(ctx: commands.Context, canton: CodeCanton): async def reset(ctx: commands.Context, canton: CodeCanton):
data['cantons'][canton]['capture'] = None data['cantons'][canton]['capture'] = None
data['cantons'][canton]['verrouille'] = False data['cantons'][canton]['verrouille'] = False
@ -166,7 +171,7 @@ async def reset(ctx: commands.Context, canton: CodeCanton):
return await carte(ctx) return await carte(ctx)
@bot.command() @bot.command(brief=f"Rejoindre une équipe : {PREFIX}equipe EQUIPE")
async def equipe(ctx: commands.Context, couleur: Couleur): async def equipe(ctx: commands.Context, couleur: Couleur):
author_id = ctx.author.id author_id = ctx.author.id
for membres_equipe in data['equipes'].values(): for membres_equipe in data['equipes'].values():
@ -178,78 +183,327 @@ async def equipe(ctx: commands.Context, couleur: Couleur):
await ctx.send(f"Équipe {couleur} rejointe") await ctx.send(f"Équipe {couleur} rejointe")
@bot.command() @bot.command(brief=f"Affiche la liste des noms des défis : {PREFIX}defis [capture | vol]")
async def defis(ctx: commands.Context, *, type_defi: Literal['capture', 'competition'] = "capture"): async def defis(ctx: commands.Context, *, type_defi: Literal['capture', 'vol'] = "capture"):
await ctx.send(f"Liste des défis de {type_defi} :\n" + "\n".join(f"* {defi['id']} : {defi['nom']}" for defi in DEFIS[type_defi])) await ctx.send(f"Liste des défis de {type_defi} :\n" + "\n".join(f"* {defi['id']} : {defi['nom']}" for defi in DEFIS[type_defi]))
@bot.command() @bot.command(brief=f"Affiche la description des défis")
async def description(ctx: commands.Context, type_defi: Literal['capture', 'competition'] = "capture"): async def description(ctx: commands.Context, type_defi: Literal['capture', 'vol'] = "capture", id_defi: int | None = None):
defis = DEFIS[type_defi] defis = DEFIS[type_defi]
embeds = [] embeds = []
if id_defi is not None:
embed = discord.Embed(title=f"Description du défi {id_defi}", colour=discord.Colour.gold())
try:
defi = next(defi for defi in defis if defi['id'] == id_defi)
except StopIteration:
raise commands.BadArgument(f"Le défi de {type_defi}{id_defi} n'existe pas.")
embed.add_field(name=f"{defi['nom']} {defi['bonus'] * ":star:"}", value=defi['description'], inline=False)
embeds.append(embed)
else:
for page in range((len(defis) - 1) // 25 + 1): for page in range((len(defis) - 1) // 25 + 1):
defis_page = defis[page * 25:(page + 1) * 25] defis_page = defis[page * 25:(page + 1) * 25]
embed = discord.Embed(title=f"Description des défis", colour=discord.Colour.gold()) embed = discord.Embed(title=f"Description des défis", colour=discord.Colour.gold())
embed.set_footer(f"Page {page}/{(len(defis) - 1) // 25 + 1}") embed.set_footer(text=f"Page {page + 1}/{(len(defis) - 1) // 25 + 1}")
for defi in defis_page: for defi in defis_page:
embed.add_field(name=f"{defi['nom']} (n°{defi['id']})", value=defi['description'], inline=False) embed.add_field(name=f"{defi['nom']} {defi['bonus'] * ":star:"} (n°{defi['id']})", value=defi['description'], inline=False)
embeds.append(embed) embeds.append(embed)
await ctx.send(embeds=embeds) await ctx.send(embeds=embeds)
@bot.command() @bot.command()
async def tirage(ctx: commands.Context, nb_defis: int = 7): async def tirage(ctx: commands.Context, nb_defis: int = 5):
if data['defis']['mains']['rouge'] or data['defis']['mains']['vert']: if any(data['defis']['mains'][equipe] for equipe in EQUIPES):
raise commands.BadArgument("Les mains sont déjà initialisées") raise commands.BadArgument("Les mains sont déjà initialisées")
defis_libres = copy.deepcopy(DEFIS['capture']) defis_libres = copy.deepcopy(DEFIS['capture'])
for equipe in ('rouge', 'vert'): for equipe in EQUIPES:
for _i in range(nb_defis): for _i in range(nb_defis):
defi = random.choice(defis_libres) defi = random.choice(defis_libres)
defis_libres.remove(defi) defis_libres.remove(defi)
data['defis']['mains'][equipe].append(defi['id']) data['defis']['mains'][equipe].append(defi['id'])
data['defis']['tires_capture'].append(defi['id']) data['defis']['tires_capture'].append(defi['id'])
main = data['defis']['mains'][equipe]
embeds = []
colour = discord.Color.red() if equipe == "rouge" else discord.Color.green()
for id_defi in main:
defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi)
embed = discord.Embed(title=defi['nom'], description=defi['description'], colour=colour)
embed.set_footer(text=f"Défi n°{defi['id']}")
embeds.append(embed)
for member_id in data['equipes'][equipe]: for member_id in data['equipes'][equipe]:
channel_dm = await bot.create_dm(namedtuple("User", "id")(member_id)) await afficher_main(ctx, author_id=member_id)
await channel_dm.send("Vos défis en main :", embeds=embeds)
with DATA_FILE.open('w') as data_file: with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2) json.dump(data, data_file, indent=2)
await ctx.send("Les mains de départ ont bien été tirées ! Le contenu vous a été envoyé en MP.") await ctx.send("Les mains de départ ont bien été tirées ! Le contenu vous a été envoyé en MP.")
@bot.command() @bot.command()
async def main(ctx: commands.Context): async def vol(ctx: commands.Context):
author_id = ctx.author.id defi_vol = random.choice([defi for defi in DEFIS['vol'] if defi['id'] not in data['defis']['tires_vol']])
data['defis']['tires_vol'].append(defi_vol['id'])
embed = discord.Embed(title=defi_vol['nom'], description=defi_vol['description'])
embed.set_footer(text=f"Défi de compétition n°{defi_vol['id']}")
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
await ctx.send("@everyone Un canton est attaqué ! L'équipe vainqueure de ce défi conservera son contrôle jusqu'à la fin du jeu :", embed=embed)
@bot.command()
async def remiser(ctx: commands.Context, type_defi: Literal['capture', 'vol'] = "capture", id_defi: int | None = None):
defis = DEFIS[type_defi]
try:
defi = next(defi for defi in defis if defi['id'] == id_defi)
except StopIteration:
raise commands.BadArgument(f"Le défi de {type_defi}{id_defi} n'existe pas.")
if id_defi in data['defis'][f'tires_{type_defi}']:
data['defis'][f'tires_{type_defi}'].remove(id_defi)
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
await ctx.reply(f"Le défi de {type_defi}{id_defi} ({defi['nom']}) a été retiré de la défausse et pourra à nouveau être tiré au sort.")
else:
await ctx.reply(f"Le défi de {type_defi}{id_defi} ({defi['nom']}) n'était déjà pas dans la défausse.")
class MainView(discord.ui.View):
def __init__(self, ctx: commands.Context, user_id: int, defis: list[dict], timeout: float | None = 180.0):
super().__init__(timeout=timeout)
async def terminer_defi(self, id_defi: int, user_id: int, interaction: discord.Interaction):
await interaction.response.defer()
await terminer(ctx, id_defi, user_id, interaction.channel)
for id_defi in defis:
defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi)
button = discord.ui.Button(style=discord.ButtonStyle.success, label=f"Terminer {defi['nom']}")
button.callback = partial(terminer_defi, self, id_defi, user_id)
self.add_item(button)
@bot.command(name="main")
async def afficher_main(ctx: commands.Context, mode: Literal['public', 'prive'] = "prive", author_id: int | None = None):
author_id = author_id or ctx.author.id
for couleur, membres_equipe in data['equipes'].items(): for couleur, membres_equipe in data['equipes'].items():
if author_id in membres_equipe: if author_id in membres_equipe:
break break
else: else:
raise commands.BadArgument("Vous n'appartez à aucune équipe. Merci de faire `$equipe [rouge|vert]`.") raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
main = data['defis']['mains'][couleur] main = data['defis']['mains'][couleur]
nb_bonus = data['defis']['bonus'][couleur]
embeds = [] embeds = []
colour = discord.Color.red() if couleur == "rouge" else discord.Color.green() colour = discord.Color.red() if couleur == "rouge" else discord.Color.green()
for id_defi in main: for id_defi in main:
defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi) defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi)
embed = discord.Embed(title=defi['nom'], description=defi['description'], colour=colour) embed = discord.Embed(title=f"{defi['nom']} {defi['bonus'] * ":star:"}", description=defi['description'], colour=colour)
embed.set_footer(text=f"Défi n°{defi['id']}") embed.set_footer(text=f"Défi n°{defi['id']}")
embeds.append(embed) embeds.append(embed)
channel_dm = await bot.create_dm(ctx.author) if mode == "public":
await channel_dm.send("Vos défis en main :", embeds=embeds) await ctx.send(f"Défis de l'équipe **{couleur}** :", embeds=embeds)
else:
channel_dm = await bot.create_dm(namedtuple('User', 'id')(author_id))
await channel_dm.send(f"Vous disposez de **{nb_bonus} bonus {nb_bonus * ":star:"}**.\nVos défis en main :", embeds=embeds, view=MainView(ctx, author_id, main))
@bot.command()
async def terminer(ctx: commands.Context, id_defi: int, author_id: int | None = None, channel: discord.abc.Messageable | None = None):
if all(id_defi != defi['id'] for defi in DEFIS['capture']):
raise commands.BadArgument(f"Erreur : Le défi {id_defi_1} n'existe pas")
defi = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi)
author_id = author_id or ctx.author.id
for equipe, membres_equipe in data['equipes'].items():
if author_id in membres_equipe:
break
else:
raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
main = data['defis']['mains'][equipe]
if id_defi not in main:
raise commands.BadArgument(f"Le défi {id_defi} n'est pas dans votre main. Faites `{PREFIX}main` pour afficher votre main.")
nouveau_defi = random.choice([defi for defi in DEFIS['capture'] if defi['id'] not in data['defis']['tires_capture']])
main.remove(id_defi)
main.append(nouveau_defi['id'])
data['defis']['tires_capture'].append(nouveau_defi['id'])
data['defis']['bonus'][equipe] += defi['bonus']
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
channel = channel or ctx
await channel.send(f"Défi n°{id_defi} **{defi['nom']}** terminé ! Il est retiré de votre main.")
await channel.send(f"Votre équipe gagne **{defi['bonus']} bonus**. Vous en possédez désormais {data['defis']['bonus'][equipe]}.")
colour = discord.Color.red() if equipe == "rouge" else discord.Color.green()
embed = discord.Embed(title=f"{nouveau_defi['nom']} {defi['bonus'] * ":star:"}", description=nouveau_defi['description'], colour=colour)
embed.set_footer(text=f"Défi n°{nouveau_defi['id']}")
await channel.send("**Votre nouveau défi en main :**", embed=embed)
for member_id in data['equipes'][equipe]:
await afficher_main(ctx, author_id=member_id)
@bot.command()
async def bonus(ctx: commands.Context, equipe: Couleur | None = None, nouvelle_valeur: int | None = None):
if equipe is None:
author_id = ctx.author.id
for equipe, membres_equipe in data['equipes'].items():
if author_id in membres_equipe:
break
else:
raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
nb_bonus = data['defis']['bonus'][equipe]
if nouvelle_valeur is None:
if nb_bonus >= 1:
data['defis']['bonus'][equipe] -= 1
await ctx.send(f"L'équipe **{equipe}** vient d'utiliser un bonus !")
else:
await ctx.reply(f"Vous n'avez plus de bonus.", ephemeral=True)
else:
data['defis']['bonus'][equipe] = nouvelle_valeur
await ctx.send(f"L'équipe **{equipe}** a désormais **{nouvelle_valeur} bonus**, contre {nb_bonus} auparavant.")
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
@bot.command()
async def echange(ctx: commands.Context, id_defi_1: int, id_defi_2: int):
if all(id_defi_1 != defi['id'] for defi in DEFIS['capture']):
raise commands.BadArgument(f"Erreur : Le défi {id_defi_1} n'existe pas")
if all(id_defi_2 != defi['id'] for defi in DEFIS['capture']):
raise commands.BadArgument(f"Erreur : Le défi {id_defi_2} n'existe pas")
defi_1 = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi_1)
defi_2 = next(defi for defi in DEFIS['capture'] if defi['id'] == id_defi_2)
equipe_1, equipe_2 = None, None
for equipe in EQUIPES:
main = data['defis']['mains'][equipe]
if id_defi_1 in main:
equipe_1 = equipe
if id_defi_2 in main:
equipe_2 = equipe
if equipe_1 is None and equipe_2 is None:
raise commands.BadArgument("Erreur : Aucun des deux défis n'est fait par une équipe")
elif equipe_1 == equipe_2:
raise commands.BadArgument(f"Erreur : Les défis {id_defi_1} et {id_defi_2} sont tous les deux fait par la même équipe {equipe_1}")
if equipe_1 is not None:
data['defis']['mains'][equipe_1].remove(id_defi_1)
data['defis']['mains'][equipe_1].append(id_defi_2)
else:
tires_capture = data['defis']['tires_capture']
if id_defi_1 not in tires_capture:
tires_capture.append(id_defi_1)
if id_defi_2 in tires_capture:
tires_capture.remove(id_defi_2)
if equipe_2 is not None:
data['defis']['mains'][equipe_2].remove(id_defi_2)
data['defis']['mains'][equipe_2].append(id_defi_1)
else:
tires_capture = data['defis']['tires_capture']
if id_defi_1 in tires_capture:
tires_capture.remove(id_defi_1)
if id_defi_2 not in tires_capture:
tires_capture.append(id_defi_2)
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
if equipe_1 is not None and equipe_2 is not None:
await ctx.send(f"Le défi **{defi_1['nom']}** de l'équipe **{equipe_1}** et le défi **{defi_2['nom']}** de l'équipe **{equipe_2}** ont été échangés !")
else:
await ctx.send(f"Les défis **{defi_1['nom']}** et **{defi_2['nom']}** ont été échangés !")
@bot.command()
async def melanger(ctx: commands.Context, nb_defis: int = 5):
author_id = ctx.author.id
for equipe, membres_equipe in data['equipes'].items():
if author_id in membres_equipe:
break
else:
raise commands.BadArgument(f"Vous n'appartez à aucune équipe. Merci de faire `{PREFIX}equipe [{"|".join(EQUIPES)}]`.")
main = data['defis']['mains'][equipe]
for _i in range(nb_defis):
nouveau_defi = random.choice([defi for defi in DEFIS['capture'] if defi['id'] not in data['defis']['tires_capture']])
main.append(nouveau_defi['id'])
data['defis']['tires_capture'].append(nouveau_defi['id'])
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
await ctx.send(f"Main de l'équipe {equipe} mélangée !")
for member_id in data['equipes'][equipe]:
await afficher_main(ctx, author_id=member_id)
@bot.command()
async def de(ctx: commands.Context, nb_faces: int = 6):
resultat = random.randint(1, nb_faces + 1)
await ctx.reply(f":game_die: Résultat du dé à {nb_faces} faces : **{resultat}**")
@bot.command()
async def chronometre(ctx: commands.Context, minutes: int = 30, secondes: int = 0):
fin = datetime.now() + timedelta(minutes=minutes, seconds=secondes)
await ctx.send(f"Chronomètre lancé pour **{minutes:02d}:{secondes:02d}** (fin à <t:{int(fin.timestamp())}:T>)\nFin <t:{int(fin.timestamp())}:R>")
@bot.command()
async def debug(ctx: commands.Context, keys: str, *, set_value: str | None = None):
keys = keys.split('.')
parent = None
out = data
for key in keys:
if key not in out:
raise commands.BadArgument(f"Clé {key} absente du dictionnaire, valeurs possibles : {out.keys()}")
parent = out
last_key = key
out = out[key]
data_json = json.dumps(out, indent=2)
if set_value is None:
await ctx.reply(f"```json\n{data_json}\n```", ephemeral=True)
else:
new_data = json.loads(set_value)
new_data_json = json.dumps(new_data, indent=2)
parent[last_key] = new_data
await ctx.reply(f"Anciennes données :\n```json\n{data_json}\n```Nouvelles données :\n```json\n{new_data_json}\n```\nFaites `{PREFIX}save` pour sauvegarder, ou `{PREFIX}` reload pour rollback.", ephemeral=True)
@bot.command()
async def reload(ctx: commands.Context):
global data, DEFIS
with DATA_FILE.open() as data_file:
data = json.load(data_file)
with DEFIS_FILE.open() as defis_file:
DEFIS = json.load(defis_file)
await ctx.reply("Configuration rechargée.", ephemeral=True)
@bot.command()
async def save(ctx: commands.Context):
with DATA_FILE.open('w') as data_file:
json.dump(data, data_file, indent=2)
await ctx.reply("Configuration sauvegardée.", ephemeral=True)
@carte.error
@reset.error
@equipe.error @equipe.error
async def equipe_error(ctx, error): @defis.error
@description.error
@tirage.error
@vol.error
@remiser.error
@afficher_main.error
@terminer.error
@bonus.error
@echange.error
@melanger.error
@de.error
@chronometre.error
@debug.error
@reload.error
@save.error
async def on_error(ctx, error):
with DATA_FILE.open() as data_file:
data = json.load(data_file)
await ctx.send(str(error)) await ctx.send(str(error))
if not isinstance(error, commands.BadArgument):
raise error
bot.run(DISCORD_TOKEN) bot.run(DISCORD_TOKEN)

View File

@ -2,100 +2,317 @@
"capture": [ "capture": [
{ {
"id": 1, "id": 1,
"nom": "Défi 1", "nom": "Artisan⋅e chocolatièr⋅e",
"description": "Défi 1" "description": "À partir de chocolat local, construis un pont.\n\nLa structure doit rester en place pendant au moins 15 secondes sans s'effondrer.",
"bonus": 0
}, },
{ {
"id": 2, "id": 2,
"nom": "Défi 2", "nom": "Manger (chez) les riches",
"description": "Défi 2" "description": "Mange une spécialité locale du canton.",
"bonus": 0
}, },
{ {
"id": 3, "id": 3,
"nom": "Défi 3", "nom": "Non-alignement",
"description": "Défi 3" "description": "Devant une institution locale, adresse une liste de 20 revendications géopolitiquement neutres.",
"bonus": 0
}, },
{ {
"id": 4, "id": 4,
"nom": "Défi 4", "nom": "Low five",
"description": "Défi 4" "description": "Fais un low-five au point le plus bas d'altitude du canton.",
"bonus": 0
}, },
{ {
"id": 5, "id": 5,
"nom": "Défi 5", "nom": "Artiste GPS",
"description": "Défi 5" "description": "Trace la croix du drapeau suisse à l'aide de ton tracé GPS.\n\nLa croix doit être ressemblante sans être nécessairement parfaite.",
"bonus": 0
}, },
{ {
"id": 6, "id": 6,
"nom": "Défi 6", "nom": "Prendre de la hauteur",
"description": "Défi 6" "description": "Rends-toi sur le toit d'un bâtiment de manière légale.",
"bonus": 1
}, },
{ {
"id": 7, "id": 7,
"nom": "Défi 7", "nom": "Marcher sur l'eau",
"description": "Défi 7" "description": "Traverse un cours d'eau d'une rive à l'autre.\n\nLa distance traversée doit être d'au moins un mètre.\nIl n'est pas possible d'emprunter une structure existante pour le traverser, en particulier d'utiliser un pont.\n\nSi le cours d'eau fait la frontière entre 2 cantons, les deux cantons sont capturés.",
"bonus": 1
}, },
{ {
"id": 8, "id": 8,
"nom": "Défi 8", "nom": "Randonnée",
"description": "Défi 8" "description": "Construis une tour de cailloux à plus de 800 mètres d'altitude.",
}, "bonus": 1
{
"id": 9,
"nom": "Défi 9",
"description": "Défi 9"
}, },
{ {
"id": 10, "id": 10,
"nom": "Défi 10", "nom": "Panorama",
"description": "Défi 10" "description": "Photographie taon partenaire de l'autre côté d'un lac.\n\nLe lac doit être reconnu comme tel sur les cartes, et l'étendue d'eau doit être pleinement visible sur l'image.",
"bonus": 1
}, },
{ {
"id": 11, "id": 11,
"nom": "Défi 11", "nom": "Rageux⋅se",
"description": "Défi 11" "description": "Auprès de la meilleure attraction touristique (musée, monument, paysage,…) du canton, fais une critique négative en 5 points en vidéo.",
"bonus": 0
}, },
{ {
"id": 12, "id": 12,
"nom": "Défi 12", "nom": "Non-binaire",
"description": "Défi 12" "description": "Dans un lieu que l'on peut qualifier d'entre-deux (milieu d'un pont, mezzanine entre 2 étages,…), chante « The Code » de Nemo.",
"bonus": 0
}, },
{ {
"id": 13, "id": 13,
"nom": "Défi 13", "nom": "Démocratie",
"description": "Défi 13" "description": "Organiser une votation citoyenne sur un sujet au choix. La votation doit récolter au moins 10 votant·es.",
"bonus": 1
}, },
{ {
"id": 14, "id": 14,
"nom": "Défi 14", "nom": "Inaction climatique suisse",
"description": "Défi 14" "description": "Nettoie 10 déchets jetés au sol d'un espace.",
"bonus": 0
}, },
{ {
"id": 15, "id": 15,
"nom": "Défi 15", "nom": "Révolution bovine",
"description": "Défi 15" "description": "Parle à une vache dans un pré et tente de la syndicaliser en lui suggérant de se libérer de sa condition d'élevage.\n\nIl n'est pas nécessaire de se faire comprendre.",
"bonus": 1
}, },
{ {
"id": 16, "id": 16,
"nom": "Défi 16", "nom": "Génie incivil",
"description": "Défi 16" "description": "Passe sous 3 ponts.",
} "bonus": 0
],
"competition": [
{
"id": 1,
"nom": "Compétition 1",
"description": "Compétition 1"
}, },
{
"id": 17,
"nom": "Tourisme minimal",
"description": "Va faire du tourisme dans le village le moins peuplé du canton disposant d'une gare à moins de 1 km.",
"bonus": 1
},
{
"id": 18,
"nom": "Jeu de mots au menu",
"description": "Trouve du riz cantonais.",
"bonus": 0
},
{
"id": 19,
"nom": "Patois",
"description": "Demande un mot en romanche.",
"bonus": 1
},
{
"id": 20,
"nom": "Du Jura aux Alpes",
"description": "Transporte une pierre jurassienne dans les Alpes, ou inversement. En cas de succès, les cantons de départ et d'arrivée sont capturés, à moins que le canton de départ n'ait été capturé par l'équipe adverse entre temps, auquel cas c'est un échec.\n\nIl est possible de faire d'autres défis pendant le transport. Les points de départ et d'arrivée doivent être à au moins 500 mètres d'altitude pour le Jura et 700 mètres d'altitude pour les Alpes.",
"bonus": 2
},
{
"id": 21,
"nom": "Du Rhin au Rhône",
"description": "Touche le Rhône et le Rhin dans la même journée. En cas de succès, les cantons de départ et d'arrivée sont capturés, à moins que le canton d'origine n'ait été capturé avant l'accomplissement du défi, auquel cas c'est un échec.\n\nIl est possible de réaliser d'autres défis pendant l'accomplissement de celui-ci.",
"bonus": 2
},
{
"id": 22,
"nom": "Pas moche, juste différent",
"description": "Auprès du bâtiment le plus moche identifié selon ses propres goûts, rédige un poème qui en fait l'éloge et le traduire en français, allemand et italien.\n\nLe poème doit être rédigé manuscrit sur une feuille.",
"bonus": 0
},
{
"id": 23,
"nom": "Rançon",
"description": "Écris « Bataille4Suisse » avec des papiers découpés et ré-agencés, tel un message de rançon.",
"bonus": 0
},
{
"id": 24,
"nom": "Moyenne",
"description": "Rends-toi le plus proche possible du barycentre (point central) d'une commune qui a entre 4100 et 4300 habitant⋅es. De ce point de vue, choisir jusqu'à 10 sommets visibles et non cachés par des bâtiments, et trouver quel est le sommet d'altitude moyenne.",
"bonus": 1
},
{
"id": 25,
"nom": "Helvetica",
"description": "Trouve un texte écrit en police d'écriture Helvetica.\n\nIl peut être imprimé ou sur écran d'affichage, et doit être trouvé et ne peut pas être créé ou mis en cette police par les joueureuses ni à leur demande.",
"bonus": 1
},
{
"id": 26,
"nom": "Terre en vue",
"description": "Positionne-toi à au moins 10 km de tout lac.",
"bonus": 1
},
{
"id": 27,
"nom": "Vue de haut",
"description": "Rends-toi à la gare ferroviaire accessible la plus haute du canton.",
"bonus": 1
},
{
"id": 28,
"nom": "Pi Day",
"description": "Rends-toi dans une commune dont l'un de ses noms officiels contient la lettre P, et dessiner un cercle le plus ressemblant possible.",
"bonus": 1
},
{
"id": 29,
"nom": "Quelle heure est-il ?",
"description": "Trouve une montre ou une horloge à cadran qui indique une heure entre 10h10 et 10h15.\n\nIl n'est pas autorisé de régler l'heure ni de demander à la régler, et il ne peut pas s'agir d'une montre qu'on possède ou portée par un·e passant·e.",
"bonus": 0
},
{
"id": 30,
"nom": "Vers l'infini et au-delà",
"description": "Photographie un sommet à au moins 2000 mètres d'altitude.\n\nUne photo où le sommet est caché par les nuages ou par la nuit est acceptée.",
"bonus": 1
},
{
"id": 31,
"nom": "Traversée",
"description": "Traverse un canton B depuis un canton A vers un canton C.\n\nLe canton B est capturé.",
"bonus": 1
},
{
"id": 32,
"nom": "Promotion culturelle",
"description": "Prends un flyer dans un lieu culturel, et en fais-en la promotion dans un lieu culturel similaire d'un autre canton.\n\nEn cas de succès du défi, les deux cantons sont capturés.",
"bonus": 1
},
{
"id": 33,
"nom": "Placement de produit",
"description": "Filme une pub de parfum.",
"bonus": 0
},
{
"id": 34,
"nom": "Polyglote",
"description": "Trouve une personne qui parle français, une personne qui parle allemand, une personne qui parle italien.\n\nCes trois personnes ne sont pas nécessairement distinctes.",
"bonus": 1
},
{
"id": 35,
"nom": "Cabotage",
"description": "Dans le même canton, prends un train et descend au prochain arrêt sans changer de canton.",
"bonus": 0
},
{
"id": 36,
"nom": "Cadeau !",
"description": "Offre un cadeau à quelqu'un⋅e, que vous l .",
"bonus": 0
},
{
"id": 37,
"nom": "Culture",
"description": "Trouve « Astérix chez les Helvètes » ou un livre sur le Nicaragua.",
"bonus": 0
},
{
"id": 38,
"nom": "Mairiephilie",
"description": "Dessine l'administration municipale d'une commune de ton canton devant elle.",
"bonus": 0
},
{
"id": 39,
"nom": "Sing'in the rain",
"description": "Chante « Sing'in the rain » sous la pluie ou sous la neige. S'il neige, remplace « rain » par « snow ».",
"bonus": 0
},
{
"id": 40,
"nom": "Touch the ~~grass~~ snow",
"description": "Touche de la neige naturelle.",
"bonus": 1
}
],
"vol": [
{ {
"id": 2, "id": 2,
"nom": "Compétition 2", "nom": "Chocolat :chocolate_bar:",
"description": "Compétition 2" "description": "Photographie le plus de chocolats différents (marque/chocolatièr⋅e et nom) en 15 minutes.",
"bonus": 0
}, },
{ {
"id": 3, "id": 3,
"nom": "Compétition 3", "nom": "Fromage :cheese:",
"description": "Compétition 3" "description": "Photographie le plus de fromages différents en 15 minutes.",
"bonus": 0
},
{
"id": 4,
"nom": "Vélo :bike:",
"description": "Photographie le plus de vélos différents en 15 minutes.",
"bonus": 0
},
{
"id": 5,
"nom": "Mot aléatoire",
"description": "Fais dire à un⋅e passant⋅e le mot aléatoire de ton choix le plus rapidement possible, sans donner le mot ni sa définition exacte.",
"bonus": 0
},
{
"id": 7,
"nom": "Luxe :money_with_wings:",
"description": "Trouve un objet vendu le plus cher possible en 15 minutes.",
"bonus": 0
},
{
"id": 8,
"nom": "Bonne affaire :gift:",
"description": "Trouve un objet vendu le moins cher possible en 15 minutes.",
"bonus": 0
},
{
"id": 9,
"nom": "Banques :bank:",
"description": "Photographie le plus de banques en 15 minutes.",
"bonus": 0
},
{
"id": 10,
"nom": "Animaux :dog2:",
"description": "Trouve le plus d'espèces animales en 15 minutes.",
"bonus": 0
},
{
"id": 11,
"nom": "Fuis ! :person_running:",
"description": "Quitte ton canton le plus rapidement possible.",
"bonus": 0
},
{
"id": 12,
"nom": "Château-fort :european_castle:",
"description": "Trouver la façade la plus imposante en 15 minutes.",
"bonus": 0
},
{
"id": 13,
"nom": "Cinéphile :cinema:",
"description": "Trouve un cinéma le plus rapidement possible.",
"bonus": 0
},
{
"id": 14,
"nom": "Fièr⋅e :rainbow_flag:",
"description": "Trouve un drapeau du parapluie LGBT le plus rapidement possible.\n\nIl ne peut pas être sur toi, ni fabriqué, ni sur un écran.",
"bonus": 0
},
{
"id": 15,
"nom": "Panneaux",
"description": "Trouve le plus le panneaux de signalisation possible en 15 minutes, qui ne sont pas des panneaux indiquant des directions.",
"bonus": 0
} }
] ]
} }