Tests de contournement de code Inform

Certains bêta-testeurs se sont fait une spécialité de tenter des commandes insolites dans des jeux publiés, par exemple pour toucher ou s’approprier des objets improbables, distants ou prétendument disparus. C’est une très bonne chose sur laquelle s’appuyer pour rendre le modèle de monde plus cohérent.

Afin d’aider les développeurs de jeux Inform (et plus généralement, en mode parser) à anticiper ces commandes et à y répondre de manière judicieuse, comme par exemple:

> Take the Moon
The Moon is too far to be touched and too big to be taken, so you prefer sit for a while and think about your future.

nous proposons ce sujet qui a pour vocation de lister un maximum de commandes insolites ou très inclusives (take all, smell all...) générant des réponses inattendues.

Cela permettra aux auteurs de jeux en mode parser de les prendre en compte dans leurs tests unitaires et de durcir leur code avant les beta-tests.

1 Like

Alors, pour moi, la première étape, et qui échappe souvent aux personnes qui se lancent dans leur première FI parser, c’est de connaître l’ensemble des actions qui sont proposées par le système utilisé (dans mon cas inform 6) et notamment quelles réponses par défaut peuvent être piégeuses.

Dans le cas d’inform 6, par exemple, la réponse par défaut à “Touch” (“You feel nothing unexpected”) ou à “Taste” (“You feel nothing unexpected”) laisse penser qu’on a pu faire l’action, ce qui oblige à prévoir les cas où ça n’a pas de sens.

Il me semble que TADS avait été plus malin avec une réponse du type “Cela n’a pas l’air appétissant” pour Taste, ce qui me semble un excellent choix car cela élimine le besoin de se poser la question de gérer l’action sur les objets pour lesquels ça n’a pas vraiment d’utilité.

Donc pour moi, les premières choses à faire quand on part d’un système existant, c’est :

  • Ne pas hésiter à supprimer des verbes inutiles. Cela fera ça de soucis en moins (et d’un autre côté il y a des chances qu’on veuille rajouter d’autres verbes qui ont du sens dans notre jeu).
  • Ne pas hésiter à revoir les réponses par défaut aux actions (idéalement nécessite de bien maîtriser dans quels cas elles sont affichées) afin de se simplifier la vie. Les réponses par défaut qui viennent avec un système tel qu’inform ont une pertinence éprouvée par le temps, mais aussi une part d’arbitraire historique dont on peut être tenté de penser que c’est comme ça et pas autrement qu’il faut faire.
  • Doit-on toujours justifier le refus pour une action ? Pour moi, on doit le justifier quand l’action a du sens : je tente de tirer un truc qui est fixe, il faut que le jeu m’explique pourquoi mon action ne peut pas aboutir. Mais si je tente de goûter le mur ou de fermer un caillou, un simple “Non.” sera tout à fait acceptable dans de nombreux cas ! Et on évite de se prendre les pieds dans le tapis en hasardant une justification qui pourrait ne pas marcher avec tous les objets.

Il y a aussi tout un tas de personnes qui partent du principe que si la commande tapée n’a aucun sens, ce n’est pas tellement grave que la réponse non plus n’ait aucun sens. Et il est vrai que très rares sont les joueurs/joueuses qui goûtent le soleil, donc il peut être vu comme légitime pour quelqu’un qui fait un jeu de ne pas chercher à traiter le cas. Je préfère cependant l’option mentionnée ci-dessus, un “Non.” bête et méchant, à une réponse complètement à côté de la plaque. Ou alors une réponse qui tient compte de caractéristiques de l’objet, mais il faut alors accepter en tant que développeur du jeu qu’on va y passer beaucoup de temps.

Voilà, j’ai pour l’instant esquivé la question de départ, mais ça me semblait intéressant comme préambule.

2 Likes

Il y a de nombreuses années, pour mes premières tentatives de jeux, je m’étais créé un document qui listait toutes les actions supportées par inform, regroupées ainsi :sweat_smile:

  • Actions sans objet direct (dormir, sauter, penser…), généralement à gérer une fois par lieu.
  • Actions à un objet (prendre objet, sentir objet, …).
  • Actions à deux objets (attacher objet1 avec objet2, jeter objet1 sur objet2, mettre objet1 dans objet2…).

Et j’avais noté chacune de ces actions avec zéro à deux étoiles:

  • ** à gérer sur tous les lieux ou objets. prendre est bien évidemment dans ce cas, c’est l’un des plus importants.
  • * à surveiller, même si la réponse par défaut pourrait convenir dans la majorité des cas.
  • aucune étoile: n’a qu’exceptionnellement besoin d’être géré.

Je ne peux qu’encourager quiconque à au minimum se faire sa propre liste à deux étoiles, et à vérifier systématiquement les objets de son jeu à l’aide de cette liste.

Suivre rien que ce principe de base peut éliminer une grande quantité de bugs.

Voici le fichier en question, un peu obsolète, et que je n’utilise plus directement, mais qui peut donner une idée :

