Juste avant les vacances de Noël, vous avez pu apprendre à utiliser l’aléatoire en ink grâce à Selsynn. Cette semaine, nous allons créer un système d’inventaire en ink.

Pour ceux qui préfèrent Twine, nous avons également un tuto à disposition pour faire une boutique avec Harlowe !

Pour ce faire nous allons utiliser des LISTs, des threads et des tunnels. Nous allons les mettre en œuvre dans du code facile à réutiliser mais sans expliciter vraiment les concepts. Une fois que vous voudrez en savoir plus (ou si vous préférez commencer par la théorie avant la pratique), vous pourrez vous reporter aux articles : Premiers pas avec les listes en ink ; Les fils (thread), une fonction avancée ; et enfin Ink et les tunnels.

Ink possède un format de données qui ne ressemble à aucun autre connu dans l’univers des langages de programmation. Ce sont les LIST. Si vous avez l’habitude de coder en JavaScript, oubliez tout ce que vous savez à propos des tableaux/arrays, ça n’a rien à voir.

Mais trêve de bavardages, plongeons dans le vif du sujet avec un exemple.

Si vous n’avez pas installé Inky ou si vous voulez simplement vous frotter à ink sans engagement, vous pouvez utiliser Borogove. Choisissez « Ink » et copiez-collez les exemples.

Commençons avec un exemple non interactif : vous êtes un chef et vous désirez cuisiner. Vous commencez avec un saladier vide.

VAR saladier = () // cela crée une variable nommée `saladier`, vide

Vous avez un saladier entre les mains, contenant {LIST_COUNT(saladier)} ingrédients.

Regardons ce que nous avons sur notre plan de travail :

LIST PlanDeTravail = (farine), (sucre), (oeufs), (lait)

// On déclare une LIST dont tous les éléments sont ACTIFS.

Sur le plan de travail, vous pouvez voir {LIST_COUNT(PlanDeTravail)} ingrédients : {PlanDeTravail}.

En ink, lorsque vous déclarez une LIST, vous devez déclarer tous les éléments possibles lors de sa création. Vous ne pourrez pas ajouter de nouvel élément, plus tard, à cette définition.

Mettons que vous attendez une livraison de rhum pour mettre un peu de goût dans vos recettes. Si ce rhum est destiné à être placé sur le plan de travail, vous devez le déclarer dès maintenant.

Mettons à jour notre définition :

LIST PlanDeTravail = (farine), (sucre), (oeufs), (lait), rhum

// On déclare une LIST dont presque tous les éléments sont ACTIFS.

Sur le plan de travail, vous pouvez voir {LIST_COUNT(PlanDeTravail)} ingrédients : {PlanDeTravail}.

Vous remarquerez que rhum n’est pas entre parenthèses puisqu’il n’est pas encore actif. La phrase affichée en-dessous reste inchangée : vous avez toujours seulement 4 ingrédients sur le plan de travail puisque LIST_COUNT ne compte que le nombre d’éléments actifs.

Commençons à cuisiner

Mettons maintenant quelques ingrédients dans notre saladier :

~ saladier += (oeufs, lait)

Vous avez un saladier entre les mains, contenant {LIST_COUNT(saladier)} ingrédients : {saladier}.

Nous avons mis des parenthèses autour de l’ensemble des options que nous avons placées dans le saladier. Le saladier a maintenant deux éléments actifs. Nous voilà avec notre inventaire personnel !

L’opérateur ~ sert à modifier les variables, voir « Ink et les variables ».

La phrase affichée en-dessous dit désormais que nous avons 2 ingrédients. Cela n’aurait pas de sens d’ajouter un élément inactif dans une VAR (souvenez-vous, regardez plus haut, saladier est une VAR), du coup ink ne le permet tout simplement pas.

Ajoutons un ingrédient supplémentaire :

~ saladier += (sucre)
~ saladier += farine // Lorsqu'on ajoute un seul élément, on peut omettre les parenthèses.

Ah mais attendez, erreur de ma part, c’est une recette sans sucre :

~ saladier -= sucre
Vous avez un saladier entre les mains, contenant {LIST_COUNT(saladier)} ingrédients : {saladier}

Dans le monde merveilleux de la ink-cuisine, il est possible de retirer du sucre d’un saladier plein de lait, oui oui.

Qu’est-ce qu’il se passe si on essaie d’ajouter de la farine deux fois ?

~ saladier = () //vide à nouveau
~ saladier += farine
~ saladier += farine

Vous avez un saladier entre les mains, contenant {LIST_COUNT(saladier)} ingrédients : {saladier}

Ce n’est pas possible. Ink ne vous empêche pas de l’écrire, mais la farine ne sera ajoutée qu’une seule fois. Par défaut, Ink envisage chaque objet comme étant fondamentalement unique et ne pouvant être dupliqué. Il y a des manières de contourner cela, mais ce sera l’objet d’un prochain article !

