Projets pro et perso/TactiForge

TactiForge

Un jeu de management de football jouable dans le navigateur, que je développe seul sur mon temps libre. C'est mon terrain d'entraînement sur Laravel, React et les applications lourdes en données.

  • Projet personnel
  • Gratuit, sans publicité
  • En développement actif
Écran tactique de TactiForge

Pourquoi ce projet

L'idée de départ était simple : reprendre les mécaniques d'un Football Manager et les porter sur une stack web moderne. Gestion d'effectif, tactiques, matchs simulés minute par minute, calendrier complet, mercato et négociations de contrats. Rien n'est acheté ni généré : le moteur de simulation, le modèle de données et les interfaces sont écrits à la main, puis ajustés partie après partie.

Un jeu de gestion, c'est un sacré banc d'essai. Il rassemble dans un seul projet ce que je retrouve éparpillé chez mes clients : un modèle relationnel dense, des calculs métier trop lourds pour rester dans la requête HTTP, des écrans qui affichent des milliers de lignes sans ramer, et des règles de gestion qui doivent encore être lisibles six mois plus tard.

Le jeu est gratuit. Il n'y a ni publicité, ni achat intégré, ni collecte de données à des fins commerciales. C'est un projet d'entraînement rendu public, pas un produit.

Sur les données joueurs : les attributs des joueurs et des clubs proviennent de SoFIFA, qui a donné son accord pour leur utilisation dans le jeu et qui est crédité dans l'application. Le projet n'est affilié à aucun éditeur, aucun club ni aucune ligue, et ne génère aucun revenu.

Côté technique

Les choix d'architecture qui structurent le projet, et ce qu'ils m'apportent au quotidien sur mes missions.

Moteur de simulation maison

Chaque match se joue minute par minute côté serveur : occasions, fatigue, cartons, notes des joueurs, récit de la rencontre. Aucune bibliothèque tierce, uniquement des règles écrites et équilibrées à la main.

Typage de bout en bout

Les objets PHP sont exposés au front via spatie/laravel-data, puis convertis automatiquement en types TypeScript. Renommer une propriété côté serveur casse la compilation du front : les régressions se voient au build, pas en production.

Modèle relationnel dense

PostgreSQL pour les clubs, joueurs, contrats, calendriers, transferts et statistiques. Une centaine de migrations et des requêtes pensées dès le départ pour rester rapides sur des volumes réels.

Traitements asynchrones

Redis assure le cache, les sessions et les files d'attente. Les avancées de calendrier et les simulations lourdes partent en job plutôt que de bloquer la requête HTTP.

Front React sous Inertia

Des interfaces React et TypeScript servies directement par Laravel, sans API REST à maintenir en double. Tableaux triables, écran tactique en glisser-déposer, interface bilingue français / anglais.

Docker du local à la prod

Le même environnement conteneurisé en développement et en production, déployé derrière un reverse proxy sur serveur dédié. C'est aussi ce qui me rend rapidement opérationnel sur la stack de mes clients.

Sous le capot

Les morceaux les plus intéressants à écrire, et ceux qui ont demandé le plus d'itérations.

01Le moteur de match

Tout part des attributs. Chaque joueur reçoit une note offensive et une note défensive calculées à partir de ses caractéristiques, pondérées selon le poste qu'il occupe réellement sur le terrain : un attaquant est jugé sur sa finition, sa puissance de frappe et son placement, un milieu central sur sa passe courte, sa vision et son contrôle de balle, un gardien sur ses réflexes et son jeu à la main. Aligner un joueur hors de son poste lui coûte une partie de son efficacité.

Ces notes passent ensuite dans une série de coefficients : la forme moyenne sur les cinq derniers matchs, le moral, la condition physique et l'endurance, la courbe d'âge, la personnalité, l'aura du capitaine, la position au classement, l'avantage du terrain, et les consignes tactiques des deux entraîneurs : mentalité, style de jeu, bloc en possession et hors possession.

Le rapport de force entre l'attaque d'un camp et la défense de l'autre donne un nombre de buts attendus pour chaque équipe. Le score est ensuite tiré selon une loi de Poisson bivariée corrigée à la Dixon-Coles. Cette correction compense un défaut connu du Poisson simple, qui sous-estime systématiquement les scores serrés : 0-0, 1-0, 1-1.

