En 2020, j’ai fait quelque chose d’un peu débile : j’ai écrit Tristam Island, un jeu d’aventure textuel inspiré par les jeux d’Infocom, avec la contrainte que le format de sortie devait être le format Z3 de la Z-machine. Le format Z3 a des limitations relativement fortes, comme le fait de ne pouvoir avoir que 255 objets et le fait de ne pas pouvoir dépasser 128 Kio. J’ai fait une jolie carte, fait la liste des énigmes et leurs dépendances, commencé à remplir la carte avec les objets, puis j’ai commencé à coder en me disant « bah, ça passera ». Bien sûr, ça n’est pas passé, et il a fallu développer des trésors d’imagination pour que le jeu que j’avais en tête entre dans le format sans devoir faire de concessions sur le gameplay — dont de nombreuses optimisations, des trucs qui n’avaient jamais vraiment été faits avant, et des choses qui rendent le code très peu lisible. Au final, j’avais 254 objets et 127,5 Kio. J’en ai tiré un article, publié dans ces colonnes l’année dernière, et j’ai un autre article dans les cartons sur les astuces d’optimisation, que je ressortirai si vous n’êtes pas sages.

En 2021, j’ai fait quelque chose d’encore plus débile : j’ai traduit Tristam Island en français, ce qui a donné L’île Tristam. Pourquoi encore plus débile ? Parce que :

  • La bibliothèque que j’utilisais (PunyInform) n’avait jamais été traduite en français ;
  • Le français est une langue plus expansive que l’anglais ;
  • L’encodage des caractères accentués coûte plus cher que les lettres non accentuées.

Ces deux derniers points ont été résolus extrêmement péniblement, et j’en ai encore les cicatrices ; Henrik Åsman a amélioré son algorithme de calcul d’abréviations optimales et m’a économisé plus de 5000 octets (heureusement, car sinon j’étais cuit), j’ai énormément travaillé sur le poids du texte (jusqu’à avoir un script qui me dit quelles chaînes de caractères sont bien plus lourdes dans la traduction que dans l’original, que je puisse essayer de les traduire mieux), j’ai utilisé beaucoup de trucs de micro-optimisation, et j’ai un peu sabré dans la bibliothèque.

Je ne sais pas si revenir sur ces astuces serait très intéressant ; ce travail est une conséquence directe de mon caractère têtu, et je doute que quelqu’un d’autre se retrouvera dans cette situation de sitôt. Par contre, il y a des choses intéressantes à savoir si vous voulez écrire un jeu au format Z3 en français, ou dans toute autre langue que l’anglais d’ailleurs. Les voici, un peu en vrac !

Je compte sortir la version française de PunyInform bientôt sur mon GitHub (on va dire avant le 15 mars 2022 ?). Quand ça sera fait, je ne serai pas contre un peu d’aide : un rapport de bug, une nouvelle idée, une petite optimisation bien sentie, ou juste de l’aide pour propager les changements du dépôt de PunyInform dans mon dépôt ! Merci d’avance !

Pourquoi Z3 et pas Z5 ?

J’en ai déjà parlé dans l’article qui présentait le format Z3, mais je le redis : le format Z5 est un format plus flexible, avec des caractéristiques plus sympathiques, qui font que les difficultés mentionnées plus bas disparaissent en majeure partie. Je ne voulais pas démordre du format z3, car certaines vieilles plateformes sont incapables de jouer à du Z5 (par exemple l’Oric ou la Game Boy) ; mais si c’est moins important pour vous, ne vous gênez pas. Les différences principales entre les formats, dont je vais parler dans cet article, sont :

  • La contrainte d’espace : 128 Kio pour le Z3 (chaque Kio compte), 256 Kio pour Z5 (ouf, ça va) ;
  • La résolution du dictionnaire, c’est-à-dire le nombre de caractères considérés quand on analyse un mot dans les commandes (6 unités en Z3, 9 en Z5) ;
  • La conversion de l’entrée en « tokens » (lecture + tokenisation en un seul opcode en Z3, deux opcodes en Z5, ce qui veut dire en particulier que l’on peut « retokeniser » en Z5) ;
  • L’encodage de la table des caractères (fixé pour le Z3, adaptable pour le Z5, et incohérences dans les interpréteurs).

La contrainte d’espace

Si vous êtes comme moi et que votre jeu fait pas loin de 128 Kio, le fait d’écrire en français vous complique la tâche. Déjà, parce que le français est une langue plus expansive (mais je l’ai déjà dit), donc votre texte sera plus long. Mais je me suis aperçu en traduisant PunyInform en français que certains facteurs intrinsèques font gonfler la taille de la bibliothèque, de sorte qu’il y a une différence de taille (plusieurs Kio, un gouffre à cette échelle) entre un jeu vide avec la bibliothèque PunyInform anglophone et le même jeu avec la bibliothèque francophone.

