Retour d'une intégration partie 1/2 : Le pentago en Python et Symfony

Clément Le Biez
Clément Le BiezMarch 17, 2021
#integration#php#python

Durant 4 semaines, il m'a été demandé d'implémenter le jeu Pentago de différentes manières et dans différents langages informatiques.

  • D'abord pouvoir jouer dans mon Terminal en Python.
  • Puis à l'aide d'un Framework MVC en Symfony.
  • De créer ensuite une IA permettant de conseiller les meilleurs coups à jouer en Go.
  • Enfin, d'y jouer sur une application mobile, en React Native.

Chez Marmelab, toute nouvelle recrue passe par une période d'intégration de ce type. Les objectifs sont multiples :

  • Pour l'équipe, il est d'évaluer les compétences des petits nouveaux.
  • Pour moi, ce fut surtout une superbe opportunité d'apprendre plusieurs langages, algorithmes et façons de faire.

Ce premier article s'attachera à un retour d'expérience sur mes deux premières semaines d'intégrations.

Mais avant cela, expliquons le principe du jeu.

C'est quoi le pentago ?

Le pentago est un jeu à 2 joueurs qui se déroule sur un plateau carré de 6x6 cases séparées en 4 carrés (de 3x3).

Un plateau de pentago

Chacun à son tour, les joueurs vont placer une bille de sa couleur dans l'un des emplacements et tourner obligatoirement l'un des 4 carrés d'un quart de tour dans le sens de son choix.

La partie s'arrête lorsque l'un des 2 joueurs a réussi à aligner 5 de ses billes. Elle peut aussi se terminer par une égalité si le plateau a été rempli sans alignement ou si les deux joueurs ont chacun réussi à aligner 5 billes en même temps.

Au Pentago, le gagnant n'est déterminé qu'à la fin du tour d'un joueur. C'est-à-dire qu'il ne peut gagner qu'après avoir tourné un carré. C'est cette étape qui peut provoquer un alignement des billes de l'adversaire !

Le Pentago en ligne de commande avec Python

Lundi, au matin, le petit café en visio avec toute l'équipe sonna donc le top départ pour mes 4 semaines d'intégration chez Marmelab !

La première semaine d'intégration met le focus sur l'algorithmie et les tests unitaires. J'ai donc eu pour mission d'implémenter le Pentago dans un langage que je ne connaissais que très peu : Le Python.

Logo de Python

Ce fut également l'occasion de me familiariser avec la façon de développer de l'équipe et ses bonnes pratiques (code reviews, pull requests, dailys, Trello, Makefile etc...)

To-do list d'un bon répertoire Marmelab

Chaque répertoire Github chez Marmelab possède la configuration suivante :

  • Un Readme.MD : permettant de lancer le projet sur ton poste sans aide du mainteneur.
  • Un Docker ou Docker-compose: pour ne pas avoir à installer le langage sur ta machine (et déployer en production facilement !)
  • Un Makefile, avec une commande help permettant de lister toutes les commandes disponibles.
  • Jouer les vérifications (tests, lint) en CI, généralement on utilise Travis mais pour ma part j'ai choisi Github Actions

Concernant les pull requests, pour faciliter les relectures, on privilégie un certain format :

  • Lien vers la carte Trello
  • Une todo list de ce que tu as implémenté
  • Une image ou un GIF montrant ce que la PR apporte en plus au produit.

Une pull request bien documentée

François me présente donc le Trello comportant les users stories que je devrais implémenter en Python. Très vite se pose la question du choix du modèle de données.

Comment représenter mon board ?

Ma première idée est de commencer déjà par un tableau à deux dimensions de taille 6x6, c'est l'option la plus évidente.

Très vite, j'identifie qu'une fonctionnalité va me poser problème. Comment tourner un carré de mon tableau à deux dimensions ?

Exemple animée d'une partie de pentago

Tourner un tableau avec NumPy

Après avoir écumé les moteurs de recherche pour trouver des solutions algorithmiques, je finis par tomber sur ma sauveuse : la librairie NumPy.

En plus de fournir des méthodes de mathématiques avancées, NumPy propose également des fonctions permettant de manipuler très simplement des tableaux à N dimensions, dont effectuer une rotation à 90 degrés.

import numpy as np
// Définit arr comme étant un tableau NumPy.
arr = np.array(arrayClassic)

// Je tourne donc ici mon tableau à 2 dimensions de 90 degrés.
// 1 tourne dans le sens anti-horaire, -1 tourne dans le sens horaire.
arr = np.rot90(arr, 1)

Étant donné que l'on ne tourne pas l'ensemble du board, mais simplement une partie, il faut être capable de l'extraire.

Là encore, en utilisant la fonction slice de Python, NumPy a la solution :

