singe/docs/fr/lessons/03-decisions.adoc

541 lines
20 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[#_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.