David Madore's WebLog: Programmation et flexibilité

Index of all entries / Index de toutes les entréesXML (RSS 1.0) • Recent comments / Commentaires récents

Entry #1677 [older|newer] / Entrée #1677 [précédente|suivante]:

(vendredi)

Programmation et flexibilité

Une idée avec laquelle je joue de temps en temps est celle d'arriver à écrire un moteur informatique pour programmer des jeux d'aventure : je ne veux pas parler de jeux graphiques (les graphismes ne me semblent pas apporter grand-chose pour ce genre de trucs, et de toute façon je n'ai pas les moyens d'en produire) mais d'aventures purement en mode texte dans lesquelles on rentre les commandes soit en tapant des phrases (éventuellement en style télégraphique) soit en choisissant des actions (et les objets auxquels les appliquer) dans un système de menus (mais avec tout de même des choix beaucoup plus systématiques que dans les « livres dont vous êtes le héros » chers à mon enfance où on ne pouvait généralement effectuer qu'une parmi deux ou trois choix d'actions dans une situation donnée). Les modèles de jeux auxquels je pense seraient la (Colossal Cave) Adventure ou bien Zork. A priori, ça n'a pas l'air dur (et reprogrammer un de ces jeux-là ne devrait effectivement pas l'être).

Là où les choses se corsent, c'est que mon idée est de programmer un moteur de jeu d'aventure et pas un jeu particulier : je voudrais fournir un cadre général à partir duquel quelqu'un qui connaîtrait le langage de programmation utilisé pourrait facilement baser n'importe quel jeu de la sorte. (Vous allez me dire, ce genre de chose existe déjà et d'ailleurs Inform n'est pas le seul : oui, je sais bien, et c'est d'ailleurs de là que vient mon inspiration. Mais je n'aime pas trop la direction dans laquelle Inform est parti, notamment l'idée de programmer des choses en langage naturel me déplaît pas mal. Et puis si je veux réinventer la roue, en l'occurrence, c'est aussi pour mieux comprendre comment la fabriquer.) J'avais fait (et déjà mentionné sur ce blog) une tentative dans ce genre par le passé (écrite en JavaScript), qui n'était probablement pas trop mal partie et qui m'a appris un certain nombre de choses sur les difficultés de l'entreprise, mais qui butait quand même sur le fait que JavaScript, s'il est un langage très agréablement flexible, a tout de même une bibliothèque d'exécution complètement pourrie, sans parler des différences navrantes entre navigateurs (j'imagine que mon petit jeu ne marche pas du tout sous plein de navigateurs). Je précise à tout hasard que le jeu proposé dans cette tentative est complètement sans intérêt (il n'y a essentiellement rien à faire : à la rigueur on pourrait dire que le but du jeu est d'aller dans l'endroit secret, mais c'est tout), c'est juste une démonstration minimale du moteur.

La difficulté, c'est de faire du code aussi flexible que possible. Par flexible, je veux dire que si je veux fournir un cadre dans lequel on a facilement, disons, quatre ou six directions (nord/sud, est/ouest et haut/bas) dans lesquelles on peut se diriger à partir d'un endroid donné, un certain nombre de verbes standards (aller, regarder, prendre, jeter, utiliser, parler, que sais-je), et des classes d'objets (ceux qu'on peut prendre, ceux qui ont des exemplaires indifférenciés — commme des pièces d'or —, et bien sûr la notion de personnage, de lieu, et de temps), on veut que tout ceci puisse être à peu près complètement redéfinissable par celui qui programmerait un jeu d'aventure ; ou du moins, qu'il puisse en ajouter ad. lib. (un nouveau verbe, une nouvelle direction, une nouvelle catégorie d'objets, ou une catégorie de circonstance ou d'état — comme fatigué, confus, etc.). De plus, tout doit pouvoir avoir des effets inattendus sur tout (avoir un certain objet avec soi doit permettre d'ajouter de nouveaux verbes, placer un objet dans un lieu doit pouvoir influencer ce que feront d'autres objets, etc.). Et ce, sans redéfinir ce qui n'a pas à l'être (si avoir un certain objet X avec soi interdit d'en utiliser un autre Y ou lui donne un effet secondaire — par exemple — on veut que la programmation de cette interdiction puisse se faire au sein de l'objet X et sans toucher à l'objet Y ni encore moins au code du verbe utiliser). Les choses deviennent confuses !