Liste_des_actions.pdf (49,1 Ko)

2 Likes

Lorsque je teste un jeu, j’y vais au feeling, donc je ne teste pas de manière exhaustive (ce qui est de toutes façons quasi impossible, la combinatoire est trop grande dans la plupart des jeux).

En revanche, pour mon jeu Station Spatiale S16 Prologue, j’ai commencé à suivre deux principes :

  1. Des classes qui me permettent de gérer par avance des caractéristiques telles que “élément du décor non prenable”, “object lointain” “objet mentionné mais pas là”… Inform est très souple, mais la contrepartie est que sa modélisation du monde est assez faible (contrairement à TADS 3 notamment, dont j’ai trouvé le code horrible, mais la puissance géniale. Malheureusement, on n’a jamais fait de traduction en Français.). Je rejoins donc ton approche consistant à te faire un framework pour blinder un peu les choses, même si je pense qu’en j’en suis loin avec mes petites tentatives.

Exemples :

Class LieuObjet ! objet dans le lieu représentant le lieu pour l'interaction avec le joueur
  class Decor
  with  description [; <<Look>>;],
        react_before [;
            EmptyT: if (second == self) "Tentez plutôt de vider ", (the) noun, " sur autre chose. Du moins, si cela a un sens...";
            Remove: if (second == self) {
                if (noun == joueur) <<Exit self>>;
                "Tentez plutôt de prendre ", (the) noun, " avec vous et de sortir avec. Du moins, si c'est possible...";
            }
        ],
        before [;
            Enter: "Vous y êtes déjà.";
            Exit: <<Go out_obj>>;
            Smell: <<Smell>>;
            Listen: <<Listen>>;
            Taste: "L'endroit est peut-être à votre goût, mais ce n'est pas la maison en pain d'épices de la sorcière !";
            Rub: "Faire le ménage ne fait pas partie de votre mission.";
            Dig: <<Dig>>;
            Search, LookUnder: "Veuillez choisir un objet plus précis que le lieu entier pour ce type d'action.";
            Receive: "Ça y est déjà, directement ou indirectement.";
            ! Actions pour lesquelles les messages par défaut sont généralement OK
            !Take: Drop: Pull: Push: Turn: PushDir: Attack: Squeeze: Cut: Consult: Drink: Burn:
            !Climb: JumpOver: Swing: Buy: Fill: Empty: Tie: Set: SetTo: SwitchOff: SwitchOn: Wave:
            !Unlock: Lock: Eat: Wear: Disrobe: Blow: 
        ];

Class Decor
  has   scenery;

Class LieuObjet ! objet dans le lieu représentant le lieu pour l'interaction avec le joueur
  class Decor
  with  description [; <<Look>>;],
        react_before [;
            EmptyT: if (second == self) "Tentez plutôt de vider ", (the) noun, " sur autre chose. Du moins, si cela a un sens...";
            Remove: if (second == self) {
                if (noun == joueur) <<Exit self>>;
                "Tentez plutôt de prendre ", (the) noun, " avec vous et de sortir avec. Du moins, si c'est possible...";
            }
        ],
        before [;
            Enter: "Vous y êtes déjà.";
            Exit: <<Go out_obj>>;
            Smell: <<Smell>>;
            Listen: <<Listen>>;
            Taste: "L'endroit est peut-être à votre goût, mais ce n'est pas la maison en pain d'épices de la sorcière !";
            Rub: "Faire le ménage ne fait pas partie de votre mission.";
            Dig: <<Dig>>;
            Search, LookUnder: "Veuillez choisir un objet plus précis que le lieu entier pour ce type d'action.";
            Receive: "Ça y est déjà, directement ou indirectement.";
            ! Actions pour lesquelles les messages par défaut sont généralement OK
            !Take: Drop: Pull: Push: Turn: PushDir: Attack: Squeeze: Cut: Consult: Drink: Burn:
            !Climb: JumpOver: Swing: Buy: Fill: Empty: Tie: Set: SetTo: SwitchOff: SwitchOn: Wave:
            !Unlock: Lock: Eat: Wear: Disrobe: Blow: 
        ];

class LieuObjetVoisin ! objet dans un lieu représentant un autre lieu voisin et potentiellement accessible
  class Decor
  with  description 0,
        enter_to 0,
        before [;
            Examine: if (self.description == 0) return self.default_msg();
            Enter: if (self.enter_to ~= 0) <<Go self.enter_to>>;
            default: return self.default_msg();
        ],
        default_msg [; "Si ", (the) noun, " vous intéresse, tentez plutôt d'y aller.";];

Class Inaccessible
  class Decor
  with  before [;
            ! On laisse quelques actions quand même
            Examine, Listen, Smell, Montrer:
            ! Ainsi que celles qui ont une réponse par défaut déjà utile
            Exit:
            ! Et on rejette le reste
            default :
                self.default_msg();
                rtrue;
        ],
        default_msg [; "C'est inaccessible.";];

