Connect 4
Idée
Un jour je jouais au puissance 4 avec un ami sur Discord (logiciel de discussion en ligne similaire à Skype).
On utilisait les caractères Unicode suivant en guise de plateau :
⚫⚫⚫⚫⚫⚫⚫
⚫⚫⚫⚫⚫⚫⚫
⚫⚫⚫⚫⚫⚫⚫
⚫⚫⚫⚫⚫⚫⚫
⚫⚫⚫⚫🟡⚫⚫
⚫⚫⚫🔴🔴🟡⚫
C'était assez fun mais très laborieux. Mon ami m'a alors donné l'idée de créer un bot pour automatiser le procédé. Nous avons aussi eu l'idée d'ajouter un système de classement type Échecs avec un tableau des scores, car l'idée de rendre compétitif ce jeu simple nous faisait marrer.
Principe d'un bot Discord
Discord fonctionne de manière différente à Skype : Il y a bien des canaux de discussion privés et des groupes de discussion, cependant la majorité des utilisateurs préfère rejoindre des serveurs. Un serveur Discord contient plusieurs canaux de discussion vocaux ou textuels. Les serveurs Discord regroupent généralement un grand nombre d'utilisateurs, pouvant aller jusqu'à plusieurs milliers.
Les serveurs ont aussi la possibilité d'inviter des bots. Ce sont des comptes qui se présentent comme un compte utilisateur classique, mais qui est géré par un programme à travers une API. Les utilisateurs interagissent avec les bots par le biais de commandes : Par exemple quelqu'un écrit dans le tchat &help
et le bot lui répond avec un message d'aide.
Réalisation
Généralités
J'ai développé le bot en TypeScript et avec la stack Node.js.
Le principe de fonctionnement de bot est le suivant :
- Un utilisateur qui veut jouer tape la commande
&play
et le bot répond avec un message contenant un plateau de jeu. - Le bot ajoute ensuite des réactions à son propre message. Chaque reaction est un numéro de colonne jouable.
- Pour jouer dans une colonne, le joueur n'a plus qu'à cliquer sur une des réactions comme s'il s'agissait d'un bouton.
- Le bot met à jour son message et le second joueur peut à son tour jouer.
À la fin de chaque partie, les participants gagnent ou perdent une certaine quantité de points. Cette quantité de point est déterminé par une implémentation du classement Elo qui est utilisé au Échecs. Les meilleurs joueurs apparaissent dans le classement global qui est visible quand on tape la commande &lead
:
Le bot possède plein d'autre fonctionnalités / commandes :
- On peut lister toutes les parties non terminées.
- On peut reprendre une vielle partie non terminée.
- Il est possible de déclarer forfait.
- Il est possible de réclamer la victoire quand l'adversaire a arrêté de jouer mais n'a pas déclaré forfait.
- On peut afficher les statistiques d'un joueur.
- Afin d'être conforme à la RGPD, toutes les données d'un utilisateur peuvent être supprimées et un utilisateur peut cacher ses statistiques.
- Le bot est paramétrable par le gestionnaire du serveur où il a été invité : la langue du bot peut être modifiée ainsi que son fuseau horaire.
- Un système monétaire a été mis en place : en gagnant une partie, les joueurs gagnent de l'argent qu'ils peuvent ensuite utiliser pour modifier l'apparence de leurs pions.
Base de données
Une base de données est nécessaire pour stocker toutes les données relatives aux parties, aux joueurs et aux serveurs Discord. J'ai choisi d'utiliser MySQL car la partie du code qui gère les données est orientée objet et il est facile de mapper une classe à une table via un ORM.
En ce qui concerne l'ORM, j'ai choisi d'utiliser TypeORM, car il s'intègre très bien avec TypeScript. Il est également beaucoup plus simple d'utilisation et est moins lourd que le précédent que j'avais testé : Sequelize.
Pour ce qui est des données elles-mêmes, elles sont toutes stockées en base de données. Les seules données stockées en RAM consistent uniquement de données misent en cache et de données concernant les interactions immédiates avec les utilisateurs. Cela permet une grande résilience : Si le bot se coupe pour un problème quelconque : coupure réseau, coupure électrique, crash du bot, downtime de l'API, mais aussi mise-à-jour, au redémarrage il peut reprendre comme si de rien n'était, sans perdre d'information. Cela a aussi l'avantage qu'il sera plus facile de mettre en place le sharding qui consiste à diviser la charge de travail sur différents processus. Le sharding est obligatoire pour les bots présents sur beaucoup de serveurs et il sera peut-être nécessaire que je le mette en place.
CI / CD
Le bot est déployé sur un Raspberry Pi 4. Vu le nombre de serveurs déjà présent sur la machine, tout est géré avec des conteneurs pour éviter les conflits et aussi tout simplement, car c'est beaucoup plus simple à gérer en général.
Il faut donc construire une image Docker. La forge que j'utilise (GitLab) permet de mettre en place un pipeline qui est lancé à chaque commit et qui peut être utilisé pour construire l'image. Coup de chance : GitLab permet aussi d'héberger des images Docker.
Malheureusement les Raspberry utilisent une architecture ARM et GitLab est sous x86-64. Afin de construire une image pour une architecture différente j'utilise Buildx qui de son côté utilise Qemu.
Pour résumer, à chaque fois que je fais un commit, une image Docker est construite et je n'ai plus qu'à la télécharger sur le Raspberry et la lancer. Tout ça à l'air très compliqué, mais dans les fait, tout tient dans le petit fichier suivant :
variables:
CI_BUILD_ARCHS: linux/arm64,linux/amd64
DOCKER_HOST: tcp://docker:2375/
stages:
- build_files
- build_image
build_files_job:
only:
- master
image: cl00e9ment/node.js-builder:light
stage: build_files
script:
- pnpm i
- pnpm run build
- pnpm prune --production
- mkdir app
- mv build lang node_modules res package.json app
artifacts:
paths:
- app
expire_in: 1 hour
build_image_job:
only:
- master
image: jonoh/docker-buildx-qemu
stage: build_image
services:
- docker:dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
# Use docker-container driver to allow useful features (push/multi-platform)
- docker buildx create --driver docker-container --use
- docker buildx inspect --bootstrap
script:
- update-binfmts --enable # Important: Ensures execution of other binary formats is enabled in the kernel
- docker buildx build --platform $CI_BUILD_ARCHS --progress plain --pull -t "$CI_REGISTRY_IMAGE" --push .
Un succès inattendu
Contre toute attente le bot a eu un succès largement supérieur à ce que j'avais imaginé. Visiblement le Puissance 4 compétitif a de l'avenir...
J'ai publié le bot sur un site non officiel qui recense les bots et serveurs Discord (top.gg). À partir de ce moment là j'ai gagné beaucoup d'utilisateurs. Au moment où j'écris cet article plusieurs milliers d'utilisateurs ont joué au bot et ce dernier est présent sur plus de 500 serveurs.
Le bot à ensuite été vérifié officiellement par Discord, ce qui est nécessaire pour croître au-delà de 100 serveurs.
Ce succès vient avec des causes intéressantes, comme des joueurs qui tentent d'exploiter le système d'Elo pour atteindre la première place du classement en utilisant plusieurs comptes secondaires qu'ils font perdre à leur avantage. J'ai donc dû mettre en place un système d'anti-triche qui attribue, à chaque joueur, un coefficient indiquant la probabilité de triche. Quand ce coefficient est trop haut, une vérification manuelle est faite et si triche il y a, le joueur est banni.
Conclusion
J'ai vraiment apprécié ce petit projet car il m'a permis de toucher à plein de choses différentes :
- TypeScript / Node.js
- MySQL / SQL
- le CI / CD de GitLab
- Docker
C'est également un projet que j'ai terminé en un lapse de temps raisonnable : quelques semaines. Enfin, il y a non seulement la satisfaction d'un projet terminé, mais également la satisfaction que je ne l'ai pas fait que pour moi : d'autres personnes semblent apprécier le bot.