singe/docs/fr/lessons/08-when-it-goes-wrong.adoc

532 lines
23 KiB
Text
Raw 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_8_when_it_goes_wrong]
== Leçon 8 : Quand ça tourne mal
image::learn/08-when-it-goes-wrong.png[La leçon terminée, 480]
Vous avez écrit un jeu. À partir de maintenant, vous passerez plus de temps à
réparer des programmes qu'à en écrire, et ce n'est pas le signe que quelque
chose ne va pas chez vous. C'est le métier. La différence entre quelqu'un qui
aime programmer et quelqu'un qui abandonne n'est presque jamais le talent ;
c'est de savoir s'il a appris à lire ce que l'ordinateur lui dit.
Cette leçon n'a donc rien de nouveau à construire. Ce sont les cinq messages que
vous verrez le plus souvent pour le reste de votre vie, ce que chacun veut
vraiment dire, et quatre manières de traquer l'erreur qui se cache derrière.
Gardez ouvert `dodge.singe` de la leçon sept, parce que vous allez le casser
exprès plusieurs fois.
[#_two_kinds_of_stop]
=== Deux sortes d'arrêt
Singe s'arrête pour deux raisons différentes, et il le dit de deux manières
différentes.
Quand le script ne veut même pas démarrer, vous obtenez cette forme :
----
Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua lit votre fichier entier avant d'en exécuter un seul mot, et ceci veut dire
qu'il n'a pas pu comprendre ce qu'il a lu. Rien ne s'est exécuté du tout.
Quand le script a bien démarré mais que quelque chose a mal tourné plus tard,
vous obtenez plutôt cette forme :
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge/dodge.singe:98: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
Le jeu tournait. Singe a appelé votre `onOverlayUpdate`, comme il le fait
soixante fois par seconde, et cette fois l'appel n'est pas revenu.
Les deux formes contiennent les trois mêmes choses, et ce sont les trois choses
que vous voulez : un fichier, une ligne et une plainte. Lisez-les dans cet
ordre, à chaque fois.
Les numéros de ligne de cette leçon viennent du script terminé de la leçon sept.
Si les vôtres sont décalés d'une ligne ou deux, votre fichier l'est aussi, et
rien dans la lecture d'une erreur ne dépend de ce que le numéro corresponde.
[#_the_ones_that_stop_it_before_it_starts]
=== Celles qui l'arrêtent avant qu'il démarre
Ce sont les faciles, et ce sont aussi celles qui font le plus mauvais effet,
parce que l'écran reste noir et qu'il n'y a rien à regarder.
Allez dans `updatePlaying` et supprimez le mot `then` de la ligne
`if spawnTimer <= 0 then`. Enregistrez :
----
Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua est arrivé au bout de la ligne du `if`, voulait le mot `then`, et a trouvé à
la place le début de la ligne suivante -- qui est `spawnBlock()`, et c'est la
partie `near 'spawnBlock'`. « Near » veut dire « c'est là que je m'en suis
aperçu », et c'est en général la première chose *après* ce que vous avez oublié.
Maintenant, une plus méchante. Remettez `then`, et supprimez plutôt le `end` de
la dernière ligne de `spawnBlock`. Enregistrez :
----
Error running script: dodge/dodge.singe:148: 'end' expected (to close 'function' at line 58) near <eof>
----
La ligne 148 est la fin du fichier, juste après sa dernière ligne. La ligne 148
n'a rien qui cloche.
Lua a lu jusqu'au bout -- `<eof>` est son nom pour la fin d'un fichier -- en
attendant toujours un `end` qui n'est jamais venu, et à ce moment-là il avait
avalé toutes les fonctions qui suivent `spawnBlock` comme si elles en faisaient
partie.
La moitié utile de ce message, c'est `(to close 'function' at line 58)`, qui est
l'endroit où commence `spawnBlock`. Quand un message montre la fin du fichier,
croyez ce qu'il dit entre parenthèses, pas le numéro de ligne. C'est aussi pour
ça que l'indentation compte tant : un `end` manquant est presque invisible dans
une suite de lignes, et saute aux yeux dès que tout ce qui est en dessous est
décalé d'un cran de trop.
Deux autres à savoir reconnaître, parce que vous les rencontrerez toutes les
deux cette semaine :
----
Error running script: dodge/dodge.singe:41: unfinished string near '"GAME OVER)'
----
Un guillemet ouvert et jamais fermé, que vous avez rencontré à la leçon un.
----
Error running script: dodge/dodge.singe:135: 'then' expected near '='
----
Celle-là, c'est `if state = "playing" then`. Un seul `=` donne une valeur à une
variable, deux `==` posent une question, et Lua ne vous laisse pas donner une
valeur à l'intérieur d'un `if`. La plainte a l'air étrange, et elle veut
toujours dire la même chose.
[#_attempt_to_index_a_nil_value]
=== Attempt to index a nil value
C'est celle que vous verrez le plus, et de loin.
Allez dans `spawnBlock` et faites une faute dans `block` sur la première ligne
qui l'utilise, pour qu'elle se lise
`blocl.x = math.random(0, screenWidth - BLOCK_SIZE)`. Enregistrez, appuyez sur
Espace :
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:60: attempt to index a nil value (global 'blocl')
stack traceback:
dodge/dodge.singe:60: in function 'spawnBlock'
dodge/dodge.singe:92: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
*Indexer*, c'est ce que fait le point. `blocl.x` veut dire « va dans la chose
appelée `blocl` et trouve le `x` qui s'y trouve ». On ne peut faire ça qu'avec
une table.
*Nil*, c'est ainsi que Lua appelle un nom derrière lequel il n'y a rien. Vous ne
l'avez pas beaucoup vu parce que vous avez pris soin de donner une valeur à tout,
mais chaque nom dont vous ne vous êtes jamais servi vaut déjà `nil`, et taper
un nom de travers en invente un tout neuf.
Le message entier dit donc : *vous avez utilisé un point sur quelque chose qui
n'existe pas*.
La partie entre parenthèses est le cadeau. `global 'blocl'` est le nom exact de
la chose qui était vide. Cherchez-le dans votre fichier. S'il n'apparaît qu'une
fois, c'est une faute d'orthographe, et vous l'avez trouvée en dix secondes.
Le mot avant le nom vous dit où Lua l'a cherché :
* `global` -- un nom qui n'a de `local` devant lui nulle part.
* `local` -- un nom que vous avez déclaré avec `local` dans la fonction où vous
êtes.
* `field` -- un nom après un point, comme le `y` de `block.y`.
* `upvalue` -- un `local` venu de plus loin, que cette fonction peut voir, et
c'est ce que sont toutes les variables de `dodge.singe`, puisqu'elles sont
déclarées en haut du fichier et utilisées à l'intérieur des fonctions.
`field` est l'autre cause courante, et ce n'est pas du tout une faute
d'orthographe. Si vous avez écrit `block.hit.when` sans jamais avoir rempli
`block.hit`, vous obtenez `attempt to index a nil value (field 'hit')`. Le point
qui échoue, c'est le deuxième. Lua vous dit lequel en le nommant.
[#_attempt_to_call_a_nil_value]
=== Attempt to call a nil value
Remettez `blocl` comme il faut. Maintenant, faites une faute dans une fonction du
moteur : dans `drawGame`, changez `overlayPrint` en `overlayPirnt`.
Enregistrez :
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:36: attempt to call a nil value (global 'overlayPirnt')
stack traceback:
dodge/dodge.singe:36: in function 'drawGame'
dodge/dodge.singe:140: in function 'onOverlayUpdate'
----
Même `nil`, autre crime. *Appeler*, c'est ce que font les parenthèses, et on ne
peut faire ça qu'avec une fonction. Il n'y a pas de fonction appelée
`overlayPirnt`, donc `overlayPirnt` vaut `nil`, donc les parenthèses n'ont rien
à appeler.
Ça arrive pour quatre raisons, de la plus fréquente à la plus rare :
. Vous avez fait une faute dans le nom d'une de vos propres fonctions.
. Vous avez fait une faute dans le nom d'une de celles de Singe. Rien dans le
moteur ne vérifie votre orthographe au chargement du fichier, donc un nom du
moteur mal écrit n'est qu'un nom de plus qui se trouve être vide, et ça ne
vous coûte rien tant que la ligne ne s'exécute pas.
. La fonction n'existe pas du tout dans Singe -- c'était une supposition
raisonnable, et les suppositions raisonnables sont fausses à peu près une fois
sur deux. La liste des fonctions de la Référence est le seul moyen d'en être
sûr.
. Vous l'avez rendue `local` et vous l'avez appelée depuis un endroit situé
au-dessus de sa déclaration. C'est exactement pour ça que les fonctions de
`dodge.singe` sont globales.
Il existe une version bien pire de cette erreur, et vous l'avez rencontrée à la
leçon un sans le savoir : écrivez mal le nom d'un *callback* et vous n'obtenez
aucun message du tout. `onOverlayUpdated` n'est pas une erreur. C'est une
fonction parfaitement correcte dont Singe n'a jamais entendu parler et qu'il
n'appellera jamais, alors l'écran reste noir et rien ne se plaint. Quand toute
une fonctionnalité ne fait absolument rien, soupçonnez le nom.
[#_attempt_to_perform_arithmetic_on_a_nil_value]
=== Attempt to perform arithmetic on a nil value
Remettez `overlayPrint`. Dans `updatePlaying`, faites une faute dans
`blockSpeed` sur la ligne de chute, pour qu'elle se lise
`block.y = block.y + blockSped` :
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge/dodge.singe:98: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
L'arithmétique, c'est `+`, `-`, `*` et `/`. On ne peut pas ajouter rien à un
nombre. Là encore, les parenthèses nomment la chose vide, et là encore la cause
la plus courante est une faute de frappe -- mais celle-ci a une seconde cause
qu'il vaut la peine de connaître, parce qu'elle est plus difficile à voir.
Une variable à laquelle vous n'avez jamais donné de nombre vaut `nil`, pas zéro.
Si vous ajoutez un meilleur score au jeu et écrivez `local hiScore` en haut sans
`= 0`, elle contient `nil` jusqu'à ce que quelque chose la remplisse, et le
premier `hiScore + score` arrête le jeu. Lua ne fait pas partir les nombres de
zéro pour vous. C'est vous qui les faites partir de zéro.
Deux proches parents, avec la même cause et le même remède :
----
dodge/dodge.singe:37: attempt to concatenate a nil value (upvalue 'hiScore')
----
`..` qui joint une chaîne à quelque chose qui n'est pas là. Vous verrez
celle-là chaque fois que vous afficherez un score que vous avez oublié de
remplir -- ici, `hiScore` ajouté à la ligne du score dans `drawGame`, qui est la
ligne 37 maintenant que `local hiScore` se trouve au-dessus.
----
dodge/dodge.singe:105: attempt to compare nil with number
----
Ça, c'est `if score > hiScore then`, placé sous `state = "over"` pour garder le
meilleur score à la fin d'une partie, sans rien dans `hiScore`. Remarquez que ce
message
ne nomme rien entre parenthèses. Quand Lua ne peut pas vous dire quel côté était
vide, la section après la suivante vous montre comment le découvrir.
[#_an_argument_of_the_wrong_kind]
=== Un argument du mauvais type
Tous les messages jusqu'ici venaient de Lua. Celui-ci vient de Singe, et il a
l'air complètement différent, alors ça vaut la peine de le rencontrer exprès.
Supposez que vous vouliez voir ce que contient `blocks`, et que vous essayiez de
l'afficher. Ajoutez cette ligne dans `drawGame`, sous le score :
[source,lua]
----
overlayPrint(1, 3, blocks)
----
----
37:overlayPrint: Argument 3 must be a string.
----
Pas de nom de fichier, pas de trace de pile, aucun `attempt to` quoi que ce
soit. Une plainte du moteur commence par le numéro de ligne, puis le nom de la
fonction qui a refusé, puis ce qu'elle voulait. Ici, on a donné à
`overlayPrint` une table là où il lui faut du texte, et elle s'est arrêtée
plutôt que de dessiner quelque chose qui n'a pas de sens.
Le message cousin compte au lieu de vérifier :
----
34:overlayBox: Expected 4 argument(s), got 3.
----
C'est un `overlayBox` auquel il manque un coin. Le curieux `argument(s)` est
celui du moteur, pas une faute de frappe de votre part.
Quand vous obtenez l'un de ces messages, l'entrée de cette fonction dans la
Référence est la réponse, et ça vous prendra une minute. Chaque entrée donne les
arguments dans l'ordre, avec ce que chacun doit être. Singe les vérifie parce
que l'autre possibilité, c'est un jeu qui tourne dix minutes puis dessine
n'importe quoi.
[#_finding_out_what_is_really_in_there]
=== Découvrir ce qu'il y a vraiment dedans
La moitié du débogage, ce n'est pas du tout lire des erreurs. C'est le cas où
rien ne plante et où rien ne marche : des blocs qui n'apparaissent jamais, un
joueur qui refuse de bouger, un score coincé à zéro. Pour ça, il vous faut voir
à l'intérieur du jeu en marche, et il y a deux manières de le faire.
La première, c'est `debugPrint`, qui écrit une ligne dans le terminal depuis
lequel vous avez lancé Singe. Prenez la ligne à laquelle vous ne comprenez rien
et mettez-en un au-dessus :
[source,lua]
----
debugPrint("blocks " .. #blocks .. ", timer " .. spawnTimer)
----
Vous regardez maintenant ce qui est réellement là au lieu de ce que vous êtes
sûr d'y trouver, et c'est dans cet écart que vivent les bugs. Affichez
`playerX` de la même façon et vous verrez aussi la vérité à son sujet : il
s'affiche `160.0`, pas `160`, parce qu'une division avec `/` donne toujours en
Lua un nombre avec une partie fractionnaire, même quand cette partie vaut zéro.
Deux choses vont vous mordre. La première, c'est que `debugPrint` exige du texte
ou un nombre ; donnez-lui autre chose et il arrête le jeu comme l'a fait
`overlayPrint`. `debugPrint(blocks)` ne vous montrera pas une liste, et
`debugPrint("left " .. goingLeft)` tout en haut de la fonction `updatePlaying`
ne vous montrera pas non plus un `true` ou un `false` :
----
dodge/dodge.singe:78: attempt to concatenate a boolean value (upvalue 'goingLeft')
----
`tostring` est le remède. Il prend absolument n'importe quoi et vous en donne
du texte, donc `tostring(goingLeft)` vaut `"true"` ou `"false"`, et la ligne
marche.
La seconde, c'est que `onOverlayUpdate` tourne soixante fois par seconde, donc
un `debugPrint` à l'intérieur affiche soixante lignes par seconde et vous ne
pouvez en lire aucune. La Référence le dit dans son entrée : l'appel ne coûte pas
cher, mais rien ne le freine.
Avant toute chose, mettez un interrupteur devant chaque ligne de débogage que
vous ajoutez, pour pouvoir couper le bruit sans avoir à les traquer de nouveau :
[source,lua]
----
local DEBUG = true
function debugLog(text)
if DEBUG then
debugPrint(text)
end
end
----
Chaque `debugLog` du jeu obéit à ce seul `true`. Changez-le en `false` et le jeu
se tait ; remettez-le et tout revient.
Ensuite, affichez quelque chose quand il se *passe* quelque chose plutôt qu'à
chaque image :
[source,lua]
----
debugLog("hit at y " .. math.floor(block.y) .. ", lives now " .. lives)
----
Ou affichez avec un minuteur, décompté exactement comme l'apparition des
blocs :
[source,lua]
----
reportTimer = reportTimer - 1
if reportTimer <= 0 then
debugReport()
reportTimer = REPORT_FRAMES
end
----
Le dossier `learn` contient le tout sous le nom `08-when-it-goes-wrong.singe` :
c'est le jeu de la leçon sept avec un rapport environ toutes les deux secondes,
une ligne quand vous êtes touché, et une ligne quand la partie se termine.
La seconde manière de voir à l'intérieur, c'est de le mettre à l'écran, ce qui
vaut mieux pour tout ce qui change à chaque image, parce que soixante lignes par
seconde dans un terminal, ça ne sert à rien, alors qu'un nombre qui défile dans
le coin se surveille facilement :
[source,lua]
----
if DEBUG then
overlayPrint(1, 3, "BLOCKS " .. #blocks .. " PLAYER " .. playerX)
end
----
[#_cutting_the_problem_in_half]
=== Couper le problème en deux
Quand vous n'arrivez pas à voir laquelle de vingt lignes est fausse, arrêtez de
regarder et commencez à retirer.
Mettez deux tirets au début d'une ligne et Lua ignore tout le reste de la ligne.
C'est un *commentaire*, et on s'en sert d'habitude pour se laisser des notes,
mais le meilleur usage est celui-ci :
[source,lua]
----
-- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives)
----
Mettez en commentaire la moitié de `drawGame` et lancez. Si le problème est
toujours là, il est dans la moitié que vous avez gardée. S'il a disparu, il est
dans la moitié que vous avez retirée. Remettez cette moitié et coupez-*la* en
deux. Vingt lignes demandent cinq tours de ce petit jeu, et chaque tour, c'est
un enregistrement et un coup d'œil.
Ça marche pour autre chose que des lignes. Mettez en commentaire le corps de
`updatePlaying` et les blocs arrêtent de bouger, ce qui vous dit si ce que vous
traquez est dans le déplacement ou dans le dessin. Mettez en commentaire la
branche de `collideRects` et voyez si le jeu arrête de planter. Vous n'essayez
de rien réparer pendant que vous faites ça. Vous essayez de découvrir où le
problème n'est pas.
[#_going_round_faster]
=== Faire le tour plus vite
Tout ceci repose sur la possibilité d'essayer quelque chose en deux secondes, et
c'est à ça que sert `-R`. Vous vous en servez depuis la leçon un ; voici ce
qu'il fait vraiment, et ça vaut la peine de le savoir maintenant que vous allez
vous appuyer dessus.
Singe surveille chaque fichier de script que votre jeu a chargé. Quand vous en
enregistrez un, le jeu repart du début : tout ce que le script a fabriqué est
jeté et votre fichier est exécuté à neuf, dans le temps qu'il faut pour lire le
fichier plutôt que dans celui qu'il faut pour démarrer le moteur. `F5` fait la
même chose à la demande, sans rien enregistrer.
Une touche que vous gardez enfoncée pendant un rechargement est ignorée jusqu'à
ce que vous la relâchiez, donc le joueur ne partira pas tout seul après un
rechargement.
Les deux sortes d'arrêt se comportent différemment ici, et c'est utile de savoir
laquelle vous avez sous les yeux sans lire le message. Un script qui ne compile
pas est *signalé, et Singe y survit* : la fenêtre reste ouverte et vide, le
fichier reste surveillé, et enregistrer la correction ramène le jeu. Un plantage
à l'intérieur d'un callback -- tous les messages `attempt to`, et les plaintes du
moteur sur les arguments -- fait tomber Singe, et vous le relancez à la main.
[#_reading_a_traceback]
=== Lire une trace de pile
Les lignes sous `stack traceback:` sont la liste des fonctions qui étaient en
cours quand tout s'est arrêté, la plus intérieure en premier. Voici de nouveau
celle de `blocl` :
----
dodge/dodge.singe:60: in function 'spawnBlock'
dodge/dodge.singe:92: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
Lisez-la de bas en haut et c'est une phrase. Singe a appelé `onOverlayUpdate`. À
la ligne 136, `onOverlayUpdate` a appelé `updatePlaying`. À la ligne 92,
`updatePlaying` a appelé `spawnBlock`. À la ligne 60, `spawnBlock` a abandonné.
La ligne du haut, c'est l'endroit où ça a cassé. Les lignes en dessous, c'est le
chemin pour y arriver, et c'est ce qu'il vous faut quand la ligne du haut a l'air
innocente. Une fonction qui ajoute un nombre à `block.y` n'a rien de faux ; une
fonction qui a mis un bloc dans la liste sans `y`, si. Seule la piste relie les
deux, et la piste est affichée pour vous à chaque fois, sans exception.
[#_the_habit]
=== L'habitude
Tout ce qui précède, c'est de la technique. Voici l'habitude, et elle compte
davantage :
*Changez une chose. Lancez. Regardez ce qui s'est passé.*
Quand quelque chose refuse de marcher, la tentation est de changer quatre choses
à la fois, parce que l'une d'elles sera forcément la bonne. Faites ça et ça
arrête de marcher d'une nouvelle façon, et vous ne savez plus laquelle des quatre
en est responsable, ni si deux d'entre elles s'annulent. Vous avez agrandi le
problème et vous l'avez mieux caché.
Un changement. Lancez. Regardez. Ça a l'air plus lent. C'est la méthode la plus
rapide qui existe, et tous les programmeurs expérimentés que vous rencontrerez
font comme ça, exactement pour la raison que vous êtes sur le point de
découvrir.
[#_try_it_8]
=== Essayez
. *Rencontrez-les toutes.* Faites exprès chacune des cinq erreurs de cette leçon
dans `dodge.singe`, une à la fois, et lisez le message avant de corriger. Elles
ne vous feront plus jamais peur.
. *Surveillez les apparitions.* Mettez dans `spawnBlock` un `debugPrint` qui
affiche le `x` du nouveau bloc. Jouez trente secondes, puis regardez les
nombres. Sont-ils répartis sur tout l'écran, ou préfèrent-ils un côté ?
. *Coupez-le.* Changez `DEBUG` en `false` dans le script de la leçon, et
vérifiez par vous-même que rien ne s'affiche et que le jeu n'a pas changé.
. *Un bug sans erreur.* Dans `updatePlaying`, changez `if spawnTimer <= 0 then`
en `if spawnTimer == 0 then`, qui a l'air de vouloir dire la même chose.
Lancez et commencez une partie. Il n'y a pas d'erreur, rien ne plante, et pas
un seul bloc ne tombe, si bien que vous ne pouvez même pas perdre. Mettez un
`debugLog` dans `spawnBlock`, ajoutez `spawnTimer` à la première ligne
qu'affiche `debugReport`, et rejouez. Déduisez de ce qu'ils disent, et de ce
que l'un d'eux ne dit jamais, pourquoi `==` était la mauvaise question à
poser.
. *Cassez le nom d'un callback.* Renommez `onInputPressed` en `onInputPress` et
lancez. Aucune erreur ne s'affiche, rien ne plante, le joueur refuse de bouger, et
Espace ne lance pas de partie. Puis expliquez-vous pourquoi c'est l'erreur la
plus dangereuse de cette leçon.
[#_what_you_learned_8]
=== Ce que vous avez appris
* Les erreurs ont un fichier, une ligne et une plainte. Lisez-les dans cet
ordre.
* `Error running script` veut dire que rien ne s'est exécuté ; la forme avec une
trace de pile veut dire que ça tournait et que ça s'est arrêté.
* `nil` est un nom derrière lequel il n'y a rien, et il est derrière la plupart
des messages d'erreur que vous verrez jamais.
* Le mot entre parenthèses nomme la chose vide, et dit si c'était une variable
globale, une variable locale, un champ ou une upvalue.
* Un `end` manquant est signalé à la fin du fichier, et les parenthèses vous
disent où est vraiment l'erreur.
* Les plaintes du moteur ont un autre air : un numéro de ligne, un nom de
fonction, et ce qu'elle voulait à la place.
* `debugPrint` vous montre ce que contient vraiment une variable. `tostring`
rend n'importe quoi affichable, et un interrupteur comme `DEBUG` coupe le
bruit.
* Mettez une moitié en commentaire avec `--` pour découvrir dans quelle moitié
est le problème.
* Un callback mal nommé ne produit aucune erreur, ce qui le rend pire qu'un
callback qui en produit une.
* Changez une chose, lancez, regardez.
[#_next_time_8]
=== La prochaine fois
C'est la fin de la première partie. Vous savez écrire un jeu, et maintenant vous
savez en réparer un, ce qui veut dire que tout ce qui suit est un ajout et non
une fondation. La deuxième partie commence par remplacer ces boîtes de couleur
par de vraies images, et à partir de là, le jeu que vous construisez commence à
ressembler à un jeu auquel quelqu'un d'autre aurait envie de jouer.