[#_lesson_12_score_lives_and_game_over] == Leçon 12 : Score, vies et fin de partie image::learn/12-score.png[La leçon terminée, 480] Votre jeu tient le score, retire des vies et affiche GAME OVER. C'est presque un jeu. Il manque encore trois choses. Il n'a pas de porte d'entrée : il est au milieu de l'action à l'instant où il se charge. Il oublie : quittez-le, et le meilleur score jamais obtenu part avec. Et il garde la trace de ce qu'il fait avec deux variables vrai-ou-faux, `over` et `dying`, ce qui est le piège dont la leçon sept vous a prévenu. Cette leçon règle les trois. Une seule variable pour ce que fait le jeu, un écran titre d'où partir, un meilleur score qui est toujours là le lendemain, et une deuxième partie qui commence exactement comme la première. [#_a_game_is_always_in_exactly_one_state] === Un jeu est toujours dans exactement un état Regardez n'importe quelle borne d'arcade pendant deux minutes. Elle affiche un titre et attend quelqu'un. Quelqu'un appuie sur start et elle joue. Le joueur n'a plus de vies et elle le dit, puis elle se remet à attendre. La machine fait toujours exactement une de ces choses. Dodge, à la leçon sept, gardait cela dans une seule variable qui contenait un mot : `"waiting"`, `"playing"` ou `"over"`. Le jeu de la leçon neuf est revenu à un `over` vrai-ou-faux, et la leçon dix a ajouté `dying` à côté. Comptez ce que deux de ces variables peuvent dire. Chacune vaut `true` ou `false`, donc ensemble elles font quatre combinaisons. Trois veulent dire quelque chose : la partie en cours, la mort du vaisseau et la fin de partie. La quatrième, les deux à la fois, est absurde, et votre code doit prendre soin de ne jamais la produire. Il en prend soin : regardez `endGame`, qui remet `dying` à `false` avant de mettre `over` à `true`. Ajoutez maintenant un écran titre. Ça fait un troisième drapeau, huit combinaisons, dont quatre absurdes. C'est exactement le piège de la leçon sept, et c'est là que les petits jeux vont mourir. Alors faites comme Dodge. Quatre états, une seule variable. Mettez les noms des états sous vos autres constantes : [source,lua] ---- local TITLE = "title" local PLAYING = "playing" local DYING = "dying" local OVER = "over" ---- Puis retirez `over` et `dying` de vos variables, et mettez une seule ligne à leur place : [source,lua] ---- local state = TITLE ---- Dodge se servait des mots eux-mêmes. Des noms en majuscules ne sont pas plus malins, et ils ne rattrapent pas une faute de frappe : Lua traite un nom mal tapé comme `nil` et un mot mal tapé comme un mot qui ne correspond à rien, et l'un comme l'autre vous donne un jeu qui cesse sans bruit de répondre. Ce que les noms vous apportent, c'est une liste. Chaque état dans lequel le jeu peut se trouver est écrit une fois, en haut du fichier, là où vous voyez les quatre d'un coup d'œil. Partout où le jeu levait un drapeau, il règle maintenant l'état. Dans `updateRocks`, là où la dernière vie s'en va : [source,lua] ---- if lives <= 0 then state = DYING deathChannel = channel ---- Dans `endGame`, deux lignes n'en font plus qu'une, et la combinaison absurde a disparu parce qu'il ne reste plus rien à combiner : [source,lua] ---- function endGame() deathChannel = -1 state = OVER musicStop(theme, 1500) end ---- Et dans `startGame`, les deux lignes qui baissaient les deux drapeaux deviennent `state = PLAYING`. [#_one_place_that_decides] === Un seul endroit qui décide À chaque image, `onOverlayUpdate` prend les deux décisions qui comptent : ce qui bouge, et quels mots vont par-dessus. Ce qui bouge tient en un seul test : [source,lua] ---- if state == PLAYING then updateShip() updateShots() updateRocks() end ---- Ça remplace `if not over and not dying then`, et ça dit ce que ça veut dire. Sur l'écran titre, pendant que joue la dernière explosion, et une fois la partie finie, rien ne bouge. Vous l'obtenez en n'appelant pas les fonctions qui font bouger les choses. Arrêter un jeu n'est pas une fonctionnalité spéciale ; c'est laisser des choses de côté. Le monde est toujours dessiné à chaque image, quel que soit l'état, exactement comme Dodge le dessinait. Seuls les mots changent. Remplacez la ligne `drawHud()` par ceci : [source,lua] ---- if state == TITLE then drawTitle() else drawHud() end ---- `drawTitle` est une nouvelle fonction, et elle va après `drawStars` : [source,lua] ---- function drawTitle() overlayPrint(25, 6, "R O C K S") overlayPrint(25, 9, "BEST " .. best) overlayPrint(22, 12, "PRESS 1 TO PLAY") end ---- `best` est le meilleur score jusqu'ici, et il arrive à la section suivante ; le jeu ne tournera pas tant qu'il ne sera pas là. Avant la première partie, la liste des rochers est vide, donc ce que montre l'écran titre, ce sont les étoiles qui défilent et le vaisseau qui attend en bas, et c'est tout ce dont un écran titre a besoin. Une ligne doit partir, sinon vous ne verrez jamais l'écran titre. Supprimez le `startGame()` tout en bas du fichier. Le jeu commence maintenant sur son écran titre, et *Recommencer une partie* explique pourquoi cette ligne compte. `drawHud` pose lui-même une question. Changez son `if over then` en : [source,lua] ---- if state == OVER then ---- Le callback des commandes demande le même genre de réflexion. Le voici en entier : [source,lua] ---- function onInputPressed(what) if what == SWITCH_LEFT then goLeft = true elseif what == SWITCH_RIGHT then goRight = true elseif what == SWITCH_BUTTON1 and state == PLAYING then shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY } soundPlay(shootClip, 0, 35) elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then soundPlay(clickClip) startGame() end end ---- Lisez-le depuis le haut. Les touches fléchées sont notées quel que soit l'état, parce qu'elles disent seulement quelles touches sont enfoncées, et rien ne fait bouger le vaisseau tant que l'état n'est pas `PLAYING`. Le tir ne veut dire quelque chose que pendant la partie. Et `1` lance une partie depuis l'écran titre ou depuis l'écran de fin de partie, mais pas pendant que la dernière explosion résonne encore : la partie n'est pas finie tant qu'elle ne l'a pas dit. Les parenthèses marchent comme en arithmétique. Le `or` qu'elles contiennent est calculé d'abord, puis le `and` le relie à la touche. [#_score_and_lives] === Score et vies Vous avez les deux depuis la leçon neuf, et ils ne changent presque pas. Ce qui est nouveau, c'est le meilleur score, qui commence sa vie comme une variable ordinaire. Mettez-le avec les autres, sous `lives` : [source,lua] ---- local best = 0 ---- Affichez-le sur la ligne du haut, entre les deux autres. Dans `drawHud` : [source,lua] ---- overlayPrint(1, 1, "SCORE " .. score) overlayPrint(25, 1, "BEST " .. best) overlayPrint(50, 1, "LIVES " .. lives) ---- Et donnez à `endGame` la tâche de remarquer un record : [source,lua] ---- if score > best then best = score end ---- Lancez-le. L'écran titre affiche `BEST 0`. Appuyez sur `1`, tirez sur quelque chose, perdez, et l'écran de fin de partie montre votre score comme meilleur score. Appuyez de nouveau sur `1` : le meilleur score reste, et le score repart de zéro. Maintenant, quittez, relancez, et regardez l'écran titre : `BEST 0`. Le record a vécu exactement aussi longtemps que le programme. C'est la deuxième moitié de cette leçon. Pendant que vous regardez les vies, remarquez que la leçon neuf a écrit le test `lives <= 0`, et non `lives == 0`, et gardez-le ainsi. Aujourd'hui, rien ne peut retirer deux vies à la fois, donc les deux tests donnent forcément la même réponse. Le jour où vous ajouterez un rocher qui en coûte deux, `== 0` enjambera zéro et le joueur continuera de jouer avec moins une vie, alors que `<= 0` continuera de marcher. Demandez ce que vous voulez dire. [#_starting_again] === Recommencer une partie Vous avez déjà supprimé le `startGame()` du bas du fichier. À la leçon dix, la première partie était lancée par cette ligne et toutes les suivantes par un appui sur `1`, ce qui faisait deux entrées. Maintenant, il n'y en a plus qu'une. Chaque partie, la première comprise, commence quand quelqu'un appuie sur `1` et que `startGame` s'exécute, donc la première partie et la cinquième commencent exactement de la même façon. `startGame` doit remettre en place tout ce qu'une partie change : [source,lua] ---- function startGame() shipX = (screenWidth - shipWidth) / 2 score = 0 lives = START_LIVES state = PLAYING deathChannel = -1 shots = {} for i = 1, ROCK_COUNT do rocks[i] = {} newRock(rocks[i]) end musicSetVolume(50) musicPlay(theme, -1) end ---- Il est tout aussi important de savoir ce qu'il laisse tranquille, et pourquoi. `best` survit à toutes les parties ; c'est tout son travail. Les étoiles sont du décor. Et `goLeft` et `goRight` n'appartiennent pas du tout au jeu. Elles représentent le clavier, et seul le clavier doit les changer. Ce dernier point est un bogue que vous venez de corriger sans le remarquer. Le callback des commandes de la leçon dix commençait par `if over then`, et tant que la partie était finie, il jetait toutes les touches sauf `1`. Appuyez sur la flèche droite pendant que GAME OVER est affiché, gardez-la enfoncée, et appuyez sur `1`. Le nouveau vaisseau reste immobile : l'appui sur la flèche droite a été jeté, le relâchement n'a pas encore eu lieu, et donc `goRight` vaut `false` alors que la touche est de toute évidence enfoncée. Le vaisseau ne bouge pas tant que vous ne lâchez pas la touche pour appuyer de nouveau. Le nouveau callback note les touches fléchées dans tous les états, donc la touche tenue est toujours tenue quand la partie commence. Les bogues de redémarrage sont les bogues les plus courants de tous les jeux, et ils donnent tous la même impression quand on joue : la deuxième partie est subtilement fausse alors que la première allait bien. Quand ça arrive, lisez `startGame` et posez-vous deux questions. Qu'est-ce que la partie précédente a changé et que celle-ci ne remet pas en place ? Et qu'est-ce que celle-ci remet en place alors que ce n'était pas au jeu d'y toucher ? [#_a_number_that_outlives_the_program] === Un nombre qui survit au programme Tout ce que votre jeu a gardé jusqu'ici -- `score`, `lives`, la liste des rochers -- vit en mémoire, et la mémoire disparaît à l'instant où le programme s'arrête. Singe donne à chaque jeu une sauvegarde bien à lui : un ensemble de noms avec des valeurs rangées dessous, écrit sur le disque pour vous. C'est une autre partie du kit de jeu de la leçon onze, donc c'est le même genre de chose que `collideRects` : de simples appels, rien à charger ni à libérer. [source,lua] ---- saveSet("highScore", 4200) ---- `saveSet` prend un nom -- la *clé* -- et une valeur. La clé, c'est vous qui la choisissez, et elle ne change jamais ; la valeur, c'est ce dont vous voulez vous souvenir. Les nombres, les chaînes, `true` et `false`, et des tables entières de tout cela peuvent y aller. Ce qui n'a de sens que pendant que le programme tourne, non. Un handle de sprite n'est qu'un nombre, donc il se sauvegarde, et demain c'est un nombre qui ne veut plus rien dire. Une fonction ne peut pas du tout s'écrire sur le disque, et essayer d'en sauvegarder une arrête le jeu sur une erreur plutôt que de vous laisser le découvrir la semaine prochaine. Relire la valeur, c'est l'autre moitié. Changez la ligne que vous avez ajoutée pour `best` : [source,lua] ---- local best = saveGet("highScore", 0) ---- Elle s'exécute une fois, au démarrage du jeu. Le second argument est la *valeur par défaut* : ce qu'il faut répondre quand rien n'a jamais été sauvegardé sous cette clé. On y revient dans un instant, parce que c'est toute l'erreur de cette leçon. Et faites en sorte que `endGame` note le record quand il tombe : [source,lua] ---- function endGame() deathChannel = -1 state = OVER musicStop(theme, 1500) if score > best then best = score saveSet("highScore", best) saveFlush() end end ---- Vous n'avez pas à demander à Singe d'écrire le fichier. Il l'écrit une fois à la fin de chaque image où quelque chose a changé, quel que soit le nombre de clés que vous avez réglées, et de nouveau quand le jeu s'arrête. `saveFlush`, lui, l'écrit immédiatement : un nouveau meilleur score mérite une écriture de fichier rien qu'à lui, parce qu'on coupe une borne d'arcade à la prise et qu'une image, c'est long. Lancez-le, établissez un score, quittez, et relancez-le. L'écran titre s'en souvient. La section Sauvegarde de la Référence donne le reste de la famille. `saveDelete` oublie une clé, `saveClear` oublie tout -- c'est le « remettre à zéro les meilleurs scores » d'un menu de service -- et `saveGetAll` vous donne une copie de l'ensemble, ce qui est utile quand vous voulez afficher toute la sauvegarde pendant que vous traquez quelque chose. [#_where_the_file_actually_goes] === Où va vraiment le fichier Il ne va pas à côté de votre script, et ce n'est pas un hasard. Un jeu fini peut se trouver sur un disque, sur un partage réseau, ou dans un seul fichier empaqueté (leçon trente), et on ne peut écrire dans aucun d'eux. Un jeu qui sauvegarde à côté de son propre script marche parfaitement sur la machine où vous l'avez écrit et échoue sur la moitié des machines où il est installé. Singe 2 faisait comme ça, et c'était une erreur. Donc tout ce que Singe écrit pour un jeu -- la sauvegarde, les captures d'écran, son journal -- va au même endroit, et `singeGetDataPath` vous dira où : [source,lua] ---- debugPrint(singeGetDataPath()) ---- Mettez ça juste sous la ligne `dofile` un instant et lancez-le. Il affiche `data/rocks/` : le dossier `data` qui est apparu dans votre dossier de travail la première fois que vous avez lancé quoi que ce soit, à la leçon un, et dedans un dossier qui porte le nom du dossier de votre jeu. Vous y trouverez `save.json`. Ouvrez-le dans votre éditeur de texte : c'est du texte simple, et votre meilleur score s'y trouve, sous le nom que vous lui avez donné. C'est aussi la réponse chaque fois que vous avez besoin d'un fichier à vous. Construisez son nom à partir de `singeGetDataPath()` et il atterrit dans un endroit où l'on peut écrire ; écrivez sous un nom de votre invention et tôt ou tard ce ne sera plus le cas. [#_the_first_time_there_is_nothing] === La première fois, il n'y a rien La toute première fois que quelqu'un lance votre jeu, aucun meilleur score n'a jamais été sauvegardé. `saveGet` n'a rien à renvoyer, alors il renvoie la valeur par défaut que vous lui avez donnée : [source,lua] ---- local best = saveGet("highScore", 0) ---- Rien n'est sauvegardé, donc `best` vaut `0`, l'écran titre affiche `BEST 0`, et la première partie que quelqu'un joue établit un record. C'est exactement ce que vous voulez, et ça coûte un argument de plus. Oubliez le `0` et `saveGet` n'a pas de valeur par défaut sur laquelle se rabattre, alors il répond `nil` -- la valeur dont Lua se sert pour dire « il n'y a rien ici », que vous avez rencontrée à la leçon trois. `nil` n'est pas zéro. Vous ne pouvez rien y ajouter, vous ne pouvez pas le comparer à un nombre, et vous ne pouvez pas le coller à une chaîne. Ça tourne mal dès la première image, à chaque fois, sur chaque machine où le jeu n'a jamais été joué -- c'est-à-dire sur chaque machine où vos joueurs s'apprêtent à l'installer, et pas sur celle où vous testez. Donnez une valeur par défaut à chaque `saveGet`. C'est l'habitude la moins chère de ce livre. [#_what_just_happened_8] === Ce qui vient de se passer [source,lua] ---- local TITLE = "title" ---- Une constante : un nom que vous réglez une fois et ne changez jamais, en majuscules pour que vous voyiez d'un coup d'œil que c'en est une, comme à la leçon sept. Lua ne l'impose pas -- rien ne vous empêche d'affecter une valeur à `TITLE` -- alors les majuscules sont un message pour la prochaine personne qui lira le code, c'est-à-dire vous dans quinze jours. [source,lua] ---- if state == PLAYING then ---- La question qui décide si le monde bouge. Quand vous ajouterez un cinquième état plus tard -- une pause, ou un tour bonus -- la première chose à vous demander sera si le monde bouge dans cet état, et c'est ici que va la réponse. [source,lua] ---- elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then ---- Sans les parenthèses, Lua relierait d'abord le `and` et lirait ceci comme « la touche est `1` et l'état est le titre, ou l'état est la fin de partie ». Alors n'importe quelle touche que les branches du dessus n'ont pas attrapée lancerait une partie depuis l'écran de fin de partie, le tir compris, et un joueur qui martèle encore le bouton de tir quand le dernier rocher le touche serait jeté dans une nouvelle partie dès l'apparition de GAME OVER. Les parenthèses font passer le `or` en premier. Quand vous mélangez `and` et `or`, mettez les parenthèses même si vous croyez connaître l'ordre. La prochaine personne qui lira la ligne n'aura pas besoin de le connaître. [source,lua] ---- saveSet("highScore", best) saveFlush() ---- `saveSet` met le nombre dans la sauvegarde ; `saveFlush` met la sauvegarde sur le disque tout de suite plutôt qu'à la fin de l'image. Les deux sont dans le `if`, donc le fichier n'est écrit que quand le record est vraiment battu, ce qui arrive quelques fois par session plutôt que soixante fois par seconde. [source,lua] ---- local best = saveGet("highScore", 0) ---- Lu une fois, au démarrage, dans une variable ordinaire. Le reste du jeu lit `best` au lieu de rappeler `saveGet`, parce que la sauvegarde est un fichier et qu'une variable est une variable. Écrivez dans les deux quand le record tombe. [#_try_it_12] === Essayez . *Revenez à l'écran titre.* Une borne d'arcade ne reste pas sur GAME OVER pour toujours. Cherchez `timerAfter` dans la section Minuteur de la Référence, une autre partie du kit de jeu, et servez-vous-en dans `endGame` pour remettre l'état à `TITLE` cinq secondes après la fin de la partie. Puis appuyez vite sur `1` après la fin d'une partie et vérifiez que le minuteur ne renvoie pas votre nouvelle partie à l'écran titre au bout de trois secondes. . *Montrez les vies sous forme de vaisseaux.* Dessinez `shipSprite` une fois par vie dans le coin supérieur droit avec `spriteDraw` au lieu d'afficher le nombre. Une boucle `for` de la leçon quatre, et `shipWidth` vous dit à quelle distance les espacer. . *Sauvegardez plus qu'un nombre.* Ajoutez `saveSet("gamesPlayed", saveGet("gamesPlayed", 0) + 1)` à `startGame`, et affichez-le sur l'écran titre. Quittez, relancez, et regardez-le se souvenir. . *Trouvez le fichier et cassez-le.* Ouvrez `data/rocks/save.json` dans votre éditeur, remplacez le meilleur score par quelque chose d'absurde, et lancez le jeu. Puis supprimez complètement le fichier et relancez-le. Ces deux choses arriveront à vos joueurs. . *Remettez-le à zéro depuis le jeu.* Faites qu'une touche sur laquelle vous n'appuieriez jamais par accident appelle `saveClear()`, et regardez l'écran titre ensuite. Attention : `best` est toujours en mémoire, et `saveClear` n'y touche pas. Trouvez ce qui doit se passer d'autre. [#_break_it_on_purpose_11] === Cassez-le exprès Retirez la valeur par défaut, pour que la ligne devienne : [source,lua] ---- local best = saveGet("highScore") ---- Si vous avez déjà établi un meilleur score, il ne se passe rien du tout, et c'est là le piège : la sauvegarde a la clé, donc `saveGet` la renvoie. Supprimez d'abord `data/rocks/save.json`, puis lancez-le. L'écran titre n'apparaît jamais, et vous obtenez ceci : ---- Error executing function 'onOverlayUpdate': rocks/rocks.singe:101: attempt to concatenate a nil value (upvalue 'best') stack traceback: rocks/rocks.singe:101: in function 'drawTitle' rocks/rocks.singe:165: in function 'onOverlayUpdate' ---- Lisez-le comme la leçon huit vous l'a appris. Le fichier et la ligne vous mènent à `overlayPrint(25, 9, "BEST " .. best)` dans `drawTitle`, et la plainte dit que quelque chose qui valait `nil` a été collé à une chaîne. `..` joint deux chaînes, et `nil` n'en est pas une. Si vos lignes sont disposées autrement, vos numéros le seront aussi. La ligne qu'elle nomme n'est pas celle où se trouve l'erreur. La ligne 101 est l'endroit où le jeu a fini par trébucher dessus ; l'erreur a été commise près du haut du fichier, là où on a demandé `best` et où il n'y avait rien. Cet écart entre l'endroit où une mauvaise valeur est fabriquée et celui où on la remarque est ce qu'il y a de plus difficile avec `nil`, et c'est la raison pour laquelle une valeur par défaut coûte un argument et épargne une soirée. Remettez le `0`. [#_what_you_learned_12] === Ce que vous avez appris * Un jeu est dans exactement un état à la fois, et les drapeaux vrai-ou-faux qui essaient de dire lequel engendrent des combinaisons qui ne veulent rien dire. * Gardez l'état dans une seule variable, avec ses valeurs possibles nommées une fois en haut du fichier. * `onOverlayUpdate` décide de ce qui bouge et de ce qui est dit, en interrogeant l'état. * Arrêter un jeu, c'est simplement ne pas appeler ce qui le fait bouger. * Faites commencer chaque partie, la première comprise, par une seule fonction, et faites-lui remettre en place tout ce qu'une partie change -- et rien d'autre. * Les touches appartiennent au clavier. Notez-les dans tous les états. * `saveSet` range une valeur sous une clé, et `saveGet` la relit lors d'un lancement ultérieur. * `saveFlush` écrit le fichier tout de suite, pour les choses que vous détesteriez perdre. * La sauvegarde va dans le dossier de données propre au jeu, que nomme `singeGetDataPath`. N'écrivez jamais à côté de votre script. * `saveGet` sans valeur par défaut répond `nil` quand rien n'a été sauvegardé, et `nil` casse la première image sur une machine qui n'a jamais lancé votre jeu. * Quand vous mélangez `and` et `or`, mettez des parenthèses. [#_next_time_12] === La prochaine fois Votre jeu est maintenant un jeu, et c'est un dossier que vous seul savez lancer. La leçon treize lui donne un `games.dat`, pour que le menu du moteur lui-même le liste avec un titre et une image, et qu'un joueur qui n'a jamais entendu parler de vous puisse le lancer sans rien taper.