Une recette simple

Ouvrons maintenant notre livre de recettes et choisissons-en une.

VAR recetteCrepe = (farine, oeufs, lait, sucre, beurre)

Attendez un peu ?! Mais d’où vient ce beurre ? Il n’était pas sur notre plan de travail, pas même inactif comme l’est le rhum. Si vous essayez de copier cette ligne dans Inky, il vous renverra aussitôt une erreur. C’est ce qu’on veut. Cela vous empêchera d’utiliser des éléments qui n’ont jamais été préalablement déclarés. Ink utilise énormément cette mécanique pour vérifier que ce que vous avez écrit est cohérent : essayez donc un peu d’écrire un -> divertVersKnotInconnu ! C’est une aide précieuse qui vous empêchera de mettre votre histoire/jeu dans un état impossible ou cassé.

Où est-ce que vous stockez votre beurre vous ? Sur votre plan de travail ? Pour qu’il soit tout fondu ?! Je range le mien au frigo.

LIST Frigo = (beurre)
// En ink, les VAR et les LIST sont globales, vous pouvez donc les déclarer où vous voulez.

Pour faire des crêpes, vous aurez besoin de {LIST_COUNT(recetteCrepe)} ingrédients : {recetteCrepe}.

Comme vous pouvez le constater en regardant à nouveau la définition de notre VAR recetteCrepe, il est tout à fait possible de mélanger des définitions de plusieurs listes.

Comparer des contenus

Si l’on est au sein d’un jeu, cela peut être bien utile de donner un feedback au joueur, notamment pour lui dire :

  • Les ingrédients de la recette déjà dans son saladier en utilisant l’opérateur ^ d’intersection :
~ temp dejaDansLeSaladier = saladier ^ recetteCrepe
Vous avez déjà {dejaDansLeSaladier} dans votre saladier.

Cela renvoie la liste des éléments se trouvant dans les deux choses comparées (deux variables ou une liste et une variable)

On évitera autant que faire se peut, de définir deux éléments qui ont le même nom dans deux LIST différentes. C’est possible mais souvent troublant !

  • Ce qu’il manque dans le saladier, avec l’opérateur - de différence (c’est le même signe moins qu’au dessus)
~ temp ingredientsManquants = recetteCrepe - saladier
Il vous manque encore {ingredientsManquants} dans votre saladier.

Afficher une liste

Par défaut, ink sait montrer une liste (LIST ou VAR) d’éléments en affichant les valeurs de la liste séparées par des virgules. Si cette technique peut suffire lorsque l’on parle anglais, en français on est rapidement limité si l’on a besoin de mots composés ou de groupes nominaux, etc.

Les identifiants dans les listes (et le nom des LIST eux-mêmes !) peuvent absolument comporter des accents, des cédilles etc ! Ne vous en privez pas : LIST prénoms = (Émilie), (Héloïse), (Françoise)

J’utilise en général pour cela une fonction de traduction que je nomme t (pour traduction !)

=== function t(item) ===

{item:
    - farine: de la farine de blé
    - sucre: du sucre roux
    - lait: du lait demi-écrémé
    - oeufs: des œufs
    - rhum: du rhum vieux
    - beurre: du beurre demi-sel
    - else: {item} //traduction par défaut : l'option telle qu'elle est dans la LIST
}

Si vous vous rendez à l’épicerie, vous pourrez alors demander :

Je voudrais {t(beurre)}, s'il vous plaît !

Et j’utilise également en général une fonction bien utile pour écrire tous les éléments d’une liste :

=== function liste_et(items, si_vide) ===

{LIST_COUNT(items):
    - 2:
        {t(LIST_MIN(items))} et {liste_et(items - LIST_MIN(items), si_vide)}
    - 1:
        {t(items)}
    - 0:
        {si_vide}
    - else:
        {t(LIST_MIN(items))}, {liste_et(items - LIST_MIN(items), si_vide)}
}

Si vous ne comprenez pas le fonctionnement de cette fonction, ce n’est pas grave, vous pouvez l’utiliser telle quelle ! Elle affichera vos éléments, passés à travers la fonction de traduction, séparés par des virgules, avec un “et” avant le dernier élément.

Si items est vide, alors c’est si_vide qui sera affiché.

Vous devez encore ajouter : {liste_et(ingredientsManquants, "rien de plus")}.
// Vous devez encore ajouter : du beurre demi-sel, du sucre roux, des œufs et du lait demi-écrémé.
? Les utilisateurs d’Inform reconnaîtront dans la fonction de traduction et cette routine d’affichage un motif familier : le “printed name” et la liste des objets visibles.

