Retour d'une intégration partie 2/2 : Le pentago en Go et React Native

Clément Le Biez
Clément Le BiezApril 22, 2021
#integration#golang#react-native

Pour ce second article à propos de mon intégration chez Marmelab, je vais me concentrer sur les deux dernières semaines :

  • La création d'une aide à la décision en Go pour calculer le meilleur coup possible.
  • La réalisation d'une application mobile en React Native pour jouer au Pentago.

Pour lire la première partie, c'est par ici

Une intelligence artificielle en Go pour jouer au Pentago

Pour cette troisième semaine d'intégration, l'objectif est de créer en Go une intelligence artificielle qui retourne les coups les plus intéressants pour un joueur dans une situation donnée. Le résultat devra s'afficher dans une ligne de commande et dans un temps de traitement raisonnable.

Logo du langage Go

L'algorithme le plus connu pour ce genre de calcul s'appelle le Minimax.

Calculer le meilleur coup possible grâce au Minimax

Minimax est un algorithme idéal pour déterminer le meilleur coup possible dans des jeux comme les échecs, le morpion, le puissance 4 ou encore dans notre cas le Pentago. Ce sont des jeux où la défaite d'un joueur entraîne la victoire de l'autre.

L'objectif de cet algorithme est de passer en revue l'ensemble des coups possibles et de leur assigner une valeur qui prend en compte les bénéfices pour le joueur et pour son adversaire.

Le meilleur choix devient alors le coup qui a le score le plus élevé pour nous, sachant que l'adversaire lui essayera de faire l'inverse et de minimiser notre gain. L'algorithme Minimax est donc un algorithme récursif qui passera en revue tous les mouvements pour un joueur dans une situation. Et pour chacun de ces mouvements, passera en revue tous les mouvements suivants dans cette nouvelle situation.

Cela forme donc un arbre de situations à évaluer.

Lorsque l'on arrive au feuille de notre arbre, c'est à dire une situation finale, il y a différentes possibilités :

  • La situation est finie, quelqu'un a gagné
  • Il y a égalité : le plateau est rempli et personne n'a gagné
  • Les deux joueurs ont un alignement au même moment et dans ce cas, c'est également une égalité
  • L'algorithme a atteint une profondeur maximale passé en paramètre.

Représentation d'un arbre de possibilité de l'algorithme Minimax

Parcourir l'ensemble des possibilités du Pentago requiert une puissance de calcul gigantesque et durerait plusieures heures. On préférera alors ne parcourir que jusqu'à une certaine profondeur. Dans mon cas j'ai majoritairement utilisé cette algorithme à une profondeur de 3 ou 4.

