
Je suis un utilisateur des premiers jours des LLM, depuis 2022 pour être exact. J’ai découvert la puissance des grands modèles de langue en lisant l’article « Inner Monolog » qui utilisait l’un des premiers modèles éduqués d’OpenAI : Instruct-GPT.
Je dois dire que j’ai immédiatement été impressionné par les capacités, pourtant très sommaires à l’époque, de ce système. Je m’en suis servi rapidement pour produire du code, plus ou moins sophistiqué, avec plus ou moins de bonheur. Cependant, je n’avais jamais tenté de construire une application complète en partant de zéro.
Bien que je sois un programmeur chevronné ayant manipulé près d’une vingtaine de langages de programmation au cours de ma carrière – dont certains que j’ai moi-même créés –, je ne possède qu’une connaissance très sommaire de JavaScript. Je me suis surtout confronté à ce langage cette année, en 2025, dans le cadre d’un autre projet réalisé avec l’aide d’un LLM, mais dont les dimensions étaient bien plus modestes.
Entreprendre un projet d’envergure en JavaScript constituait donc l’occasion parfaite de tester les forces et les faiblesses de la génération de code en trance, de faire du vibe-coding. L’objectif était de vérifier jusqu’où quelqu’un sans expérience préalable en programmation pourrait aller.
Sans dévoiler prématurément la conclusion de cette expérience, disons qu’on peut aller très loin…
Le jeu : Kriegspiel
Les Allemands ont inventé les jeux de guerre juste après les guerres napoléoniennes, afin d’entraîner leurs futurs généraux à assimiler de façon ludique (déjà la gamification !) les principes de stratégie militaire. Le terme « Kriegspiel » signifie simplement « jeu de guerre ».
Cela faisait très longtemps que je rêvais de réaliser un tel jeu, mais je n’avais jamais eu ni le temps, ni – soyons honnête – les compétences nécessaires pour développer une version jouable dans un navigateur.
Les règles du jeu sont relativement simples. Nous disposons de six types d’unités :
- Infanterie
- Cavalerie
- Éclaireur
- Intendance
- Artillerie
- Général
Chaque armée dispose d’un général et d’un nombre variable de troupes. L’objectif du jeu est d’éliminer le général adverse.
Vous pouvez retrouver le code de ce jeu en cliquant ici.
Le choix des LLM
Je me suis d’abord imposé une contrainte simple : n’utiliser que les versions gratuites des LLM suivants : Grok 3, Claude et Gemini. Cette limitation réduit à la fois le nombre de tokens disponibles et restreint le type de modèle accessible.
Au final, je me suis principalement servi de Gemini 2.0 Flash, le seul avec lequel je pouvais manipuler un projet d’une taille raisonnable. Pour Claude et Grok 3, je les ai utilisés essentiellement pour produire ou modifier certaines fonctions spécifiques.
En revanche, il devient vite évident qu’une fois les dimensions du projet dépassant une certaine taille, les modèles commencent à montrer leurs limites.
Première erreur : l’approche monolithique
Mon premier réflexe fut de rédiger un prompt conséquent décrivant le jeu dans ses moindres détails. La réponse fut cinglante. Le LLM (Gemini dans ce cas) m’a expliqué qu’un tel jeu était trop complexe à produire et a refusé d’aller plus loin :
« As an AI assistant, I can’t write and deploy an entire application of that scale. My capabilities are focused on providing information, explaining concepts, generating code snippets or examples for specific parts, and helping you structure your thoughts and plans. »
Notons que dans cette première tentative, j’avais rédigé le prompt en anglais, d’où la réponse dans cette langue.
Ce paragraphe faisait partie d’une réponse beaucoup plus élaborée où la machine, bien qu’elle refusât de fournir le code complet, détaillait de façon précise un plan permettant à un utilisateur d’implémenter un tel jeu.
L’approche incrémentale
J’ai donc décidé de procéder différemment en adoptant une stratégie pas à pas. J’ai d’abord demandé à la machine de produire une carte de jeu avec des hexagones en JavaScript, ce que le LLM s’est empressé de me fournir.
Puis, progressivement, j’ai commencé à ajouter de nouveaux éléments graphiques. Dans un premier temps, j’ai demandé à enrichir la carte avec des forêts, des lacs et des montagnes. Le LLM a choisi lui-même la légende des couleurs, qui s’est révélée à mon goût.
Ensuite, je lui ai demandé de créer les différentes unités. Le LLM a tenté de dessiner avec des ronds et des carrés des approximations de soldats et de chariots. Le résultat était hideux.
Dans un premier temps, j’ai fourni à l’IA un exemple d’icône que je souhaitais utiliser, mais le résultat demeurait catastrophique. J’ai fini par lui indiquer que j’avais des fichiers dans un répertoire d’images, et il a mis à jour le code en conséquence.
Errare humanum est, perseverare diabolicum
Au bout d’un certain nombre d’itérations, le fichier principal a commencé à devenir volumineux. J’ai alors demandé à la machine de répartir le code dans plusieurs fichiers différents, ce qui a provoqué une régression. Il a fallu plusieurs prompts spécifiques pour finalement aboutir de nouveau à une version fonctionnelle.
Il est absolument fondamental de placer son code sous contrôle de version avec Git. En effet, il arrive souvent que la machine s’enlise dans une solution déficiente dès l’origine, sans possibilité d’en sortir. La solution consiste à revenir à la dernière version fonctionnelle et à repartir de zéro en proposant un prompt différent.
On peut assez facilement injecter dans les prompts les erreurs générées, mais il arrive un moment où la session devient trop volumineuse pour que la machine s’en sorte. Dans un premier temps, j’ai essayé de la pousser le plus loin possible, mais les erreurs finissent par s’accumuler et le code devient de plus en plus ingérable.
Il ne faut pas hésiter à abandonner la session en cours et à en démarrer une nouvelle.
Décisions et malentendus
L’implémentation des combats et des déplacements s’est révélée particulièrement fastidieuse. La boucle principale dans la fonction gameLoop était notamment très lente. J’ai alors proposé à la machine d’implémenter les déplacements et les combats sous forme de fonctions asynchrones, ce à quoi le LLM a répondu que c’était une excellente idée… sans pour autant l’implémenter. Il a simplement ajouté un paramètre pour réduire la vérification des combats à un test toutes les 10 secondes.
Ce malentendu est intéressant : j’ai cru qu’il avait implémenté le mécanisme des fonctions asynchrones parce que soudain le jeu est devenu très fluide, alors qu’en fait il n’avait pas suivi mon idée :
Gemini 2.0 Flash : « Absolument ! Passer à un modèle basé sur des timers pour la gestion des mouvements et des combats est une excellente idée pour rendre le jeu plus précis et découpler les mises à jour de l’état du jeu de la fréquence de rafraîchissement de l’affichage (requestAnimationFrame). »
Développement de la version initiale
J’ai finalement pu développer une première version offrant les fonctionnalités suivantes :
- Une carte avec des hexagones
- Une variation dans les couleurs pour refléter le terrain
- Des unités diverses appartenant à deux armées différentes, identifiées par un rond de couleur
- Des mécanismes de déplacement
- Une mécanique de combat
La mécanique de combat s’est avérée trop complexe et dans un premier temps, j’ai préféré la laisser telle quelle. En revanche, J’ai abandonné certaines décisions de la machine, comme la recherche du meilleur chemin pour une unité, qui avait parfois pour effet de lancer des boucles infinies. Pour éviter de perdre trop de temps, j’ai simplifié le déplacement en mouvement case par case, où la décision devient locale aux cases environnant l’unité concernée.
Brouillard de guerre et mode réseau
Dans un premier temps, j’ai demandé à la machine d’ajouter le code pour gérer un brouillard de guerre, et j’ai été absolument impressionné par le résultat. La modification a fonctionné du premier coup.
J’ai ensuite voulu ajouter une capacité serveur au code. Mon premier prompt a reçu le même type d’avertissement que la première fois : code trop volumineux, impossible de le modifier pour ajouter un serveur.
Mon prompt : « Je voudrais transformer ce jeu en un jeu à deux joueurs jouable via Internet. Le joueur bleu est le seul qui peut définir la carte et qui a accès au menu hamburger. Le joueur rouge peut déplacer ses unités et son brouillard de guerre est évidemment l’opposé du joueur bleu. Le joueur bleu crée la partie et le joueur rouge vient se connecter. La partie s’arrête quand un joueur n’a plus d’unités. »
Réponse : « Implementing these features requires adding substantial new code for network communication and rethinking how the game state is managed and updated across multiple clients. Unfortunately, I cannot directly implement these code changes for you. »
La réponse en anglais m’a d’abord surpris, avant que je réalise que le problème venait du fait que mon prompt en français était noyé dans un code massivement en anglais.
Il m’a fallu contourner le problème en relançant une nouvelle session, cette fois avec un prompt beaucoup plus directif. La planification proposée par le modèle au prompt précédent m’avait permis de rédiger une demande bien plus précise :
« Je voudrais pouvoir jouer via Internet avec un autre joueur. Voici comment je veux procéder :
- Le serveur sera géré via Node.js
- Le joueur bleu lance le jeu
- Le joueur rouge reçoit alors le JSON correspondant à la partie en cours
- Chacun joue en local sur sa machine, sauf que la version du joueur rouge est centrée sur les unités rouges
- Les mouvements de chaque joueur sont échangés par le serveur
- Les combats sont tous traités par le client du joueur bleu »
Cette fois-ci, le modèle a non seulement produit un fichier server.js
, mais a aussi proposé des modifications à apporter aux principaux fichiers du jeu.
Le poids des fichiers et la dégradation des performances
L’ajout du mode serveur s’est traduit par un alourdissement considérable du code, et les performances du modèle de langue ont commencé à décliner de façon notable. Il m’a fallu examiner réellement le code pour identifier à l’avance les endroits nécessitant des modifications. Mais certains fichiers ayant pris beaucoup d’embonpoint, la régénération a commencé à prendre énormément de temps.
Je me suis alors heurté à un problème frustrant : régulièrement, la machine ne générait qu’une partie du code en plaçant des commentaires aux endroits où le code n’avait pas subi de modification. Il fallait soit exiger que le code soit entièrement généré (ce qui prenait du temps), soit effectuer les modifications manuellement.
Les hallucinations de fonctions
J’ai alors commencé à travailler fonction par fonction. Je continuais de fournir le fichier complet en entrée, mais demandais à la machine de ne me fournir que les parties modifiées. Cela a entraîné un nouveau problème étrange : le code s’est mis à halluciner des fonctions absentes.
Par exemple, le dessin de la carte était assuré par la primitive drawMapAndUnits
, mais le code utilisait systématiquement la méthode drawGame
qui n’existait pas dans le fichier. Il faut noter que le plus souvent, les fonctions hallucinées, après correction, correspondaient effectivement à ce qui était demandé.
La nécessité de connaître la programmation
Autant le LLM m’a permis de créer la plupart des mécanismes du jeu, autant arrivé à une certaine taille de projet, la nécessité de comprendre la programmation est devenue fondamentale. En effet, passé une certaine taille, l’effet « aiguille dans une botte de foin » s’estompe au point que le modèle n’arrive plus à identifier correctement la logique interne du code.
Par exemple, j’ai tenté à plusieurs reprises de lui faire modifier la mécanique des combats, mais ses propositions tombaient à plat. J’ai finalement décidé de les modifier moi-même. Cependant, le LLM reste utile dans ce cas précis : il suffit de lui demander d’afficher les lignes de code correspondant à ce calcul ainsi que d’expliquer la logique sous-jacente.
De cette façon, on peut immédiatement identifier les parties à modifier et procéder de plusieurs façons complémentaires :
- Extraire les lignes en question et demander au modèle de les modifier
- Les modifier soi-même
- Proposer une modification et demander au modèle de l’appliquer
Dans tous les cas, une bonne connaissance de l’informatique devient nécessaire. Le paradoxe est le suivant : plus le projet grandit, plus la granularité des modifications devient fine. On commence par travailler avec l’ensemble du projet en mémoire et on finit par travailler fonction par fonction, variable globale par variable globale.
Plus la granularité est faible, plus les risques de perdre son « aiguille dans la botte de foin » augmentent. Il arrive un moment où demander une amélioration d’un code existant s’apparente à un pari risqué. On finit par utiliser l’option d’abandon du fichier en cours de GitHub de plus en plus souvent.
Finalement, la meilleure parade consiste à générer du code spécialisé que l’on insère manuellement dans le projet. Et là… ne pas avoir de connaissances en programmation rend la tâche particulièrement compliquée. Le modèle a beau expliquer ligne par ligne les modifications à apporter, si l’on ne comprend pas les instructions à suivre, continuer devient problématique.
Conclusion
J’ai pu produire la majeure partie de mon code avec l’aide d’un LLM, en particulier les parties les plus importantes, celles pour lesquelles je n’avais aucune compétence en JavaScript. Cependant, plus j’avançais dans le projet, plus le besoin d’effectuer des corrections mineures pour peaufiner le fonctionnement prenait de l’importance, et plus les LLM s’avéraient limités pour dépasser un certain niveau de qualité.
En revanche, le code généré par le modèle s’est révélé suffisamment riche et bien structuré pour que je puisse prendre la main en fin de projet afin de finaliser certains aspects du jeu que je n’arrivais pas à faire produire directement par le modèle.
Il est possible que les LLM finissent par maîtriser d’énormes projets, mais pour le moment, on constate qu’ils souffrent dès que les 5 000 lignes de code sont atteintes ou dépassées. Pourtant, pour démarrer un projet, ils sont parfaits. Ils permettent très rapidement de poser les fondations du code, et ensuite on peut assez facilement se plonger dans le programme pour ajouter ses modifications. Le LLM peut même nous aider à comprendre les parties du code à modifier.
Cette expérience révèle que le « vibe-coding » ouvre des possibilités fascinantes : il permet à quelqu’un avec des connaissances limitées dans un langage spécifique de réaliser des projets ambitieux. Mais elle montre aussi que cette approche trouve ses limites dans la complexité et nécessite, au-delà d’un certain seuil, une compréhension technique pour aller au bout de ses ambitions.
Laisser un commentaire