class Lointain
  class Inaccessible
  with  before [;
            Smell: self.default_msg(); rtrue;
        ],
        default_msg [; print_ret (The) self, " ", (isorare) self, " hors de portée.";];

class PasLa ! Je voulais Absent mais c'est déjà un nom réservé par inform pour un attribut
  class Decor
  with  before [;
            Ask, Answer: <<ParlerSansPrecision self>>; ! Pour éviter le message "pour parler avec..."
            ! Pour quelque chose qui n'est pas là, on rejette toutes les actions, purement et simplement
            default:
                self.default_msg();
                rtrue;
        ],
  with  default_msg [; print_ret (The) self, " ", (isorarenot) self, " pas là.";];

class Porte
  with  before [;
            LookUnder: 
                if (self has open) "Regarder sous une porte ouverte, je n'arrive pas à conceptualiser.";
                else "Aucun interstice ne permet de voir dessous.";
            Turn, Wave: "C'est une porte coulissante, pas le type de porte qu'on peut faire pivoter.";
            Pull, Push: "Vous essayez de faire coulisser la porte, mais elle ne bouge pas.";
            ThrownAt: "La porte vous lance - métaphoriquement - un regard dissuasif.";
            Attack: "La porte reste intacte.";
            Climb: "C'est bien trop lisse. Et puis pour aller où ?";
            Squeeze: "Vous en êtes incapable";
            Dig: "La porte est trop solide.";
        ],
        affiche_statut [;
            if (self has open) print (The) self, " est ouvert", (es) self, ".";
            else print (The) self, " est fermé", (es) self, ".";
        ],
  has   female scenery door openable ~open;
  1. Des templates de code à copier-coller pour chaque objet. Leur tête peut faire peur car ils sont très longs, mais je fais juste un petit passage rapide sur la liste des actions, je supprime tout ce que je n’ai pas vraiment besoin de gérer, et ne garde que le reste. Ainsi, peu de chances d’oublier des actions importantes à gérer (mais on en oublie quand même toujours).
Lieu lieu_unlieu ""
  with  description "",
        !in_to "FIXME",
        !out_to [; return self.cant_go();],
        !cant_go "FIXME",
        compass_look [;
            !if (noun == u_obj) "FIXME"; ! regarder en haut
            !if (noun == d_obj) "FIXME"; ! regarder en bas
            !if (noun == out_obj) "FIXME"; ! regarder dehors
        ],
        before [;
            !Listen: if (~~noun) rfalse;
            !Smell: if (~~noun) rfalse;
            !CrierSansPrecision: rfalse;
            !Jump: rfalse;
            !Swim: rfalse;
            !WaveHands: rfalse;
            !Dig: rfalse;
            !Sing: rfalse;
            !Pray: rfalse;
            !Think: rfalse;
            !Sleep: rfalse;
            !SeLever: rfalse;

            ! Penser aussi au sol, au plafond, à l'intérieur et à l'extérieur, si cela a du sens
        ],
  has   voit_pas_dehors; ! OU simulation;

LieuObjet -> lieu_unlieu_proxy ""
  with  name '',
        adjective '',
        before [;
            !Open: rfalse;
            !Close: rfalse;
            !Lock: rfalse;
            !Unlock: rfalse;
            !Voir la liste des actions ci-dessous pour le reste

            !Enter: déjà géré : "Vous y êtes déjà."
            !Exit: déjà redirigé en GoOut
        ],
  has   ;


Decor -> unlieu_un_objet ""
  with  name '',
        adjective '',
        description "",
        before [;
            ! Actions à un objet
            !LookUnder: rfalse;
            !Search: rfalse;
            !Take: rfalse;
            !Drop: rfalse;
            !Pull: rfalse;
            !Push: rfalse;
            !Turn: rfalse;
            !PushDir: rfalse;
            !Wave: rfalse;
            !Smell: rfalse;
            !Listen: rfalse;
            !Taste: rfalse;
            !Touch: rfalse;
            !Rub: rfalse;
            !Enter: rfalse;
            !Exit: rfalse;
            !Open: rfalse;
            !Close: rfalse;
            !Attack: rfalse; ! peut aussi être à deux objets : "casser x avec y"
            !Squeeze: rfalse;
            !Cut: rfalse; ! peut aussi être à deux objets : "couper x avec y"
            !Eat: rfalse;
            !Drink: rfalse;
            !Burn: rfalse; ! peut aussi être à deux objets : "brûler x avec y"
            !Consult: rfalse;
            !Climb: rfalse;
            !JumpOver: rfalse;
            !Swing: rfalse;
            !Buy: rfalse;
            !Fill: rfalse;
            !Empty: rfalse;
            !Wear: rfalse;
            !Disrobe: rfalse;
            !Dig: rfalse; ! peut aussi être à deux objets : "creuser x avec y"
            !Tie: rfalse; ! peut aussi être à deux objets : "attacher x avec y". Attention, formulation réversible.
            !Blow: rfalse;
            !Set: rfalse;
            !SetTo: rfalse; ! supprimé du jeu
            !SwitchOff: rfalse;
            !SwitchOn: rfalse;

            ! Actions à deux objets
            !Unlock: rfalse;
            !Lock: rfalse;
            !Give: rfalse;
            !Insert: rfalse;
            !PutOn: rfalse;
            !Remove: rfalse;
            !ThrowAt: rfalse;
          
            ! "Fake actions", générées en réaction à une autre action
            ! On peut aussi vouloir intervenir en react_before si on veut réagir sans vérifier que 'noun' accepte l'action
            !Receive: rfalse;
            !LetGo: rfalse;
            !ThrownAt: rfalse;

            ! Conversation et interaction avec les personnages
            !ParlerSansPrecision: rfalse;
            !Ask: rfalse;
            !Tell: rfalse;
            !Answer: rfalse;
            !AskFor: rfalse;
            !AskTo: rfalse;
            !WaveHands: rfalse;
            !WakeOther: rfalse;
            !Kiss: rfalse;

            !default: rfalse;
        ],
        ! Pour les créatures
        react_before [;
            ! Actions à deux objets - partie interceptée dans react_before
            !Montrer: if (second == self) rfalse;
            !EmptyT: if (second == self) rfalse;
        ],
        life [;
            ! Actions à deux objets - partie interceptée dans life: Give et Show, en tant que "receveur"
            ! (mais dans ce jeu pas de Show, remplacé par Montrer)
            !Give: rfalse;
        ],
        orders [;
            !rfalse;
        ]
  has   ;