On attribuera à tous ces états finaux un score qui représente l'avantage qu'a un joueur par rapport à un autre. Dans notre cas ce calcul retourne une valeur comprise entre +10000 (le joueur a gagné) et -10000 (l'adversaire a gagné).

Plus le joueur réussira à aligner un nombre important de combinaison plus son score sera élevé. L'inverse pour son adversaire est aussi vrai.

Par exemple si le joueur possède 2 alignements de 4 billes et 6 alignements de 2 billes, le score sera de 2060.

L'objectif ensuite sera de faire remonter les valeurs calculées dans les feuilles de notre arbre vers les nœuds parents.

Chaque nœud parent recevra donc N valeurs et ne devra en garder qu'une seule à faire remonter à son nœud parent. Si c'est au tour de l'adversaire, il minimise le score en prenant le coup qui a le plus faible score, car c'est celui le plus à son avantage.

Calcul du score le plus intéressant pour l'adversaire dans un arbre Minimax

Dans notre exemple, pour le premier noeud jaune, il choisit donc -120 qui est la plus petite valeur parmis ses enfants. Les autres nœuds du même niveau font également le choix du plus petit chiffre.

Si le nœud parent correspond au tour du joueur 1, cela signifie qu'il prendra le meilleur coup possible parmi ces N valeurs provenant de ses nœuds enfants. On dit qu'il maximise.

Représentation du tour du joueur 1 qui maximise dans l'algorithme Minimax

Dans notre cas, il choisira donc la valeur 450 et on considèrera que le coup 1, pour ce joueur 1 aura pour conséquence d'amener à une situation qui vaut 450.

On cherchera alors finalement le meilleur score remonté parmi tous les coups possibles pour ce joueur 1.

Hormis le calcul du score et la génération des possibilités pour une situation donnée, l'algorithme Minimax est un algorithme plutôt générique dont voici le code que j'ai largement commenté pour l'occasion.

func Minimax(depth int, board game.Board, firstPlayer string, currentPlayer string, move Move) int {

    // On regarde si pour cette situation, la partie n'est pas terminée
    gameStatus, score := EvaluateGameStatus(player1, player2, firstPlayer)

    // Si la partie est terminé, c'est une feuille de notre arbre
	if gameStatus != constants.GAME_RUNNING {
        // On retourne donc le score
		return score
	}

    // Si la partie n'est pas terminé mais qu'on a atteint notre profondeur de recherche
	if depth == 0 {
        // On calcule et on retourne le score
		score := EvaluateScore(player1, player2)
		return score
	}

    // A ce stade, cela signifie que le nœud ou nous sommes n'est pas une feuille
    // Il faut donc aller chercher plus profondément.

    // On récupère tous les coups possibles
	moves := GetAllPossibleMoves(board);

	var bestScore int

    // On définit le plus mauvais score pour un joueur donné.
    // Pour le joueur 1 c'est -10000
	if currentPlayer == firstPlayer {
		bestScore = -10000
	} else {
        // Pour le joueur 2 c'est 10000
		bestScore =  10000
	}

    // On itère sur chacun des coups
	for _, move := range moves {

        // Ce coup est ajouté au plateau de jeu
		newBoard := ApplyMoveOnBoard(board, move, currentPlayer)
		opponent := SwitchPlayer(currentPlayer)

        // On appel récursivement le minimax en ayant pris soin de modifier le joueur courant.
		childScore := Minimax(depth - 1, newBoard, firstPlayer, opponent, move)

        // Chaque coup nous aura retourné un score.

		if currentPlayer == firstPlayer && bestScore < childScore {
            // Ici bestScore contiendra toujours forcément le plus haut score retourné par les enfants.
            // C'est ici que l'on maximise.
            bestScore = childScore
		} else if currentPlayer != firstPlayer && bestScore > childScore {
			// A l'inverse, on minimise en prenant toujours le plus petit score si le joueur courant est le joueur 2.
            bestScore = childScore
		}
	}
	return bestScore
}

L'élagage Alpha Beta : un gain de performance surprenant

En réalité, nous n'avons pas besoin de parcourir toutes les possibilités de notre arbre. On peut, dans certaines situations, interrompre la recherche d'une branche. L'une des techniques les plus répandues s'appelle l'élagage Alpha - Beta (Alpha Beta pruning en anglais).

Elle consiste à envoyer dans les nœuds enfants une valeur Alpha qui contient la valeur la plus haute pour l'instant trouvé par les nœuds enfants et une valeur Beta qui contient la valeur la plus basse pour l'instant détecté dans les nœuds enfants. Prenons un exemple avec un arbre de profondeur 3.

Exemple d'un élagage Beta avec le minimax

Mon nœud courant maximise (c'est un tour du joueur 1), et va donc appeler pour chaque coup possible, tous les coups du joueur 2, qui lui appelle tous les coups possibles pour sa situation pour choisir le plus petit score.

Le premier coup retourne -120 et le second 450. Cela signifie que pour le moment, puisque l'on maximise, le meilleur coup vaut 450.

Pour le troisième coup, on analyse chaque possibilité. Le premier résultat vaut -1000. On sait que le joueur 2 lui va choisir le plus petit score possible et donc retournera quelque chose au moins inférieur ou égal à -1000. Comme on a déjà une valeur de 450 en haut de notre arbre, ça ne sert à rien de continuer la recherche dans ce nœud enfant. On coupe alors la boucle et on fait un élagage par Alpha.

L'inverse est donc tout aussi fonctionnel en utilisant la valeur Beta pour un nœud minimisant.

Côté code l'implémentation est d'autant plus simple puisque cet élagage ne tient qu'en quelques lignes :

// On itère sur chacun des coups
for _, move := range moves {

    // Ce coup est ajouté au plateau de jeu
    newBoard := ApplyMoveOnBoard(board, move, currentPlayer)
    opponent := SwitchPlayer(currentPlayer)

    // On appel récursivement le minimax en ayant pris soin de modifier le joueur courant.
    childScore := Minimax(depth - 1, newBoard, firstPlayer, opponent, move, alpha, beta)

    // Chaque coup nous aura retourné un score.

    if currentPlayer == firstPlayer && bestScore < childScore {
        // Ici bestScore contiendra toujours forcément le plus haut score retourné par les enfants.
        // C'est ici que l'on maximise.
        bestScore = childScore

        // Élagage Alpha
        alpha = math.Max(alpha, bestScore)
        // Ici on maximise, donc si beta est d'ors est déjà inférieur à Alpha, quand on minimisera, on peut couper.
        if beta <= alpha  {
            break
        }

    } else if currentPlayer != firstPlayer && bestScore > childScore {
        // A l'inverse, on minimise en prenant toujours le plus petit score si le joueur courant est le joueur 2.
        bestScore = childScore

        // Élagage Beta
        beta = math.Min(beta, bestScore)
        // On minimise ici donc si on alpha est d'ors est déjà supérieur à beta, on peut couper.
        if beta <= alpha {
            break
        }
    }
}

J'ai trouvé la théorie de cet élagage vraiment complexe à comprendre et j'ai même commencé à recopier l'algorithme afin de constater en premier lieu les changements que cela apportait. Finalement j'ai fini par comprendre qu'Alpha représentait la plus haute valeur du dernier nœud maximisant, et Beta la plus basse valeur du nœud minimisant et au bout du compte, c'est finalement devenu plus clair pour moi.

Cet élagage est d'autant plus intéressant que la profondeur de recherche est grande, car elle permet de s'abstenir de parcourir de nombreuses branches coûteuses en ressource. Globalement cela m'a permis de diviser par 2 le temps de traitement et de réussir à atteindre une profondeur de 4. C’est-à-dire trouver la meilleure situation pour 4 tours en 2 minutes.

Ce que j'ai appris

Effectuer un parcours récursif d'un arbre de possibilités est un algorithme complexe que je n'avais pas l'habitude de faire. C'est d'ailleurs très plaisant de voir que l'algorithme arrive à se sortir d'une situation que l'on aurait pu conclure comme perdue !

Une des forces de Go, c'est la gestion de sa concurrence avec les Go routines qui se répartissent d'elles-mêmes sur les différents cœurs de notre processeur suivant la charge. Cela m'a permis de diviser par 2 les temps de traitement de mon Minimax et d'atteindre une profondeur de 5 en 4 minutes lorsqu'on lui donnait un plateau de jeu à moitié rempli.

J'ai trouvé le code permettant de mettre en place cette concurrence très abordable et plutôt bien expliqué dans la documentation officielle.

Ce que je peux améliorer

Go est un langage assez strict. À la moindre erreur de typage, d'import ou de variable non utilisés, le programme refuse de se lancer. C'est effectivement très embêtant lorsque l'on souhaite déboguer ou simplement tester une partie de son code, un linter fait très bien l'affaire dans d'autres langages sans être aussi bloquant !

Go permet de scinder son code en packages, mais également d'en utiliser des externes. Ces packages sont ensuite automatiquement recherchés via des variables GOPATH (le projet courant) et GOROOT (l'installation de Go, ou se trouve les packages). Ces variables sont très pénibles à configurer et la documentation associée est très floue. J'ai perdu beaucoup de temps à réussir à scinder mon code en packages et n'ai pas réussi à ce jour à les utiliser correctement.

Enfin il est plus pertinent d'explorer plus de possibilités, et donc avoir une plus grande profondeur de recherche, lorsqu'une partie est au milieu ou tend vers la fin. Au début il conviendrait par exemple simplement de prendre possession des centres des quadrants qui sont très puissants, car ils restent en position malgré les rotations. La mise en place d'un système de profondeur en fonction de l'avancée de la partie permettrait d'avoir un calcul rapide en début de partie et de plus en plus précis au fur et à mesure que la partie avance.

Résultat de l'algorithme Go du calcul du meilleur coup

Vous pouvez retrouver le code sur ce répertoire Github

Jouer au Pentago sur mobile avec React Native

Pour ma quatrième est dernière semaine d'intégration, l'objectif était de pouvoir jouer au Pentago sur son smartphone. C'est donc avec React Native que j'ai eu l'occasion d'aborder cet ultime itération.

Logo de React Native

Du mobile en JavaScript avec React Native

Pour réaliser l'application en React Native je me suis largement aidé d'Expo qui est un écosystème très complet et qui permet de rapidement lancer un projet sur son téléphone sans avoir besoin d'une configuration spécifique liée à Android ou IOS. Je ne maîtrise pas vraiment cet outil, mais il permet, entre autre, deux choses très puissantes :

  • de créer en deux commandes un projet React Native fonctionnel
  • de builder en une commande expo build:android un bundle pour Android sans avoir à installer Graddle, Java, Android Studio etc...

Cette commande permet en fait de compiler l'application React Native en un APK (ou AppBundle) pour Android. Cette compilation ne se fait pas sur notre machine mais sur leur plateforme. Une fois que le bundle est prêt on peut le télécharger !

Build d'un APK android via Expo

Je n'ai pas été jusqu'à la publication sur les stores du Pentago mais Expo semble avoir déjà pensé à tout.

Pour gagner du temps sur la mise en place de mon interface, j'ai utilisé un kit de composants UI qui s'appelle React Native Paper et dont la documentation m'a paru très complète.

Il n'y a pas d'élément HTML classique lorsque l'on fait du React Native, il convient d'utiliser tout un lot de composants déjà prévu par le Framework. C'est grâce à cela qu'il peut ensuite générer du code natif.

Voici un exemple de ma vue de la page d'accueil que j'ai un peu simplifié pour étayer mon propos :

// On utilise React et même des hooks !
import React, { useState } from "react";

// Tout pleins d'éléments sont fournis pour construire notre application avec React Native
import { StyleSheet, View, SafeAreaView } from "react-native";

// utilisation d'un UI Kit externe
import { Button } from "react-native-paper";

// On peut faire nous mêmes des composants
import Title from "../components/Title";

// Ici on retrouve un composant plutôt classique
const HomeScreen = ({ navigation }) => {
  const [creatingGame, setCreatingGame] = useState(false);

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <Title>Ready to aligned marbles ?</Title>
        <Button
          onPress={() => {}}
          style={styles.button}
          loading={creatingGame}
          icon="plus"
          mode="contained"
        >
          Create new game
        </Button>
        <Button
          onPress={() => {
            navigation.navigate("Lobby");
          }}
          style={styles.button}
          color={colors.secondary}
          icon="send"
          mode="contained"
        >
          Join existing one
        </Button>
      </View>
    </SafeAreaView>
  );
};

