Le jeu du Taquin en php (framework Symfony)
Cette série d'articles retrace mon parcours d'intégration au sein de Marmelab qui s'articule autour du jeu du Taquin. (Part 2. Le jeu du Taquin en Go)
Après un peu plus d'une semaine passée sur l'implémentation en GO du jeu, il est temps de passer à la suite de l'intégration. Bien entendu, comme tout projet informatique, celui-ci n'est pas parfait (nous le verrons dans la suite de cet article). Néanmoins il répond assez bien à la demande initiale, à savoir permettre de jouer au Taquin en ligne de commande. J'ai d'ailleurs obtenu un petit supplément de temps pour améliorer l'algorithme de suggestion de coups lors de la présentation du jeu à l'équipe.
Ma nouvelle épreuve est donc de mettre en place un serveur php à l'aide du framework Symfony, dans le but de remplacer le terminal par un navigateur web. En effet, après avoir réécrit le jeu dans deux langages différents, il n'est désormais plus question de repartir de zéro, mais bien de capitaliser sur l'existant afin de fournir de nouvelles fonctionnalités. En plus de créer une interface graphique, il faudra intégrer le jeu multijoueur et utiliser l'api go.
Infrastructure et conteneurisation
Comme on l'a vu en introduction, je vais devoir réutiliser le jeu du Taquin en GO dans un projet php sous Symfony. Il s'agit d'une avancée majeure dans l'élaboration d'un écosystème autour de ce jeu, et, comme toute amélioration, elle s'accompagne bien entendu de contraintes techniques importantes. Il sera dorénavant nécessaire d'associer différents projets et langages pour les faire fonctionner ensemble.
Docker compose, véritable chef d'orchestre
Combiner différents langages et projets a souvent été une tâche compliquée à réaliser, si bien que, souvent, cette contrainte est prise en compte en amont des projets durant la phase d'analyse. Cela implique malheureusement de longues phases de réflexion et d'élaboration de cahier des charges et d'écriture de spécifications techniques précises ; ce qui est souvent long et très couteux. Cependant, nous utilisons une méthode différente ici. Nous parlons en effet d'agilité, ce qui signifie notamment que les besoins d'infrastructure sont apparus avec la vie du projet, et qu'il est de fait nécessaire de les traiter lorsque, et si ils apparaissent. On évite par la même occasion consacrer des ressources (temps, argent) sur des problématiques qui n'arriveront potentiellement jamais.
Pour l'anticiper, la solution choisie a été d'utiliser dès le départ des conteneurs docker, puis de les associer les uns avec les autres à l'aide de docker compose
. On parlera ici d'orchestration, chaque container ayant son propre rôle. Il devient désormais facile d'ajouter ou de changer de conteneur au fur et à mesure de l'évolution du projet.
Architecture des conteneurs au sein du projet
Le projet se compose de quatre dockers distincts : php, nginx, postgres, et l'api.
Ceux-ci seront configurés à l'aide d'un fichier yaml docker-compose.yaml
qui permet notamment de spécifier des ports pour faire communiquer les conteneurs entre-eux.
[...]
service_php:
build: docker/php
tty: true
labels:
kompose.service.type: nodeport
ports:
- 9000:9000
volumes:
- ./app:/app
service_nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./app:/app
- ./docker/nginx/site.conf:/etc/nginx/conf.d/default.conf
service_postgres:
image: postgres:9.6
environment:
- POSTGRES_USER=docker
- POSTGRES_PASSWORD=docker
ports:
- "5433:5432"
expose:
- 5433
volumes:
- ./app/var/data:/var/lib/postgresql/data
service_puzzle_api:
image: luwangel/15-puzzle-api
command: go run main/main-server.go .
ports:
- "2000:2000"
[...]
Spécificité de l'api 15 puzzle
L'api 15 puzzle est une image un peu particulière puisqu'elle correspond en fait à la partie webserveur du projet go. Pour les besoins du projet, j'ai donc construit une image spécifique que vous pourrez trouver sur Docker Hub.
Le framework Symfony au coeur du projet
Bien qu'utilisant différents blocs logiciels (base de données, serveur http, etc.), on peut dire que ce projet s'articule autour du framework Symfony. C'est grâce à lui que l'utilisateur final peut jouer.
L'une des contraintes particulières de ce projet a été d'utiliser le framework Symfony 4, ainsi que php 7.1.
Pour ce qui est de php, je ne connaissais pas le langage, mais je n'ai pas eu de réels problèmes à m'y accoutumer. Outre certaines spécificités syntaxiques qui me perturberont toujours (je pense ici aux ->
), j'ai été assez vite à l'aise.
En ce qui concerne Symfony, ce n'est pas vraiment la même chose. En effet, une des philosophies de cette nouvelle version est d'automatiser un maximum de tâches si bien qu'opère une forme de magie. De plus, toute l'architecture a été revue, et le framework a beaucoup été allégé : de nombreux modules qui étaient auparavant natifs doivent désormais être ajoutés après coup.
L'un des avantages de Symfony est de proposer énormément de modules, qu'ils soient officiels ou non. On peut dire qu'à chaque problématique, il existe une solution sous forme de bundle à installer. Néanmoins il est souvent nécessaire de savoir quoi chercher, et comment le configurer pour qu'il s'adapte à nos besoins. Et c'est là que réside la principale difficulté. Je n'ai ainsi pas toujours trouvé ce que je voulais, ou alors ce qui existait était trop complexe pour les besoins assez simples de ce projet.
Le jeu multijoueur
Une des demandes du client était de permettre à deux joueurs de jouer l'un contre l'autre, l'autre demande concernait le mode spectateur. En effet, à partir d'un lien unique, il devait être possible pour n'importe qui de regarder en temps réel une autre personne jouer. Heureusement, le framework Symfony fut très utile sur ce point.
Structure de données
Le jeu est organisé autour de deux entités, Game
et Player
. Celle-ci est assez simple et peut être décrite comme suit :
Game
gère un ou deux joueurs, le vainqueur et la grille de référence (la grille résolue).Player
contient la grile qu'un joueur est en train de modifier. À chaque fois qu'il déplace une pièce, la grille est mise à jour et un tour est ajouté. Si la grille issue de ce mouvement est gagnante, le jeu est considéré comme terminé et le joueur vainqueur.
Afin de lier ces deux entités à Symfony, j'ai pu utiliser l'ORM doctrine. Il permet, à l'aide d'un système d'annotations, d'automatiquement créer le schéma de la base de données, puis de gérer les entités dans le code comme des objets classiques.
/**
* @ORM\Entity(repositoryClass="App\Repository\PlayerRepository")
* @ORM\Table
*/
class Player {
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function getId() : int {
return $this->id;
}
public function setId(int $id) {
$this->id = $id;
}
}
Utilisation de cookies
Une des contraintes de la plateforme étant le mode spectateur, il ne suffit pas de générer un lien unique pour chaque partie. Dans le cas contraire, n'importe qui pourrait jouer à la place d'un autre. Afin de sécuriser cet accès, j'ai mis en place un système d'authentification à base de cookies.
À chaque fois qu'un joueur créé ou rejoint une partie, un jeton d'authentification est généré à l'aide du TokenGenerator
. Celui-ci prend la forme d'une chaîne de caractère générée aléatoirement comme par exemple : e2758c6c64dec60d6089
.
class TokenGenerator {
public const TOKEN_LENGTH = 10;
public function generate(int $tokenLength = self::TOKEN_LENGTH) : string {
if ($tokenLength <= 0) {
$tokenLength = self::TOKEN_LENGTH;
}
return bin2hex(random_bytes($tokenLength));
}
}
Il s'agit donc simplement de stocker sur le navigateur de la personne ce jeton, il sera ainsi transmis à chaque requête et permettra d'identifier le joueur.
Automatisation de l'authentification
Symfony propose dans son système de routing des ArgumentResolver
. Comme leur nom l'indique, il s'agit d'une classe permet de résoudre l'injection de dépendance. Pour l'utiliser, il suffit de créer une classe implémentant une interface simple, ici GameContextResolver
, et de l'injecter en tant que paramètre de méthode. Symfony se charge automatiquement du reste.
Exemple de configuration :
App\ArgumentResolver\GameContextResolver:
tags:
- { name: controller.argument_value_resolver, priority: 50 }
Exemple d'injection dans la route cancel
:
public function cancel(GameContext $context) {
$response = $this->redirectToRoute('index');
if ($context->getIsPlayer()) {
$this->gameRepository->remove($context->getGame()->getId());
CookieAuthManager::removePlayer($response);
}
return $response;
}
Le paramètre ainsi passé sera un objet contenant une prioriété indiquant si l'utilisateur est identifié ou non. En fonction de ce paramètre, il sera aisé d'autoriser ou pas l'utilisateur a effectuer une action. Dans le cas présent seul le propriétaire de la partie pourra la supprimer.
Scénario d'utilisation
Pour finir, je vais vous présenter le travail réalisé au travers d'un scénario d'utilisation.
Tom est un jeune homme passionné de Taquin. Il avait beaucoup apprécié la version python du jeu, mais l'interface était un peu trop rudimentaire pour lui.
Après avoir téléchargé et lancé le jeu (il n'existe pas encore de version en ligne), il ouvre donc son navigateur sur localhost
et se retrouve face à l'écran principal l'invitant à créer une partie.
Une fois la partie créée, il se retrouve avec une grille mélangée prête à être résolue.
On remarque que le compteur de tour est assez important pour évaluer si il est loin de la solution finale ou pas. En moyenne, il est possible de résoudre un Taquin en moins de 200 coups.
Enfin après avoir placé la dernière case au bon endroit, il n'a plus qu'à savourer sa victoire, puis à recommencer un nouveau puzzle :).
Bilan
Personnel
Travailler sur ce projet s'est avéré être une expérience assez différente des deux précédentes semaines. D'une part, je n'ai pas eu à reprendre le code source, ce qui a amené une logique de programmation assez différente qui m'a permis de me focaliser sur d'autres aspects de l'informatique comme l'imbrication de plusieurs briques logicielles entre elles.
D'autre part j'ai découvert Symfony, un framework qui nécessite beaucoup d'implication ne serait-ce que pour comprendre son fonctionnement. Je pense d'ailleurs que pour un projet aussi basique, l'utiliser n'est pas un choix très rationnel. Mais étant en période d'apprentissage, tout l'intérêt était justement de découvrir et installer un gros framework.
J'ai également appris avec ce projet à travailler avec des employés à distance. Il faut savoir que chez Marmelab, certains employés travaillent depuis chez eux à plusieurs centaines de kilomètres des bureau. C'est le cas d'Alexis qui m'a accompagné, vous pouvez d'ailleurs retrouver son retour d'expérience sur le télétravail dans cet article. Le travail ne change pas beaucoup si ce n'est que les réunions quotidiennes se passent par conférence vidéo et qu'il faut parfois savoir surmonter les instabilités de la connexion wifi. Quoi qu'il en soit j'ai beaucoup apprécié travailler de cette manière.
15-puzzle-web
La mise en place de l'architecture a été assez laborieuse. La phase d'installation appelée bootstrap
a pris plus de deux jours ce qui est énorme sur un projet d'une semaine. Cela m'a malheureusement empêcher de mener le projet dans les temps et j'ai dû de fait prendre également deux jours supplémentaires pour mener à terme la partie multijoueur.
Fort de ce projet Symfony, je vais désormais être chargé de développer une application React Native qui, à l'aide d'une api réutilisant ce serveur, permettra de connecter deux téléphones pour les faire jouer en réseau. Les deux projets vont donc cohabiter lors de la prochaine itération.
Le code source du jeu est bien entendu disponible sur GitHub marmelab/15-puzzle-web. N'hésitez pas à le reprendre et à l'améliorer =).
Crédits: image de couverture par Richard Clyborne de Music Strive. Merci à lui !