L'utilisation du mot objet dans ma description ci-dessus laisse penser que le bon cadre pour écrire quelque chose de la sorte est le paradigme orienté-objet de programmation. Il y a de ça, certainement, mais ça ne résout pas tout : la plupart des langages qui fournissent un support orienté objet n'ont pas, ou n'ont que partiellement, la notion de multiméthodes : on demande à un objet de gérer une action (=méthode), qui le concerne lui-même ; or un jeu d'aventure comme je l'imagine utilise plus naturellement un concept de multiméthode : quand le héros ramasse un objet, l'action à gérer concerne à la fois le héros (a priori générique, mais le code gérant le héros pourrait avoir été étendu/surchargé par le concepteur du jeu), l'objet ramassé, l'action même de ramasser, et sans doute aussi les circonstances périphériques à l'action (le lieu où elle se déroule, peut-être le temps, les conditions du héros, voire les autres objets présents au voisinage ou dans l'équipement du héros…) : tout cela doit pouvoir avoir connaissance de l'action, avoir la possibilité de l'interdire (tel objet ne veut pas être ramassé, évidemment, mais aussi : tel objet ne veut pas qu'on en ramasse un autre, tel lieu ne veut pas qu'on ramasse tel objet, etc.).

Dans mon petit exemple en JavaScript (mentionné ci-dessus), je procédais en faisant parcourir à chaque action une cascade de méthodes des différents objets impliqués, dans un ordre un peu arbitraire à vrai dire : quand l'utilisateur demande à ramasser un objet (take foobar, par exemple), une fois identifié l'objet obj concerné, le système d'analyse grammaticale va appeler hero.pickUp(obj,src,loc) avec loc le lieu où l'objet se trouvait et src une chaîne qui vaut location si l'objet à été identifié dans la pièce ou le héros se trouvait (à l'inverse, c'est inventory si l'objet a été identifié comme étant déjà parmi les possessions du héros, auquel cas la fonction produira un message d'erreur disant qu'on est déjà en train de transporter cette chose) ; cette fonction va à son tour appeler obj.pickUp(h) (où h est le héros lui-même), laquelle va appeler h.addToInventoryCheck(this) (où this==obj) pour vérifier que le héros n'a pas d'empêchement structural à ramasser l'objet puis, si cette fonction donne son « accord », loc.removeFromContentsCheck(this) pour vérifier que le lieu n'a pas d'objection à ce qu'on lui retire l'objet et, si tout se passe bien, loc.removeFromContents(this) pour retirer effectivement l'objet de là où il était, h.addToInventory(this) pour ajouter effectivement l'objet dans les possessions du héros, et enfin this.pickedUp() pour permettre à l'objet obj d'avoir le dernier mot ou d'afficher un message (par défaut, Taken). Cette espèce de jeu de ping-pong entre les différentes méthodes est inévitable (chacun doit pouvoir donner son avis, annuler l'action ou la modifier pour la transformer en autre chose), mais je m'y suis pris de façon sans doute trop arbitraire.

Maintenant j'ai plutôt à l'esprit de créer un objet « phrase » pour chaque action tentée (l'objet en question étant capable de décrire le verbe, le sujet, les objets et d'éventuelles circonstances de l'action tentée), et d'interroger tous les périphériques de l'action (voire tous les objets du monde qui auront voulu s'enregistrer comme contrôleurs de toutes actions) en leur passant l'objet « phrase » pour qu'ils puissent annuler l'action ou exécuter du code. Il faudra sans doute prévoir un système de priorité des empêchements (si un objet ne peut pas être ramassé parce qu'il semble collé au sol, mais que par ailleurs le héros est trop chargé pour ramasser quoi que ce soit d'autre, quel empêchement sera affiché à l'utilisateur ?). Le fait qu'un objet puisse éventuellement vouloir empêcher un empêchement ou agir sur une action modifiée, ou quelque chose comme ça, serait évidemment souhaitable, mais à ce stade-là il faudra sans doute se résigner à ce que les objets prévoient du code ad hoc (par exemple modifier les méthodes d'un autre objet qui allaient produire un empêchement ; mais pour les simples empêchements on peut sans doute prévoir du code de contrôle au deuxième degré, ou à n'importe quel degré). Toujours est-il que je pense avoir expliqué pourquoi les choses ne sont pas aussi simples qu'on peut se l'imaginer si on veut faire du code complètement flexible.