// Pour styliser nos composants, il faut créer un objet StyleSheet.
const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 8,
    marginHorizontal: 16,
  },
  button: {
    marginTop: 16,
  },
  image: {
    width: 300,
    height: 200,
    margin: "auto",
    marginVertical: 12,
  },
});

export default HomeScreen;

Ce qu'il y a d'intéressant c'est que l'on peut utiliser les hooks React et tout autre code JavaScript. Il est d'ailleurs tout à fait possible comme je l'ai fait avec le composant Title de créer ses propres composants pour ensuite les consommer sur nos pages.

Ce qui est surtout important, c'est les composants que l'on utilise ici SafeAreaView, View, Button etc... qui sont en fait des wrappers. View peut être, en simplifiant de façon barbare, un peu identique à l'utilisation de l'élément <div>.

Ce que j'ai trouvé super, c'est l'explication très détaillée de la documentation de React Native avec ses exemples interactifs et la liste des différentes propriétés que l'on peut ajouter aux éléments.

Enfin pour appliquer les styles, on initialise un objet StyleSheet qui contiendra un JSON de toutes nos propriétés qui pourront par la suite être traduites en natif. Tous les éléments React Native ont une propriété style.

<SafeAreaView style={styles.container}> permet donc d'appliquer les styles que j'ai défini dans mon nœud container à l'élément SafeAreaView.