Cela m’aide d’aimer jouer et d’être le type de joueur qui aime vérifier la “finition” des jeux, car j’arrive assez bien à me mettre dans la peau d’un joueur de mon type. Bien sûr, cela n’est pas suffisant, mais je crois pouvoir dire qu’avant même les beta-tests, mon jeu était déjà d’une qualité honorable.

2 Likes

(dans mon prochain message, ma dernière approche en date avec RegTest)

Déjà merci, c’est très concret et très qualitatif. N’hésite surtout pas si tu souhaites ajouter des éléments au fur et à mesure.

De mon côté, je suis d’autant plus attentif à cette question que j’écris un jeu à scope étendu, j’ai donc un système que je perfectionne progressivement pour permettre ou non les interactions avec les objets distants (trop loin, trop petit, pas encore découvert, dans un bois touffu, perdus au milieu d’une room immense, pas assez éclairé, et surtout : connus du joueur ou pas, et à quelles conditions). C’est effectivement complexe car une vue subjective de l’environnement est ainsi présentée au joueur, dynamiquement. Mais bizarrement, plus je gomme les incohérences, plus le système se clarifie. Lorsqu’il sera stable, je posterai quelque chose ici pour le présenter.

Et oui, j’utilise aussi les templates ! Pas seulement pour les objets, d’ailleurs. Comme quoi les approches systématiques et rigoristes aboutissent à des méthodes similaires…

2 Likes

!!! Avec des expressions rationnelles ? On peut en injecter dans le prompt ?

RegTest veut dire “Regression Tester” ici : RegTest: Simple IF Regression Tester

Il y a bien la possibilité de faire des expressions rationnelles, mais pas dans le prompt (j’ai du mal à voir comment on s’en sortirait d’ailleurs) : juste pour la vérification du texte affiché, pour être moins rigide qu’avec une vérification de texte identique.

2 Likes

Ah OK, merci, je ne connaissais pas du tout.

Pour mon tout petit jeu Le miroir d’Ozivior, je suis parti dans une direction différente.

Déjà, la situation est simplifiée dans le sens où je suis parti d’une idée d’énigme, et que j’ai construit le lieu et les objets sur le papier autour de ça.

Ensuite, je me suis interdit de me lancer dans le code, sachant pertinemment que je risquais de me perdre une fois de plus sur des éléments de détail, voire de perdre la vision d’ensemble une fois le nez dans le guidon.

Donc, j’ai suivi un conseil entendu maintes fois : écris d’abord un transcript d’une partie complète de ton jeu. Mais j’étais aussi conscient d’un risque avec cette approche, un piège dans lequel bien trop de personnes tombent : celui de ne développer que le squelette, le chemin critique, au risque d’ignorer tout un tas d’actions légitimes des joueurs et joueuses auxquelles on n’aura pas pensées, trop focalisés sur notre scénario nominal.

Donc, en écrivant mon transcript, j’ai fait le joueur qui se balade, qui digresse, qui tente des trucs, surtout quand je me disais “ah, là ils vont forcément tenter ça, il faut que je le prévoie”. Bien sûr, je n’ai pas tout prévu, mais ça m’a donné déjà pas mal de chair mise sur le squelette.

Puis, j’ai fourni tout ça à RegTest, et ai commencé à coder, dans une approche Test Driven Development qui, pour la première fois de ma vie, avait du sens (trop dur pour moi à appliquer dans des cas plus complexes qu’un jeu où finalement tout est affaire d’input et d’output en tour par tour). J’ai bien sûr trouvé des cas où ce que j’avais prévu n’était pas idéal, et j’ai adapté les tests, mais résultat, en quelques heures d’implémentation j’avais mon jeu jouable et déjà pas loin d’être complet. Et avec la garantie qu’il est finissable, puisque j’avais les tests en place.