Par exemple, les accords : traduire « open » quand on parle d’un objet du jeu nécessite d’ajouter deux petits tests (genre et nombre) pour « ouvert-e-s ». On peut tenter de finasser et utiliser des formulations épicènes, mais ça n’est pas toujours possible (et parfois ça rallonge le texte, donc autant ne pas le faire). De même pour les « they » et « it », qui doivent être adaptés en fonction du genre… D’autres choses présentes dans le français, comme l’élision, ont dues être abandonnées pour des raisons de place : si vous avez un objet « l’albatros » dans votre jeu, à vous de définir la propriété « article » dans l’objet, car faire en sorte que le jeu détecte l’élision correctement aurait pris trop de place…

Une dernière chose qui coûte cher en espace dans la version française de PunyInform, c’est la résolution du dictionnaire. Ça tombe bien, c’est notre prochain sujet.

La résolution du dictionnaire

En Z3, l’analyseur syntaxique regarde les 6 premières unités de chaque mot, et les compare à son dictionnaire interne, pour pouvoir représenter les mots par des identifiants internes. (C’est ce processus-là que j’appelle la tokenisation.) C’est une limitation majeure dans le cas du français ; je n’irai pas jusqu’à dire que ça rend le jeu injouable, car il y a des solutions, mais il faut faire très attention, et ça peut coûter cher en place. (Et ça veut aussi dire que votre jeu ne saura pas faire la différence entre un jardin et un jardinier, ce qui peut poser problème.)

C’est aussi une limitation majeure dans le cas d’autres langues, par exemple l’espagnol : « prender » (« prendre «), « prendelo » (« prends-le »), et « prendela » (« prends-la ») sont impossibles à distinguer. En français, je suis plus mitigé : certes, « prends-le » est difficile à comprendre, mais on peut très bien dire au joueur d’utiliser l’infinitif, et « le prendre » est alors compréhensible par le parser – je vous expliquerai comment je l’ai fait dans la section suivante de cet article.

Le français est une langue plus expansive que l’anglais. Paradoxalement, ici, ça peut parfois aider. Dans Tristam Island, j’avais des tests du genre « si le mot est « bug » ou « bugs » ou … ». En français, le mot « insecte », et son pluriel « insectes », font tous les deux plus de 6 unités, donc sont les mêmes mots : ça simplifie les tests et fait gagner un peu de place.

Par contre, ça pose des problèmes pour aller dans l’autre sens, c’est-à-dire quand il faut prendre le mot du dictionnaire et l’afficher entièrement. C’est la routine VerbName de PunyInform, et elle sert quand vous écrivez « Ce n’est pas quelque chose que l’on peut ouvrir/fermer/manger/déverrouiller/etc. ». Cette routine dit : « Si le verbe correspond au token « sauveg », écrire « sauvegarder », et ainsi de suite, et dans tous les autres cas, écrire les 6 unités que tu as dans le dictionnaire. » Vous voyez venir le truc : si un verbe a plus de 6 unités, il faut écrire dans VerbName comment il doit s’afficher à l’écran, car le dictionnaire ne garde que 6 unités. En français, il y en a des dizaines, alors qu’il y en a une demi-douzaine en anglais… C’est une partie longue et rébarbative de la bibliothèque, qui prend beaucoup d’espace pour pas grand-chose.

Vous avez peut-être remarqué quelque chose de bizarre : dans les paragraphes précédents, j’ai écrit « unités » au lieu de « lettres » ou « caractères ». C’est parce qu’il y a anguille sous roche ! Concrètement : dans l’encodage de la Z-machine version 3, les lettres minuscules anglophones coûtent 1 unité, donc c’est effectivement 6 lettres ; les lettres majuscules coûtent 2, mais comme elles sont automatiquement converties en minuscule, ça n’a pas d’importance. Par contre, les autres caractères ASCII coûtent aussi 2 unités, dont notamment le trait d’union ! Ce qui fait que « rond-point » sera juste « rond- », et ne pourra pas être immédiatement distingué de « rond-de-cuir »… Mais le pire exemple était « nord-ouest » et « nord-est », qui sont juste « nord- » ! C’est un énorme problème car les directions sont fondamentales dans les jeux d’aventure ; pour le résoudre, j’ai dû écrire un code qui détecte ce cas et qui va regarder dans le texte original du joueur si c’était un « o » ou un « e » après le trait d’union. Et c’est ce qu’il faut faire à chaque fois a priori, ce qui coûte un peu cher…