Lorsque j'ai développé cette application mobile, je l'ai développé dans mon navigateur car Expo permet d'ouvrir un onglet du navigateur et de rendre ces composants comme si c'était une page web. Toutes les unités de mesures que l'on connait en CSS sont utilisables pixels, rem, em, pourcentage, etc... Mais lorsque l'on compilera ensuite pour créer une application native, elle plantera car React Native ne fonctionne que si on lui passe des unités entières.

Ainsi marginTop: "8px" fonctionnera très bien durant le développement mais fera planter l'application après compilation. J'ai donc du à un moment donné remplacer dans mon projet toutes les unités de mes composants.

Passage de Symfony en mode API

Pour pouvoir persister les données de nos parties de Pentago et pouvoir jouer à plusieurs en ligne, il faut évidemment avoir une solution back-end. J'ai eu à implémenter le Pentago avec Symfony durant ma seconde semaine d'intégration.

Le but était de reprendre ce projet et de transformer un site qui servait simplement du HTML en une API utilisant des requêtes HTTP. J'ai donc transformé les routes qui autrefois renvoyaient un template Twig pour leur faire renvoyer du JSON.

J'ai trouvé cela assez rapide. Il faut définir et instancier un serializer qui va nous permettre de prendre une entité et la convertir en JSON.