Prendre et donner

Que serait un inventaire s’il n’était pas possible de prendre ou de donner des objets !

On se concentre ici sur le seul verbe prendre, et le reste sera un exercice pour le lecteur.

Nous allons utiliser un tunnel comportant des paramètres, le premier paramètre sera ce que nous souhaitons prendre et le second sera la source de cet objet (dans notre exemple, le plan de travail ou le frigo). Pourquoi un tunnel ? Parce qu’il s’agit d’une action effectuée par le joueur qui ramène celui-ci exactement là où il était précédemment. Rien n’empêche évidemment de venir déclencher d’autres actions dans des cas particuliers.

=== prendre(object, ref from) ===

->->

On veut maintenant distinguer les cas d’erreurs d’un joueur qui tenterait n’importe quoi et ajouter l’action effective :

=== prendre(ingredient, ref source) ===

{saladier has ingredient: // Le saladier contient déjà l'ingrédient.
    Vous avez déjà cet ingrédient.
    ->->
}
{source hasnt ingredient: // Le plan de travail n'a plus l'ingredient.
    Vous ne voyez rien de tel.
    ->->
}
~ saladier += ingredient // On ajoute l'ingredient au saladier.
~ source -= ingredient // On désactive l'ingredient du plan de travail.
->->

On peut alors venir jouer avec notre nouveau tunnel :

// Vous devez encore ajouter : du beurre demi-sel, du sucre roux, des œufs et du lait demi-écrémé.

-> prendre(lait, PlanDeTravail) ->
// Vous ajoutez du lait demi-écrémé à votre saladier.

Vous devez encore ajouter : {liste_et(recetteCrepe - saladier, "rien de plus")}.
// Vous devez encore rajouter : du beurre demi-sel, du sucre roux et des œufs.

-> prendre(beurre, PlanDeTravail) ->
// Vous ne voyez rien de tel.

-> prendre(beurre, Frigo) ->
// Vous ajoutez du beurre demi-sel à votre saladier.

Vous devez encore rajouter : {liste_et(recetteCrepe - saladier, "rien de plus")}.

Et ainsi de suite jusqu’à ce que la recette soit complète !

Vous pouvez retrouver tout le code écrit jusque là dans ce snippet Borogove.

Exercice 1 : Écrivez un tunnel qui permet de poser un élément de son propre inventaire (ici un ingrédient) dans/sur une destination (ici, plan de travail ou frigo)

Si vous avez envie de nous proposer une réponse et de recevoir des conseils, retrouvez nous sur le channel #ink de notre Discord !

Un peu d’interactivité !

Jusque là, nous avons surtout modelé un monde passif, nous n’avons jamais laissé la main au joueur. Ce que nous aimerions, c’est placer notre joueur dans la cuisine, un saladier à la main et le laisser prendre ce qu’il souhaite.

On commence par mettre en place cet univers comme un “monde ouvert”, c’est à dire que l’on peut toujours circuler librement entre le frigo et le plan de travail.

-> plan_de_travail

=== plan_de_travail ===

Vous vous trouvez {|maintenant} devant votre plan de travail.

+ [Aller au frigo]
    -> frigo

=== frigo ===

Vous vous trouvez maintenant devant le frigo.

+ [Aller au plan de travail]
    -> plan_de_travail

Vous remarquerez que les noms des nœuds (knot) sont écrits avec des tirets bas alors que les noms des LIST sont souvent écrits avec une majuscule initiale. Ce n’est qu’une convention. Ici cette convention nous est bien utile car autrement les noms de nœuds entreraient en conflit avec ceux de nos LISTs.

Et on vient copier nos LISTs… et les fonctions et tunnels dont nous avons parlé plus haut : t, liste_et et prendre.

VAR saladier = ()
LIST PlanDeTravail = (farine), (sucre), (oeufs), rhum
LIST Frigo = (beurre), (lait)

-> plan_de_travail

=== plan_de_travail ===

Vous vous trouvez {|maintenant} devant votre plan de travail.

+ [Aller au frigo]
    -> frigo

=== frigo ===

Vous vous trouvez maintenant devant le frigo.

+ [Aller au plan de travail]
    -> plan_de_travail

=== function t(item) ===

{item:
    - farine: de la farine de blé
    - sucre: du sucre roux
    - lait: du lait demi-écrémé
    - oeufs: des oeufs
    - rhum: du rhum vieux
    - beurre: du beurre demi-sel
    - else: {item} 
}

=== function liste_et(items, si_vide) ===

    {LIST_COUNT(items):
    - 2:
        	{t(LIST_MIN(items))} et {liste_et(items - LIST_MIN(items), si_vide)}
    - 1:
        	{t(items)}
    - 0:
			{si_vide}
    - else:
      		{t(LIST_MIN(items))}, {liste_et(items - LIST_MIN(items), si_vide)}
    }