Mais je vous ai gardé le meilleur pour la fin : les accents dans l’entrée. En fait, et j’y reviendrai, la Z-machine version 3 n’est même pas censée avoir des accents et autres caractères non anglophones. Mais certaines machines les afficheront, et certains interpréteurs les prendront en charge ; et ces caractères-là coûteront 4 unités ! Oui, 4 ! Donc, un téléphone est un « tél », un éléphant est un « él », etc. C’est une limite très rude, qui peut poser beaucoup de problèmes ; c’est pour cette raison que je demande aux joueurs de L’île Tristam de taper leurs commandes sans accents…

La conversion de l’entrée en « tokens »

On vient de parler de ce processus de « tokenisation », où le jeu convertit des lettres en un mot de son dictionnaire, puis le compare à d’autres mots pour savoir à quel objet on est en train de se référer. En Z5, il y a un opcode (une commande adressée à l’interpréteur) pour recueillir l’entrée du joueur, et un autre pour le tokeniser ; cela veut dire en particulier que l’on peut demander à retokeniser, c’est-à-dire demander à l’interpréteur de reconvertir les lettres en tokens. Pourquoi vouloir faire ça ? Ça permet de modifier l’entrée du joueur, puis de pouvoir obtenir les tokens correspondants et faire les trucs habituels avec. Par exemple, dans la bibliothèque francophone, on a une fonction enleve_accents, qui regarde l’entrée du joueur, convertit les « é » en « e » (etc.), puis demande une retokenisation ; ce faisant, on n’a pas besoin de dire plus loin dans notre code « est-ce que le joueur a tapé « telephone » ou « teléphone » ou ‘téléphone’ ou ‘télèphone’ ou… », juste « est-ce que le joueur a tapé « telephone » ». (C’est impossible de faire ce genre de comparaisons entre mots du dictionnaire si on n’a pas refait de tokenisation.) Un autre exemple est la gestion des ordres avec pronoms : « le prendre » est réécrit en « prendre -le », puis reconnu ; et « prends-lui » est transformé en « prends -lui », puis reconnu. Bref, la retokenisation, c’est une pièce fondamentale de la compréhension en français.

Et donc, quand je vous dis que la retokenisation est impossible pour le format Z3, vous… pleurez, oui, bien joué ! Comment peut-on s’en sortir ? Assez mal, je ne vous le cache pas. Le fait que les accents pèsent lourd (voir plus haut) et ne peuvent pas être convertis en caractères sans accents veut dire que l’on devrait écrire « si le joueur a écrit « telephone » ou « tél » ou « tèl » », ce qui fait faire plus de tests ; ou alors, et c’est la solution que j’ai adoptée pour gagner de la place, dire au joueur que les accents dans les commandes ne sont pas pris en charge. Pour « le prendre », c’est en fait gérable, car on n’a en fait pas besoin de retokeniser : les « tokens » / mots sont bons, juste dans le désordre (le verbe est en 2e position, alors qu’Inform les veut en 1er). J’ai donc une routine qui dit « avant même de chercher qui est le verbe ou quoi, si le premier mot est ‘le’/’la’/’les’, inverser l’ordre des deux premiers mots » ; en pratique, ça marche très bien. Pour « prends-le », c’est trop compliqué ; je dis au joueur de préférer l’infinitif, et je croise les doigts pour qu’il écrive « le prendre », ce que je suis capable de gérer !

Bref, les solutions qui existent pour ce point en particulier sont incomplètes — et encore, je pense que ça a le potentiel d’être pire dans d’autres langues… J’espère qu’ils liront cet article avant de se lancer dans un jeu Z3 !

L’encodage de la table des caractères

J’ai galéré pour comprendre (voir le thread sur intfiction.org) mais voici une petite explication. Quand Infocom a créé le format Z3, ils ont considéré qu’un caractère était une valeur entre 0 et 127 ; leurs interpréteurs appliquent ainsi un masque sur 7 bits la plupart du temps. Puis ils ont créé le format Z5, et dit « OK, on va avoir 256 caractères » ; ils ont utilisé ces caractères pour ajouter des diacritiques (pour une traduction de Zork en allemand qui fut inachevée) et des symboles étranges (les glyphes/runes dans Beyond Zork), avec une table de caractères connue et documentée (la table ZSCII, qui est dans le Designer’s Manual 4 notamment). Ils ont aussi créé une directive permettant de redéfinir la table de caractères, ce qui est plutôt utile, car cela permet de remplacer des caractères ASCII inutiles (mais qui coûtent 2 unités) par ceux avec diacritique (qui en coûteraient normalement 4). Voir par exemple la section « string packing » de cette page. Et ce qui est beau, c’est que ça s’applique aussi à l’entrée ; on peut donc redéfinir la table de caractères de sorte à ce qu’une lettre accentuée ne coûte que 2 unités, et ainsi lire plus de lettres dans chaque mot.