use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\UidNormalizer;
use Symfony\Component\Serializer\Serializer;

class GameController extends AbstractController
{

    public function __construct(GameService $gameService, PlayerService $playerService)
    {
        $encoders = [new JsonEncoder()];
        $normalizers = [new UidNormalizer(), new ObjectNormalizer()];

        $this->serializer = new Serializer($normalizers, $encoders);
    }
}

Ce serializer permet donc d'encoder en JSON via le JsonEncoder. En entrée il est capable de prendre des objets grâce au ObjectNormalizer et de convertir proprement les propriétés de ce dernier.

Le petit cas à part est celui des ID, car j'utilise le format User Identifier (Uid). Voici un uid d'exemple : 66a91816-74c3-4e13-98fc-dd0bb061cab6. Généralement dans une base de données, cet Uid est stocké au format chaîne de caractères. Dans Symfony le type Uid est en fait un objet qui contient évidemment la valeur de cet identifiant, mais aussi des méthodes permettant de le formater ou de le manipuler. Pour pouvoir correctement dans ma requête API avoir un id contenant une chaîne de caractère, je dois spécifier le normalizer UidNormalizer.

Prenons ensuite une route de notre contrôleur :

use Symfony\Component\HttpFoundation\JsonResponse;

    /**
     * @Route("/games", name="game_create", methods={"POST"})
     */
    public function newGame(request $request): JsonResponse
    {

        $game = $this->gameService->createGame();

        $entityManager = $this->getDoctrine()->getManager();
        $entityManager->persist($game);
        $entityManager->flush();


        $response = JsonResponse::fromJsonString(
            $this->serializer->serialize($game, 'json'),
            JsonResponse::HTTP_CREATED,
        );
        return $response;
    }

J'ai enlevé les validations et les contraintes pour ne garder que l'essentiel.

Je crée un objet Game que j'initialise et que je fais persister en base de données grâce à l'entityManager de Doctrine.

Ensuite je retourne une réponse de type JsonResponse. C'est un type qui étend Response mais qui spécifie les headers nécessaires pour retourner du JSON. Enfin je dois convertir mon objet $game en JSON et c'est là que le serializer que j'ai créé plus tôt intervient. $this->serializer->serialize($game, 'json') transforme mon objet pour donner le résultat suivant :