=== prendre(ingredient, ref source) ===

{saladier has ingredient: // Le saladier contient déjà l'ingrédient.
    Vous avez déjà cet ingrédient.
    ->->
}
{source hasnt ingredient: // La source (par ex : plan de travail) n'a plus l'ingredient.
    Vous ne voyez rien de tel.
    ->->
}
Vous ajoutez {t(ingredient)} à votre saladier.
~ saladier += ingredient // On ajoute l'ingredient au saladier.
~ Source -= ingredient // On désactive l'ingredient de la Source (par ex : plan de travail)
->->

Dans chacun de ces « lieux », on vient lister ce que l’on peut y voir :

=== plan_de_travail ===

Vous vous trouvez {|maintenant} devant votre plan de travail.

Vous pouvez voir : {liste_et(PlanDeTravail, "rien du tout")}.

[...]

=== frigo ===

Vous vous trouvez maintenant devant le frigo.

Vous pouvez voir : {liste_et(Frigo, "rien du tout")}.

[...]

Il s’agit maintenant de proposer, en plus du choix d’aller voir ailleurs, tous les choix de prendre chaque ingrédient. Une solution de facilité consisterait à les lister un par un dans chacun des lieux et à conditionner l’apparition de chaque choix avec un has. Voire, si l’on peut prendre et déposer un ingrédient, de tous les lister partout :

 === frigo ===

Vous vous trouvez maintenant devant le frigo.

{liste_et(Frigo)}

-(opts)

+ {Frigo has beurre} Prendre du beurre
	-> prendre(beurre, Frigo) -> opts
+ {Frigo has lait} Prendre du lait
	-> prendre(lait, Frigo) -> opts
// Et ainsi de suite.

Outre le fait que c’est long et fastidieux dès que l’on veut rajouter un nouvel élément, c’est une source d’erreurs potentielles (ex: la liste_et déclare qu’un élément est là, mais comme on a oublié de mettre le choix, le joueur ne peut pas prendre l’objet…) Et si l’on veut rajouter un « lieu » et une LIST qui va avec… n’en parlons pas.

Générer des choix à partir d’une liste

Nous allons générer les choix à partir d’une liste en utilisant un thread paramétré avec la liste à afficher et un retour de tunnel. Voyons à quoi ressemble ce thread :

=== choixPrendre(list, -> puis_vers) ===

~ temp ingredient = LIST_MAX(list)
    
{ LIST_COUNT(list) > 0 :
    <- choixPrendre(list - ingredient, puis_vers)
    + [Prendre {t(ingredient)}]
        -> prendre(ingredient, list) -> puis_vers
}

Pour décortiquer son fonctionnement :

  • On choisit le dernier ingredient de la liste avec la fonction LIST_MAX.
  • S’il y a effectivement des éléments dans cette liste :
    • On insère récursivement les choix précédents.
    • On crée un choix : [Prendre l'ingrédient] qui déclenche le tunnel vers prendre et à son retour, fait suivre vers puis_vers

On remarquera la similitude dans le choix créé automatiquement avec ce qu’il aurait fallu écrire à la main si on avait choisi la solution plus haut.

Il ne nous reste plus qu’à ajouter notre thread un peu partout !

=== plan_de_travail ===

Vous vous trouvez {|maintenant} devant votre plan de travail.

Vous pouvez voir : {liste_et(PlanDeTravail, "rien du tout")}.

-(opts)

<- choixPrendre(PlanDeTravail, ->opts)

[...]

=== frigo ===

Vous vous trouvez maintenant devant le frigo.

Vous pouvez voir : {liste_et(Frigo, "rien du tout")}

-(opts)

<- choixPrendre(Frigo, ->opts)

[...]

Et voilà !

Retrouvez le code complet jouable de cet exemple sur ce snippet Borogove !

Le code complet contient :

  • En plus du plan de travail et du frigo : une armoire à épices en quelques lignes de code supplémentaires seulement !
  • Une version améliorée (mais améliorable) du thread d’action qui retire correctement les éléments de leur source (ce n’est pas le cas de l’exemple simplifié ci-dessus)

Votre imagination est désormais votre limite !

Si vous en manquez, voici quelques exercices pour vous entrainer :

Exercice 2 : en utilisant l’action de déposer un élément de l’inventaire de l’exercice 1, créez un thread permettant de déposer un élément n’importe où.

Exercice 3 : créez une recette : en début de partie, un client vous la soumet, vous devez alors rassembler les ingrédients et lui donner pour le satisfaire.

Si vous avez fabriqué un joli prototype, on se fera un plaisir d’y jouer, venez nous le proposer sur le channel #betatest de notre Discord !