Un langage simple et naturel pour écrires des aventures textuelles interactives
Code sourcePyChemin utilise une syntaxe très brève, constituée de séquences de caractères que l'on ne peut trouver facilement dans du texte, permettant l'écriture de contenu sans encombres. Pas d'ambiguité possible.
Quand le narrateur parle écrivez simplement votre texte. Pas de syntaxe nécéssaire, car c'est le plus utilisé dans une histoire textuelle:
Il était une fois, en 2020, un langage de description d'histoire
Quand un personnage parle, la syntaxe reste très légère:
[Personnage] Bonjour, monde!
Personnage
fait référence à l'objet correspondant définit dans characters.py
. Le nom affiché à l'utilisateur sera la propriété name
de l'objet.
Vous serez sûrement amené, dans votre histoire, à interagir avec votre personnage.
Voici comment s'y prendre:? Psst, choisissez un langage de programmation! >> js, javascript, python, ruby [Ewen] Tu aimes le haut-niveau toi! >> cpp, c++, c [Ewen] Hmm. Je vois. Moi les pointeurs, ça me fait peur! >?. [Ewen] Ah. Je ne connais pas ton langage. Tan pis, ça à l'air de te plaire, et c'est l'essentiel!
La première ligne, commençant par un "?", est facultative. Elle permet d'aider le joueur à repondre à la question, mais, la plupart du temps, le narrateur guide les choix du joueur.
Les lignes commençant par ">>" indiquent les choix possibles, séparés par des virgules. Tout les réponses données par le joueur qui sont dans cette liste effectueront les actions dans ce bloc.
Ne vous inquiétez pas, PyChemin est assez intelligent pour comprendre que "javascirpt" correspond à la première ligne
La ligne ">?." Indique quoi faire si le joueur répond en dehors des clous. En écrivant ">?" au lieu de ">?.", on indique à PyChemin que la question doit ensuite être posée de nouveau. Avec un point à la fin, l'histoire continue normalement.
"Et si j'ai plusieurs choix et que le joueur doit passer par toutes les réponses?"
Pas de soucis. Utilisez ">*" au lieu de ">>":
[Mitsuha] Il doit bien être quelque part! Cherche dans ce cabanon Vous vous dirigez vers un cabanon, recouvert par des lianes. Après plusieurs minutes de combat, vous entrez dans le cabanon. Il y a un pot de miel, une guitare et un trombone, tous posés sur un coffre au fond de la salle. >* Miel Le miel semble bon. Mais vu la date de péremption, dépassée depuis 32 ans, vous ne risquez pas une tartine. >* Guitare Elle est totalement désaccordée >* Trombonne Il requiert tant de souffle que vous n'arrivez pas à en faire sortir un seul son Après avoir inspecté tout les objets de la pièce, vous remarquez que le soleil a changé.
Vous pouvez facilement découper votre histoire en actes et en chapitres:
=== Le Nouvel Ordre === [GénéralDuWSL] Suivez moi. --- Le mystère des acronymes ---
Notez que le nombre de tirets ou de signes égal ("=") doit être au minimum de 2
La numérotation des chapitres & actes se fait automatiquement. Voici ce que verra l'utilisateur:
Quand votre magnifique histoire interactive aura grandi, il est important de rester organisé et de ne pas avoir 90 000 000 lignes dans un fichier.
C'est pour ça qu'il est important de créer plusieurs fichiers, et de charger les bons au bon moment.
Pour ce faire, créer d'autres fichiers .pychemin
dans le même dossier, et utilisez des underscores "_" au lieu des espaces.
En suite, pour les appeler dans votre histoire, rien de plus simple:
Vos choix pourront peut-être changer l’histoire... A vous de choisir votre camp. >> reine, lydia, von hardenberg, royaume -> côté reine 1 >> resistance, revolte, rebellion, résistant -> côté résistance 2 >? Veuillez choisir un camp !
Votre joueur ainsi que les personnages avec qui il interagit ont sûrement plusieurs propriétés pouvant changer en fonction des actions. Vous pouvez changer ces propriétés facilement, voici la syntaxe:
@Cible:propriété opération
Plusieurs opérations sont disponibles
Utilisez les symboles usuels: +
, -
et *
@Melinda:relation +5
=
Règle à une valeur donnée, peut importe la valeur précédente.
C'est la seule opération qui peut accepter des interpolations.
Vous finissez par craquer et mangez le pot de miel entier. @Player:food +100 Il est bon, certes, mais vous avez une sensation curieuse pendant un instant. Vous en revoulez. Vous en revoulez tellement qu'il y avait sûrement de la nicotine dedans. Hélàs, le pot n'est plus qu'un verre totalement transparent. @Player:hungry_for_honey =1
Quand la cible est le joueur, vous pouvez l'omettre:
@food -15
Revient à faire...
@Player:food -15
Quand votre stat sera changée, le joueur le vera dans l'interface.
Comme le nom de propriété lié à l'objet python n'est pas toujours très joli, il faut associer le nom de propriété à un nom lisible.
Pour ce faire, ouvrez constants.py
et ajouter des entrées dans le dictionnaire STAT_NAMES
Les propriétés n'ayant pas de nom correspondant seront quand même modifiée, mais n'apparaîtront pas dans l'interace, ce qui peut être pratique.
Pour exécuter du code conditonnellement sans avoir à demander l'avis du joueur, vous pouvez utiliser des conditions:
=? #is_from == 'siberia' 1 [Boris] Salut, je te sers une bière? =? #is_from == 'grand_harz' 2 Борис ne semble pas vouloir vous parler. =! 3 [Boris] Salut !
=?
est du code python, avec #truc
remplacé par self.player.truc
.=?
peuvent être comme on echaînerait des if...elif...elif...
Comment faire dire à un personnage "Salut, (nom du joueur ici)" ?
PyChemin propose la syntaxe suivante:
[Melinda] Hello, #name !
Ici, #name
sera remplacé par le nom du joueur.
Voici la liste des variables utilisables
Certaines données sont décrites par des objets python. Tout les personnages sont des objets de la classe Character
et le personnage est un objet de la classe Player
.
Par exemple, voici comment est ajouté un personnage que l'on pourra référencer par "GénéralDuWSL" dans le fichier pychemin:
GénéralDuWSL = Character( name="Général du WSL", # ... )
Les deux classes héritent de la classe Being
, qui implémente le changement de stat, car cette action est la même que la cible soit le joueur ou un personnage.
La grammaire décrit la syntaxe d'un langage, par exemple, la ligne suivante
[Momodini] Vous avez eu 20/20 en IZN, #name !
est décrite comme une char_line
— ou ligne de personnage — et représente un texte prononcé par le personnage "Momodini".
PyChemin utilise une libraire appelée lark pour décrire et parser sa grammaire.
La ligne de l'exemple précédent est décrite comme ceci:
_char_text: _text _char_name: "[" /[^\]]+/ "]" char_line.2: _char_name " "* _char_text
On n'entrera pas dans les détails de cette syntaxe mais, basiquement, un fichier de grammaire est composé de plusieurs règles, chacune décrivant des motifs pouvant être rencontré dans le texte à parser.
Ici, la ligne _char_name: "[" /[^\
]+/ "]"] déclare une règle, appelée char_name
, qui décrit la composition d'un nom de personnage:
Une règle peut en contenir d'autres: la ligne char_line.2: _char_name " "* _char_text
fait référence à deux autres règles.
Le fichier contient plusieurs de ces règles, et lark va les utiliser pour parser notre fichier .pychemin
Le travail du parser, c'est de scanner notre fichier .pychemin
, et de, à l'aide du fichier décrivant la grammaire, extraire les informations utiles à l'exécution.
Il y a ici deux concepts importants:
Ce sont des objets contenant une propriété value
, qui contient la valeur qu'ils représentent, et une propriété type
qui indique la nature de la valeur qu'ils représentent.
Les trees sont composés d'un type, qui est égal au nom de la règle, et de plusieurs tokens ou autres trees.
Similairement aux tokens, les trees ont aussi un type.
Notre syntaxe est découpée de la manière suivante:
Dialog |—— Ligne 1 |—— Indentations 2 |—— Directive 3
.pychemin
, tel qu'une ligne du narrateur.Un petit travail de post traitement transforme cette structure d'arbre en une liste de couples (niveau_dindentation, directive)
.
Connaître ce niveau d'indentation nous sera utile pour traiter les blocs indentés.
[Ewen] Je ne sais pas quoi dire. >> moi non plus [Ewen] Et Melinda ? Tu es d'accord avec elle ? >> oui @Melinda:relation +5 >?. [Ewen] Bref, il faut sortir de cet exemple...
Par exemple, voici une représentation d'un AST pour la cinquième ligne de l'extrait ci-dessus
Une fois que notre fichier a été transformé en une liste de directives (avec leur niveau d'indentation respectifs), il faut enfin exécuter ce fichier, et afficher quelque chose au joueur.
C'est le travail du walker, qui va itérer chaque ligne et appeler les méthodes correspondantes au type de directive. Chaque directive est associée à une méthode, appelée handle_<type de la directive>
.
Cependant, ce n'est pas aussi simple pour les directives qui sont conditionnées et ne s'éxecuteront pas à chaque fois.
[Artur] C'est bien #name ? >> oui @Artur:relation +5 >> non Il soupire, désespéré >?. Il soupire [Artur] Je prends ça pour un oui...
Ici, la ligne Il soupire, désespéré
ne sera exécutée que si le joueur répond "oui": on ne peut pas juste itérer les lignes une par une et les exécuter: pour gérez des conditions, on doit d'abord collecter les conditions et leur associer des directives à exécuter. Pour cela, la classe Walker
possède plusieurs propriétés.
On prendra par la suite l'exemple des directives constituants des questions à poser au joueur, mais une logique similaire est applicables aux directives =?
et =!
CurrentInput
)C'est la propriété centrale de current_input
. Elle contient une corresponsance entre une liste de choix et une liste de directives à exécuter si la réponse de l'utilisateur fait partie des choix correspondant.
Le souci, c'est que python n'accepte pas d'utiliser des listes comme clés de dictionnaires. Il faut donc transformer cette liste en une chaîne de caractères, avec le module json
.
Pour se faire une idée de la logique, voici un exemple:
On va exécuter l'algorithme suivant pour chaque entrée:
current_input
Si nous sommes en train de collecter des informations sur current_input
, il y a deux cas possibles:
current_input
Dans ce cas là, la directive actuelle est indentée par rapport à la liste de choix précédente: c'est une directive à exécuter si la liste de choix la plus proche est celle choisie par l'utilisateur
current_input.actions_map[current_case
]Dans ce cas là, on est face à deux situations:
Dans ce cas, nous sommes passé à la description d'un autre cas.
current_input.actions_map
current_case
pour l'actualiser à la nouvelle liste de choix (c'est à dire la directive actuelle)Dans ce cas, nous sommes sortis de la condition, et cette ligne décrit tout simplement une directive quelconque qui ne concerne plus la réponse du joueur.
ui.ask
pour poser la question au joueurCet algorithme marche également dans des situations de blocs imbriqués, avec plusieurs conditions de choix les unes dans les autres, grâce à de la récursion: la méthode appelée pour exécuter les directives collectées est la même que celle dans laquelle l'appel est fait. Par ce moyen, peut importe à quel niveau d'indentation nous sommes, on peut traiter des conditions.
Ce module décrit exclusivement des éléments liés à ce qui est visible au joueur: comment devrais-je afficher le texte du narrateur, et cætera.
Au lieu d'écrire du texte instantanéement, il est plus sympathique et captivant de lire un texte qui s'écrit au fur et à mesure.
La fonction typewritter
répond précisémment à ce besoin, en fragmentant le texte en caractères et en ajoutant un délai entre l'écriture de chaque caractère vers le terminal. De plus, cette fonction ajoute des pauses à la fin des phrases.
Quand une statistique a un nom humain associé, il faut que l'utilisateur voit que sa stat a changée.
Pour ce faire, on calcule la différence entre l'ancienne valeur et la nouvelle afin de décider de la couleur du texte.
On affiche ensuite l'opération et sa valeur, le nom de la stat et la nouvelle valeur:
Les paroles prononcées par des personnages sont indentiques à celles du narrateur pour la partie texte sauf que l'on préfixe leur texte avec le nom du personnage.
Chaque personnage est un objet instancié de la classe Character
, et possède donc une couleur. Elle est utilisée comme couleur de fond pour le nom du personnage, et l'on décide de la couleur du texte en fonction: noir si l'arrière-plan est clair, et blanc dans le cas inverse, pour améliorer la lisibilité du nom.
1 Nous n'expliquerons pas les expressions régulières ici, mais vous pouvez vous renseigner en ligne ↑