{
    "id": "66a91816-74c3-4e13-98fc-dd0bb061cab6", // Ici on a une chaine de caractère graĉe à l'UidNormalizer,
    "status": "waiting_opponent",
    "player1": "ID_PLAYER_1",
    "player2": null,
    "currentPlayer": null,
    "board": [[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]
}

Finalement je n'ai eu qu'à changer les entrées et sorties de mes controllers pour transformer le projet que j'avais fait lors de ma seconde semaine d'intégration pour en faire une API.

Mon projet Symfony utilisait Mercure pour notifier en temps réel les joueurs des coups de l'adversaire. Cela nécessite côté client d'utiliser l'API EventSource qui n'est pas supportée nativement par React Native.

J'ai tenté d'utiliser différents packages ou polyfills de la communauté mais en vain, il m'a été impossible de faire fonctionner les notifications Mercure sur mon application en React Native. Cela m'a fortement ralenti et j'ai perdu beaucoup de temps à tenter de trouver une solution.

Finalement, pour simuler du temps réel, j'ai dû implémenter une boucle qui va à intervalles réguliers récupérer la partie via un appel API.

Passage de Go en API

La semaine précédente, j'ai eu à développer un algorithme permettant de donner le meilleur coup possible pour une situation donnée. Cette semaine était aussi l'occasion de réutiliser ce programme pour :

  • Donner au joueur un conseil sur le meilleur coup à jouer.
  • Pouvoir jouer contre un ordinateur.

Pour ce faire, j'ai décidé que toutes les requêtes passeraient par l'API en Symfony.

L'application appelle l'API Symfony, qui appelle à son tour le serveur HTTP Go. Ce serveur calcule le meilleur coup possible et retourne cela à Symfony qui retourne cela à l'application.

La difficulté ici a donc été de passer mon service Go qui s'exécutait à un instant donné en ligne de commande pour le faire écouter des requêtes HTTPS.

Le package net/http de Go d'ailleurs fait ça très bien.

import (
    "net/http"
)

func main() {
    http.HandleFunc("/", getBestMoveForPlayer)

    http.ListenAndServe(":8083", nil)

}

Ici on définit tout d'abord les routes que l'on souhaite ouvrir et les fonctions Go associées. Puis on lance le serveur sur un port donné. Pour ma part, c'était le port 8083. Et c'est tout !

Une fois que la route est appelée, il faut pouvoir récupérer les données de la partie qui sont envoyées dans le body de ma requête en JSON, calculer le meilleur coup possible et le retourner également en JSON.

Là encore, j'ai trouvé cela plutôt simple grâce au package encoding/json.

import (
	"encoding/json"
)
func getBestMoveForPlayer(w http.ResponseWriter, r *http.Request) {
	// On décode le body de la request que l'on met dans la variable game précédemment instanciée.
    json.NewDecoder(r.Body).Decode(&game)

    // Ici on calcule le meilleur coup grâce au minimax.

    // Enfin on retourne la réponse encodé en JSON.
    w.Header().Set("Content-type", "application/json;charset=UTF-8")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(data)
}

Finalement, j'ai trouvé que monter un serveur web en Go était assez simple et rapide, car le langage fournit tous les packages nécessaires.

Ce que j'ai appris

Faire une application en React Native permet de se rapprocher au plus près des performances d'une application native tout en gardant la simplicité d'un seul code source. De plus utiliser Expo est un vrai confort puisque cela abstrait complètement l'étape de compilation d'un code JavaScript en bundle pour mobile. J'ai trouvé que c'était un vrai luxe et un gain de temps inestimable !

Je n'ai pas eu à utiliser pour mon API Symfony de système comme FOSRestBundle car mon API était plutôt simple et surtout mes contrôleurs existaient déjà. Il a été très plaisant de voir la facilité de migrer d'une application classique qui sert du HTML vers une API HTTP.

Ce que je peux améliorer

Lors du déploiement de mon serveur Go, j'ai rencontré des soucis de variables d'environnement propres au langage qui sont GOPATH et GOROOT. Ayant dockerisé le service, je me suis étonné du fait que cela fonctionnait bien en local. La solution fut de réussir à compiler un binaire grâce à la commande go build main.go qui prend un fichier en entrée, récupère l'ensemble des fichiers et packages utilisés et le compile en un exécutable stand-alone. J'ai pu ensuite le lancer dans une image docker. Mais j'ai perdu beaucoup de temps à tenter de configurer ces variables qui sont toujours un mystère pour moi. Après ce projet, Alexis m'a envoyé un lien qui aurait pu sans doute me sauver la vie à ce moment-là Go Releaser. Ce sujet reste donc à éclaircir !

Le temps réel de cette application n'est pas optimal puisque c'est une boucle de rafraîchissement basique. Si j'avais eu plus de temps j'aurais pu migrer tout mon système de notification avec Mercure vers une solution en Websockets, car ce système est supporté par React Native.

Vous pouvez retrouver le code sur ce répertoire Github.

Pour conclure

J'ai passé 4 semaines très intenses et enrichissantes où j'ai appris beaucoup d'éléments techniques que j'ai pu exposer dans ce retour d'expérience. J'ai pu également retrouver des valeurs que transmet Marmelab dans ses projets :

  • La nécessité de toujours aller à l'essentiel pour délivrer rapidement un maximum de valeur ajouté à un produit.
  • La stabilité d'une application via notamment ses tests automatisés et ses codes reviews.
  • La communication avec ses pairs pour valider ou étayer ses choix.
  • Que l'échec n'est pas vu négativement, mais plutôt comme un apprentissage qui permet avant tout de progresser.

En tous les cas, cette intégration fut une chouette expérience et une belle entrée en matière. Je suis impatient de pouvoir travailler sur des projets innovants.

Did you like this article? Share it!