import numpy as np
// board ayant été défini comme un tableau NumPy à 2 dimensions.
quarter = board[(slice(0, 3), slice(3, 6)];
len(quarter) // === 3
len(quarter[0]) // === 3

// Je tourne mon carré dans le sens anti-horaire
quarter = np.rot90(quarter, 1);

// J\'écrase l\'ancien carré de mon board par celui qui a été tourné.
board[(slice(0, 3), slice(3, 6)] = quarter;

slice(3, 6) permet de retourner un tableau contenant les entrées de la colonne 3 (inclus) à 6 (exclus). Donc les 3 dernières colonnes.

Grâce à NumPy je peux interroger un tableau à N dimensions en lui demandant une slice pour chaque dimension.

board[(slice(0, 3), slice(3, 6)] permet donc de récupérer les 3 dernières colonnes des 3 premières lignes.

Il suffit ensuite de tourner ce carré puis de réécrire aux mêmes tranches le carré du board par celui qui vient d'être tourné.

Finalement ce qui me semblait prendre du temps et de la réflexion fut extrêmement simple !

Au final, j'ai été très satisfait du résultat puisque j'ai réussi à terminer l'implémentation du jeu jouable à deux joueurs.

Exemple animée d'une victoire au pentago

Ce que j'ai appris

Python est un langage que je ne connaissais pas, mais qui est très facile d'apprentissage.

En une semaine je suis rapidement monté en compétence dessus.

Évidemment je ne suis pas devenu un expert en Python, mais je pense être capable à présent de réaliser des programmes poussés en utilisant ce langage.

Mention spéciale pour le site W3School pour la qualité de sa documentation.

Ce que je peux améliorer

Lors d'un sprint planning, on estime le temps (dans mon cas en fraction de journée) pour chaque tâche. La difficulté ici était d'estimer chaque tâche d'un projet que je ne connaissais pas dans un langage que je ne connaissais pas non plus. Toutes mes estimations étaient donc globalement fausses.

Aussi, j'ai oublié qu'il n'y avait pas que la partie "développement" dans la réalisation d'une tâche mais que d'autres éléments chronophages venaient s'y ajouter.

La réalisation des tests, la code review qui génère des allers-retours et les ajustements de dernières minutes font globalement gonfler le temps effectif d'une carte. Prévoyons en conséquence !

J'ai également eu droit à plusieurs remontées de mes collègues m'ayant relu car j'étais un peu avare sur les commentaires. Notamment en utilisant NumPy.

Le code est disponible sur ce repo GitHub.

Deuxième semaine : Framework MVC avec Symfony

Pour la deuxième semaine de cette intégration, mon objectif était d'implémenter le Pentago à 2 joueurs sur 2 écrans différents à l'aide d'un framework MVC de mon choix. Le jeu devait également se dérouler en temps réel et ainsi voir les coups de l'adversaire en temps réel sur son écran.

Ce fut donc Symfony: Il profite d'une excellente communauté et j'avais déjà quelques bases que je souhaitais fortement approfondir.

Logo de Symfony

Implémenter le multijoueur ajoute également la contrainte de devoir déployer cette application sur le web. Quelque chose dont je ne suis pas particulièrement familier.

Mon premier choix fut donc d'utiliser un PaaS (Platform as a Service), globalement pour sa simplicité d'utilisation : j'ai choisi Clever Cloud.

En plus, si tu te connectes avec ton compte Github, tu peux directement spécifier quel répertoire déployer, et il s'occupe de tout pour toi à chaque push sur la branche principale !

1 heure plus tard : juste le temps qu'il faut pour configurer la connexion avec la base de données, la gestion des migrations etc... mon application était en ligne et utilisable en HTTPS.

Page d'accueil en ligne du jeu Pentago en Symfony

Le temps réel avec Mercure

Pour pouvoir voir en temps réel les coups de mon adversaire, j'ai choisi d'utiliser Mercure.

C'est un protocole implémentant le Server Send Event (SSE) créé par Kévin Dunglas permettant d'envoyer des informations à des clients sans que ceux-ci n'aient à demander la ressource.

L'autre solution plus connue est d'utiliser des websockets, très utilisé en NodeJS, moins en PHP. En plus Symfony embarque depuis sa version 4.2 un composant Mercure qui simplifie énormément son implémentation.

Mercure en bref

Pour utiliser Mercure, il faut lancer un hub développé en Go que l'on peut télécharger sur le site sous forme d'exécutable ou d'image Docker. Ce hub permet de faire le passe-plat entre les clients qui attendent une notification et le serveur qui notifie.

Schéma d'utilisation de mercure

Concrètement, le back end va envoyer un appel HTTP Post classique à Mercure via le code suivant :

use Symfony\Component\Mercure\PublisherInterface;
use Symfony\Component\Mercure\Update;

public function notify(PublisherInterface $publisher, UuidV4 $gameId, array $params)
{
    // Generate this URL :
    $url =  $this->generateUrl('game', [
        'id' => $gameId,
    ], UrlGeneratorInterface::ABSOLUTE_URL);

    // Create an Update object
    $update = new Update(
        $url,
        json_encode(["status" => $params["status"], "value" => $params["value"]])
    );

    // The Publisher service is an invokable object
    $publisher($update); // Publish to the mercure hub to notify listeners
}

Le hub mercure n'oblige pas à grand chose en termes de formalisme, si ce n'est qu'il a besoin tout de même d'un topic, généralement l'url de la ressource que nos utilisateurs écoutent, passé en premier paramètre :$url qui correspond à la route de ma ressource Game. Le second paramètre étant pour moi un tableau associatif, mais c'est en fait ce que vous voulez, qui sera communiqué à tous les clients qui écoutent le même topic et encodé en Json.

Pour que cette communication soit effective, il faut que le hub Mercure et le serveur partage un Json Web Token (JWT) en guise d'authentification, sinon le Hub vous renverra une erreur HTTP 401.

Côté Front, on utilise l'API EventSource que j'ai également trouvé très simple :

  // URL is a built-in JavaScript class to manipulate URLs
  const url = new URL("{{ MERCURE_PUBLISH_URL }}");
  url.searchParams.append('topic', 'game/:id');

  const eventSource = new EventSource(url);
  eventSource.onmessage = event => {
    const data = JSON.parse(event.data);
    console.log("Data received form Mercure", data);
  }

La variable data contiendra tout ce que l'on aura envoyé depuis le serveur.

C'est ainsi que j'ai mis en place le temps réel durant une partie de Pentago :

Démonstration d'une partie de pentago en Symfony

Un système de permission plus poussé

En fait Mercure va encore plus loin.

Il possède un système de permission très avancé qui permet de partager des JWT entre le server et le Hub, ou entre le client et le Hub avec des routes autorisées permettant à un certain type d'utilisateurs de ne pouvoir écouter que certains topics. C'est au final un outil très puissant et complet.

Durant l'authentification du back-End vers le Hub, il est possible de spécifier dans le JWT quel sont les topics qui peuvent être notifié :

{
  "mercure": {
     "publish": [
        "http://example.com/games/*",
        "http://example.com/resources/one-specific"
     ]
  }
}

Dans mon cas, je n'ai qu'un seul back-end et je n'avais aucun intérêt à restreindre ses permissions. Le JWT possédait plutôt ce contenu :

{
  "mercure": {
     "publish": ["*"]
  }
}

Durant l'authentification, le client peut recevoir un JWT sous forme de cookie en provenance du serveur contenant quelque chose de ce type :

{
  "mercure": {
     "subscribe": [
        "http://example.com/games/*",
        "http://example.com/resources/one-specific"
     ]
  }
}

Côté client on lui demande ensuite d'utiliser ce cookie :

const eventSource = new EventSource(url, {
    withCredentials: true
});

Et le client va automatiquement l'utiliser pour s'authentifier auprès du hub mercure. Il ne pourra alors écouter que des topics dont l'URL, ou le format d'URL, ont été passés en paramètre.

Pour ma part je n'ai gratté que la surface de cette technologie et n'ai pas installé un tel système.

C'était une superbe expérience que de mettre en place Mercure.

Malheureusement, je n'ai pas réussi à le déployer sur Clever Cloud car il semble y avoir un souci de mapping entre le port HTTPS qu'utilise Clever Cloud et celui dont notre Hub a besoin pour fonctionner.

Cela semble en fait possible mais comme je n'avais qu'une petite semaine pour tout faire, j'ai préféré rapidement passer sur un hebergement AWS en déployant, via un docker-compose, tous mes services d'un coup afin d'assurer une livraison du projet fonctionnel dans les temps.

Ce que j'ai appris

  • Utiliser un PaaS comme Clever Cloud est une opération simple et efficace qui permet en un temps record d'avoir une application qui tourne en HTTPS sur le web. Évidemment, tout ce qui est simple reste peu configurable et atteint certaines limites que j'ai pu rencontrer notamment avec Mercure.

  • Déployer toute une stack sur un AWS en utilisant un EC2 était quelque chose que je n'avais jamais fait. La documentation très complète de la plateforme et mes collègues aguéris en la matière m'ont permis d'enjamber cette difficulté sans perdre trop de temps.

  • Mettre en place du temps réel avec Mercure qui s'avère être une chouette alternative aux websockets !

Ce que je peux améliorer

  • Les animations de déplacement du pion et de rotation ne s'effectuent que pour le joueur qui ne joue pas.

  • La seule entité Doctrine que j'ai eu à utiliser est celle représentant un jeu. Il n'y en avait pas pour les joueurs. Cela aurait pu être intéressant d'avoir un système d'authentification très basique afin de pouvoir par exemple retrouver ses anciennes parties.

  • Au final, mon application est hébergée sur AWS. Comme j'ai dû migrer la stack d’hébergement à quelques heures de la fin du projet, je n'ai pas pu remettre en place un système de déploiement automatisé lors d'un merge sur la branche principale.

Démonstration d'une victoire au pentago en Symfony

Le code est disponible sur ce repo GitHub.

Did you like this article? Share it!