À la base, j'avais besoin d'un outil pour simuler des lancers de dés qui soit simple à utiliser, flexible et disponible à tout moment. Comme j'en avais besoin pour jouer à des jeux de rôles avec mes amis sur Discord. Je me suis donc lancé dans la création d'un bot Discord pour répondre à mon besoin.
Au début, le bot ne servait pas à grand-chose, si ce n'est lancer des dés. Maintenant, c'est complètement différent, il peut générer des images grâce à une installation locale de Stable Diffusion, il intègre des petits jeux jouables avec ses amis, il peut jouer de la musique d'ambiance, il est aussi capable de vous faire suivre une aventure dans le style des livres "dont vous êtes le héros" et encore quelques autres fonctionnalités qui sont listées plus bas.
Je le développe depuis février 2022 et je compte poursuivre son développement en fonction de mes besoins, car pour moi ce bot montre l'évolution de mes capacités de développement au fil du temps.
Chaque nouvelle fonctionnalité est arrivée avec son propre challenge et ses solutions, je vais essayer de retranscrire comment j'ai surmonté ou non ces difficultés.
La première fonctionnalité était donc le lancer de dés. Pour cela rien de plus simple que de générer des nombres aléatoires et de renvoyer le résultat. Le principal problème ici était l'utilisateur. En effet, ce bot a été conçu dans le but d'être utilisé non seulement par moi, mais aussi par mes camarades, qui ne sont pas des pros de la programmation. Le bot devait donc être capable de gérer les erreurs humaines. Pour cela, j'ai simplement ajouté des vérifications sur les entrées utilisateurs avant le traitement des commandes.
Pour faciliter l'utilisation du bot par les utilisateurs, j'ai aussi ajouté des commandes d'aides qui décrivent les fonctionnements des commandes ainsi que leur utilisation.
La deuxième fonctionnalité a été le changement de la configuration du bot. Pour fonctionner le bot lit tous les messages qui sont envoyés sur un serveur Discord. Pour différencier les messages lambdas des messages qui lui sont destinés, les utilisateurs utilisent un préfixe défini. Ici le préfixe est le caractère *. Cependant, je voulais que l'on puisse changer ce préfixe en cas de soucis de compatibilité entre plusieurs bots. C'est pour cela que j'ai créé une commande de configuration afin de changer les paramètres du bot.
Le bot est aussi capable d'interagir avec certains salons vocaux. Ces interactions ont un but purement divertissant et ne servent aucunement des besoins réels. Par exemple, le bot permet de jouer à la roulette russe grâce à des salons vocaux. Ces derniers possèdent tous le même nom, mais quand vous rejoignez le mauvais, vous êtes exclu temporairement du serveur Discord.
La principale difficulté d'implémentation liée à ces comportements était la récupération des états des salons vocaux puisque ces derniers sont stockés dans un cache spécial.
La suite logique à l'utilisation du bot pour des jeux de rôles était d'en faire plus qu'un simple simulateur de lancer de dés. J'ai donc implémenté des commandes pour récupérer des musiques depuis youtube et les faire jouer en vocal par une instance du bot. Pour cela, je me suis appuyé sur le framework Discord Player spécialisé dans le développement de bot de musique pour Discord. Le framework utilise du JavaScript et du TypeScript pour fonctionner.
Grâce au framework, je peux facilement récupérer des sons de vidéo youtube en ayant soit leur lien, soit par une recherche. Ainsi, même si l'utilisateur ne connaît pas le lien exact de la musique qu'il souhaite jouer, il peut simplement la chercher comme il le ferait sur youtube pour la jouer. Le bot intègre un système de queue pour jouer des sons à la chaîne. Il peut être mis en pause, arrêter ou encore jouer en boucle la queue.
Pour jouer les sons, le bot utilise la bibliothèque "voice" de DiscordJS. Le bot créé des "voice connections" au travers desquelles il joue des "audio player". Pour faire simple le bot se connecte dans le salon vocal des utilisateurs qui en font la demande, crée un lecteur audio qui diffusera la musique qu'ils ont demandée. Une fois la musique terminée, le bot détruit ce lecteur puis se déconnecte.
La principale difficulté de cette fonctionnalité était de faire jouer les musiques par le bot, car il fallait d'abord récupérer les ressources audio depuis les vidéos trouvées, puis les envoyer dans les lecteurs pour qu'elles puissent être jouées.
Vous voyez à droite un exemple de commande pour jouer la musique "One Kiss" de Calvin Harris et Dua Lipa.
Une fois le bot capable de jouer de la musique, je me suis demandé s'il ne pouvait pas encore plus m'aider pour mes parties de jeux de rôle. Ainsi, j'ai développé des commandes pour sauvegarder et gérer facilement plusieurs données liées aux personnages que l'on incarne dans les jeux de rôle. Le bot est capable de générer une nouvelle fiche personnage pour un nouveau joueur. Il peut aussi afficher cette fiche de personnage qui contient toutes les informations importantes du personnage. Il peut servir à gérer l'inventaire de son personnage et son porte-monnaie ou encore à récupérer des informations sur des PNJs. Les utilisateurs peuvent donc, facilement jouer leur partie de jeu de rôle via Discord tout en ayant à portée de main un outil pour sauvegarder leur progression.
Pour cela, je devais stocker les informations des joueurs, j'avais deux choix : soit passer par une base de données, soit stocker en local ces informations. J'ai choisi la deuxième option parce qu'elle me permettait d'être beaucoup flexible sur les données que je sauvegardais. Une base de données relationnelles comme MariaDB ou PostGre demandait de suivre des schémas de modélisation (forme normale) et donc de réfléchir bien en avance aux données que je voulais sauvegarder et manipuler. Comme je n'avais pas envie d'utiliser une telle base de données, j'aurais pu me tourner vers des alternatives comme MongoDB, mais à l'époque où je développais cette fonctionnalité, je ne connaissais tout simplement pas cette technologie. J'ai donc décidé de stocker mes informations sur des documents JSON stockés en local dans les fichiers du bot.
La principale difficulté de cette fonctionnalité était la manipulation des données, et encore une fois de gérer la liaison entre les utilisateurs inexpérimentés et le bot. Pour résoudre ce problème, j'ai repensé mon système de commande d'aide pour qu'il soit plus simple à comprendre et à utiliser. En voici un exemple à droite. J'ai rencontré plusieurs problèmes pendant le développement de ce jeu.
Le premier jeu que j'ai implémenté sur le bot était une reprise d'un autre bot dédié à ça que nous avons perdu. J'ai donc voulu essayer de recréer l'expérience. Le but du jeu est simple, chaque joueur possède des points de vie et toutes les trois heures, il peut récupérer une caisse qu'il pourra ouvrir pour récupérer de l'équipement : des armes, munitions ou du soin. Le but étant de tuer les autres joueurs pour récupérer leur équipement.
Le premier était d'implémenter un système de "loot box". En effet, les caisses ont des rangs de rareté et ont donc des chances différentes d'être récupérée, c'est pareil pour les armes, les munitions et les soins. Une fois le système de loot créé, il me fallait aussi un système pour récupérer ses caisses quand le bot n'était pas connecté. Comme j'héberge le bot sur ma propre machine, il n'est disponible que quand j'ai décidé de le rendre disponible, ce qui posait un problème d'égalité entre les joueurs au niveau de la récupération de caisse. Puisqu'elle n'était récupérable que quand j'étais disponible.
J'ai donc développé un système de récupération de message dans un salon spécifique. Dès que le bot se connecte, il récupère tous les messages du salon et les traite un à un. Il vérifie qu'ils soient bien espacés de trois heures et ajoute une caisse aléatoire dans l'inventaire du joueur qui a fait la commande avant de supprimer le message.
Le jeu avait maintenant toutes les bases pour fonctionner, les joueurs peuvent facilement récupérer des caisses et les utiliser. J'ai ensuite implémenter des nouvelles mécaniques de jeu : des objets d'armure pour encaisser des dégâts, des objets de soin pour récupérer de la vie, des nouvelles armes avec des mécaniques plus poussées comme des mines qui se déclenchent quand un joueur attaque, des C4 qui se déclenchent seulement quand le joueur veut, des armes qui touchent plusieurs personnes à la fois comme des grenades, des grenades flashs qui aveuglent et annulent l'attaque d'un joueur. J'ai également ajouté un système de statistique qui traque le nombre mort/kill d'un joueur, les dégâts qu'il a subit et infligé, combien de vie il s'est soignée, ... Un système de marché existe aussi, où l'on peut vendre et acheter des armes.
Actuellemnt le jeu fonctionne en "saison" c'est à dire qu'on définit une plage de temps pendant laquelle on joue, et à la fin le joueur qui a tué le plus de monde gagne, puis on réinitialise tous les équipements et on recommence. Ce système permet de donner des chances à tous le monde de gagner et d'avoir un effet boule de neige sur les premiers tués moindre.
Le deuxième jeu que j'ai implémenté est un jeu de collection de pokémons. Toutes les heures on peut explorer les hautes herbes à la recherche d'un pokémon sauvage. Le jeu nous propose de choisir entre 3 et 5 pokémons aléatoires à attraper. Le but du jeu étant de récupérer des pokémons et les entrainer pour disputer des duels contre les autres joueurs. Un mode PVE (contre des personnages non-joueur) est en cours de développement. La principale difficulté d'implémentation du jeu réside dans le système de combat. En effet tout le système de récupération des pokémons est fortement insipiré du jeu précédent mais avec quelques nouvelles spéficités.
Premièrement pour récupérer les pokémons il suffit simplement de réagir au message, plus besoin de se compliquer la vie avec beaucoup de commandes. Avant ça pour utiliser le bot il fallait forcément passer par des commandes. J'ai donc développé un système qui permet de passer par les réactions aux messages pour faire des choix sur les commandes précédente.
Par exemple ici pour attraper "Nidoqueen" il suffit de réagir avec l'émoji 1️⃣ au message. Chaque pokémon possède ses propres caractéristiques, un poids, une taille, un sexe, des statistques et ivs aléatoires, s'il est shiny (de couleur différente), des capacités différentes.
Une fois un pokémon attrapé, il peut être entrainer pour gagner de l'expérience, monter en niveau et débloquer des nouvelles compétences ainsi qu'améliorer ses statistiques. Les pokémons peuvent également évoluer lorsqu'ils atteignent un certain niveau. Comme dit plus haut la principale difficulté de ce système était de gérer les réactions aux messages pour éviter à l'utilisateur d'avoir à faire des commandes complexes.
Cependant, il est, pour l'instant, toujours impossible de se battre avec ses pokémons. C'est ici que s'est posée la plus grande difficulté technique de ce deuxième jeu, réussir à faire un système de combat qui gère à la fois le PVP (combat entre joueurs humains) et le PVE (combat contre des personnages non-joueur (PNJ)).
Le système de combat du jeu est au tour par tour, à chaque tour le joueur choisit s'il veut changer de pokémon ou attaquer, et s'il attaque, avec quelle compétence attaquer. Ensuite l'autre joueur fait la même chose. Puis on procède à l'exécution des choix. Si les points de vie d'un des pokémons tombent sous les 0, il est K.O. pour le combat et son propriétaire doit en envoyer un nouveau si c'est possible, sinon il a perdu le combat.
Pour gérer le système de combat, j'ai decidé de procéder commme pour la capture des pokémons, en utilisant les réactions aux messages, cela permet facilement d'indiquer au joueur les choix qu'il peut faire et de les récupérer pour les traiter. De plus cela limite l'intéraction de l'utilisateur et donc le nombre d'erreur qu'il peut commettre.
Le bot envoie donc un message pour afficher quels sont les premiers pokémons qui vont combattre et affiche leur "barre" de vie. Puis il demande à l'utilisateur de choisir son attaque. Une fois l'attaque lancée il affiche à nouveau les barres de vie actualisées et demande à l'autre utilisateur ce qu'il souhaite faire et ainsi de suite. Une fois le combat terminé il affiche qui est le gagnant.
Avec ces fonctionnalités implémentées, le jeu est jouable, mais il manque encore beaucoup de choses. Il est prévu que je peaufine le système de combat pour qu'il soit plus digeste visuellement, pour l'instant il y a beaucoup de message qui s'envoie et la partie asynchrone du code pose un problème dans l'ordre d'affichage des messages, souvent le message de fin de combat est envoyé avant celui de la mise K.O. du dernier pokémon.
Il est aussi prévu que j'ajoute une sorte de mode histoire, dans laquelle il faudra battre des arènes pour gagner des badges et dans laquelle les joueurs auront leur propre arène. Un système pour gérer des objets est aussi prévu, mais arrivera beaucoup plus tard.
Une autre fonctionnalité du bot est de pouvoir faire jouer une aventure personnalisée basée sur le fonctionnement des livres "dont vous êtes le héros". Cette fonctionnalité est assez simple à utiliser. On lance l'histoire de son choix avec une commande, le bot affiche un message contenant l'aventure qui propose des choix, l'utilisateur choisit ce qu'il veut faire et l'aventure continue.
Pour gérer le choix des joueurs, je me suis sur la même fonctionnalité que pour les combats pokémons, c'est-à-dire en réagissant au message. La spécificité de cette fonctionnalité est plutôt orienté pour les auteurs des aventures. Un joueur peut faire plusieurs choses : soit un choix définit, soit un test de compétence, soit mourir, soit terminer un chapitre ou alors rien du tout. Dans chaque cas, sauf celui de la mort où l'histoire s'arrête, le résultat est que l'histoire est amené à une nouvelle "page". Il faut donc pouvoir dire au bot quelle page choisir en fonction du résultat indiqué par l'utilisateur.
Pour cela chaque page est stockée dans un fichier JSON qui possède plusieurs champs, un champ "text" qui comprend le texte à afficher dans le message (limité à 2000 caractères par Discord) et un autre champ personnalisé qui indique le type d'évènement : "dice_roll", "choice", "death", "end" ou "nextPage".
Dans le cas d'un test de compétence, le champ "dice_roll" est un objet qui indique le nom de la compétence, s'il y a un bonus ou un malus, le numéro de la page en cas de réussite et en cas d'échec.
Pour un choix simple, le champ "choice" est un tableau qui comprend dans l'ordre des choix le numéro des pages où naviguer quand le choix est fait.
En cas d'un choix qui amène à la mort du joueur, le champ "death" est défini sur true, ce qui permet au bot de bloquer l'histoire et de mettre à jour le personnage du joueur.
Si la page est la dernière page d'un chapitre ou de l'histoire, le champ "end" est défini sur true.
Si aucun de ces cas ne se produit, c'est que la page mène simplement à une autre page, dans ce cas le champ "nextPage" est un entier qui indique le numéro de la page suivante.
Maintenant que la logique des pages est créé, il faut créer une logique pour les personnages des joueurs. Les personnages sont sauvegardés dans un fichier JSON à part, pour chaque personnage, on va sauvegarder plusieurs informations importantes : l'identifiant discord du propriétaire afin de pouvoir l'identifier facilement, le numéro du chapitre où se trouve le joueur ainsi que le numéro de la page, ensuite les informations sur le personnage (ses statistiques) et enfin on sauvegarde aussi si le personnage est mort ou non.
Avec toutes ses informations, il est facile pour le bot de savoir où se trouve le joueur et ce qu'il peut faire ou non. Par exemple si un personnage est mort et qu'un joueur essaie de continuer l'histoire avec lui, le bot va simplement afficher un message d'erreur disant que s'il veut continuer l'histoire, il doit réinitialiser son personnage, ou sa progression.
Ainsi, le bot peut maintenant facilement afficher les textes correspondants aux pages auxquelles sont rattachés les joueurs, tout en gardant une trace de leur progression et de leur statut, ce qui permet aux joueurs de suivre l'histoire avec fluidité. D'autres commandes sont intégrées, des commandes pour arrêter ou reprendre une histoire afin de pouvoir faire une pause, des commandes pour réinitialiser sa progression et bien sûr des commandes pour générer des personnages avant le début d'une histoire.
La dernière fonctionnalité que j'ai ajoutée au bot est de pouvoir générer des images grâce à une installation de Stable Diffusion 2, installée en locale sur ma machine.
Cette fonctionnalité permet en spécifiant la description d'une image, de générer 4 images avec une IA, d'afficher les résultats et de laisser le choix à l'utilisateur d'agrandir une des 4 images ou de relancer une génération avec les mêmes paramètres. De plus il est possible de configurer la génération directement depuis le bot.
Pour cela, j'ai utilisé A1111, qui est une interface web pour Stable Diffusion implémentée en utilisant Gradio library. Plus d'informations ici.
A1111 en plus d'ajouter une interface facile à utiliser, met en place une API pour discuter avec l'installation SD2, ce qui permet de changer la configuration de cette dernière ou de lancer et récupérer des générations.
Ainsi pour cette fonctionnalité, j'ai simplement configuré l'API de A1111 pour l'appeler depuis mon bot. La principale difficulté de cette fonctionnalité ne réside pas dans l'implémentation des appels à l'API, mais à la récupération des images une fois générée et aux stockages de ces dernières, puisque j'ai besoin d'avoir ces images pour pouvoir les agrandir si nécessaire.
Une fois que A1111 renvoie mes images à mon bot, ce dernier les stocke dans un fichier local avec comme nom l'identifiant du message de succès envoyé à l'utilisateur. Puis, il les compile et les affiche dans un message qu'il renvoie à l'utilisateur.
Au démarrage du bot, il nettoie entièrement le dossier de stockage des images, ce qui permet de ne les garder en mémoire que peu de temps et donc de ne pas surcharger rapidement l'espace de stockage du bot.
Comme dit plus haut, l'utilisateur peut, au choix, modifier la configuration de la génération des images via une commande. Il peut choisir de modifier différent champ comme la hauteur et la largeur de sortie des images, le modèle de génération et d'autres paramètres techniques.
Grâce à ça, il est beaucoup plus facile de générer des images en accord avec ses attentes.
En plus de générer les images, l'utilisateur peut choisir d'agrandir une image qui lui plait parmi celles générées. Pour cela le bot passe encore par l'API A1111 et le système de récupération des images est le même que celui présenté au-dessus. Voici un résultat d'agrandissement d'une des images générées précédemment.
Pour chaque génération / agrandissement, le bot affiche aussi un message montrant la progression de la génération et le temps approximatif restant.
Aujourd'hui les fonctionnalités du bot sont très nombreuses, même si la plupart ne sont pas/plus utilisées. Pour moi ce projet reflète l'évolution de mes compétences au fil du temps. Chaque jour, j'essaie petit à petit d'améliorer tout ce que j'ai fait afin que le bot soit de plus en plus performant, facile à utiliser et surtout plus facile à implémenter.
Actuellement, j'essaie de faire passer l'exécution des commandes directement en "slash command". Avant pour saisir une commande, il fallait simplement envoyer un message avec un prefix définit ("*"). Cependant, Discord implémente un système de commande avec le préfixe "/" qui permet de ne pas directement envoyer de message et qui permet de faire passer l'exécution de ces commandes en "local", c'est-à-dire qu'elles ne sont visibles uniquement par l'utilisateur et non par tous ceux qui ont accès au salon.
La transition des commandes textuelles vers les commandes "slash" demande beaucoup de temps puisqu'il faut que j'adapte tout mon code à cette nouvelle fonctionnalité.