Mais ça, c’est le monde merveilleux du Z5. Pour le Z3, la table est fixée : on ne peut pas faire de redéfinitions comme ça. Mais en fait, il y a deux interprétations du standard pour le format Z3 :

  • L’époque Infocom, qui est « un caractère c’est un nombre entre 0 et 127 » ;
  • Et l’époque post-Infocom, qui est « un caractère c’est un nombre entre 0 et 255, et la table est fixée (mais contient les accents français et d’autres caractères, comme le œ ) ».

Et c’est là que ça devient rigolo : les interpréteurs ont des comportements différents ! Florilège :

  • Frotz sur DOS : pas de souci, je vois le caractère ZSCII 170, j’écris « é ».
  • Frotz sur Game Boy Advance : un caractère supérieur à 128 ? Ouh là, je le saute et j’affiche rien.
  • AmigaZIP sur Amiga : je ne vois même pas que le caractère est supérieur à 128, j’applique mon masque sur 7 bits et j’obtiens 42 (170 – 128 = 42), donc j’affiche « * ».
  • JZip sur AtariST : caractère ZSCII 170 ? OK, j’affiche le caractère 170 de l’Atari ST, qui est « ¬ ».
  • Interpréteur Infocom sur Apple II : même comportement qu’AmigaZIP.
  • Interpréteur Infocom sur Amstrad CPC : même comportement que JZip !

C’est donc le gros, gros bazar pour l’affichage des accents en Z3 ! (Et même en Z5, en fait — je ne suis pas convaincu que tous les interpréteurs Z5 écrits par des anglophones affichent les accents correctement, même quand ils disent suivre le standard. AmigaZIP, par exemple, est un interpréteur Z5, qui pourtant n’affiche pas les accents correctement…) Il a donc fallu que je trouve une solution. (Déjà, il a fallu que je fasse la liste des interpréteurs et de leurs soucis… N’hésitez pas à demander.)

Pour les interpréteurs qui marchent (ZXZVM sur ZX Spectrum, Ozmoo sur C64, etc.), pas de souci. Pour ceux qui ne marchent pas mais qui ont un code source disponible, on peut tenter de patcher l’interpréteur : je l’ai fait pour JZip pour Atari ST (sources sur mon GitHub), Batteman et __sam__ ont porté JZip sur Amiga et ajouté l’affichage correct des accents, et ça devrait être faisable pour d’autres (FrotzPSP, de mémoire). D’autres interpréteurs comprennent les caractères entre 0 et 255, mais affichaient les mauvais, mais la fonte qu’ils utilisaient était stockée dans un fichier à part (FrotzDC sur Dreamcast, par exemple) ; j’ai donc redessiné la fonte pour qu’elle reflète la table ZSCII. Le point le plus compliqué, ce sont les interpréteurs qui ne comprennent pas les caractères supérieurs à 128. Pour ceux-là, il a fallu être machiavélique : si la fonte utilisée peut être redéfinie, je l’ai redessinée en dessinant « é » à la place du caractère « { », « â » à la place du « _ », etc. ; puis j’ai créé une version de mon jeu où les « é » sont remplacés par des « { », les « â » par des « _ », etc. L’interpréteur verra ainsi des « _ », dira « OK j’affiche un _ », mais comme j’ai changé la fonte il affichera « â » ! Ceci correspond au script « transform_latin1_z3.py » sur mon GitHub ; j’ai fait cela pour l’interpréteur VIC-20 de Edilbert Kirk, et pour FrotzGBA, mais ça devrait être généralisable à plus de plateformes (Amstrad CPC, MSX, Atari 8-bit, Oric, etc.). Cependant, si la fonte ne peut pas être modifiée (BBC, Apple II, etc.), cette méthode ne peut pas s’appliquer, et on doit se contenter d’un jeu sans accents… Dommage !

Fun, n’est-il pas ?

Conclusion

Je ne sais pas si j’ai une conclusion, à part de dire « tout ceci m’a pris un temps fou, et je veux que ce savoir gagné dans le sang et les larmes puisse en théorie profiter à d’autres personnes ». J’espère que ces leçons, ainsi que le travail que j’ai fourni (sur PunyInform, sur les interpréteurs, etc), vous feront gagner du temps, et j’espère que ça vous facilitera la tâche ; ça serait vraiment bien si il pouvait y avoir plus de jeux textuels dans une langue autre que l’anglais sur machines rétro !