Concrètement, je pense utiliser Java : ça me permettrait de faire tourner les jeux sur un certain téléphone et aussi d'apprendre un peu mieux ce langage qui, par ailleurs, n'est pas grossièrement inadapté au problème. D'un autre côté, Java souffre d'un certain manque de dynamisme (il ne permet pas, par exemple, de modifier les méthodes d'une classe une fois que celle-ci a été chargée, ni de changer la classe d'une instance déjà créée) et ne permet pas aussi facilement de faire du paradigme fonctionnel que le permet JavaScript, ni d'invoquer sur un objet quelconques les méthodes d'une classe quelconque (ce que JavaScript permet avec apply(), ce qui donne une sorte d'héritage multiple cheap) ; en contrepartie, Java permet facilement de sérialiser les objets, et a des classes internes assez puissantes pour contourner les problèmes soulevés[#].

Ce qui est bizarre avec le code que j'ai vaguement commencé à écrire, c'est qu'à chaque fois que je commence à mettre du code dans une fonction, je me rends compte que, non, ce n'est pas vraiment l'endroit pour le mettre, il faudrait déléguer ce travail à une autre fonction plus bas pour offrir plus de flexibilité : j'ai peur d'être entré dans une régression infinie où chaque fonction ne fait rien mais appelle juste une autre fonction pour faire le même travail (vous allez me dire, la récursivité, ça fonctionne un peu comme ça, sauf que là ce serait à moi d'écrire chacune de ces fonctions, et elles sont différentes).

[#] Un exemple de technique qu'on m'a expliquée et que je ne connaissais pas : imaginons qu'on ait deux interfaces A et B et une troisième, C, qui étend à la fois A et B (dans mon code de jeu d'aventure, par exemple, A pourrait être l'interface d'une chose qui peut être déplacée et B pourrait être l'interface d'un contenant, tandis que C pourrait être l'interface d'un sac ou d'un personnage, qui à la fois est mobile et peut contenir/transporter d'autres choses) ; on veut fournir des implémentations de référence de ces interfaces sous forme de classes (éventuellement abstraites s'il ne s'agit que d'implémenter certaines méthodes) : on peut facilement faire des classes KA et KB qui implémentent A et B respectivement — mais comment faire pour implémenter C en utilisant les deux ensembles de méthodes déjà définies dans KA et KB, sans dupliquer le code, de façon élégante, sachant qu'on ne peut pas hériter à la fois de KA et de KB ? La solution proposée consiste à ce que la classe KC qui implémente C hérite de KA uniquement, et utilise une classe interne « déléguée » qui, elle, hérite de KB : pour implémenter les méthodes de B dans KC, on les délègue à cette classe interne (qui a automatiquement le code voulu puisqu'il est hérité de KB). Je trouve ça très joli comme façon de contourner les difficultés conceptuelles de l'héritage multiple.

↑Entry #1677 [older|newer] / ↑Entrée #1677 [précédente|suivante]

Recent entries / Entrées récentesIndex of all entries / Index de toutes les entrées