#!/usr/bin/env python3 from collections import namedtuple import copy from datetime import datetime, timedelta from functools import partial import json from pathlib import Path import random from typing import Literal from xml.dom import minidom import cairosvg import discord from discord.ext import commands from config import * CANTONS = { "AG": "Argovie", "AI": "Appenzell Rhodes-Intérieures", "AR": "Appenzell Rhodes-Extérieures", "BE": "Berne", "BL": "Bâle-Campagne", "BS": "Bâle-Ville", "FR": "Fribourg", "GE": "Genève", "GL": "Glaris", "GR": "Grisons", "JU": "Jura", "LU": "Lucerne", "NE": "Neuchâtel", "NW": "Nidwald", "OW": "Obwald", "SG": "Saint-Gall", "SH": "Schaffhouse", "SO": "Soleure", "SZ": "Schwytz", "TG": "Thurgovie", "TI": "Tessin", "UR": "Uri", "VD": "Vaud", "VS": "Valais", "ZG": "Zoug", "ZH": "Zurich", } EQUIPES = ["rouge", "vert"] CodeCanton = Literal["AG", "AI", "AR", "BE", "BL", "BS", "FR", "GE", "GL", "GR", "JU", "LU", "NE", "NW", "OW", "SG", "SH", "SO", "SZ", "TG", "TI", "UR", "VD", "VS", "ZG", "ZH"] Couleur = Literal["rouge", "vert"] intents = discord.Intents.default() intents.message_content = True PREFIX = '!' bot = commands.Bot(command_prefix=PREFIX, intents=intents) DATA_FILE = Path(__file__).parent / "data.json" if DATA_FILE.exists(): with DATA_FILE.open() as data_file: data = json.load(data_file) else: data = { 'equipes': {equipe: [] for equipe in EQUIPES}, 'cantons': {code_canton: {'capture': None, 'verrouille': False} for code_canton in CANTONS.keys()}, 'defis': { 'mains': {equipe: [] for equipe in EQUIPES}, 'bonus': {equipe: 0 for equipe in EQUIPES}, 'tires_capture': [], 'tires_vol': [], } } with DATA_FILE.open('w') as data_file: json.dump(data, data_file, indent=2) DEFIS_FILE = Path(__file__).parent / "defis.json" with DEFIS_FILE.open() as defis_file: DEFIS = json.load(defis_file) def generer_carte(): doc = minidom.parse("map_blank.svg") for code_canton, data_canton in data['cantons'].items(): if data_canton['capture']: path = next(e for e in doc.getElementsByTagName('path') if e.getAttribute('id') == code_canton) couleur = data_canton['capture'] if data_canton['verrouille']: path.setAttribute('fill', f"url(#verrouille-{couleur})") else: path.setAttribute('class', f"capture-{couleur}") with open('map.svg', 'w') as f: doc.writexml(f) cairosvg.svg2png(url='map.svg', write_to='map.png') @bot.command(brief="Affche la carte des cantons capturés.") async def carte(ctx: commands.Context): rouges = list(canton_code for canton_code, canton in data['cantons'].items() if canton['capture'] == "rouge") rouges_verrouilles = list(canton_code for canton_code, canton in data['cantons'].items() if canton['capture'] == "rouge" and canton['verrouille']) noms_rouges = ", ".join(code_canton + (":lock:" if code_canton in rouges_verrouilles else "") for code_canton in rouges) verts = list(canton_code for canton_code, canton in data['cantons'].items() if canton['capture'] == "vert") verts_verrouilles = list(canton_code for canton_code, canton in data['cantons'].items() if canton['capture'] == "vert" and canton['verrouille']) noms_verts = ", ".join(code_canton + (":lock:" if code_canton in verts_verrouilles else "") for code_canton in verts) libres = list(canton_code for canton_code, canton in data['cantons'].items() if canton['capture'] is None) message = f""":red_circle: Équipe rouge : **{len(rouges)} canton{"s" if len(rouges) > 1 else ""}** (dont **{len(rouges_verrouilles)} verrouillé{"s" if len(rouges_verrouilles) > 1 else ""}**) : {noms_rouges} :green_circle: Équipe verte : **{len(verts)} canton{"s" if len(verts) > 1 else ""}** (dont **{len(verts_verrouilles)} verrouillé{"s" if len(verts_verrouilles) > 1 else ""}**) : {noms_verts} :white_circle: **{len(libres)} canton{"s" if len(libres) > 1 else ""}** libre{"s" if len(libres) > 1 else ""} : {", ".join(libres)}""" generer_carte() with open('map.png', 'rb') as f: await ctx.send(message, file=discord.File(f, filename="battle4suisse.png")) @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): if couleur is None: author_id = ctx.author.id for couleur, 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)}]`.") data['cantons'][canton]['capture'] = couleur with DATA_FILE.open('w') as data_file: json.dump(data, data_file, indent=2) await ctx.send(f"@everyone L'équipe {couleur} a capturé le canton de **{CANTONS[canton]}** !") return await carte(ctx) @capturer.error async def capture_error(ctx, error): if isinstance(error, commands.BadLiteralArgument): await ctx.send(f"Canton inconnu : {error.argument}, valeurs possibles : {", ".join(error.literals)}") else: await ctx.send(str(error)) @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): if couleur is None: author_id = ctx.author.id for couleur, 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)}]`.") data['cantons'][canton]['capture'] = couleur data['cantons'][canton]['verrouille'] = True with DATA_FILE.open('w') as data_file: json.dump(data, data_file, indent=2) generer_carte() await ctx.send(f"@everyone L'équipe {couleur} a capturé le canton de **{CANTONS[canton]}** !") return await carte(ctx) @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): data['cantons'][canton]['capture'] = None data['cantons'][canton]['verrouille'] = False with DATA_FILE.open('w') as data_file: json.dump(data, data_file, indent=2) generer_carte() return await carte(ctx) @bot.command(brief=f"Rejoindre une équipe : {PREFIX}equipe EQUIPE") async def equipe(ctx: commands.Context, couleur: Couleur): author_id = ctx.author.id for membres_equipe in data['equipes'].values(): if author_id in membres_equipe: membres_equipe.remove(author_id) data['equipes'][couleur].append(author_id) with DATA_FILE.open('w') as data_file: json.dump(data, data_file, indent=2) await ctx.send(f"Équipe {couleur} rejointe") @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', '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])) @bot.command(brief=f"Affiche la description des défis") async def description(ctx: commands.Context, type_defi: Literal['capture', 'vol'] = "capture", id_defi: int | None = None): defis = DEFIS[type_defi] 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} n°{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): defis_page = defis[page * 25:(page + 1) * 25] embed = discord.Embed(title=f"Description des défis", colour=discord.Colour.gold()) embed.set_footer(text=f"Page {page + 1}/{(len(defis) - 1) // 25 + 1}") for defi in defis_page: embed.add_field(name=f"{defi['nom']} {defi['bonus'] * ":star:"} (n°{defi['id']})", value=defi['description'], inline=False) embeds.append(embed) await ctx.send(embeds=embeds) @bot.command() async def tirage(ctx: commands.Context, nb_defis: int = 5): if any(data['defis']['mains'][equipe] for equipe in EQUIPES): raise commands.BadArgument("Les mains sont déjà initialisées") defis_libres = copy.deepcopy(DEFIS['capture']) for equipe in EQUIPES: for _i in range(nb_defis): defi = random.choice(defis_libres) defis_libres.remove(defi) data['defis']['mains'][equipe].append(defi['id']) data['defis']['tires_capture'].append(defi['id']) for member_id in data['equipes'][equipe]: await afficher_main(ctx, author_id=member_id) with DATA_FILE.open('w') as data_file: 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.") @bot.command() async def vol(ctx: commands.Context): 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} n°{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} n°{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} n°{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(): 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'][couleur] nb_bonus = data['defis']['bonus'][couleur] embeds = [] colour = discord.Color.red() if couleur == "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=f"{defi['nom']} {defi['bonus'] * ":star:"}", description=defi['description'], colour=colour) embed.set_footer(text=f"Défi n°{defi['id']}") embeds.append(embed) if mode == "public": 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 à )\nFin ") @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 @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)) if not isinstance(error, commands.BadArgument): raise error bot.run(DISCORD_TOKEN)