Un match ne se résume pas à son score. Il se déroule par fenêtres de minutes, avec occasions, buts, cartons, blessures, remplacements décidés par un planificateur, récit textuel et notes individuelles. Les consignes criées depuis le banc durent 15 minutes, se rechargent en 10 et sont plafonnées à 6 par match : impossible de les enchaîner sans réfléchir. Prolongations et séance de tirs au but suivent les mêmes règles.

  • Pondération des attributs par poste
  • Poisson bivariée corrigée Dixon-Coles
  • Forme calculée sur une fenêtre SQL de 5 matchs
  • Consignes : 15 min d'effet, 10 min de recharge, 6 max

02Déterminisme : rejouer un match à l'identique

Tous les tirages du jeu passent par une seule source d'aléatoire, un générateur Xoshiro256** initialisé par une graine. À graine égale, la séquence produite est identique, donc le match aussi.

Ça ressemble à un détail, c'est en fait ce qui rend le reste possible : reprendre un match en cours à la minute près, rejouer sa fin après un changement de consigne, et surtout reproduire un bug de simulation à l'identique dans un test au lieu de le voir passer une fois sur cinquante. Les tirages sensibles, jetons et sessions, restent sur le générateur cryptographique de PHP, qui lui n'est volontairement pas rejouable.

  • PRNG Xoshiro256** ensemencé
  • Reprise et rejeu d'une fin de match
  • Bugs de simulation reproductibles en test

03Valorisation et marché des transferts

Chaque joueur porte une valeur marchande recalculée en continu. Son niveau global passe d'abord dans une courbe non linéaire, parce que l'écart de prix entre un bon et un très bon joueur ne l'est pas non plus. S'y ajoutent son âge, son poste (à niveau égal, un buteur coûte plus cher qu'un gardien), sa nationalité, son temps de jeu, ses statistiques de la saison et la durée de contrat restante. La clause libératoire s'en déduit.

Les clubs pilotés par l'IA passent leur effectif au crible poste par poste, repèrent leurs manques, se fixent des cibles et construisent des offres compatibles avec leur budget de transfert et leur masse salariale. Des garde-fous empêchent les dérives : deux offres par jour et par club, sept jours avant de pouvoir revenir sur le même joueur, filtres de réalisme sur le montant proposé.

Côté joueur, la réponse n'est pas un simple test sur le salaire. Un score de décision agrège le temps de jeu qu'il peut espérer dans le nouvel effectif, le standing du club, le salaire proposé face à son estimation du marché, son moral, son âge et son ancienneté : passé 28 ans, un joueur installé se déloge plus difficilement. Un bruit aléatoire empêche de deviner le seuil, et l'agent applique son propre biais selon sa personnalité : un mercenaire réclame 20 % de salaire en plus, un loyal en concède 5 %.

Transferts secs, prêts avec ou sans option d'achat, pré-contrats, participation au salaire, agents libres, renégociations en cours de contrat : chaque opération a ses règles propres et sa fenêtre dans le calendrier.

  • Valeur, salaire et clause recalculés en continu
  • IA acheteuse guidée par les manques et le budget
  • Score d'acceptation multi-critères
  • Biais de négociation selon la personnalité de l'agent

04Le temps qui passe

Entre deux matchs, le monde continue de tourner. L'entraînement hebdomadaire fait progresser ou régresser les attributs selon le couple niveau actuel / potentiel, avec un déclin ralenti pour les vétérans qui jouent encore et accéléré pour ceux qui restent sur le banc. S'y ajoutent les blessures, les suspensions, les contrats qui arrivent à terme, les fins de carrière, la génération du calendrier en matchs aller-retour et la bascule de saison.

Ces traitements sont trop lourds pour une requête HTTP : ils partent dans une file d'attente Redis et rendent la main immédiatement à l'interface.

  • Progression pilotée par niveau actuel / potentiel
  • Calendrier généré en tournoi toutes rondes
  • Avancées de saison traitées en job Redis

En chiffres

8 Championnats simulés
4 400+ Joueurs et 155 clubs en base
~90 000 Lignes de PHP et TypeScript
100 Migrations de base de données

Aperçu du jeu

Captures prises dans la version actuelle. Le projet évolue régulièrement, les écrans aussi.

Un projet à construire ?

Ce que je mets dans TactiForge, je le mets aussi dans les projets de mes clients : du code lisible, un modèle de données qui tient la route et des interfaces qui restent utilisables quand le volume grimpe. Si vous cherchez un renfort back-end sur Prestashop, Symfony ou Laravel, parlons-en.