[#_lesson_3_making_decisions] == Leçon 3 : Prendre des décisions image::learn/03-decisions.png[La leçon terminée, 480] Tout ce que vous avez écrit jusqu'ici fait la même chose à chaque image. Le mot de la leçon deux bougeait, que vous le vouliez ou non, et la seule raison pour laquelle il faisait demi-tour, c'étaient quatre blocs de code qu'on vous avait demandé de croire sur parole. Cette leçon, ce sont ces quatre blocs, expliqués pour de bon. À la fin, votre programme posera des questions, agira selon les réponses, et -- parce que la question la plus intéressante qu'un jeu puisse poser est « que fait le joueur ? » -- vous piloterez une boîte dans la fenêtre avec les touches fléchées. Gardez cette boîte. C'est le joueur du jeu que vous construirez à la leçon sept. [#_start_a_new_folder_2] === Créez un nouveau dossier Un dossier appelé `decisions`, dedans un fichier appelé `decisions.singe`, et `Singe -R decisions` qui tourne dans un terminal. Comme la dernière fois. [#_what_an_if_really_says] === Ce que dit vraiment un `if` Voici le bloc de la leçon deux, tout seul. [source,lua] ---- if wordX > lastColumn then wordX = lastColumn end ---- Quatre parties. Le mot `if`. Une question. Le mot `then`. Et un corps de lignes qui ne s'exécutent que lorsque la réponse à la question est oui, fermé par `end`. La question est `wordX > lastColumn`, et c'est le `>` qui fait le vrai travail. Il compare les deux nombres de part et d'autre et produit une réponse qui est soit *vrai*, soit *faux*. Ces deux mots ne sont ni du texte ni des nombres ; ce sont des valeurs à part entière, appelées *booléens*, et un booléen est la seule chose qui intéresse un `if`. L'indentation, c'est la même idée qu'à la leçon un : les lignes à l'intérieur sont indentées pour que vous voyiez d'un coup d'œil ce qui appartient au `if`, et le `end` s'aligne sur le `if` qui l'a ouvert. Lua l'exécuterait sans les espaces. Vous, vous ne pourriez pas le lire. Il y a six de ces comparaisons, et vous les utiliserez toutes. [cols="1,4"] |=== | `a < b` | a est plus petit que b | `a > b` | a est plus grand que b | `a <= b` | a est plus petit que b ou égal à b | `a >= b` | a est plus grand que b ou égal à b | `a == b` | a est la même chose que b | `a ~= b` | a n'est pas la même chose que b |=== Ce sont les deux dernières qui demandent de la prudence, pour des raisons différentes. `~=` veut dire « n'est pas égal à ». La plupart des langages l'écrivent `!=`, et si vous avez déjà vu un programme, vos doigts taperont `!=` et Lua le refusera. Le tortillon se trouve sur votre clavier à côté du `1` ou sous le `#`, selon l'endroit où vous vivez. `==`, ce sont deux signes égal, et ce n'est pas une faute de frappe pour un seul. C'est l'une des erreurs les plus courantes en programmation, et elle mérite qu'on s'y arrête un instant. À la leçon deux, vous avez appris qu'un seul signe égal veut dire *mettez le côté droit dans le nom de gauche*. C'est une chose que vous faites. Ça change quelque chose. Deux signes égal veulent dire *ces deux-là sont-ils les mêmes ?* C'est une question que vous posez. Ça ne change rien, et ça renvoie vrai ou faux. [source,lua] ---- lives = 3 if lives == 3 then ---- La première ligne met `lives` à trois. La seconde demande s'il vaut trois. Des travaux différents, des symboles différents, à un caractère près. Lua vous attrapera si vous les confondez, et vous rencontrerez l'erreur qu'il emploie à la fin de cette leçon. [#_when_the_answer_is_no] === Quand la réponse est non Un `if` tout seul ne fait rien du tout quand la réponse est fausse. Très souvent, vous voulez que l'autre chose se produise à la place, et c'est `else`. [source,lua] ---- if lives > 0 then message = "Ready" else message = "Game Over" end ---- Exactement un de ces deux corps s'exécute, jamais les deux, jamais aucun, donc `message` finit toujours par contenir l'une des deux chaînes, et jamais rien. Quand il y a plus de deux possibilités, `elseif` les enchaîne. [source,lua] ---- if score >= 10000 then rank = "ace" elseif score >= 5000 then rank = "pilot" elseif score >= 1000 then rank = "rookie" else rank = "passenger" end ---- Lua essaie les questions en partant du haut et s'arrête à la première qui est vraie. Un score de 6000 n'est pas plus grand que 10000, donc il essaie la suivante, qui est vraie, donc `rank` devient `pilot` et les deux autres questions ne sont jamais posées. Ce dernier point compte : le `elseif` pour 1000 est vrai aussi pour un score de 6000, et il n'est jamais atteint, c'est pourquoi l'ordre va du plus grand au plus petit. Toute la chaîne n'a besoin que d'un seul `end`, quel que soit le nombre de parties `elseif` qu'elle contient. C'est une seule décision, pas quatre. [#_true_false_and_nothing_at_all] === Vrai, faux et rien du tout Pas besoin de comparer deux choses pour obtenir un booléen. Vous pouvez en garder un. [source,lua] ---- paused = false ---- C'est une variable comme une autre, et la valeur qu'elle contient est le booléen `false` lui-même, écrit sans guillemets parce que ce n'est pas le mot « false », c'est l'idée. Il en va de même pour `true`. Une variable qui contient l'un des deux peut aller directement dans un `if`, sans aucune comparaison : [source,lua] ---- if paused then ---- Lisez-le « si paused ». Ça se lit mieux que `if paused == true`, qui fait la même chose par le chemin le plus long, et les programmeurs expérimentés vous regarderont bizarrement si vous l'écrivez. Trois mots combinent et inversent ces réponses. [source,lua] ---- if alive and not shielded then if coins > 0 or freePlay then ---- `and` est vrai quand les deux côtés le sont. `or` est vrai quand l'un des côtés l'est, ou les deux. `not` change vrai en faux et faux en vrai. Ils se lisent comme de l'anglais et, chose rare en programmation, ils veulent dire ce qu'ils veulent dire en anglais. Il reste une valeur à connaître, et c'est celle qui produit les erreurs les plus déroutantes de votre première année. [source,lua] ---- overlayPrint(0, 0, "lives " .. tostring(lives)) ---- Si vous n'avez jamais rien mis dans `lives`, ça affiche `lives nil`. Lua ne se plaint pas d'un nom qu'il n'a jamais vu ; il vous donne `nil`, qui est son mot pour *rien ici*. Ce n'est pas zéro et ce n'est pas une chaîne vide. Zéro est un nombre que vous avez voulu ranger. `nil`, c'est l'absence de valeur. Vous avez déjà rencontré `nil` deux fois sans qu'on vous le dise. `frames = frames + 1` sans la ligne `frames = 0` au-dessus échoue parce que vous avez demandé à Lua d'ajouter un à rien. Et un callback mal orthographié est `nil` lui aussi, c'est pourquoi un `onOverlayUpdate` mal écrit vous donne une fenêtre noire et aucune erreur : Singe cherche le nom, n'y trouve rien, et poursuit tranquillement sa journée. `tostring` est dans cette ligne parce que `..` ne joint un booléen ou un `nil` à rien du tout. Les nombres, il les convertit pour vous ; ceux-là, non. C'était la promesse de la fin de la leçon deux, et maintenant vous en avez l'usage. Enfin, la règle qui relie toute cette partie, parce que `if` accepte tout ce que vous lui donnez, pas seulement vrai et faux : [quote] Tout est vrai en Lua, sauf `false` et `nil`. Zéro est vrai. Une chaîne vide est vraie. Seuls ces deux-là ne le sont pas, et une fois que vous le savez, `if movingLeft then` se lit parfaitement, que `movingLeft` contienne `true`, `false`, ou n'ait jamais été rempli du tout. [#_reading_the_controls] === Lire les commandes Il est temps de demander quelque chose au joueur. Il faut pour ça un nouveau callback. [source,lua] ---- function onInputPressed(what) if what == SWITCH_RIGHT then boxX = boxX + 10 end end ---- `onInputPressed` est le deuxième callback que vous écrivez, et il marche exactement comme la leçon un l'a décrit : vous l'écrivez, Singe l'appelle. `onOverlayUpdate` est appelé à intervalles réguliers. Celui-ci est appelé quand quelque chose se passe -- quand le joueur appuie sur n'importe quoi que le moteur reconnaît. La différence avec `onOverlayUpdate`, c'est le mot entre les parenthèses. Singe doit vous dire *quelle* commande a été enfoncée, alors il passe une valeur à la fonction, et `what` est le nom sous lequel vous avez choisi de la recevoir. La leçon un appelait arguments les choses que vous passez à une fonction ; vu de l'intérieur, le nom qui en attrape un est un *paramètre*. Le nom est à vous : appelez-le `what`, appelez-le `button`, appelez-le `k`. Singe le remplit dans tous les cas. `SWITCH_RIGHT` est un nom que le moteur a déjà donné à une valeur, comme `OVERLAY_UPDATED` à la leçon un. Vous n'avez rien à charger pour vous en servir ; Singe les définit tous avant que votre script s'exécute. Il y en a un pour tout ce qu'a une borne : `SWITCH_LEFT`, `SWITCH_RIGHT`, `SWITCH_UP`, `SWITCH_DOWN`, de `SWITCH_BUTTON1` à `SWITCH_BUTTON4`, `SWITCH_START1`, `SWITCH_COIN1`, et d'autres encore. Le tableau Constantes du moteur de la référence couvre toute la famille, qui va de `SWITCH_UP` à `SWITCH_RECORD`. Ils ne s'appellent pas « la flèche droite », et c'est voulu, parce que le même `SWITCH_RIGHT` arrive, que le joueur ait poussé une touche fléchée, un joystick ou le stick d'une manette. Vous écrivez le jeu une fois et il marche sur les trois. Ajoutez ce callback, donnez à `boxX` une valeur de départ en dehors des fonctions, et dessinez quelque chose à `boxX`. Appuyez sur la flèche droite. La boîte saute de dix pixels à chaque appui. [#_it_happened_and_it_is_happening] === C'est arrivé, et c'est en train d'arriver Gardez la touche fléchée enfoncée. Il ne se passe rien de plus. `onInputPressed` est appelé une fois, quand la touche descend, et pas une seconde fois avant que vous l'ayez lâchée et enfoncée de nouveau. Il n'y a pas de répétition automatique. C'est exactement ce qu'il faut pour certaines choses -- tirer, insérer une pièce, choisir un élément de menu -- et exactement ce qu'il ne faut pas pour marcher, qui doit continuer tant que la touche est tenue. Ce sont deux questions vraiment différentes. *C'est arrivé*, c'est un événement, et vous l'apprenez une fois. *C'est en train d'arriver*, c'est un état, et ça dure. Singe vous informe des événements, et la façon de transformer des événements en état, c'est de vous en souvenir vous-même. Deux callbacks, une variable entre eux. [source,lua] ---- movingRight = false function onInputPressed(what) if what == SWITCH_RIGHT then movingRight = true end end function onInputReleased(what) if what == SWITCH_RIGHT then movingRight = false end end ---- `onInputReleased` est le troisième callback, et c'est le miroir du deuxième : Singe l'appelle quand la touche remonte, avec la même valeur `SWITCH_`. À eux deux, ils font que `movingRight` dit la vérité sur ce que fait le doigt du joueur à cet instant. Rien n'a encore bougé, et c'est voulu. Appuyer et bouger sont maintenant deux travaux séparés. Le mouvement va là où va tout le travail de chaque image. [source,lua] ---- function onOverlayUpdate() if movingRight then boxX = boxX + BOX_SPEED end ... ---- La boîte glisse maintenant en douceur tant que la touche est enfoncée, à une vitesse que vous choisissez, et l'arithmétique est revenue dans la fonction qui s'exécute soixante fois par seconde, là où est sa place. Cette forme -- les événements lèvent des drapeaux, l'image agit selon les drapeaux -- est la façon dont presque tous les jeux gèrent leurs commandes, et vous vous en servirez de nouveau dans chaque leçon où il y a un joueur. [#_drawing_a_box] === Dessiner une boîte `overlayBox` prend deux coins : la gauche et le haut du rectangle, puis la droite et le bas. [source,lua] ---- colorForeground(255, 220, 0) overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE) ---- Elle dessine un *contour*, quatre traits d'un pixel d'épaisseur, et non un rectangle plein. Il n'y a aucun rectangle plein parmi les appels de dessin de l'overlay. Quand vous voudrez un bloc plein plus tard, il y a des façons d'en obtenir un, et la leçon neuf présente la façon habituelle. Ses nombres sont des pixels, pas des cellules de caractère. Ça piège du monde, parce que `overlayPrint`, à la leçon deux, comptait en cellules de six pixels sur treize, et tout le reste de Singe compte en pixels. C'est `overlayPrint` l'exception ; les autres appels de dessin -- `overlayBox`, `overlayLine`, `overlayCircle`, `overlayEllipse` et `overlayPlot` -- travaillent tous dans les mêmes pixels que ceux qu'indique `overlayGetWidth`. `colorForeground` règle la couleur, avec trois nombres de 0 à 255 : combien de rouge, combien de vert, combien de bleu. Les trois à 255, c'est du blanc, tous à 0, c'est du noir, et `(255, 220, 0)` est un jaune chaud. Remarquez que ce n'est pas un argument passé à `overlayBox`. C'est un réglage : vous le changez, et tout ce que vous dessinez ensuite sort dans la nouvelle couleur jusqu'à ce que vous le changiez de nouveau. `overlayPrint` fait encore exception -- la police de console arrive avec ses propres couleurs intégrées et ne vous écoute pas. [#_the_whole_thing_2] === Le tout [source,lua] ---- BOX_SIZE = 20 BOX_SPEED = 2 width = overlayGetWidth() height = overlayGetHeight() maxX = width - BOX_SIZE - 1 maxY = height - BOX_SIZE - 1 boxX = width // 2 boxY = height // 2 movingLeft = false movingRight = false movingUp = false movingDown = false function onInputPressed(what) if what == SWITCH_LEFT then movingLeft = true elseif what == SWITCH_RIGHT then movingRight = true elseif what == SWITCH_UP then movingUp = true elseif what == SWITCH_DOWN then movingDown = true end end function onInputReleased(what) if what == SWITCH_LEFT then movingLeft = false elseif what == SWITCH_RIGHT then movingRight = false elseif what == SWITCH_UP then movingUp = false elseif what == SWITCH_DOWN then movingDown = false end end function onOverlayUpdate() if movingLeft and not movingRight then boxX = boxX - BOX_SPEED elseif movingRight and not movingLeft then boxX = boxX + BOX_SPEED end if movingUp and not movingDown then boxY = boxY - BOX_SPEED elseif movingDown and not movingUp then boxY = boxY + BOX_SPEED end if boxX < 0 then boxX = 0 elseif boxX > maxX then boxX = maxX end if boxY < 0 then boxY = 0 elseif boxY > maxY then boxY = maxY end overlayClear() if movingLeft or movingRight or movingUp or movingDown then colorForeground(255, 220, 0) else colorForeground(80, 160, 255) end overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE) overlayPrint(0, 0, "x " .. boxX .. " y " .. boxY) return OVERLAY_UPDATED end ---- Quatre choses là-dedans méritent un second regard. `movingLeft and not movingRight` est là parce qu'un joueur peut tenir les deux flèches à la fois. Sans le `not`, les deux se battraient et la boîte tremblerait ou dériverait ; avec, tenir les deux veut dire qu'aucune ne gagne et que la boîte reste immobile, ce qu'un joueur attend. `maxX` est calculé une fois en haut, avec `overlayGetWidth` comme à la leçon deux, et le `- 1` est là parce que le dernier pixel de la surface vaut un de moins que sa largeur. Enlevez le `- 1` et le bord droit de la boîte tombe hors de la surface et disparaît, parce que Singe jette sans se plaindre tout ce que vous dessinez en dehors de l'overlay. Les blocs de limitation utilisent `elseif` plutôt que deux blocs `if` séparés. La boîte ne peut pas dépasser le bord gauche et le bord droit en même temps, donc poser la deuxième question quand la première était vraie, c'est du travail perdu. Et la décision de couleur est un `if` dont le seul travail est de choisir une valeur. Jaune quand le joueur fait quelque chose, bleu quand il ne fait rien, ce qui est un moyen gratuit de voir que votre gestion des commandes marche vraiment. [#_try_it_3] === Essayez . *Changez la vitesse.* `BOX_SPEED = 1` et `BOX_SPEED = 8`. À huit, regardez ce que fait la limitation quand vous poussez la boîte dans un coin. . *Affichez vos drapeaux.* Ajoutez une ligne à `onOverlayUpdate` : `overlayPrint(0, 1, "left " .. tostring(movingLeft))`. Tenez puis relâchez la flèche gauche et regardez-le basculer. Puis enlevez le `tostring` et lisez l'erreur que Lua vous donne. . *Accélérez quand un bouton est enfoncé.* Ajoutez un drapeau `running` piloté par `SWITCH_BUTTON1` dans les deux callbacks d'entrée, et utilisez un `if` dans `onOverlayUpdate` pour avancer de deux fois `BOX_SPEED` tant qu'il est vrai. . *Marquez le milieu.* Dessinez une deuxième boîte, petite, au centre de la fenêtre, qui change de couleur quand la boîte du joueur la chevauche. Il vous faudra `and` pour combiner quatre comparaisons, et c'est plus difficile qu'il n'y paraît. Se tromper un peu est normal ; la leçon onze le fait correctement. . *Retirez les relâchements.* Supprimez toute la fonction `onInputReleased` et lancez. La boîte fait un pas et ne s'arrête plus jamais. Trouvez quelle ligne n'est maintenant jamais atteinte. [#_break_it_on_purpose_3] === Cassez-le exprès Trouvez le bloc de limitation et remplacez le `<` de sa première comparaison par un seul signe égal, pour qu'elle devienne : [source,lua] ---- if boxX = 0 then ---- Enregistrez-le. La fenêtre reste vide et le terminal affiche : ---- Error running script: decisions/decisions.singe:57: 'then' expected near '=' ---- C'est l'erreur contre laquelle tout le passage sur `==` vous mettait en garde, et le message est un bon exemple d'erreur parfaitement exacte et parfaitement inutile si vous la prenez au pied de la lettre. Lua ne vous demande pas d'ajouter le mot `then`. Lua est allé jusqu'à `if boxX`, attendait ensuite une comparaison ou un `then`, et a trouvé `=`, qui ne peut être ni l'un ni l'autre. Chaque fois que vous voyez `'then' expected` après un `if`, cherchez un `=` simple qui devrait être double. C'est presque toujours ça. Remarquez où celle-ci est apparue : le jeu n'a jamais démarré, comme pour la chaîne non terminée de la leçon un, parce que Lua ne pouvait pas lire le fichier. L'erreur du compteur d'images de la leçon deux s'est produite pendant l'exécution. Les erreurs dans la forme de votre code vous arrêtent avant que le jeu démarre. Les erreurs dans ce que fait votre code attendent qu'il tourne. [#_what_you_learned_3] === Ce que vous avez appris * `if question then ... end` n'exécute un corps que quand la réponse est vraie. * `else` donne l'autre cas, et `elseif` en enchaîne d'autres ; la première question vraie gagne et les autres ne sont jamais posées. * Les comparaisons sont `<`, `>`, `<=`, `>=`, `==` et `~=`. * `=` met une valeur dans un nom. `==` demande si deux choses sont les mêmes. * `true` et `false` sont des valeurs, et vous pouvez les garder dans des variables. * `and`, `or` et `not` combinent et inversent ces réponses. * `nil` est ce que vous obtenez d'un nom que vous n'avez jamais rempli, et ce n'est pas zéro. * Tout est vrai, sauf `false` et `nil`. * `onInputPressed` et `onInputReleased` sont des callbacks que Singe appelle avec une valeur `SWITCH_` quand le joueur appuie sur une commande ou la relâche. * Un appui est un événement et n'arrive qu'une fois ; gardez l'état dans une variable et agissez selon lui à chaque image. * `overlayBox` dessine un contour en pixels, et `colorForeground` est un réglage, pas un argument. [#_next_time_3] === La prochaine fois Une boîte, c'est facile. Cent étoiles, non, du moins pas de la façon dont vous avez écrit les choses jusqu'ici, parce que taper cent lignes `overlayPlot` n'est pour personne l'idée d'une bonne soirée. La leçon quatre, c'est la boucle : dire à l'ordinateur de faire la même chose de nombreuses fois, avec quelque chose de différent à chaque tour, et c'est ce qui rend possible un écran rempli de n'importe quoi.