Et surtout, cela m’a donné une base de tests solide sur laquelle m’appuyer pour tous les ajustements et ajouts que j’ai fait par la suite, et cela m’a sauvé la mise plus d’une fois.

Les quelques jours qui ont suivi, j’ai réussi à trouver 7 personnes pour le tester (dont seulement trois dans mon foyer immédiat :D), et j’ai conservé tous les transcripts et leurs remarques. Plusieurs ont eu du mal à trouver la logique de l’énigme, et j’ai donc modifié le jeu pour le rendre plus évident, reformulé des indices… Pour chaque personne, j’ai créé un fichier de test nommé “retours-nom.test”, et j’ai écrit des tests couvrant les problèmes qu’ils avaient rencontrés, voire les quelques bugs trouvés (rares, mais il y en avait. Par exemple, tu prends le balai, tu regardes la cheminée, ça te dit toujours que le balai est posé à côté) - auxquels j’ai ajouté des tests pour des améliorations qui me venaient à l’esprit grâce à leurs retours.

Dans le prochain message… L’acharnement…

2 Likes

Donc, j’avais un jeu qui fonctionnait bien, mais j’avais une frustration : je n’avais pas appliqué ma revue systématique des actions importantes sur chaque objet. Je savais donc qu’il restait des trous dans le filet.

J’ai donc, courageusement comme un inconscient, décidé de tester toutes les actions sur tous les objets (et malgré ça, on est loin d’une exhaustivité, car il y a tellement de verbes, d’objets qu’on pourrait essayer de faire interagir, que cela ne serait vraiment pas faisable).

D’abord, lister tous les objets. Bon, il y en a environ 35, et certains peuvent changer d’état. Ça m’a pris dans le meilleur des cas 10 minutes pour tester un objet, dans le pire des cas 1h30 voire 2h (mais POURQUOI ai-je autorisé les joueurs à entrer dans cette satané cheminée ? Ça ne sert à rien, mais ça oblige à faire un tri atroce entre actions permises et actions interdites depuis cet emplacement !).

Je n’ai pas encore terminé. Il m’en reste 7. Quand j’en aurai fini avec eux, ce sera la version 1.0 du jeu.

Voici mon template, que j’ai fait évoluer au fur et à mesure.

** game: ../jeu/miroir.z8
** interpreter: tools/bocfel/bocfel

#1. ajouter des tests pour les actions ou formules grammaticales spécifiques à votre jeu
#2. ajouter des tests pour les comportements spécifiques
#3. adapter le template (remplacer "le truc", puis "truc", "le machin", puis "machin"...)

* Tests relatifs à l'objet truc

# Manières de se référer à l'objet
> x le truc
TODO

# Comportements spécifiques


# Actions à un objet
> regarder sous le truc
aucune découverte notable

> fouiller le truc
Vous ne trouvez rien d'intéressant.

> regarder derrière le truc
# pas top, mais on est limité par la lib, à moins d'ajouter une action
Vous ne trouvez rien d'intéressant.

# La réponse à cette question influence potentiellement la réponse à plusieurs autres questions
# qui déclenchent un Take implicite.
> prendre le truc
C'est trop difficile à transporter.
#C'est fixe
#D'accord

> poser le truc
déjà là.
#D'accord

> tirer le truc
Rien d'évident ne se produit.
#Vous en êtes incapable

> pousser le truc
Rien d'évident ne se produit.
#Vous en êtes incapable

> tourner le truc
Rien d'évident ne se produit.
#Vous en êtes incapable

> pousser le truc vers le nord
Vous ne pouvez donc rien imaginer de mieux ?

> prendre le truc. agiter le truc. poser le truc
Vous n'avez pas cela
#Vous auriez l'air ridicule en agitant

> secouer le truc
Vous n'avez pas cela

> sentir le truc
Vous ne sentez rien d'inattendu

> écouter le truc
Vous n'entendez rien d'inattendu

> goûter le truc
Vous préférez ne pas goûter n'importe quoi.

> toucher le truc
Vous ne sentez rien d'inattendu.

> frotter le truc
Vous n'arrivez à rien ainsi.

> entrer dans le truc
!Programming error
Vous ne pouvez pas

> sortir de le truc
Vous n'êtes pas

> sortir par le truc
Vous ne pouvez pas y aller

# enterables (containers/supporters)
#> entrer dans le truc. sortir
#/Vous sortez.*truc
#> entrer. sortir de le truc
#Vous entrez dans le truc
#> monter. descendre de le truc
#Vous montez sur le truc

> ouvrir le truc
Vous ne pouvez pas ouvrir

> fermer le truc
Vous ne pouvez pas fermer

> presser le truc
Vous n'arriverez à rien ainsi.

> manger le truc
non comestible

> boire le truc
Ceci n'a rien de buvable.

> consulter le truc au sujet de le machin
/Vous ne découvrez rien. Soit.*pas consultable(s)? ainsi, soit.*mauvais sujet

> escalader le truc
Escalader ceci n'accomplirait pas grand chose.
# Supporters: climb à rediriger en Enter
#Vous montez

> sauter par dessus le truc
Vous n'arriveriez à rien en faisant cela.

> sauter sur le truc
TODO

> sauter dans le truc
TODO

> se suspendre à le truc
Ce n'est pas une chose à laquelle il est utile de se balancer.

> acheter le truc
pas à vendre.

> remplir le truc
ne vous avancerait pas

> vider le truc
pas un récipient que l'on peut vider
# ATTENTION containers et supporters: risque de bypass de certaines règles ?

> balayer le truc
pas sale

> salir le truc
Non, non, et NON ! On ne va pas faire ce genre de choses à la chambre d'un ami !

# habits
> mettre le truc
Reformulez
#Vous mettez

> mettre le truc sur soi. poser le truc
(vous prenez d'abord
C'est trop difficile à transporter
#C'est fixe
#Vous ne pouvez pas revêtir ou porter cela sur vous.
#Vous mettez

> retirer le truc
C'est trop difficile à transporter.
#C'est fixe
#D'accord
> poser le truc

> souffler le truc
Souffler ne produit rien d'utile.

> souffler dans le truc
Souffler ne produit rien d'utile.

> souffler sur le truc
Souffler ne produit rien d'utile.

> régler le truc
vous ne pouvez pas

> régler le truc sur 10
vous ne pouvez

> éteindre le truc
Vous ne pouvez pas allumer ou éteindre cela.

> allumer le truc
Vous ne pouvez pas allumer cela.


# Actions à un ou deux objets

> frapper à le truc
La seule chose que vous briserez

> casser le truc
La seule chose que vous briserez

> prendre le machin. casser le truc avec le machin. poser le machin
La seule chose que vous briserez

> brûler le truc
rien pour faire du feu

> brûler le truc avec le machin
rien pour faire du feu

> couper le truc
ne mènerait pas à grand-chose.

> couper le truc avec le machin
ne mènerait pas à grand-chose.

> creuser le truc
Creuser cela ne mènerait à rien.

> creuser le truc avec le machin
Creuser cela ne mènerait à rien.

> attacher le truc
Vous n'arriveriez à rien en faisant cela.

> attacher le truc avec le machin
Vous n'arriveriez à rien en faisant cela.

> attacher le machin avec le truc
Vous n'arriveriez à rien en faisant cela.
> poser le machin


# Actions à deux objets

> déverrouiller le truc
Vous ne pouvez pas déverrouiller cela.

> déverrouiller le truc avec le machin
Vous ne pouvez pas déverrouiller cela.

> verrouiller le truc
Vous ne pouvez pas verrouiller cela.

> verrouiller le truc avec le machin
Vous ne pouvez pas verrouiller cela.

> donner le truc à quelqu_un
TODO

> montrer le truc à quelqu'un
TODO

> donner le truc à moi. poser le truc
(vous prenez d'abord
C'est trop difficile à transporter
#C'est fixe
#Vous vous remerciez pour ce cadeau.

> prendre le truc. mettre le truc sur le lit
/Vous devez avoir en main .* avant de pouvoir
#Vous mettez
> retirer le truc de le lit
Ça n'y est pas.
#D'accord
> poser le truc

# supporters
> mettre le machin sur le truc
/Poser des objets sur .* ne mènerait à rien.
#Vous mettez
# vérifier qu'on voit bien le contenu en regardant / fouillant
#> regarder le truc
#machin
#> fouiller le truc
#machin
> retirer le machin de le truc
Ça n'y est pas.
#D'accord

# containers
> mettre le machin dans le truc
/ne peu(ven)?t pas contenir
#Vous mettez
# vérifier qu'on voit bien le contenu en regardant / fouillant
#> regarder le truc
#machin
#> fouiller le truc
#machin
> retirer le machin de le truc
Ça n'y est pas.
#D'accord

> jeter le truc sur le machin
(vous prenez
C'est trop difficile à transporter
#C'est fixe
#Futile.

> jeter le machin sur le truc. prendre le machin
Futile.

> jeter le machin dans le truc. prendre le machin
ne peut pas contenir

> jeter le machin par le truc. prendre le machin
ne peut pas contenir


# Conversation et interaction avec les personnages
> parler à le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> interroger le truc au sujet de le machin
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> parler de le machin à le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> dire bonjour à le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> demander le machin à le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> demander à le truc d'ouvrir le machin
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> saluer le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> réveiller le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> embrasser le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> montrer le machin à le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

> donner le machin à le truc
[Vous ne pouvez agir ainsi qu'avec une chose animée.]

J’ai eu dans l’idée de le remplacer par un script python qui me poserait quelques questions (est-ce un conteneur, peut-on y entrer en tant que personnage, peut-on le prendre, …) et générerait un fichier de test un peu plus adapté, mais je n’ai pas (encore ?) créé cette chose. Si je le faisais, j’ai dans l’idée d’avoir plusieurs modes. Le mode essentiel, où on teste les actions les plus importantes, et le mode parano, où on génère le test pour toutes les actions. J’ai dans l’idée que le mode essentiel serait le plus rentable.

Quoi qu’il en soit, cette systématisation du test m’a permis de trouver pas mal de réponses pas adaptées, et pas que des trucs improbables qui ne sortiront jamais dans aucune partie d’aucun joueur. Cela dit, je serais peut-être allé plus vite et aussi loin en reprenant plutôt l’approche du template de code. Mais je n’aurais pas eu autant de tests de non régression me protégeant en cas de bourde future.

2 Likes

Tiré du template ci-dessus, exemple de regexp:

> consulter le truc au sujet du machin
/Vous ne découvrez rien. Soit.*pas consultable(s)? ainsi, soit.*mauvais sujet
1 Like

Un autre point : j’ai parlé d’actions, mais que faut-il prévoir et tester. Des actions, ou des formulations (verbes + suite) ? Très clairement, pour moi, des formulations, des verbes. Car c’est ça qui est donné en entrée. Les actions (ou commandes, dans le jargon Inform 7 ?), c’est ce vers quoi on traduit ces formulations en entrée, mais, notamment en Français, un même verbe peut mener vers plein d’actions différentes, en fonction des objets, du contexte…

Sauter sur un ennemi, c’est l’attaquer.

Sauter sur un lit, c’est monter dessus, ou l’utiliser comme trampoline…

Sauter dans le lit, c’est comme sauter sur le lit.

Mais sauter dans un bassin ce n’est pas vraiment sauter sur un bassin.

Frapper une porte et frapper à une porte, ce n’est pas la même chose. Mais par défaut on n’aura pas d’action prévue pour “frapper à la porte”. Doit-on l’ajouter dans notre jeu, en étendant la grammaire et le nombre d’actions à gérer, ou accepter l’approximation, et prévoir une réponse un peu fourre-tout à l’action Attack sur une porte ? (ce que j’ai fait dans ce jeu avec la réponse “Vous frappez vigoureusement à la porte. Cela ne la fait pas bouger et ne provoque pas de réponse.”).

On a tellement plus de verbes et de formules possibles que d’actions vers lesquelles on les traduit, et en théorie les joueurs n’ont pas à le savoir. Mais maintenir l’illusion nécessite d’éviter les erreurs d’interprétation autant que possible.

Tout cela pour en venir à ce que je veux dire dans ce dernier (?) message : j’ai certes constitué ma liste de tests à partir des actions que gère Inform 6, mais comment faire pour couvrir les cas où des intentions différentes vont mener de manière inappropriée à la même action ? Je n’ai pas vraiment de réponse, mais le sujet revient souvent, dès qu’on se retrouve face à une réponse nulle dans un jeu parce que dans le contexte ce qu’on voulait c’était A mais le jeu ne sait que comprendre B. Et plus on tente de tout prévoir, plus on se rend compte que le langage c’est quand même vraiment complexe :smiley:

2 Likes

Voilà, sinon, dans les choses que j’aime bien tester, il y a mettre de gros objets dans des petits (encore une chose que, je crois, TADS gère par défaut en te proposant de donner des dimensions à tes objets, mais ma lecture de la doc remonte à très loin).

Et si j’ai deux conteneurs, mettre A dans B puis B dans A et voir les deux disparaître, c’est assez drôle quand ça “marche”.

Ou alors, exemple récent, on me donne une flamme magique pour brûler un truc en particulier. Bien évidemment que je vais essayer de tout cramer. Et quand le jeu me répond “vous n’avez rien pour faire du feu” parce que la flamme a visiblement été créée pour un seul but et que rien d’autre n’en tient compte, jusque dans les messages de refus, je grimace un peu. Une grosse partie du boulot, c’est de réduire la liberté des joueurs, mais avec tact.

2 Likes

Merci mille fois pour toutes ces explications très utiles.

Je ne comprends pas (encore) tout de l’emploi de RegTest, qui m’a tout l’air d’une puissante approche industrielle pour les tests. Il faudra que je m’y intéresse de près, notamment en m’inspirant ton Retex. Comme je ne suis pas développeur de métier, ça va me demander un petit temps d’adaptation.

Je ne peux pas me permettre l’approche TTD dont tu parles, parce-que je me suis engouffré dans un projet vaste, celui d’une campagne de JDR de ma création, avec beaucoup de lieux et d’objets. Encore que je vais probablement commencer par un prologue qui tiendra lieu de tutoriel ; mais disposer d’une campagne de tests organisée constitue de toute façon un passage obligé, vu le nombre de tests manuels que je m’inflige déjà (je souhaite développer plusieurs jeux dans le même univers, donc j’éprouve ce que je code).

Par contre, je suis partisan de prendre le problème par l’autre bout de sa racine, et ici ma grosse expérience de jeu de rôle sur table m’est très bénéfique. Il est impossible de tout prévoir et de tout improviser quand on se retrouve face à 6 joueurs pendant des heures, joueurs à l’imagination ô combien surprenante, même pour moi. Ici, j’ai appris à exploiter les univers de jeu dans toute la puissance ce que leur aspect fictionnel propose : des thèmes, une perspective (donc un choix, donc une exclusion), qui permettent d’approfondir une narration tout en réduisant (concentrant) le scope des verbes/commandes/actions pertinent(e)s.

Par exemple, dans un jeu subtil d’investigation, on n’a pas trop besoin de détails sur les actions de type sportif ; dans une histoire orientée angoisse métaphysique, on évacue les aspects romantiques des relations entre les êtres (cf. Lovecraft). Cette pratique du genre narratif rejoint ce que tu dis par ailleurs sur le traitement des verbes, dont le nombre peut aussi être concentré au portefeuille nécessaire par un effet d’entonnoir ou par esquive pure et simple avec le ton qui convient. Non seulement cela nous évite le scope creep (pauvres auteurs qui implémentent ou conservent tasting sans avoir travaillé le thème au préalable pour périmétrer le truc…), mais de plus cela cadre le résultat attendu des tests unitaires : en dehors de la cuisine du grand restaurant, notre personnage chef de brigade ne veut absolument rien goûter : Vous n'êtes pas au boulot, là. Essayez un peu de vous distraire.

Comme tu dis, d’ailleurs coder me fait parfois l’impression d’une impossible explicitation de cette complexité.

2 Likes

Clairement, je ne suis pas sûr qu’écrire les tests avant de coder soit toujours faisable (quoique, au final dans les deux cas c’est écrire du texte. Le test n’est peut-être qu’une vue différente sur le code - ou sur le monde, et à ce titre, je pense que cela reste souvent faisable d’écrire le résultat attendu avant le code qui le produira).

Si je pense avoir bien compris ce que tu écris sur la réduction du scope notamment en fonction du genre narratif (et je suis tout à fait d’accord), je n’ai pas bien saisi ce que sont les deux bouts de la racine dans ta métaphore :).

1 Like

Pardon, je clarifie : pour disposer d’un code qui prend en compte de façon cohérente le plus grand nombre de commandes possibles, les deux méthodes radicales qui se complètent sont donc :

  • Une thématique forte et assumée qui périmètre le scope ;
  • Une méthode de type systématique à tendance exhaustive de tests.

Notre discussion m’a déjà permis de franchir un cap majeur dans mon code. En effet, comme je travaille avec un scope étendu (le personnage principal est un investigateur conçu et entraîné pour être particulièrement attentif à son environnement), le parser ne décrit donc pas seulement la room, mais une vision subjective de ce qui se passe ou de ce qui existe autour de la room, en tenant compte de facteurs physiques qui rendent possibles ou pas cette perception, le tout dynamiquement (le texte est généré par construction).

Je suis tombé dans le problème classique de la superposition non parfaite entre les objets du scope et ceux dont le joueur a connaissance. En effet, si la room R+2 est une caverne en limite de champ de vision, je souhaite l’intégrer dans le scope car une fois éclairée, le joueur pourra agir avec les objets qu’elle contient, à distance (examiner) ou en se déplaçant automatiquement (après confirmation) pour réaliser l’action.

Le problème, c’est que les objets de la caverne trop petits pour être vus à distance intègrent aussi le scope, ce qui veut dire que le joueur peut passer des commandes de type examine bones et si plusieurs ossements sont disséminés dans le scope et/ou si le jeu intègre un ack qui reformule la commande du joueur comme feedback de bonne compréhension, le joueur peut essayer de deviner ou peu être perturbé par la présence d’objets non encore découverts dans la narration (notamment via les mécanisme de levée d’ambiguïté). Non pas qu’il soient nécessairement cachés, j’ai des mécanismes pour gérer cela. Simplement, ces objets ne sont pas encore entrés dans l’histoire. Une fois que les objets sont connus, pas de problème s’ils sont encore dans le scope et on peut même rappeler au joueur où ils sont localisés.

Réaliser cette superposition en temps réel est donc indispensable. Pour ce faire, je ne place plus les éléments du monde directement dans les rooms. Je crée une relation bidirectionnelle entre les objets et leur holder (room, container, supporter) puis j’ajoute ces éléments au fur et à mesure de l’exploration du joueur au moment où le scope est évalué par la règle consacrée. Les objets sont intégrés au fur et à mesure de leur perception.

Paradoxalement, cette règle stricte contribue à clarifier mon code, notamment par suppression de règles de gestion de cas particuliers. Pas mal de réponses non convenables du parser reviennent dans le cadre standard. Je vais avoir aussi besoin de documenter un peu tout ça pour m’y retrouver et faire des tests., car une session de refactoring m’attend :slight_smile:

1 Like